xdry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +11 -0
  3. data/Rakefile +76 -0
  4. data/VERSION +1 -0
  5. data/bin/xdry +4 -0
  6. data/lib/xdry.rb +18 -0
  7. data/lib/xdry/boxing.rb +212 -0
  8. data/lib/xdry/generators/ctor_from_field.rb +91 -0
  9. data/lib/xdry/generators/dealloc.rb +53 -0
  10. data/lib/xdry/generators/dictionary_coding.rb +129 -0
  11. data/lib/xdry/generators/field_from_property.rb +20 -0
  12. data/lib/xdry/generators/property-from-field.rb +22 -0
  13. data/lib/xdry/generators/storing_constructor.rb +72 -0
  14. data/lib/xdry/generators/synthesize.rb +25 -0
  15. data/lib/xdry/generators_support.rb +42 -0
  16. data/lib/xdry/parsing/driver.rb +106 -0
  17. data/lib/xdry/parsing/model.rb +272 -0
  18. data/lib/xdry/parsing/nodes.rb +260 -0
  19. data/lib/xdry/parsing/parsers.rb +166 -0
  20. data/lib/xdry/parsing/parts/selectors.rb +95 -0
  21. data/lib/xdry/parsing/parts/var_types.rb +66 -0
  22. data/lib/xdry/parsing/pos.rb +75 -0
  23. data/lib/xdry/parsing/scope_stack.rb +68 -0
  24. data/lib/xdry/parsing/scopes.rb +61 -0
  25. data/lib/xdry/parsing/scopes_support.rb +143 -0
  26. data/lib/xdry/patching/emitter.rb +60 -0
  27. data/lib/xdry/patching/insertion_points.rb +209 -0
  28. data/lib/xdry/patching/item_patchers.rb +74 -0
  29. data/lib/xdry/patching/patcher.rb +139 -0
  30. data/lib/xdry/run.rb +227 -0
  31. data/lib/xdry/support/enumerable_additions.rb +35 -0
  32. data/lib/xdry/support/string_additions.rb +27 -0
  33. data/lib/xdry/support/symbol_additions.rb +14 -0
  34. data/site/_config.yml +3 -0
  35. data/site/_example +9 -0
  36. data/site/_layouts/default.html +30 -0
  37. data/site/_plugins/example.rb +16 -0
  38. data/site/_plugins/highlight_unindent.rb +17 -0
  39. data/site/index.md +417 -0
  40. data/site/master.css +94 -0
  41. data/spec/boxing_spec.rb +80 -0
  42. data/spec/ctor_from_field_spec.rb +251 -0
  43. data/spec/dealloc_spec.rb +103 -0
  44. data/spec/dictionary_coding_spec.rb +132 -0
  45. data/spec/field_from_prop_spec.rb +72 -0
  46. data/spec/prop_from_field_spec.rb +39 -0
  47. data/spec/readme_samples_spec.rb +76 -0
  48. data/spec/spec.opts +3 -0
  49. data/spec/spec_helper.rb +53 -0
  50. data/spec/synthesize_spec.rb +94 -0
  51. data/xdry.gemspec +103 -0
  52. metadata +141 -0
