wrapture 0.3.0 → 0.4.0

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  # frozen_string_literal: true
4
4
 
5
+ #--
5
6
  # Copyright 2019 Joel E. Anderson
6
7
  #
7
8
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,8 +16,7 @@
15
16
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
17
  # See the License for the specific language governing permissions and
17
18
  # limitations under the License.
18
-
19
- require 'wrapture/version'
19
+ #++
20
20
 
21
21
  module Wrapture
22
22
  # Normalizes a spec key to be boolean, raising an error if it is not. Keys
@@ -0,0 +1,132 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ # frozen_string_literal: true
4
+
5
+ #--
6
+ # Copyright 2020 Joel E. Anderson
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #++
20
+
21
+ require 'wrapture/type_spec'
22
+
23
+ module Wrapture
24
+ # A description of a parameter used in a generated function.
25
+ class ParamSpec
26
+ # Returns a list of new ParamSpecs based on the provided array of parameter
27
+ # specification hashes.
28
+ def self.new_list(spec_list)
29
+ spec_list.map { |spec| new(spec) }
30
+ end
31
+
32
+ # Returns a normalized copy of a list of parameter hash specifications in
33
+ # place.
34
+ #
35
+ # Multiple variadic parameters (named '...') will be removed and only the
36
+ # first used. If the variadic parameter is not last, it will be moved to
37
+ # the end of the list.
38
+ def self.normalize_param_list(spec_list)
39
+ if spec_list.nil?
40
+ []
41
+ elsif spec_list.count { |spec| spec['name'] == '...' }.zero?
42
+ spec_list.map { |spec| normalize_spec_hash(spec) }
43
+ else
44
+ error_msg = "'...' may not be the only parameter"
45
+ raise(InvalidSpecKey, error_msg) if spec_list.count == 1
46
+
47
+ i = spec_list.find_index { |spec| spec['name'] == '...' }
48
+ var = spec_list[i]
49
+
50
+ spec_list
51
+ .reject { |spec| spec['name'] == '...' }
52
+ .map { |spec| normalize_spec_hash(spec) }
53
+ .push(var)
54
+ end
55
+ end
56
+
57
+ # Returns a normalized copy of the hash specification of a parameter in
58
+ # +spec+. See normalize_spec_hash! for details.
59
+ def self.normalize_spec_hash(spec)
60
+ normalize_spec_hash!(Marshal.load(Marshal.dump(spec)))
61
+ end
62
+
63
+ # Normalizes the hash specification of a parameter in +spec+ in place.
64
+ # Normalization will remove duplicate entries from include lists and
65
+ # validate that required key values are set.
66
+ def self.normalize_spec_hash!(spec)
67
+ Comment.validate_doc(spec['doc']) if spec.key?('doc')
68
+ spec['includes'] = Wrapture.normalize_includes(spec['includes'])
69
+
70
+ spec['type'] = '...' if spec['name'] == '...'
71
+
72
+ unless spec.key?('type')
73
+ missing_type_msg = 'parameters must have a type key defined'
74
+ raise(MissingSpecKey, missing_type_msg)
75
+ end
76
+
77
+ spec
78
+ end
79
+
80
+ # A string with a comma-separated list of parameters (using resolved type)
81
+ # and names, fit for use in a function signature or declaration. param_list
82
+ # must be a list of ParamSpec instances, and owner must be the FunctionSpec
83
+ # that the parameters belong to.
84
+ def self.signature(param_list, owner)
85
+ if param_list.empty?
86
+ 'void'
87
+ else
88
+ param_list.map { |param| param.signature(owner) }.join(', ')
89
+ end
90
+ end
91
+
92
+ # The type of the parameter.
93
+ attr_reader :type
94
+
95
+ # Creates a parameter specification based on the provided hash spec.
96
+ def initialize(spec)
97
+ @spec = ParamSpec.normalize_spec_hash(spec)
98
+ @type = TypeSpec.new(@spec['type'])
99
+ end
100
+
101
+ # A Comment holding the parameter documentation.
102
+ def doc
103
+ if @spec.key?('doc')
104
+ Comment.new("@param #{@spec['name']} #{@spec['doc']}")
105
+ else
106
+ Comment.new
107
+ end
108
+ end
109
+
110
+ # A list of includes needed for this parameter.
111
+ def includes
112
+ @spec['includes'].dup.concat(@type.includes)
113
+ end
114
+
115
+ # The name of the parameter.
116
+ def name
117
+ @spec['name']
118
+ end
119
+
120
+ # The parameter type and name, suitable for use in a function signature or
121
+ # declaration. +owner+ must be the FunctionSpec that the parameter belongs
122
+ # to.
123
+ def signature(owner)
124
+ @type.resolve(owner).variable(name)
125
+ end
126
+
127
+ # True if this parameter is variadic (the name is equal to '...').
128
+ def variadic?
129
+ @type.variadic?
130
+ end
131
+ end
132
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # frozen_string_literal: true
4
4
 
