windoo 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES.md +9 -0
  3. data/LICENSE.txt +177 -0
  4. data/README.md +222 -0
  5. data/lib/windoo/base_classes/array_manager.rb +335 -0
  6. data/lib/windoo/base_classes/criteria_manager.rb +327 -0
  7. data/lib/windoo/base_classes/criterion.rb +226 -0
  8. data/lib/windoo/base_classes/json_object.rb +472 -0
  9. data/lib/windoo/configuration.rb +221 -0
  10. data/lib/windoo/connection/actions.rb +152 -0
  11. data/lib/windoo/connection/attributes.rb +156 -0
  12. data/lib/windoo/connection/connect.rb +402 -0
  13. data/lib/windoo/connection/constants.rb +55 -0
  14. data/lib/windoo/connection/token.rb +489 -0
  15. data/lib/windoo/connection.rb +92 -0
  16. data/lib/windoo/converters.rb +31 -0
  17. data/lib/windoo/exceptions.rb +34 -0
  18. data/lib/windoo/mixins/api_collection.rb +408 -0
  19. data/lib/windoo/mixins/constants.rb +43 -0
  20. data/lib/windoo/mixins/default_connection.rb +75 -0
  21. data/lib/windoo/mixins/immutable.rb +34 -0
  22. data/lib/windoo/mixins/loading.rb +38 -0
  23. data/lib/windoo/mixins/patch/component.rb +102 -0
  24. data/lib/windoo/mixins/software_title/extension_attribute.rb +106 -0
  25. data/lib/windoo/mixins/utility.rb +23 -0
  26. data/lib/windoo/objects/capability.rb +82 -0
  27. data/lib/windoo/objects/capability_manager.rb +52 -0
  28. data/lib/windoo/objects/component.rb +99 -0
  29. data/lib/windoo/objects/component_criteria_manager.rb +26 -0
  30. data/lib/windoo/objects/component_criterion.rb +66 -0
  31. data/lib/windoo/objects/extension_attribute.rb +149 -0
  32. data/lib/windoo/objects/kill_app.rb +92 -0
  33. data/lib/windoo/objects/kill_app_manager.rb +89 -0
  34. data/lib/windoo/objects/patch.rb +235 -0
  35. data/lib/windoo/objects/patch_manager.rb +240 -0
  36. data/lib/windoo/objects/requirement.rb +85 -0
  37. data/lib/windoo/objects/requirement_manager.rb +52 -0
  38. data/lib/windoo/objects/software_title.rb +407 -0
  39. data/lib/windoo/validate.rb +548 -0
  40. data/lib/windoo/version.rb +15 -0
  41. data/lib/windoo/zeitwerk_config.rb +158 -0
  42. data/lib/windoo.rb +56 -0
  43. metadata +141 -0
