File: datatypes.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (213 lines) | stat: -rw-r--r-- 7,822 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# Data types in the Puppet Language can have implementations written in Ruby
# and distributed in puppet modules. A data type can be declared together with
# its implementation by creating a file in 'lib/puppet/functions/<modulename>'.
# The name of the file must be the downcased name of the data type followed by
# the extension '.rb'.
#
# A data type is created by calling {Puppet::DataTypes.create_type(<type name>)}
# and passing it a block that defines the data type interface and implementation.
#
# Data types are namespaced inside the modules that contains them. The name of the
# data type is prefixed with the name of the module. As with all type names, each
# segment of the name must start with an uppercase letter.
#
# @example A simple data type
#   Puppet::DataTypes.create_type('Auth::User') do
#     interface <<-PUPPET
#       attributes => {
#         name => String,
#         email => String
#       }
#     PUPPET
#   end
#
# The above example does not declare an implementation which makes it equivalent
# to adding the following contents in a file named 'user.pp' under the 'types' directory
# of the module root.
#
#   type Auth::User = Object[
#     attributes => {
#       name => String,
#       email => String
#     }]
#
# Both declarations are valid and will be found by the module loader.
#
# Structure of a data type
# ---
#
# A Data Type consists of an interface and an implementation. Unless a registered implementation
# is found, the type system will automatically generate one. An  automatically generated
# implementation is all that is needed when the interface fully  defines the behaviour (for
# example in the common case when the data type has no other behaviour than having attributes).
#
# When the automatically generated implementation is not sufficient, one must be implemented and
# registered. The implementation can either be done next to the interface definition by passing
# a block to `implementation`, or map to an existing implementation class by passing the class
# as an argument to `implementation_class`. An implementation class does not have to be special
# in other respects than that it must implemented the type's interface. This makes it possible
# to use existing Ruby data types as data types in the puppet language.
#
# Note that when using `implementation_class` there can only be one such implementation across
# all environments managed by one puppet server and you must handle and install these
# implementations as if they are part of the puppet platform. In contrast; the type
# implementations that are done inside of the type's definition are safe to use in different
# versions in different environments (given that they do not need additional external logic to
# be loaded).
#
# When using an `implementation_class` it is sometimes desirable to load this class from the
# 'lib' directory of the module. The method `load_file` is provided to facilitate such a load.
# The `load_file` will use the `Puppet::Util::Autoload` to search for the given file in the 'lib'
# directory of the current environment and the 'lib' directory in each included module.
#
# @example Adding implementation on top of the generated type using `implementation`
#   Puppet::DataTypes.create_type('Auth::User') do
#     interface <<-PUPPET
#       attributes => {
#         name => String,
#         email => String,
#         year_of_birth => Integer,
#         age => { type => Integer, kind => derived }
#       }
#       PUPPET
#
#     implementation do
#       def age
#         DateTime.now.year - @year_of_birth
#       end
#     end
#   end
#
# @example Appointing an already existing implementation class
#
# Assumes the following class is declared under 'lib/puppetx/auth' in the module:
#
#   class PuppetX::Auth::User
#     attr_reader :name, :year_of_birth
#     def initialize(name, year_of_birth)
#       @name = name
#       @year_of_birth = year_of_birth
#     end
#
#     def age
#       DateTime.now.year - @year_of_birth
#     end
#
#     def send_text(sender, text)
#       sender.send_text_from(@name, text)
#     end
#   end
#
# Then the type declaration can look like this:
#
#   Puppet::DataTypes.create_type('Auth::User') do
#     interface <<-PUPPET
#       attributes => {
#         name => String,
#         email => String,
#         year_of_birth => Integer,
#         age => { type => Integer, kind => derived }
#       },
#       functions => {
#         send_text => Callable[Sender, String[1]]
#       }
#       PUPPET
#
#     # This load_file is optional and only needed in case
#     # the implementation is not loaded by other means.
#     load_file 'puppetx/auth/user'
#
#     implementation_class PuppetX::Auth::User
#   end
#
module Puppet::DataTypes
  def self.create_type(type_name, &block)
    # Ruby < 2.1.0 does not have method on Binding, can only do eval
    # and it will fail unless protected with an if defined? if the local
    # variable does not exist in the block's binder.
    #
    begin
      loader = block.binding.eval('loader_injected_arg if defined?(loader_injected_arg)')
      create_loaded_type(type_name, loader, &block)
    rescue StandardError => e
      raise ArgumentError, _("Data Type Load Error for type '%{type_name}': %{message}") % {type_name: type_name, message: e.message}
    end

  end

  def self.create_loaded_type(type_name, loader, &block)
    builder = TypeBuilder.new(type_name.to_s)
    api = TypeBuilderAPI.new(builder).freeze
    api.instance_eval(&block)
    builder.create_type(loader)
  end

  # @api private
  class TypeBuilder
    attr_accessor :interface, :implementation, :implementation_class

    def initialize(type_name)
      @type_name = type_name
      @implementation = nil
      @implementation_class = nil
    end

    def create_type(loader)
      raise ArgumentError, _('a data type must have an interface') unless @interface.is_a?(String)
      created_type = Puppet::Pops::Types::PObjectType.new(
        @type_name,
        Puppet::Pops::Parser::EvaluatingParser.new.parse_string("{ #{@interface} }").body)

      if !@implementation_class.nil?
        if @implementation_class < Puppet::Pops::Types::PuppetObject
          @implementation_class.instance_eval do
            include Puppet::Pops::Types::PuppetObject
            @_pcore_type = created_type

            def self._pcore_type
              @_pcore_type
            end
          end
        else
          Puppet::Pops::Loaders.implementation_registry.register_implementation(created_type, @implementation_class)
        end
        created_type.implementation_class = @implementation_class
      elsif !@implementation.nil?
        created_type.implementation_override = @implementation
      end
      created_type
    end

    def has_implementation?
      !(@implementation_class.nil? && @implementation.nil?)
    end
  end

  # The TypeBuilderAPI class exposes only those methods that the builder API provides
  # @api public
  class TypeBuilderAPI
    # @api private
    def initialize(type_builder)
      @type_builder = type_builder
    end

    def interface(type_string)
      raise ArgumentError, _('a data type can only have one interface') unless @type_builder.interface.nil?
      @type_builder.interface = type_string
    end

    def implementation(&block)
      raise ArgumentError, _('a data type can only have one implementation') if @type_builder.has_implementation?
      @type_builder.implementation = block
    end

    def implementation_class(ruby_class)
      raise ArgumentError, _('a data type can only have one implementation') if @type_builder.has_implementation?
      @type_builder.implementation_class = ruby_class
    end

    def load_file(file_name)
      Puppet::Util::Autoload.load_file(file_name, Puppet.lookup(:current_environment))
    end
  end
end