5
+ #--
5
6
  # Copyright 2019-2020 Joel E. Anderson
6
7
  #
7
8
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,17 +16,20 @@
15
16
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
17
  # See the License for the specific language governing permissions and
17
18
  # limitations under the License.
18
-
19
- require 'wrapture/constants'
20
- require 'wrapture/errors'
19
+ #++
21
20
 
22
21
  module Wrapture
23
22
  # A condition (or set of conditions) that a struct or its members must meet
24
23
  # in order to conform to a given specification. This allows a single struct
25
24
  # type to be equivalent to some class specifications, but not others.
26
25
  class RuleSpec
27
- # A list of valid condition strings.
28
- CONDITIONS = %w[equals not-equals].freeze
26
+ # A map of condition strings to their operators.
27
+ CONDITIONS = { 'equals' => '==',
28
+ 'greater-than' => '>',
29
+ 'greater-than-equal' => '>=',
30
+ 'less-than' => '<',
31
+ 'less-than-equal' => '<=',
32
+ 'not-equals' => '!=' }.freeze
29
33
 
30
34
  # Normalizes a hash specification of a rule. Normalization checks for
31
35
  # invalid keys and unrecognized conditions.
@@ -52,7 +56,7 @@ module Wrapture
52
56
  raise(InvalidSpecKey, extra_msg)
53
57
  end
54
58
 
55
- unless RuleSpec::CONDITIONS.include?(spec['condition'])
59
+ unless RuleSpec::CONDITIONS.keys.include?(spec['condition'])
56
60
  condition_msg = "#{spec['condition']} is an invalid condition"
57
61
  raise(InvalidSpecKey, condition_msg)
58
62
  end
@@ -64,8 +68,9 @@ module Wrapture
64
68
  #
65
69
  # The hash must have the following keys:
66
70
  # member-name:: the name of the struct member the rule applies to
67
- # condition:: the condition this rule uses (supported values are held in the
68
- # RuleSpec::CONDITIONS list)
71
+ # condition:: the condition this rule uses (supported values are keys in the
72
+ # RuleSpec::CONDITIONS map, with the mapped values being the
73
+ # operator they translate to)
69
74
  # value:: the value to use in the condition check
70
75
  def initialize(spec)
71
76
  @spec = RuleSpec.normalize_spec_hash(spec)
@@ -73,7 +78,7 @@ module Wrapture
73
78
 
74
79
  # A string containing a check for a struct of the given name for this rule.
75
80
  def check(variable: nil)
76
- condition = @spec['condition'] == 'equals' ? '==' : '!='
81
+ condition = RuleSpec::CONDITIONS[@spec['condition']]
77
82
 
78
83
  if @spec['type'] == 'struct-member'
79
84
  "#{variable}->#{@spec['member-name']} #{condition} #{@spec['value']}"
@@ -2,7 +2,8 @@
2
2
 
3
3
  # frozen_string_literal: true
4
4
 
5
- # Copyright 2019 Joel E. Anderson
5
+ #--
6
+ # Copyright 2019-2020 Joel E. Anderson
6
7
  #
7
8
  # Licensed under the Apache License, Version 2.0 (the "License");
8
9
  # you may not use this file except in compliance with the License.
@@ -15,6 +16,7 @@
15
16
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
17
  # See the License for the specific language governing permissions and