@@ -0,0 +1,408 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ # main module
10
+ module Windoo
11
+
12
+ module Mixins
13
+
14
+ # This should be included into The TitleEditor and
15
+ # Admin classes that represent API collections in their
16
+ # respective servers.
17
+ #
18
+ # It defines core methods for dealing with such collections.
19
+ module APICollection
20
+
21
+ # when this module is included, also extend our Class Methods
22
+ def self.included(includer)
23
+ Windoo.verbose_include includer, self
24
+ includer.extend(ClassMethods)
25
+ end
26
+
27
+ # REQUIRED ITEMS WHEN MIXING IN
28
+ ###################################
29
+ ###################################
30
+ # Classes mixing in this module must define these things:
31
+
32
+ # Constant RSRC_PATH
33
+ ######
34
+ # The path from which to GET, PUT, or DELETE
35
+ # lists or instances of this class. e.g 'patches'
36
+ # or 'softwaretitles'
37
+
38
+ # Constant CONTAINER_CLASS
39
+ ######
40
+ # Except for softwaretitles, the POST/create path for
41
+ # all objects is under the path of their container object.
42
+ #
43
+ # e.g. to create a patch for a softwaretitle, the path is:
44
+ # .../softwaretitles/{id}/patches
45
+ #
46
+ # and to create a killapp in a patch, it is:
47
+ # .../patches/{id}/killapps
48
+ #
49
+ # This constant allows this class to calculate
50
+ # the POST path from its container's path, and gives
51
+ # access to other data from the container class.
52
+
53
+ # instance method #handle_create_response(post_response, container_id: nil)
54
+ ######
55
+ #
56
+ # This method is called after a new object is created on the server,
57
+ # and the data from the POST response is passed in.
58
+ # This method should update the attributes with any new
59
+ # data from the result, such as modification dates, ids, etc.
60
+ #
61
+ # It must return the primary identifier of the new object
62
+
63
+ # instance method #handle_update_response(put_response)
64
+ ######
65
+ #
66
+ # This method is called after an object is updated on the server,
67
+ # and the data from the PUT response is passed in.
68
+ # This method should update the attributes with any new
69
+ # data from the result, such as modification dates, etc.
70
+ #
71
+ # It must return the primary identifier of the updated object
72
+
73
+ ###################################
74
+ ###################################
75
+
76
+ # Class Methods
77
+ #####################################
78
+ module ClassMethods
79
+
80
+ ####################
81
+ def self.extended(extender)
82
+ Windoo.verbose_extend extender, self
83
+ end
84
+
85
+ # Make a new instance on the server.
86
+ #
87
+ # The attributes marked as required must be supplied
88
+ # in the keyword args. Others may be included, or
89
+ # may be added later. To see the required args, use
90
+ # the .required_attributes class method
91
+ #
92
+ # @param container [Object] All objects other than SoftwareTitles
93
+ # are contained within other objects, and created via methods
94
+ # within those container objects. They will pass 'self'
95
+ #
96
+ # @param init_data [Hasn] The attributes of the new item as keyword
97
+ # arguments. Some may be required.
98
+ #
99
+ # @return [Object] A new instance of the class, already saved
100
+ # to the server.
101
+ #
102
+ ####################
103
+ def create(container: nil, cnx: Windoo.cnx, **init_data)
104
+ container = Windoo::Validate.container_for_new_object(
105
+ new_object_class: self,
106
+ container: container
107
+ )
108
+
109
+ unless (required_attributes & init_data.keys) == required_attributes
110
+ raise ArgumentError,
111
+ "Missing one or more required attributes for #{self}: #{required_attributes.join ', '}"
112
+ end
113
+
114
+ # validate all init values
115
+ json_attributes.each do |attr_name, attr_def|
116
+ init_val = init_data[attr_name]
117
+ if attr_def[:required]
118
+ Windoo::Validate.not_nil(
119
+ init_val,
120
+ msg: "Value for #{attr_name}: must be provided"
121
+ )
122
+ end
123
+
124
+ init_data[attr_name] = Windoo::Validate.json_attr init_val, attr_def: attr_def, attr_name: attr_name
125
+ end
126
+ # add the container if applicable
127
+ init_data[:from_container] = container if container
128
+
129
+ # Let other steps in the process know we are being called from #create
130
+ init_data[:creating] = true
131
+
132
+ # Create our instance
133
+ obj = new(**init_data)
134
+
135
+ # create it on the server
136
+ obj.create_on_server cnx: cnx
137
+
138
+ # return it
139
+ obj
140
+ end
141
+
142
+ # Instantiate from the API directly.
143
+ # @return [Object]
144
+ #
145
+ ####################
146
+ def fetch(primary_ident, cnx: Windoo.cnx)
147
+ if primary_ident.is_a? Hash
148
+ raise 'All API objects other than SoftwareTitle are fetched only by their id number'
149
+ end
150
+
151
+ init_data = cnx.get("#{self::RSRC_PATH}/#{primary_ident}")
152
+ init_data[:cnx] = cnx
153
+ init_data[:fetching] = true
154
+ new(**init_data)
155
+ end
156
+
157
+ # This is used by container classes to instantiate the objects they contain
158
+ # e.g. when when instantiating a Patch, it needs to instantiate
159
+ # killApps, components, and capabilites. it will do so with this method
160
+ #
161
+ ####################
162
+ def instantiate_from_container(container:, **init_data)
163
+ container = Windoo::Validate.container_for_new_object(
164
+ new_object_class: self,
165
+ container: container
166
+ )
167
+ init_data[:from_container] = container
168
+ init_data[:cnx] = container.cnx
169
+ new(**init_data)
170
+ end
171
+
172
+ ####
173
+ def delete(primary_ident, cnx: Windoo.cnx)
174
+ if primary_ident.is_a? Hash
175
+ raise ArgumentError, 'All API objects other than SoftwareTitle are deleted only by their id number'
176
+ end
177
+
178
+ cnx.delete("#{self::RSRC_PATH}/#{primary_ident}")
179
+ rescue Windoo::NoSuchItemError
180
+ # wasn't there to begin with
181
+ nil
182
+ end
183
+
184
+ end # module ClassMethods
185
+
186
+ # Constructor
187
+ ######################
188
+ def initialize(**init_data)
189
+ fetching = init_data.delete :fetching
190
+ @cnx = init_data.delete :cnx
191
+
192
+ @container ||= init_data.delete :from_container
193
+
194
+ # we save 'creating' in an inst. var so we know to create
195
+ # rather than update later on when we #save
196
+ @creating = true if init_data[:creating]
197
+
198
+ unless fetching || @container || @creating
199
+ raise Windoo::UnsupportedError, "#{self.class} can only be instantiated using .fetch or .create, not .new"
200
+ end
201
+
202
+ super
203
+ end
204
+
205
+ # Public Instance Methods
206
+ ####################
207
+
208
+ # @return [APICollection] The object that contains this object, or nil
209
+ # if nothing contains this object
210
+ # def container
211
+ # return @container if defined? @container
212
+
213
+ # @container =
214
+ # if defined? self.class::CONTAINER_CLASS
215
+ # container_id_key = self.class::CONTAINER_CLASS.primary_id_key
216
+ # container_id = send container_id_key
217
+ # self.class::CONTAINER_CLASS.fetch container_id
218
+ # end
219
+ # end
220
+
221
+ # @return [nil, Integer] our primary identifier value, regardless of its
222
+ # attribute name. Before creation, this is nil. After deletion, this is -1
223
+ #
224
+ ####################
225
+ def primary_id
226
+ send self.class.primary_id_key
227
+ end
228
+
229
+ # @return [Boolean] Is this object the same as another, based on their
230
+ # primary_id
231
+ ####################
232
+ def ==(other)
233
+ return false unless self.class == other.class
234
+
235
+ primary_id == other.primary_id
236
+ end
237
+
238
+ # @return [Integer] our primary identifier value before we were deleted.
239
+ # Before deletion, this is nil
240
+ #
241
+ ####################
242
+ def deleted_id
243
+ @deleted_id
244
+ end
245
+
246
+ # @return [Integer] our primary identifier value before we were deleted.
247
+ # Before deletion, this is nil
248
+ #
249
+ ####################
250
+ def deleted_id
251
+ @deleted_id
252
+ end
253
+
254
+ # @return [Windoo::APICollection] If this object is contained within another,
255
+ # then here is the object that contains it
256
+ ####################
257
+ def container
258
+ @container
259
+ end
260
+
261
+ # @return [Windoo::Connection] The server connection for this object
262
+ ####################
263
+ def cnx
264
+ @cnx
265
+ end
266
+
267
+ # @return [Windoo::SoftwareTitle] The SoftwareTitle object that ultimately
268
+ # contains this object
269
+ ####################
270
+ def softwareTitle
271
+ return self if is_a? Windoo::SoftwareTitle
272
+
273
+ return container if container.is_a? Windoo::SoftwareTitle
274
+
275
+ container.softwareTitle
276
+ end
277
+
278
+ # Delete this object
279
+ #
280
+ # @return [Integer] The id of the object that was deleted
281
+ #
282
+ #############
283
+ def delete
284
+ self.class.delete primary_id, cnx: cnx
285
+ @deleted_id = primary_id
286
+ instance_variable_set "@#{self.class.primary_id_key}", -1
287
+ @deleted_id
288
+ end
289
+
290
+ # create a new object on the server from this instance
291
+ #
292
+ # @param container_id [Integer, nil] the id of the object that will
293
+ # contain the one we are creating. If nil, then we are creating
294
+ # a SoftwareTitle.
295
+ #
296
+ # @return [Integer] The id of the newly created object
297
+ #
298
+ ####################
299
+ def create_on_server(cnx: Windoo.cnx)
300
+ unless @creating
301
+ raise Windoo::UnsupportedError,
302
+ "Do not call 'create_on_server' directly - use the .create class method."
303
+ end
304
+
305
+ @cnx = cnx
306
+ rsrc = creation_rsrc
307
+ resp = cnx.post rsrc, to_json
308
+
309
+ update_title_modify_time(resp)
310
+
311
+ # the container method woull only return nil for
312
+ # SoftwareTitle objects
313
+ container_id = container&.primary_id
314
+
315
+ new_id = handle_create_response(resp, container_id: container_id)
316
+
317
+ # no longer creating, future saves are updates
318
+ remove_instance_variable :@creating
319
+
320
+ new_id
321
+ end
322
+
323
+ # Update a single attribute on the server with the current value.
324
+ #
325
+ # @param attr_name [Symbol] The key from Class.json_attributes for the value
326
+ # we want to update
327
+ #
328
+ # @param alt_value [Object] A value to send that isn't the actual data for the attribute.
329
+ # If provided, the attr_name need not appear as a key in .json_attributes, but
330
+ # that name and this value will be sent to the API. See CriteriaManager#update_criterion
331
+ # for an example
332
+ #
333
+ #
334
+ # @return [Integer] the id of the updated item.
335
+ ####################
336
+ def update_on_server(attr_name, new_value)
337
+ # This may be nil if given an alt name for an alt value
338
+ attr_def = self.class.json_attributes[attr_name]
339
+
340
+ if attr_def&.dig attr_name, :do_not_send
341
+ raise Windoo::UnsupportedError, "The value for #{attr_name} cannot be updated directly."
342
+ end
343
+
344
+ # convert the value, if needed, to API format
345
+ value_to_send =
346
+ if attr_def&.dig attr_name, :to_api
347
+ Windoo::Converters.send attr_def[:to_api], new_value.dup
348
+ else
349
+ new_value
350
+ end
351
+
352
+ json_to_put = { attr_name => value_to_send }.to_json
353
+
354
+ # should use our @cnx...
355
+ resp = cnx.put "#{self.class::RSRC_PATH}/#{primary_id}", json_to_put
356
+ update_title_modify_time(resp)
357
+ handle_update_response(resp)
358
+ end
359
+
360
+ # Remove the cnx object from
361
+ # the instance_variables used to create
362
+ # pretty-print (pp) output.
363
+ #
364
+ # @return [Array] the desired instance_variables
365
+ ####################
366
+ def pretty_print_instance_variables
367
+ vars = instance_variables.sort
368
+ vars.delete :@cnx
369
+ vars.delete :@container
370
+ vars
371
+ end
372
+
373
+ # Private Instance Methods
374
+ ####################
375
+ private
376
+
377
+ # update the timestamp on the title that contains this object
378
+ ####################
379
+ def update_title_modify_time(resp)
380
+ if is_a? Windoo::SoftwareTitle
381
+ @lastModified = Time.parse(resp[:lastModified])
382
+ else
383
+ softwareTitle.update_modification_time
384
+ end
385
+ end
386
+
387
+ # figure out the resource path to use for POSTing this thing to the server
388
+ #
389
+ # @param container_id [Integer, nil] the id of the object that will
390
+ # contain the one we are creating. If nil, then we are creating
391
+ # a SoftwareTitle.
392
+ #
393
+ # @return [String] The resource path for POSTing to the server
394
+ #
395
+ ####################
396
+ def creation_rsrc
397
+ # if no container id was given, the only thing we can create is
398
+ # a SoftwareTitle. Everything else is created via its container.
399
+ return Windoo::SoftwareTitle::RSRC_PATH unless @container
400
+
401
+ "#{self.class::CONTAINER_CLASS::RSRC_PATH}/#{@container.primary_id}/#{self.class::RSRC_PATH}"
402
+ end
403
+
404
+ end # module APICollection
405
+
406
+ end # module Mixins
407
+
408
+ end # module Windoo
@@ -0,0 +1,43 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+
6
+ # frozen_string_literal: true
7
+
8
+ module Windoo
9
+
10
+ # When using included modules to define constants,
11
+ # the constants have to be defined at the level where they will be
12
+ # referenced, or else they
13
+ # aren't available to other broken-out-and-included sub modules
14
+ #
15
+ # See https://cultivatehq.com/posts/ruby-constant-resolution/ for
16
+ # an explanation
17
+
18
+ # The minimum Ruby version needed for windoo
19
+ MINIMUM_RUBY_VERSION = '2.6.3'
20
+
21
+ # These are handy for testing values without making new arrays, strings, etc every time.
22
+ TRUE_FALSE = [true, false].freeze
23
+
24
+ # Empty strings are used in various places
25
+ BLANK = ''
26
+
27
+ module Mixins
28
+
29
+ # Constants useful throughout Windoo
30
+ # This should be included into the Jamf module
31
+ #####################################
32
+ module Constants
33
+
34
+ # when this module is included, also extend our Class Methods
35
+ def self.included(includer)
36
+ Windoo.load_msg "--> #{includer} is including Windoo::Constants"
37
+ end
38
+
39
+ end # module constants
40
+
41
+ end # module Mixins
42
+
43
+ end # module Windoo
@@ -0,0 +1,75 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # frozen_string_literal: true
9
+
10
+ module Windoo
11
+
12
+ module Mixins
13
+
14
+ # Module methods and aliases for dealing with the default connection
15
+ # This is extended into the API module
16
+ ######################
17
+ module DefaultConnection
18
+
19
+ # When this module is extended into a class
20
+ def self.extended(extender)
21
+ Windoo.verbose_extend extender, self
22
+ end
23
+
24
+ # The current default Windoo::Connection instance.
25
+ #
26
+ # @return [Windoo::Connection]
27
+ #
28
+ def default_connection
29
+ @default_connection ||= Windoo::Connection.new name: :default
30
+ end
31
+ alias cnx default_connection
32
+
33
+ # Create a new Connection object and use it as the default for all
34
+ # future API calls. This will replace the existing default connection with
35
+ # a totally new one
36
+ #
37
+ # @param (See Windoo::Connection#initialize)
38
+ #
39
+ # @return [String] the to_s output of the new connection
40
+ #
41
+ def connect(url = nil, **params)
42
+ params[:name] ||= :default
43
+ @default_connection = Windoo::Connection.new url, **params
44
+ @default_connection.to_s
45
+ end
46
+ alias login connect
47
+
48
+ # Use the given Windoo::Connection object as the default connection, replacing
49
+ # the one that currently exists.
50
+ #
51
+ # @param connection [Windoo::Connection] The default Connection to use for future
52
+ # API calls
53
+ #
54
+ # @return [APIConnection] The connection now being used.
55
+ #
56
+ def cnx=(connection)
57
+ unless connection.is_a? Windoo::Connection
58
+ raise 'Title Editor connections must be instances of Windoo::Connection'
59
+ end
60
+
61
+ @default_connection = connection
62
+ end
63
+
64
+ # Disconnect the default connection
65
+ #
66
+ def disconnect
67
+ @default_connection.disconnect if @default_connection&.connected?
68
+ end
69
+ alias logout disconnect
70
+
71
+ end # module DefaultConnection
72
+
73
+ end # module Mixins
74
+
75
+ end # module Windoo
@@ -0,0 +1,34 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # main module
9
+ module Windoo
10
+
11
+ module Mixins
12
+
13
+ # by default, instances of JSONObject subclasses are mutable
14
+ # as a whole, even if some of their attributes are not.
15
+ #
16
+ # To make them immutable, they should extend this module
17
+ # Windoo::Mixins::Immutable,
18
+ # which overrides the mutable? method
19
+ module Immutable
20
+
21
+ def self.extended(extender)
22
+ Windoo.verbose_extend extender, self
23
+ end
24
+
25
+ # this class is immutable
26
+ def mutable?
27
+ false
28
+ end
29
+
30
+ end # module Immutable
31
+
32
+ end # module Mixins
33
+
34
+ end # module Windoo
@@ -0,0 +1,38 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ module Windoo
10
+
11
+ module Mixins
12
+
13
+ module Loading
14
+
15
+ def self.extended(extender)
16
+ Windoo.verbose_extend extender, self
17
+ end
18
+
19
+ # Use the load_msg method defined for Zeitwerk
20
+ def load_msg(msg)
21
+ WindooZeitwerkConfig.load_msg msg
22
+ end
23
+
24
+ # Mention that a module is being included into something
25
+ def verbose_include(includer, includee)
26
+ load_msg "--> #{includer} is including #{includee}"
27
+ end
28
+
29
+ # Mention that a module is being extended into something
30
+ def verbose_extend(extender, extendee)
31
+ load_msg "--> #{extender} is extending #{extendee}"
32
+ end
33
+
34
+ end # module Loading
35
+
36
+ end # module Mixins
37
+
38
+ end # module Windoo
@@ -0,0 +1,102 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ module Windoo
10
+
11
+ module Mixins
12
+
13
+ module Patch
14
+
15
+ # An module that manages the Component in a
16
+ # Patch.
17
+ #
18
+ # Even though 'components' is plural, and
19
+ # they come from the server in an Array, at the moment there
20
+ # can only be one per Paatch.
21
+ #
22
+ # This module hides that confusion and allows you to work with
23
+ # just one
24
+ #
25
+ # If there's already an component, you can access it
26
+ # from the #component getter, and use that to directly
27
+ # update its values.
28
+ #
29
+ module Component
30
+
31
+ # Construcor
32
+ ######################
33
+ def initialize(**init_data)
34
+ super
35
+ @component =
36
+ if @init_data[:components]&.first
37
+ Windoo::Component.instantiate_from_container(
38
+ container: self,
39
+ **@init_data[:components].first
40
+ )
41
+ end
42
+ end
43
+
44
+ # Public Instance Methods
45
+ ####################################
46
+
47
+ # Add a component to this Patch. After its created,
48
+ # you can add criteria to it.
49
+ #
50
+ # NOTE: There can be only one per v. You will
51
+ # get an error if one already exists.
52
+ #
53
+ # @param name [String] The name of the component. Usually
54
+ # the same as the name of the Software Title it is
55
+ # associated with
56
+ #
57
+ # @param version [String] The version of the componen.
58
+ # Usually the same as the version of the Patch if it
59
+ # associated with
60
+ #
61
+ # @return [Integer] The id of the new Extension Attribute
62
+ #
63
+ def add_component(name:, version:)
64
+ if @component
65
+ raise Windoo::AlreadyExistsError,
66
+ 'This Patch already has a Component. Either delete it before creating a new one, or update the existing one.'
67
+ end
68
+
69
+ @component = Windoo::Component.create(
70
+ container: self,
71
+ cnx: cnx,
72
+ name: name,
73
+ version: version
74
+ )
75
+
76
+ @component.componentId
77
+ end
78
+
79
+ # Delete the component from this Patch
80
+ #
81
+ # @return [Integer] The id of the deleted Extension Attribute
82
+ #
83
+ def delete_component
84
+ return unless @component
85
+
86
+ deleted_id = @component.delete
87
+ @component = nil
88
+
89
+ # patches without a component are not valid
90
+ # so must be disabled
91
+ container.disable
92
+
93
+ deleted_id
94
+ end
95
+
96
+ end # module ExtensionAttribute
97
+
98
+ end # module SoftwareTitle
99
+
100
+ end # module Mixins
101
+
102
+ end # module Windoo