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.
- checksums.yaml +4 -4
- data/bin/wrapture +26 -2
- data/lib/wrapture.rb +6 -1
- data/lib/wrapture/action_spec.rb +2 -3
- data/lib/wrapture/class_spec.rb +101 -53
- data/lib/wrapture/comment.rb +106 -0
- data/lib/wrapture/constant_spec.rb +29 -4
- data/lib/wrapture/constants.rb +9 -1
- data/lib/wrapture/enum_spec.rb +154 -0
- data/lib/wrapture/errors.rb +27 -1
- data/lib/wrapture/function_spec.rb +198 -94
- data/lib/wrapture/normalize.rb +2 -2
- data/lib/wrapture/param_spec.rb +132 -0
- data/lib/wrapture/rule_spec.rb +14 -9
- data/lib/wrapture/scope.rb +46 -14
- data/lib/wrapture/struct_spec.rb +5 -3
- data/lib/wrapture/template_spec.rb +432 -0
- data/lib/wrapture/type_spec.rb +156 -0
- data/lib/wrapture/version.rb +19 -1
- data/lib/wrapture/wrapped_function_spec.rb +2 -0
- metadata +9 -3
data/lib/wrapture/normalize.rb
CHANGED
@@ -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
|
data/lib/wrapture/rule_spec.rb
CHANGED
@@ -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
|
28
|
-
CONDITIONS =
|
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
|
68
|
-
#
|
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']
|
81
|
+
condition = RuleSpec::CONDITIONS[@spec['condition']]
|
77
82
|
|
78
83
|
if @spec['type'] == 'struct-member'
|
79
84
|
"#{variable}->#{@spec['member-name']} #{condition} #{@spec['value']}"
|
data/lib/wrapture/scope.rb
CHANGED
@@ -2,7 +2,8 @@
|
|
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");
|
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?
|
39
|
+
return if spec.nil?
|
30
40
|
|
31
41
|
@version = Wrapture.spec_version(spec)
|
32
|
-
|
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
|
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
|
107
|
+
# Returns true if there is a class matching the given +type+ in this scope.
|
72
108
|
def type?(type)
|
73
|
-
@classes.
|
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
|
data/lib/wrapture/struct_spec.rb
CHANGED
@@ -2,7 +2,8 @@
|
|
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");
|
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 <<
|
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 =
|
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
|