17
18
  # limitations under the License.
19
+ #++
18
20
 
19
21
  module Wrapture
20
22
  # Describes a scope of one or more class specifications.
@@ -22,24 +24,54 @@ module Wrapture
22
24
  # A list of classes currently in the scope.
23
25
  attr_reader :classes
24
26
 
27
+ # A list of enumerations currently in the scope.
28
+ attr_reader :enums
29
+
30
+ # A list of the templates defined in the scope.
31
+ attr_reader :templates
32
+
25
33
  # Creates an empty scope with no classes in it.
26
34
  def initialize(spec = nil)
27
35
  @classes = []
36
+ @enums = []
37
+ @templates = []
28
38
 
29
- return if spec.nil? || !spec.key?('classes')
39
+ return if spec.nil?
30
40
 
31
41
  @version = Wrapture.spec_version(spec)
32
- spec['classes'].each do |class_hash|
42
+
43
+ @templates = spec.fetch('templates', []).collect do |template_hash|
44
+ TemplateSpec.new(template_hash)
45
+ end
46
+
47
+ @classes = spec.fetch('classes', []).collect do |class_hash|
33
48
  ClassSpec.new(class_hash, scope: self)
34
49
  end
50
+
51
+ @enums = spec.fetch('enums', []).collect do |enum_hash|
52
+ EnumSpec.new(enum_hash)
53
+ end
35
54
  end
36
55
 
37
- # Adds a class specification to the scope.
56
+ # Adds a class or template specification to the scope.
38
57
  #
39
- # This does not set the scope as the owner of the class. This must be done
40
- # during the construction of the class spec.
58
+ # This does not set the scope as the owner of the class for a ClassSpec.
59
+ # This must be done during the construction of the class spec.
41
60
  def <<(spec)
61
+ @templates << spec if spec.is_a?(TemplateSpec)
42
62
  @classes << spec if spec.is_a?(ClassSpec)
63
+ @enums << spec if spec.is_a?(EnumSpec)
64
+ end
65
+
66
+ # Adds a class to the scope created from the given specification hash.
67
+ def add_class_spec_hash(spec)
68
+ ClassSpec.new(spec, scope: self)
69
+ end
70
+
71
+ # Adds an enumeration to the scope created from the given specification
72
+ # hash.
73
+ def add_enum_spec_hash(spec)
74
+ @enums << EnumSpec.new(spec)
43
75
  end
44
76
 
45
77
  # Generates the wrapper class files for all classes in the scope.
@@ -50,6 +82,10 @@ module Wrapture
50
82
  files.concat(class_spec.generate_wrappers)
51
83
  end
52
84
 
85
+ @enums.each do |enum_spec|
86
+ files.concat(enum_spec.generate_wrapper)
87
+ end
88
+
53
89
  files
54
90
  end
55
91
 
@@ -63,18 +99,14 @@ module Wrapture
63
99
  @classes.any? { |class_spec| class_spec.overloads?(parent) }
64
100
  end
65
101
 
66
- # Returns the ClassSpec for the given type in the scope.
102
+ # Returns the ClassSpec for the given +type+ in the scope, if one exists.
67
103
  def type(type)
68
- @classes.find { |class_spec| class_spec.name == type }
104
+ @classes.find { |class_spec| class_spec.name == type.base }
69
105
  end
70
106
 
71
- # Returns true if the given type is in the scope.
107
+ # Returns true if there is a class matching the given +type+ in this scope.
72
108
  def type?(type)
73
- @classes.each do |class_spec|
74
- return true if class_spec.name == type
75
- end
76
-
77
- false
109
+ @classes.any? { |class_spec| class_spec.name == type.base }
78
110
  end
79
111
  end
80
112
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  # frozen_string_literal: true
4
4
 
5
- # Copyright 2019 Joel E. Anderson
5
+ #--
6
+ # Copyright 2019-2020 Joel E. Anderson
6
7
  #
7
8
  # Licensed under the Apache License, Version 2.0 (the "License");
8
9
  # you may not use this file except in compliance with the License.