@@ -0,0 +1,129 @@
1
+
2
+ module XDry
3
+ module Generators
4
+
5
+ class DictionaryCoding < Generator
6
+ id "dict-coding"
7
+
8
+ def process_class oclass
9
+ return unless oclass.attributes.any? { |a| a.persistent? }
10
+
11
+ dictionary_var = "dictionary"
12
+
13
+ defines_emitter = Emitter.new
14
+ init_out = Emitter.new
15
+ repr_out = Emitter.new
16
+
17
+ dictionary_var = "dictionary"
18
+
19
+ oclass.attributes.select { |a| a.persistent? }.each do |oattr|
20
+ name, type = oattr.name, oattr.type
21
+ field_name = oattr.field_name
22
+ raw_name = "#{name}Raw"
23
+ capitalized_name = name.capitalized_identifier
24
+ key_const = "#{capitalized_name}Key"
25
+
26
+ type_boxer = Boxing.converter_for type
27
+ if type_boxer.nil?
28
+ puts "Persistence not (yet) supported for type #{type}"
29
+ next
30
+ end
31
+
32
+ defines_emitter << %Q`\#define #{key_const} @"#{capitalized_name}"`
33
+
34
+ init_out << %Q`id #{raw_name} = [#{dictionary_var} objectForKey:#{key_const}];`
35
+ init_out.if "#{raw_name} != nil" do
36
+ unboxed = type_boxer.unbox_retained(init_out, raw_name, name)
37
+ init_out << "#{field_name} = #{unboxed};"
38
+ end
39
+
40
+ boxed = type_boxer.box(repr_out, field_name, name)
41
+ repr_out << %Q`[dictionary setObject:#{boxed} forKey:#{key_const}];`
42
+ end
43
+
44
+ init_code = Emitter.capture do |o|
45
+ o.method "(id)initWithDictionary:(NSDictionary *)#{dictionary_var}" do
46
+ o.if "self = [super init]" do
47
+ end
48
+ o << "return self;"
49
+ end
50
+ end
51
+
52
+ repr_code = Emitter.capture do |o|
53
+ o.method "(NSDictionary *)dictionaryRepresentation" do
54
+ o << "NSMutableDictionary *#{dictionary_var} = [NSMutableDictionary dictionary];"
55
+ o << "return #{dictionary_var};"
56
+ end
57
+ end
58
+
59
+ file_scope = oclass.main_implementation.parent_scope
60
+ define_ip = MultiIP.new(AfterDefineIP.new(file_scope), BeforeImplementationStartIP.new(oclass))
61
+ define_lines = Emitter.capture do |o|
62
+ each_persistent_attr(oclass) do |oattr, capitalized_name, key_const, type_boxer|
63
+ unless file_scope.children.any? { |n| NDefine === n && n.word == key_const }
64
+ o << %Q`\#define #{key_const} @"#{capitalized_name}"`
65
+ end
66
+ end
67
+ end
68
+ define_ip.wrap_with_empty_lines_if_last!
69
+ define_ip.insert @patcher, define_lines
70
+
71
+ MethodPatcher.new(patcher, oclass, 'dictionaryRepresentation', ImplementationStartIP.new(oclass), repr_code) do |omethod|
72
+ impl = omethod.impl
73
+ ip = BeforeReturnIP.new(impl)
74
+
75
+ lines = Emitter.capture do |o|
76
+ each_persistent_attr(oclass) do |oattr, capitalized_name, key_const, type_boxer|
77
+ unless impl.children.any? { |n| NLine === n && n.line =~ /\bsetObject:.*(?:\b#{oattr.name}|\b#{oattr.field_name}\b)/ }
78
+ boxed = type_boxer.box(o, oattr.field_name, oattr.name)
79
+ o << %Q`[dictionary setObject:#{boxed} forKey:#{key_const}];`
80
+ end
81
+ end
82
+ end
83
+
84
+ ip.insert @patcher, lines unless lines.empty?
85
+ end
86
+
87
+ MethodPatcher.new(patcher, oclass, 'initWithDictionary:', ImplementationStartIP.new(oclass), init_code) do |omethod|
88
+ impl = omethod.impl
89
+ ip = InsideConstructorIfSuperIP.new(impl)
90
+ var_name = impl.start_node.selector_def.var_name_after_keyword('initWithDictionary:')
91
+
92
+ lines = Emitter.capture do |o|
93
+ each_persistent_attr(oclass) do |oattr, capitalized_name, key_const, type_boxer|
94
+ unless impl.children.any? { |n| NLine === n && n.line =~ /^(?:self\s*.\s*#{oattr.name}|#{oattr.field_name})\s*=/ }
95
+ raw_name = "#{oattr.name}Raw"
96
+ o << %Q`id #{raw_name} = [#{var_name} objectForKey:#{key_const}];`
97
+ o.if "#{raw_name} != nil" do
98
+ unboxed = type_boxer.unbox_retained(o, raw_name, oattr.name)
99
+ o << "#{oattr.field_name} = #{unboxed};"
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ ip.insert @patcher, lines unless lines.empty?
106
+ end
107
+ end
108
+
109
+ def each_persistent_attr oclass
110
+ oclass.attributes.select { |a| a.persistent? }.each do |oattr|
111
+ name, type = oattr.name, oattr.type
112
+ field_name = oattr.field_name
113
+ capitalized_name = name.capitalized_identifier
114
+ key_const = "#{capitalized_name}Key"
115
+
116
+ type_boxer = Boxing.converter_for type
117
+ if type_boxer.nil?
118
+ puts "Persistence not (yet) supported for type #{type}"
119
+ next
120
+ end
121
+
122
+ yield oattr, capitalized_name, key_const, type_boxer
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,20 @@
1
+
2
+ module XDry
3
+ module Generators
4
+
5
+ class FieldFromProperty < Generator
6
+ id "field-from-prop"
7
+
8
+ def process_attribute oclass, oattr
9
+ if !oattr.has_field_def? && oattr.type_known? && !oclass.has_method_impl?(oattr.getter_selector)
10
+ lines = [oattr.new_field_def.to_source]
11
+ if oclass.main_interface # && oclass.main_interface.fields_scope
12
+ node = oclass.main_interface.fields_scope.end_node
13
+ patcher.insert_before node.pos, lines, node.indent + INDENT_STEP
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+
2
+ module XDry
3
+ module Generators
4
+
5
+ class PropertyFromField < Generator
6
+ id "prop-from-field"
7
+
8
+ def process_attribute oclass, oattr
9
+ if !oattr.has_property_def? && oattr.wants_property? && oattr.type_known?
10
+ pd = oattr.new_property_def
11
+
12
+ ip = BeforeInterfaceEndIP.new(oclass)
13
+ lines = Emitter.capture do |o|
14
+ o << pd.to_source
15
+ end
16
+ ip.insert patcher, [""] + lines + [""]
17
+ end
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,72 @@
1
+
2
+ module XDry
3
+ module Generators
4
+
5
+ class StoringConstructor < Generator
6
+ id "store-ctor"
7
+
8
+ def process_class oclass
9
+ oclass.methods.each do |omethod|
10
+ case omethod.selector
11
+ when /^initWith/
12
+ process_method oclass, omethod
13
+ end
14
+ end
15
+ end
16
+
17
+ def process_method oclass, omethod
18
+ components = omethod.header_or_impl.selector_def.components
19
+ mapping = {}
20
+ components.each do |comp|
21
+ kw = comp.keyword_without_colon
22
+ oattr = oclass.find_attribute(kw)
23
+ if oattr.nil? and comp == components.first
24
+ stripped_kw = kw.gsub(/^initWith/, '')
25
+ [stripped_kw.downcase, stripped_kw[0..0].downcase + stripped_kw[1..-1]].each do |alt_kw|
26
+ oattr = oclass.find_attribute(alt_kw)
27
+ break unless oattr.nil?
28
+ end
29
+ end
30
+ mapping[kw] = oattr unless oattr.nil?
31
+ end
32
+
33
+ unless mapping.empty?
34
+ new_selector_def = CompoundSelectorDef.new(components.collect do |comp|
35
+ if oattr = mapping[kw = comp.keyword_without_colon]
36
+ arg_name = comp.arg_name
37
+ arg_name = oattr.name if arg_name.empty?
38
+ SelectorComponent.new(comp.keyword, arg_name, comp.type || oattr.type)
39
+ else
40
+ comp
41
+ end
42
+ end)
43
+ method_header = NMethodHeader.new(new_selector_def, omethod.ret_type)
44
+
45
+ init_out = Emitter.new
46
+
47
+ new_selector_def.components.each do |comp|
48
+ if oattr = mapping[kw = comp.keyword_without_colon]
49
+ field_name = oattr.field_name
50
+ arg_name = comp.arg_name
51
+ type = comp.type
52
+ retain_policy = Boxing.retain_policy_of(type)
53
+
54
+ retained = retain_policy.retain(arg_name)
55
+ init_out << "#{field_name} = #{retained};"
56
+ end
57
+ end
58
+
59
+ # out << "#{method_header};"
60
+ # out.block "#{method_header}" do
61
+ # out.if "self = [super init]" do
62
+ # out << init_out
63
+ # end
64
+ # out << "return self;"
65
+ # end
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,25 @@
1
+
2
+ module XDry
3
+ module Generators
4
+
5
+ class Synthesize < Generator
6
+ id "synth"
7
+
8
+ def process_attribute oclass, oattr
9
+ if oattr.has_property_def? && !oattr.has_synthesize? && !oclass.has_method_impl?(oattr.getter_selector)
10
+ synthesize = oattr.new_synthesize
11
+ impl = oclass.main_implementation
12
+ new_lines = [synthesize.to_s]
13
+ if impl.synthesizes.empty?
14
+ pos = impl.start_node.pos
15
+ new_lines = [""] + new_lines + [""]
16
+ else
17
+ pos = impl.synthesizes.sort { |a, b| a.pos.line_no <=> b.pos.line_no }.last.pos
18
+ end
19
+ patcher.insert_after pos, new_lines
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+
2
+ module XDry::Generators
3
+
4
+ ALL = []
5
+
6
+ Emitter = XDry::Emitter
7
+ Boxing = XDry::Boxing
8
+
9
+ class Generator
10
+
11
+ attr_reader :patcher
12
+
13
+ def initialize config, patcher
14
+ @config = config
15
+ @patcher = patcher
16
+ end
17
+
18
+ def verbose?
19
+ @config.verbose
20
+ end
21
+
22
+ def process_class oclass
23
+ oclass.attributes.each do |oattr|
24
+ process_attribute oclass, oattr
25
+ end
26
+ end
27
+
28
+ def process_attribute oclass, oattr
29
+ end
30
+
31
+ def self.id(value=nil)
32
+ @id = value if value
33
+ @id
34
+ end
35
+
36
+ def self.inherited subclass
37
+ ALL << subclass
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,106 @@
1
+ require 'generator'
2
+ require 'stringio'
3
+
4
+ module XDry
5
+
6
+ class ParsingDriver
7
+ attr_accessor :verbose
8
+ attr_reader :markers
9
+
10
+ def initialize oglobal
11
+ @oglobal = oglobal
12
+ @verbose = false
13
+ @markers = []
14
+ end
15
+
16
+ def parse_file file_name
17
+ gen = Generator.new { |g|
18
+ file_ref = FileRef.new(file_name)
19
+ File.open(file_name) do |file|
20
+ new_lines_generator g, file_ref, file
21
+ end
22
+ }
23
+ parse_data_in_file_scope gen
24
+ end
25
+
26
+ def parse_string file_name, source
27
+ gen = Generator.new { |g|
28
+ file_ref = TestFileRef.new(file_name, source)
29
+ new_lines_generator g, file_ref, StringIO.new(source)
30
+ }
31
+ parse_data_in_file_scope gen
32
+ end
33
+
34
+ def parse_fragment file_ref, lines, start_lineno, start_scope
35
+ gen = Generator.new { |g|
36
+ new_lines_from_array_generator g, file_ref, start_lineno, lines
37
+ }
38
+ parse_data_in_scopes gen, start_scope.all_scopes
39
+ end
40
+
41
+ private
42
+
43
+ def parse_data_in_file_scope gen
44
+ parse_data_in_scopes gen, [@oglobal.new_file_scope]
45
+ end
46
+
47
+ def parse_data_in_scopes gen, scopes
48
+ scope_stack = ScopeStack.new(scopes)
49
+ scope_stack.verbose = @verbose
50
+ while gen.next?
51
+ orig_line, line, pos, eol_comments, indent = gen.next
52
+ puts " #{pos} #{orig_line}" if @verbose
53
+ pos.scope_before = scope_stack.current_scope
54
+ scope_stack.parse_line line, eol_comments, indent do |scope, child|
55
+ # child is a Node or a Scope
56
+ if child.is_a? Node
57
+ child.pos = pos
58
+ child.indent = indent
59
+ end
60
+ if child.is_a? NMarker
61
+ @markers << child
62
+ end
63
+
64
+ puts "#{scope} << #{child}" if @verbose
65
+ scope << child
66
+ end
67
+ pos.scope_after = scope_stack.current_scope
68
+ end
69
+ end
70
+
71
+ def new_lines_generator g, file_ref, io
72
+ line_no = 0
73
+ io.each_line do |line|
74
+ line_no += 1
75
+ orig_line, line, eol_comments, indent = split_line(line)
76
+ g.yield [orig_line, line, Pos.new(file_ref, line_no), eol_comments, indent]
77
+ end
78
+ end
79
+
80
+ def new_lines_from_array_generator g, file_ref, start_lineno, lines
81
+ line_no = start_lineno - 1
82
+ lines.each do |line|
83
+ line_no += 1
84
+ orig_line, line, eol_comments, indent = split_line(line.dup)
85
+ g.yield [orig_line, line, Pos.new(file_ref, line_no), eol_comments, indent]
86
+ end
87
+ end
88
+
89
+ def split_line line
90
+ orig_line = line.dup
91
+ line.strip!
92
+
93
+ # strip end-of-line comments, but keep comment lines
94
+ eol_comments = ''
95
+ line_without_comments = line.sub(%r`//.*$`) { eol_comments = $&; '' }
96
+ unless line_without_comments.empty?
97
+ line = line_without_comments
98
+ end
99
+
100
+ indent = if orig_line =~ /^(\s+)/ then $1 else '' end
101
+ return [orig_line, line, eol_comments, indent]
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,272 @@
1
+
2
+ module XDry
3
+
4
+ class OGlobal
5
+
6
+ def initialize
7
+ @names_to_classes = {}
8
+ end
9
+
10
+ def classes
11
+ @names_to_classes.values
12
+ end
13
+
14
+ def new_file_scope
15
+ SFile.new.bind(self)
16
+ end
17
+
18
+ def << child
19
+ case child
20
+ when SInterface
21
+ lookup_class(child.class_name).add_interface child
22
+ when SImplementation
23
+ lookup_class(child.class_name).add_implementation child
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def lookup_class name
30
+ @names_to_classes[name] ||= OClass.new(self, name)
31
+ end
32
+ end
33
+
34
+ class OClass
35
+ attr_reader :oglobal
36
+ attr_reader :name, :field_defs, :attributes, :methods
37
+ attr_reader :interfaces
38
+ attr_reader :implementations
39
+
40
+ def initialize oglobal, name
41
+ @oglobal, @name = oglobal, name
42
+
43
+ @names_to_attributes = {}
44
+ @attributes = []
45
+
46
+ @selectors_to_methods = {}
47
+ @methods = []
48
+
49
+ @field_defs = []
50
+ @property_defs = []
51
+ @interfaces = []
52
+ @implementations = []
53
+ end
54
+
55
+ def add_interface child
56
+ @interfaces << child.bind(self)
57
+ end
58
+
59
+ def add_implementation child
60
+ @implementations << child.bind(self)
61
+ end
62
+
63
+ def main_interface
64
+ # FIXME
65
+ @interfaces.first
66
+ end
67
+
68
+ def main_implementation
69
+ # FIXME
70
+ @implementations.first
71
+ end
72
+
73
+ def << node
74
+ case node
75
+ when NFieldDef
76
+ @field_defs << node
77
+ attr_name = node.name.gsub /^_/, ''
78
+ lookup_attribute(attr_name).add_field_def! node
79
+ when NPropertyDef
80
+ @property_defs << node
81
+ attr_name = node.name
82
+ lookup_attribute(attr_name).add_property_def! node
83
+ when NMethodHeader
84
+ selector = node.selector
85
+ lookup_method(selector).add_method_header! node
86
+ when NSynthesize
87
+ node.items.each do |item|
88
+ lookup_attribute(item.property_name).add_synthesize! node
89
+ end
90
+ when SInterfaceFields
91
+ node.bind(self)
92
+ when SMethodImpl
93
+ lookup_method(node.selector).add_method_impl! node
94
+ end
95
+ end
96
+
97
+ def to_s
98
+ "class #{name}"
99
+ end
100
+
101
+ def find_attribute name
102
+ @names_to_attributes[name]
103
+ end
104
+
105
+ def find_method selector
106
+ @selectors_to_methods[selector]
107
+ end
108
+
109
+ def has_method_impl? selector
110
+ m = find_method(selector)
111
+ return m && m.has_impl?
112
+ end
113
+
114
+ private
115
+
116
+ def lookup_attribute name
117
+ @names_to_attributes[name] ||= create_attribute(name)
118
+ end
119
+
120
+ def lookup_method selector
121
+ @selectors_to_methods[selector] ||= create_method(selector)
122
+ end
123
+
124
+ def create_attribute name
125
+ a = OAttribute.new(self, name)
126
+ @attributes << a
127
+ return a
128
+ end
129
+
130
+ def create_method selector
131
+ a = OMethod.new(selector)
132
+ @methods << a
133
+ return a
134
+ end
135
+
136
+ end
137
+
138
+ class OAttribute
139
+ attr_reader :name
140
+ attr_reader :field_def
141
+
142
+ def initialize oclass, name
143
+ @oclass, @name = oclass, name
144
+
145
+ @field_def = nil
146
+ @property_def = nil
147
+ @synthesizes = []
148
+ end
149
+
150
+ def field_name
151
+ if @field_def
152
+ @field_def.name
153
+ else
154
+ FIELD_PREFIX + name
155
+ end
156
+ end
157
+
158
+ def add_field_def! field_def
159
+ @field_def = field_def
160
+ end
161
+
162
+ def add_property_def! property_def
163
+ @property_def = property_def
164
+ end
165
+
166
+ def add_synthesize! synthesize
167
+ @synthesizes << synthesize
168
+ end
169
+
170
+ def has_field_def?
171
+ not @field_def.nil?
172
+ end
173
+
174
+ def has_property_def?
175
+ not @property_def.nil?
176
+ end
177
+
178
+ def has_synthesize?
179
+ not @synthesizes.empty?
180
+ end
181
+
182
+ def new_property_def
183
+ NPropertyDef.new(name, type)
184
+ end
185
+
186
+ def new_field_def
187
+ NFieldDef.new(field_name, type)
188
+ end
189
+
190
+ def new_synthesize
191
+ NSynthesize.new([SynthesizeItem.new(name, (field_name == name ? nil : field_name) )])
192
+ end
193
+
194
+ def persistent?
195
+ @field_def && @field_def.persistent?
196
+ end
197
+
198
+ def type_known?
199
+ not type.nil?
200
+ end
201
+
202
+ def wants_property?
203
+ has_field_def? && field_def.wants_property?
204
+ end
205
+
206
+ def wants_constructor?
207
+ has_field_def? && field_def.wants_constructor?
208
+ end
209
+
210
+ def getter_selector
211
+ # FIXME: need to account for a possible selector override declared in @property
212
+ @name
213
+ end
214
+
215
+ def type
216
+ if @property_def
217
+ @property_def.type
218
+ elsif @field_def
219
+ @field_def.type
220
+ else
221
+ nil
222
+ end
223
+ end
224
+
225
+ def to_s
226
+ traits = []
227
+ traits << field_name if has_field_def?
228
+ "#{type} #{name}" + (traits.empty? ? "" : " (" + traits.join(", ") + ")")
229
+ end
230
+ end
231
+
232
+ class OMethod
233
+ attr_reader :selector, :header, :impl
234
+
235
+ def initialize selector
236
+ @selector = selector
237
+ end
238
+
239
+ def add_method_header! method_header
240
+ @header = method_header
241
+ end
242
+
243
+ def add_method_impl! method_impl
244
+ @impl = method_impl.bind(self)
245
+ end
246
+
247
+ def has_header?
248
+ not @header.nil?
249
+ end
250
+
251
+ def has_impl?
252
+ not @impl.nil?
253
+ end
254
+
255
+ def ret_type
256
+ header_or_impl.ret_type
257
+ end
258
+
259
+ def << child
260
+ end
261
+
262
+ def to_s
263
+ header_or_impl.to_s
264
+ end
265
+
266
+ def header_or_impl
267
+ @header || @impl.start_node
268
+ end
269
+
270
+ end
271
+
272
+ end