wrapture 0.2.2 → 0.5.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.
@@ -0,0 +1,435 @@
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
+ case spec
260
+ when Hash
261
+ replace_param_in_hash(spec, param_name, param_value)
262
+ when Array
263
+ replace_param_in_array(spec, param_name, param_value)
264
+ else
265
+ spec
266
+ end
267
+ end
268
+
269
+ # Replaces all instances of a parameter with the given name with the given
270
+ # value in the provided spec, assuming the spec is an array.
271
+ def self.replace_param_in_array(spec, param_name, param_value)
272
+ spec.map! do |value|
273
+ if param?(value, param_name)
274
+ param_value
275
+ else
276
+ replace_param!(value, param_name, param_value)
277
+ value
278
+ end
279
+ end
280
+
281
+ spec
282
+ end
283
+ private_class_method :replace_param_in_array
284
+
285
+ # Replaces all instances of a parameter with the given name with the given
286
+ # value in the provided spec, assuming the spec is a hash.
287
+ def self.replace_param_in_hash(spec, param_name, param_value)
288
+ spec.each_pair do |key, value|
289
+ if param?(value, param_name)
290
+ spec[key] = param_value
291
+ else
292
+ replace_param!(value, param_name, param_value)
293
+ end
294
+ end
295
+
296
+ spec
297
+ end
298
+ private_class_method :replace_param_in_hash
299
+
300
+ # Creates a new template with the given hash spec.
301
+ def initialize(spec)
302
+ @spec = spec
303
+ end
304
+
305
+ # True if the given spec is a reference to this template that will be
306
+ # completely replaced by the template. A direct use can be recognized as
307
+ # a hash with only a 'use-template' key and no others.
308
+ def direct_use?(spec)
309
+ use?(spec) && spec.length == 1
310
+ end
311
+
312
+ # Returns a spec hash of this template with the provided parameters
313
+ # substituted.
314
+ def instantiate(params = nil)
315
+ result_spec = Marshal.load(Marshal.dump(@spec['value']))
316
+
317
+ return result_spec if params.nil?
318
+
319
+ params.each do |param|
320
+ TemplateSpec.replace_param!(result_spec, param['name'], param['value'])
321
+ end
322
+
323
+ result_spec
324
+ end
325
+
326
+ # The name of the template.
327
+ def name
328
+ @spec['name']
329
+ end
330
+
331
+ # Replaces all references to this template with an instantiation of it in
332
+ # the given spec. Returns true if any changes were made, false otherwise.
333
+ #
334
+ # Recursive template uses will not be replaced by this function. If
335
+ # multiple replacements are needed, then you will need to call this function
336
+ # multiple times.
337
+ def replace_uses(spec)
338
+ case spec
339
+ when Hash
340
+ replace_uses_in_hash(spec)
341
+ when Array
342
+ replace_uses_in_array(spec)
343
+ else
344
+ false
345
+ end
346
+ end
347
+
348
+ # True if the given spec is a reference to this template.
349
+ def use?(spec)
350
+ return false unless spec.is_a?(Hash) && spec.key?(TEMPLATE_USE_KEYWORD)
351
+
352
+ invocation = spec[TEMPLATE_USE_KEYWORD]
353
+ case invocation
354
+ when String
355
+ invocation == name
356
+ when Hash
357
+ unless invocation.key?('name')
358
+ error_message = "invocations of #{TEMPLATE_USE_KEYWORD} must have a "\
359
+ 'name member'
360
+ raise InvalidTemplateUsage, error_message
361
+ end
362
+
363
+ invocation['name'] == name
364
+ else
365
+ error_message = "#{TEMPLATE_USE_KEYWORD} must either be a String or a "\
366
+ 'Hash'
367
+ raise InvalidTemplateUsage, error_message
368
+ end
369
+ end
370
+
371
+ private
372
+
373
+ # Replaces a single use of the template in a Hash object.
374
+ def merge_use_with_hash(use)
375
+ result = instantiate(use['use-template']['params'])
376
+
377
+ error_message = "template #{name} was invoked in a Hash with other"\
378
+ ' keys, but does not resolve to a hash itself'
379
+ raise InvalidTemplateUsage, error_message unless result.is_a?(Hash)
380
+
381
+ use.merge!(result) { |_, oldval, _| oldval }
382
+ use.delete(TEMPLATE_USE_KEYWORD)
383
+ end
384
+
385
+ # Replaces all references to this template with an instantiation of it in
386
+ # the given spec, assuming it is a hash. Returns true if any changes were
387
+ # made, false otherwise.
388
+ def replace_uses_in_hash(spec)
389
+ changes = []
390
+
391
+ if use?(spec)
392
+ merge_use_with_hash(spec) if use?(spec)
393
+ changes << true
394
+ end
395
+
396
+ spec.each_pair do |key, value|
397
+ if direct_use?(value)
398
+ spec[key] = instantiate(value[TEMPLATE_USE_KEYWORD]['params'])
399
+ changes << true
400
+ else
401
+ changes << replace_uses(value)
402
+ end
403
+ end
404
+
405
+ changes.any?
406
+ end
407
+
408
+ # Replaces all references to this template with an instantiation of it in
409
+ # the given spec, assuming it is an array. Returns true if any changes were
410
+ # made, false otherwise.
411
+ def replace_uses_in_array(spec)
412
+ changes = []
413
+
414
+ spec.dup.each_index do |i|
415
+ if direct_use?(spec[i])
416
+ result = instantiate(spec[i][TEMPLATE_USE_KEYWORD]['params'])
417
+ spec.delete_at(i)
418
+ if result.is_a?(Array)
419
+ spec.insert(i, *result)
420
+ else
421
+ spec.insert(i, result)
422
+ end
423
+ changes << true
424
+ elsif use?(spec[i])
425
+ merge_use_with_hash(spec[i])
426
+ changes << true
427
+ else
428
+ changes << replace_uses(spec[i])
429
+ end
430
+ end
431
+
432
+ changes.any?
433
+ end
434
+ end
435
+ end
@@ -0,0 +1,178 @@
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 description of a type used in a specification.
23
+ class TypeSpec
24
+ # Returns a normalized copy of the hash specification of a type in +spec+.
25
+ # See normalize_spec_hash! for details.
26
+ def self.normalize_spec_hash(spec)
27
+ normalize_spec_hash!(Marshal.load(Marshal.dump(spec)))
28
+ end
29
+
30
+ # Normalizes the hash specification of a type in +spec+ in place. This will
31
+ # normalize the include list.
32
+ def self.normalize_spec_hash!(spec)
33
+ spec['includes'] = Wrapture.normalize_includes(spec['includes'])
34
+ spec
35
+ end
36
+
37
+ # Creates a parameter specification based on the provided hash +spec+.
38
+ # +spec+ can be a string instead of a hash, in which case it will be used
39
+ # as the name of the type.
40
+ #
41
+ # Type specs must have a 'name' key with either the type itself (for example
42
+ # 'const char *') or a keyword specifying some other type (for example
43
+ # 'equivalent-struct'). The only exception is for function pointers, which
44
+ # instead use a 'function' key that contains a FunctionSpec specification.
45
+ # This specification does not need to be definable, it only needs to have
46
+ # a parameter list and return type for the signature to be clear.
47
+ def initialize(spec = 'void')
48
+ actual_spec = if spec.is_a?(String)
49
+ { 'name' => spec }
50
+ else
51
+ spec
52
+ end
53
+
54
+ @spec = TypeSpec.normalize_spec_hash(actual_spec)
55
+ end
56
+
57
+ # Compares this TypeSpec with +other+. Comparison happens by converting each
58
+ # object to a string using to_s and comparing.
59
+ #
60
+ # Added in release 0.4.2.
61
+ def ==(other)
62
+ to_s == other.to_s
63
+ end
64
+
65
+ # The name of this type with all special characters removed.
66
+ def base
67
+ name.delete('*&').strip
68
+ end
69
+
70
+ # An expression casting the result of a given expression into this type.
71
+ #
72
+ # Added in release 0.4.2.
73
+ def cast_expression(expression)
74
+ "( #{variable} )( #{expression} )"
75
+ end
76
+
77
+ # True if this type is an equivalent struct pointer reference.
78
+ def equivalent_pointer?
79
+ name == EQUIVALENT_POINTER_KEYWORD
80
+ end
81
+
82
+ # True if this type is an equivalent struct reference.
83
+ def equivalent_struct?
84
+ name == EQUIVALENT_STRUCT_KEYWORD
85
+ end
86
+
87
+ # True if this type is a function.
88
+ def function?
89
+ @spec.key?('function')
90
+ end
91
+
92
+ # A list of includes needed for this type.
93
+ def includes
94
+ includes = @spec['includes'].dup
95
+
96
+ if function?
97
+ func = FunctionSpec.new(@spec['function'])
98
+ includes.concat(func.declaration_includes)
99
+ end
100
+
101
+ includes.uniq
102
+ end
103
+
104
+ # The name of the type.
105
+ def name
106
+ @spec['name']
107
+ end
108
+
109
+ # True if this type is a pointer.
110
+ def pointer?
111
+ name.end_with?('*')
112
+ end
113
+
114
+ # Creates a new TypeSpec within the scope of +owner+ that will be directly
115
+ # usable. This will replace equivalent structs, pointers, and self
116
+ # references with a usable type name.
117
+ def resolve(owner)
118
+ owner.resolve_type(self)
119
+ end
120
+
121
+ # A string with a declaration of FunctionSpec +func+ with this type as the
122
+ # return value. +func_name+ can be provided to override the function name,
123
+ # for example if a class name needs to be included.
124
+ def return_expression(func, func_name: func.name)
125
+ name_part = String.new(func_name || '')
126
+ param_part = String.new
127
+ ret_part = name
128
+
129
+ current_spec = @spec
130
+ while current_spec.is_a?(Hash) && current_spec.key?('function')
131
+ name_part.prepend('( *')
132
+
133
+ current_func = FunctionSpec.new(current_spec['function'])
134
+ param_part.concat(" )( #{current_func.param_list} )")
135
+
136
+ current_spec = current_spec.dig('function', 'return', 'type')
137
+ ret_part = current_spec
138
+ end
139
+
140
+ ret_part << ' ' unless ret_part.end_with?('*')
141
+
142
+ "#{ret_part}#{name_part}( #{func.param_list} )#{param_part}"
143
+ end
144
+
145
+ # True if this type is a reference to a class instance.
146
+ def self?
147
+ name == SELF_REFERENCE_KEYWORD
148
+ end
149
+
150
+ # Gives a string representation of this type (its name).
151
+ #
152
+ # Added in release 0.4.2.
153
+ def to_s
154
+ name
155
+ end
156
+
157
+ # A string with a declaration of a variable named +var_name+ of this type.
158
+ # If +var_name+ is nil then this will simply be a type declaration.
159
+ def variable(var_name = nil)
160
+ if variadic?
161
+ '...'
162
+ elsif function?
163
+ func = FunctionSpec.new(@spec['function'])
164
+ func_name = "( *#{var_name} )" || '(*)'
165
+ func.return_expression(func_name: func_name)
166
+ elsif var_name.nil?
167
+ name
168
+ else
169
+ "#{name}#{' ' unless name.end_with?('*')}#{var_name}"
170
+ end
171
+ end
172
+
173
+ # True if this type is a variadic parameter type (name is equal to +...+).
174
+ def variadic?
175
+ name == '...'
176
+ end
177
+ end
178
+ end