@@ -15,6 +16,7 @@
15
16
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
17
  # See the License for the specific language governing permissions and
17
18
  # limitations under the License.
19
+ #++
18
20
 
19
21
  module Wrapture
20
22
  # A description of a struct.
@@ -69,7 +71,7 @@ module Wrapture
69
71
  members = []
70
72
 
71
73
  @spec['members'].each do |member|
72
- members << ClassSpec.typed_variable(member['type'], member['name'])
74
+ members << TypeSpec.new(member['type']).variable(member['name'])
73
75
  end
74
76
 
75
77
  members.join ', '
@@ -79,7 +81,7 @@ module Wrapture
79
81
  # values if provided, separated by commas.
80
82
  def member_list_with_defaults
81
83
  @spec['members'].map do |member|
82
- member_str = ClassSpec.typed_variable(member['type'], member['name'])
84
+ member_str = TypeSpec.new(member['type']).variable(member['name'])
83
85
 
84
86
  if member.key?('default-value')
85
87
  default_value = member['default-value']
@@ -0,0 +1,432 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ # frozen_string_literal: true
4
+
5
+ #--
6
+ # Copyright 2020 Joel E. Anderson
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #++
20
+
21
+ module Wrapture
22
+ # A template that can be referenced in other specs.
23
+ #
24
+ # Templates provide a way to re-use common specification portions without
25
+ # needing to repeat them everywhere they're needed. For example, if the error
26
+ # handling code within a wrapped library is the same for most functions, it
27
+ # can be defined once in a template and then simply referenced in each
28
+ # function specification that needs it. Not only does this reduce the size of
29
+ # the specifications, but it also allows changes to be made in one place
30
+ # instead of many.
31
+ #
32
+ # = Basic Usage
33
+ #
34
+ # Templates are defined in a top-level +templates+ member of a specification,
35
+ # which holds an array of templates. Each template has only two properties:
36
+ # +name+ which holds the name of the template that is used to invoke it from
37
+ # other specifications, and +value+ which holds the object(s) to insert when
38
+ # the template is used.
39
+ #
40
+ # Templates can be used at any point in a specification by including a Hash
41
+ # member named +use-template+ which is itself a Hash containing a +name+
42
+ # member and optionally a parameter list (see below). When a spec is created
43
+ # in a scope that has a template with the given name, the +use-template+
44
+ # object will be replaced with the template contents. Other members of the
45
+ # Hash will be left intact.
46
+ #
47
+ # To illustrate, consider a template defined with some normal class properties
48
+ # for a library:
49
+ #
50
+ # name: "standard-class-properties"
51
+ # value:
52
+ # namespace: "wrapturedemo"
53
+ # type: "pointer"
54
+ #
55
+ # This could then used in a class specification like this:
56
+ #
57
+ # classes:
58
+ # - name: "ClassA"
59
+ # use-template:
60
+ # name: "standard-class-properties"
61
+ # - name: "ClassB"
62
+ # use-template:
63
+ # name: "standard-class-properties"
64
+ #
65
+ # Which would result in an effective class specification of this:
66
+ #
67
+ # classes:
68
+ # - name: "ClassA"
69
+ # namespace: "wrapturedemo"
70
+ # type: "pointer"
71
+ # - name: "ClassB"
72
+ # namespace: "wrapturedemo"
73
+ # type: "pointer"
74
+ #
75
+ # Note that the properties included in the template were added to the other
76
+ # members of the object. If there is a conflict between members, the member of
77
+ # the invoking specification will override the template's member.
78
+ #
79
+ # In templates that don't have any parameters, you can save a small bit of
80
+ # typing by simply setting the value of the +use-template+ member to the name
81
+ # of the template directly. So, the previous invocation would become this:
82
+ #
83
+ # classes:
84
+ # - name: "ClassA"
85
+ # use-template: "standard-class-properties"
86
+ # - name: "ClassB"
87
+ # use-template: "standard-class-properties"
88
+ #
89
+ # == Usage in Arrays
90
+ # In some cases, you may want a template to expand to an array of elements
91
+ # that are added to an existing array. This can be accomplished by invoking
92
+ # the template in its own list element and making sure that the
93
+ # +use-template+ member is the only member of the hash. This will result in
94
+ # the template result being inserted into the list at the point of the
95
+ # template invocation. Consider this example specification snippet:
96
+ #
97
+ # templates:
98
+ # - name: "default-includes"
99
+ # value:
100
+ # - "struct_decls.h"
101
+ # - "error_handling.h"
102
+ # - "macros.h"
103
+ # classes:
104
+ # - name: "StupendousMan"
105
+ # equivalent-struct:
106
+ # name: "stupendous_man"
107
+ # includes:
108
+ # - "man.h"
109
+ # - use-template:
110
+ # name: "default-includes"
111
+ # - "stupendous.h"
112
+ #
113
+ # This would result in an include list containing this:
114
+ #
115
+ # includes:
116
+ # - "man.h"
117
+ # - "struct_decls.h"
118
+ # - "error_handling.h"
119
+ # - "macros.h"
120
+ # - "stupendous.h"
121
+ #
122
+ # Note that this behavior means that if your intention is to make a list
123
+ # element itself include a list, then you will need to put the template
124
+ # invocation into its own list, like this:
125
+ #
126
+ # my_list:
127
+ # - "element-1"
128
+ # - "element-2"
129
+ # -
130
+ # - use-template:
131
+ # name: "list-template"
132
+ #
133
+ # == Usage in other Templates
134
+ # Templates may reference other templates within themselves. There is no limit
135
+ # to this nesting, which means that it is quite possible for a careless
136
+ # developer to get himself into trouble, for example by recursively
137
+ # referencing a template from itself. Responsible usage of this functionality
138
+ # is left to the users.
139
+ #
140
+ # There are no guarantees made about the order in which templates are
141
+ # expanded. This is an attempt to keep template usage simple and direct.
142
+ #
143
+ # = Parameters
144
+ #
145
+ # Templates may contain any number of parameters that can be supplied upon
146
+ # invocation. The supplied parameters are then used to replace values in the
147
+ # template upon template invocation. This allows templates to be reusable in a
148
+ # wider variety of situations where they may be a small number of differences
149
+ # between invocations, but not significant.
150
+ #
151
+ # Paremeters are signified within a template by using a hash that has a
152
+ # +is-param+ member set to true, and a +name+ member containing the name of
153
+ # the parameter. In the template invocation, a +params+ member is supplied
154
+ # which contains a list of parameter names and values to substitute for them.
155
+ #
156
+ # A simple use of template parameters is shown here, where a template is used
157
+ # to wrap functions which differ only in the name of the underlying wrapped
158
+ # function:
159
+ #
160
+ # templates:
161
+ # - name: "simple-function"
162
+ # value:
163
+ # wrapped-function:
164
+ # name:
165
+ # is-param: true
166
+ # name: "wrapped-function"
167
+ # params:
168
+ # - value: "equivalent-struct-pointer"
169
+ # classes:
170
+ # - name: "StupendousMan"
171
+ # functions:
172
+ # - name: "crawl"
173
+ # use-template:
174
+ # name: "simple-function"
175
+ # params:
176
+ # name: "wrapped-function"
177
+ # value: "stupendous_man_crawl"
178
+ # - name: "walk"
179
+ # use-template:
180
+ # name: "simple-function"
181
+ # params:
182
+ # name: "wrapped-function"
183
+ # value: "stupendous_man_walk"
184
+ # - name: "run"
185
+ # use-template:
186
+ # name: "simple-function"
187
+ # params:
188
+ # name: "wrapped-function"
189
+ # value: "stupendous_man_run"
190
+ #
191
+ # The above would result in a class specification of this:
192
+ #
193
+ # name: "StupendousMan"
194
+ # functions:
195
+ # - name: "crawl"
196
+ # wrapped-function:
197
+ # name: "stupendous_man_crawl"
198
+ # params:
199
+ # - value: "equivalent-struct-pointer"
200
+ # - name: "walk"
201
+ # wrapped-function:
202
+ # name: "stupendous_man_walk"
203
+ # params:
204
+ # - value: "equivalent-struct-pointer"
205
+ # - name: "run"
206
+ # wrapped-function:
207
+ # name: "stupendous_man_run"
208
+ # params:
209
+ # - value: "equivalent-struct-pointer"
210
+ #
211
+ # == Parameter Replacement
212
+ # The rules for parameter replacement are not as complex as for template
213
+ # invocation, as they are intended to hold single values rather than
214
+ # heirarchical object structures. Replacement of a parameter simply replaces
215
+ # the hash containing the +is-param+ member with the given parameter of the
216
+ # same name. Objects may be supplied instead of single values, but they will
217
+ # be inserted directly into the position rather than merged with other hash or
218
+ # array members. If the more complex merging functionality is needed, then
219
+ # consider invoking a template instead of using a parameter.
220
+ class TemplateSpec
221
+ # Replaces all instances of the given templates in the provided spec. This
222
+ # is done recursively until no more changes can be made. Returns true if
223
+ # any changes were made, false otherwise.
224
+ def self.replace_all_uses(spec, *templates)
225
+ return false unless spec.is_a?(Hash) || spec.is_a?(Array)
226
+
227
+ changed = false
228
+ loop do
229
+ changes = templates.collect do |temp|
230
+ temp.replace_uses(spec)
231
+ end
232
+
233
+ changed = true if changes.any?
234
+
235
+ break unless changes.any?
236
+ end
237
+
238
+ changed
239
+ end
240
+
241
+ # True if the provided spec is a template parameter with the given name.
242
+ def self.param?(spec, param_name)
243
+ spec.is_a?(Hash) &&
244
+ spec.key?('is-param') &&
245
+ spec['is-param'] &&
246
+ spec['name'] == param_name
247
+ end
248
+
249
+ # Creates a new spec based on the given one with all instances of a
250
+ # parameter with the given name replaced with the given value.
251
+ def self.replace_param(spec, param_name, param_value)
252
+ new_spec = Marshal.load(Marshal.dump(spec))
253
+ replace_param!(new_spec, param_name, param_value)
254
+ end
255
+
256
+ # Replaces all instances of a parameter with the given name with the given
257
+ # value in the provided spec.
258
+ def self.replace_param!(spec, param_name, param_value)
259
+ if spec.is_a?(Hash)
260
+ replace_param_in_hash(spec, param_name, param_value)
261
+ elsif spec.is_a?(Array)
262
+ replace_param_in_array(spec, param_name, param_value)
263
+ else
264
+ spec
265
+ end
266
+ end
267
+
268
+ # Replaces all instances of a parameter with the given name with the given
269
+ # value in the provided spec, assuming the spec is an array.
270
+ def self.replace_param_in_array(spec, param_name, param_value)
271
+ spec.map! do |value|
272
+ if param?(value, param_name)
273
+ param_value
274
+ else
275
+ replace_param!(value, param_name, param_value)
276
+ value
277
+ end
278
+ end
279
+
280
+ spec
281
+ end
282
+ private_class_method :replace_param_in_array
283
+
284
+ # Replaces all instances of a parameter with the given name with the given
285
+ # value in the provided spec, assuming the spec is a hash.
286
+ def self.replace_param_in_hash(spec, param_name, param_value)
287
+ spec.each_pair do |key, value|
288
+ if param?(value, param_name)
289
+ spec[key] = param_value
290
+ else
291
+ replace_param!(value, param_name, param_value)
292
+ end
293
+ end
294
+
295
+ spec
296
+ end
297
+ private_class_method :replace_param_in_hash
298
+
299
+ # Creates a new template with the given hash spec.
300
+ def initialize(spec)
301
+ @spec = spec
302
+ end
303
+
304
+ # True if the given spec is a reference to this template that will be
305
+ # completely replaced by the template. A direct use can be recognized as
306
+ # a hash with only a 'use-template' key and no others.
307
+ def direct_use?(spec)
308
+ use?(spec) && spec.length == 1
309
+ end
310
+
311
+ # Returns a spec hash of this template with the provided parameters
312
+ # substituted.
313
+ def instantiate(params = nil)
314
+ result_spec = Marshal.load(Marshal.dump(@spec['value']))
315
+
316
+ return result_spec if params.nil?
317
+
318
+ params.each do |param|
319
+ TemplateSpec.replace_param!(result_spec, param['name'], param['value'])
320
+ end
321
+
322
+ result_spec
323
+ end
324
+
325
+ # The name of the template.
326
+ def name
327
+ @spec['name']
328
+ end
329
+
330
+ # Replaces all references to this template with an instantiation of it in
331
+ # the given spec. Returns true if any changes were made, false otherwise.
332
+ #
333
+ # Recursive template uses will not be replaced by this function. If
334
+ # multiple replacements are needed, then you will need to call this function
335
+ # multiple times.
336
+ def replace_uses(spec)
337
+ if spec.is_a?(Hash)
338
+ replace_uses_in_hash(spec)
339
+ elsif spec.is_a?(Array)
340
+ replace_uses_in_array(spec)
341
+ else
342
+ false
343
+ end
344
+ end
345
+
346
+ # True if the given spec is a reference to this template.
347
+ def use?(spec)
348
+ return false unless spec.is_a?(Hash) && spec.key?(TEMPLATE_USE_KEYWORD)
349
+
350
+ invocation = spec[TEMPLATE_USE_KEYWORD]
351
+ if invocation.is_a?(String)
352
+ invocation == name
353
+ elsif invocation.is_a?(Hash)
354
+ unless invocation.key?('name')
355
+ error_message = "invocations of #{TEMPLATE_USE_KEYWORD} must have a "\
356
+ 'name member'
357
+ raise InvalidTemplateUsage, error_message
358
+ end
359
+
360
+ invocation['name'] == name
361
+ else
362
+ error_message = "#{TEMPLATE_USE_KEYWORD} must either be a String or a "\
363
+ 'Hash'
364
+ raise InvalidTemplateUsage, error_message
365
+ end
366
+ end
367
+
368
+ private
369
+
370
+ # Replaces a single use of the template in a Hash object.
371
+ def merge_use_with_hash(use)
372
+ result = instantiate(use['use-template']['params'])
373
+
374
+ error_message = "template #{name} was invoked in a Hash with other"\
375
+ ' keys, but does not resolve to a hash itself'
376
+ raise InvalidTemplateUsage, error_message unless result.is_a?(Hash)
377
+
378
+ use.merge!(result) { |_, oldval, _| oldval }
379
+ use.delete(TEMPLATE_USE_KEYWORD)
380
+ end
381
+
382
+ # Replaces all references to this template with an instantiation of it in
383
+ # the given spec, assuming it is a hash. Returns true if any changes were
384
+ # made, false otherwise.
385
+ def replace_uses_in_hash(spec)
386
+ changes = []
387
+
388
+ if use?(spec)
389
+ merge_use_with_hash(spec) if use?(spec)
390
+ changes << true
391
+ end
392
+
393
+ spec.each_pair do |key, value|
394
+ if direct_use?(value)
395
+ spec[key] = instantiate(value[TEMPLATE_USE_KEYWORD]['params'])
396
+ changes << true
397
+ else
398
+ changes << replace_uses(value)
399
+ end
400
+ end
401
+
402
+ changes.any?
403
+ end
404
+
405
+ # Replaces all references to this template with an instantiation of it in
406
+ # the given spec, assuming it is an array. Returns true if any changes were
407
+ # made, false otherwise.
408
+ def replace_uses_in_array(spec)
409
+ changes = []
410
+
411
+ spec.dup.each_index do |i|
412
+ if direct_use?(spec[i])
413
+ result = instantiate(spec[i][TEMPLATE_USE_KEYWORD]['params'])
414
+ spec.delete_at(i)
415
+ if result.is_a?(Array)
416
+ spec.insert(i, *result)
417
+ else
418
+ spec.insert(i, result)
419
+ end
420
+ changes << true
421
+ elsif use?(spec[i])
422
+ merge_use_with_hash(spec[i])
423
+ changes << true
424
+ else
425
+ changes << replace_uses(spec[i])
426
+ end
427
+ end
428
+
429
+ changes.any?
430
+ end
431
+ end
432
+ end