interfacets 0.1.0 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +173 -1
  3. data/LICENSE +21 -0
  4. data/Rakefile +4 -6
  5. data/lib/interfacets/client/actor.rb +20 -0
  6. data/lib/interfacets/client/assets.rb +209 -0
  7. data/lib/interfacets/client/bus.rb +69 -0
  8. data/lib/interfacets/client/channels/api.rb +95 -0
  9. data/lib/interfacets/client/channels/audio.rb +101 -0
  10. data/lib/interfacets/client/channels/base.rb +28 -0
  11. data/lib/interfacets/client/channels/page_visibility.rb +21 -0
  12. data/lib/interfacets/client/channels/react/builder.rb +203 -0
  13. data/lib/interfacets/client/channels/react/channel.rb +61 -0
  14. data/lib/interfacets/client/channels/react/dom.rb +91 -0
  15. data/lib/interfacets/client/channels/react/evaluator.rb +33 -0
  16. data/lib/interfacets/client/channels/speech_to_text.rb +100 -0
  17. data/lib/interfacets/client/channels/timer.rb +51 -0
  18. data/lib/interfacets/client/channels/url.rb +52 -0
  19. data/lib/interfacets/client/config.rb +22 -0
  20. data/lib/interfacets/client/delegator.rb +37 -0
  21. data/lib/interfacets/client/facet.rb +26 -0
  22. data/lib/interfacets/client/facet2.rb +15 -0
  23. data/lib/interfacets/client/facets/attributes/accessor.rb +28 -0
  24. data/lib/interfacets/client/facets/attributes/association.rb +50 -0
  25. data/lib/interfacets/client/facets/attributes/bind.rb +25 -0
  26. data/lib/interfacets/client/facets/attributes/collection.rb +47 -0
  27. data/lib/interfacets/client/facets/attributes/readonly.rb +19 -0
  28. data/lib/interfacets/client/facets/deserializer.rb +30 -0
  29. data/lib/interfacets/client/facets/schema/deserializer.rb +63 -0
  30. data/lib/interfacets/client/facets/schema.rb +63 -0
  31. data/lib/interfacets/client/facets/serializer.rb +18 -0
  32. data/lib/interfacets/client/registry.rb +84 -0
  33. data/lib/interfacets/client/system.rb +88 -0
  34. data/lib/interfacets/client/utils/active_support_concern.rb +220 -0
  35. data/lib/interfacets/client/utils/mruby_patches.rb +81 -0
  36. data/lib/interfacets/client/utils/open_struct.rb +102 -0
  37. data/lib/interfacets/client/utils/securerandom.rb +69 -0
  38. data/lib/interfacets/client/view.rb +47 -0
  39. data/lib/interfacets/client.rb +13 -0
  40. data/lib/interfacets/mruby/build.dockerfile +66 -0
  41. data/lib/interfacets/mruby/build_config.rb +20 -0
  42. data/lib/interfacets/mruby/entrypoint.rb +23 -0
  43. data/lib/interfacets/mruby/init.c +66 -0
  44. data/lib/interfacets/server/api.rb +64 -0
  45. data/lib/interfacets/server/assets/facet.rb +61 -0
  46. data/lib/interfacets/server/assets.rb +210 -0
  47. data/lib/interfacets/server/basic_routable.rb +40 -0
  48. data/lib/interfacets/server/basic_router.rb +74 -0
  49. data/lib/interfacets/server/bus.rb +39 -0
  50. data/lib/interfacets/server/config.rb +87 -0
  51. data/lib/interfacets/server/facet.rb +51 -0
  52. data/lib/interfacets/server/facets/deserializer.rb +25 -0
  53. data/lib/interfacets/server/facets/schema/serializer.rb +54 -0
  54. data/lib/interfacets/server/facets/serializer.rb +50 -0
  55. data/lib/interfacets/server/registry.rb +212 -0
  56. data/lib/interfacets/shared/entities/bus.rb +218 -0
  57. data/lib/interfacets/shared/entities/collection_proxy.rb +190 -0
  58. data/lib/interfacets/shared/entities/specs/handlers.rb +117 -0
  59. data/lib/interfacets/shared/entities/specs.rb +124 -0
  60. data/lib/interfacets/shared/entity.rb +178 -0
  61. data/lib/interfacets/shared/entity_collection.rb +88 -0
  62. data/lib/interfacets/shared/generated_store.rb +145 -0
  63. data/lib/interfacets/shared/utils.rb +54 -0
  64. data/lib/interfacets/shared/validations.rb +71 -0
  65. data/lib/interfacets/test/browser.rb +63 -0
  66. data/lib/interfacets/test/js/inline_bus.rb +91 -0
  67. data/lib/interfacets/test/js/nodo_bus.rb +81 -0
  68. data/lib/interfacets/test/js/receivers/api.rb +37 -0
  69. data/lib/interfacets/test/js/receivers/react/node.rb +167 -0
  70. data/lib/interfacets/test/js/receivers/react.rb +31 -0
  71. data/lib/interfacets/test/js/receivers/url.rb +55 -0
  72. data/lib/interfacets/test.rb +17 -0
  73. data/lib/interfacets/version.rb +1 -1
  74. data/lib/interfacets.rb +26 -2
  75. metadata +103 -6
  76. data/README.md +0 -35
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Server
5
+ module Facets
6
+ class Serializer
7
+ def self.call(...)
8
+ new(...).call
9
+ end
10
+
11
+ attr_reader :facet, :mount_point
12
+ def initialize(facet, mount_point)
13
+ @facet = facet
14
+ @mount_point = mount_point
15
+ end
16
+
17
+ def call
18
+ {
19
+ id: facet.entity.id,
20
+ facet_class: facet.class.name,
21
+ attributes: facet.entity.serialize,
22
+ }
23
+ end
24
+
25
+ def attributes(entity)
26
+ entity
27
+ .attribute_specs
28
+ .each_with_object({}) { |(name, _attribute), json|
29
+ # refactor this, shouldn't be attributes...
30
+ # next if attribute.is_a?(Facets::Attributes::ClientEvent)
31
+ # next if attribute.is_a?(Facets::Attributes::ServerEvent)
32
+ # next if attribute.is_a?(Facets::Attributes::Inherit)
33
+
34
+ value = entity.public_send(name)
35
+
36
+ json[name] = (
37
+ if value.is_a?(Array)
38
+ value.map { _1.is_a?(Entity) || _1.is_a?(Facet::Handler) ? attributes(_1) : _1 }
39
+ elsif value.is_a?(Entity) || value.is_a?(Facet::Handler)
40
+ attributes(value)
41
+ else
42
+ value
43
+ end
44
+ )
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+
5
+ module Interfacets
6
+ module Server
7
+ class Registry
8
+ attr_reader :build_dir, :specs
9
+ def initialize(facets:, build_dir:)
10
+ @build_dir = build_dir
11
+ @specs = Array(facets)
12
+ end
13
+
14
+ def register
15
+ specs
16
+ .flat_map { _1.respond_to?(:call) ? _1.call : _1 }
17
+ .each do |klass_or_name|
18
+
19
+ klass = (
20
+ if klass_or_name.is_a?(String)
21
+ if registry.key?(klass_or_name)
22
+ next
23
+ else
24
+ Object.const_get(klass_or_name)
25
+ end
26
+ else
27
+ klass_or_name
28
+ end
29
+ )
30
+
31
+ next if registry.key?(klass.name)
32
+
33
+ shared = Class.new(Shared::Entity) do
34
+ klass.shareds.each { class_exec(&_1) }
35
+ end
36
+
37
+ entity = Class.new(Shared::Entity) do
38
+ self.manifest = shared
39
+ role("server")
40
+
41
+ klass.shareds.each { class_exec(&_1) }
42
+ klass.servers.each { class_exec(&_1) }
43
+
44
+ attr_writer :channel
45
+ def channel
46
+ @channel || parent.channel
47
+ end
48
+ end
49
+
50
+ registry[klass.name] = { klass:, entity:}
51
+ end
52
+ end
53
+
54
+ def build(name, store)
55
+ register
56
+ entry = registry.fetch(name.is_a?(Class) ? name.name : name)
57
+
58
+
59
+
60
+ Api.new(
61
+ registry: self,
62
+ name:,
63
+ entity: (
64
+ entry
65
+ .fetch(:entity)
66
+ .new(
67
+ store: store.is_a?(Hash) ? OpenStruct.new(store) : store,
68
+ nesting: ["root"],
69
+ parent: nil
70
+ )
71
+ )
72
+ )
73
+ end
74
+
75
+ def serialize
76
+ register
77
+ FileUtils.mkdir_p(build_dir)
78
+
79
+ registry.each do |name, entry|
80
+ base_name = "#{name}::Client"
81
+
82
+ header = ""
83
+ footer = ""
84
+ nesting = []
85
+ name.split("::").each do |mod|
86
+ nesting << mod
87
+ footer += "\nend"
88
+ header +=(
89
+ if nesting.join("::").constantize.is_a?(Class)
90
+ "\nclass #{mod}"
91
+ else
92
+ "\nmodule #{mod}"
93
+ end
94
+ )
95
+ end
96
+ header += "\nmodule Client"
97
+ footer += "\nend"
98
+
99
+ File.write(
100
+ File.join(build_dir, path_for("#{base_name}::Shared")),
101
+ <<~TXT
102
+ #{header}
103
+ class Shared < Interfacets::Shared::Entity
104
+ #{
105
+ entry
106
+ .fetch(:klass)
107
+ .shareds
108
+ .map { write_source(_1) }
109
+ .map { "class_exec(&#{_1})" }
110
+ .join("\n")
111
+ }
112
+ end
113
+ #{footer}
114
+ TXT
115
+ )
116
+
117
+ File.write(
118
+ File.join(build_dir, path_for("#{base_name}::View")),
119
+ <<~TXT
120
+ #{header}
121
+ class View < Interfacets::Client::View
122
+ #{
123
+ entry
124
+ .fetch(:klass)
125
+ .views
126
+ .map { write_source(_1) }
127
+ .map { "view(&#{_1})"}
128
+ .join("\n")
129
+ }
130
+ end
131
+ #{footer}
132
+ TXT
133
+ )
134
+
135
+ File.write(
136
+ File.join(build_dir, path_for("#{base_name}::Entity")),
137
+ <<~TXT
138
+ #{header}
139
+ class Entity < Interfacets::Shared::Entity
140
+ class << self
141
+ def view
142
+ #{base_name}::View
143
+ end
144
+
145
+ def store
146
+ @store ||= (
147
+ ::Interfacets::Shared::GeneratedStore.construct(self)
148
+ .tap { self.const_set("Store", _1) }
149
+ )
150
+ end
151
+ end
152
+
153
+ role("client")
154
+
155
+ #{
156
+ entry
157
+ .fetch(:klass)
158
+ .shareds
159
+ .map { write_source(_1) }
160
+ .map { "class_exec(&#{_1})" }
161
+ .join("\n")
162
+ }
163
+
164
+ #{
165
+ entry
166
+ .fetch(:klass)
167
+ .clients
168
+ .map { write_source(_1) }
169
+ .map { "class_exec(&#{_1})" }
170
+ .join("\n")
171
+ }
172
+
173
+ self.manifest = Shared
174
+
175
+ actions.each do |name, spec|
176
+ if name.start_with?("after_")
177
+ define_method(name) {}
178
+ end
179
+ end
180
+
181
+ def channel(name)
182
+ Interfacets::Client::System.current_bus.channel(name).builder
183
+ end
184
+ end
185
+ #{footer}
186
+ TXT
187
+ )
188
+ end
189
+ end
190
+
191
+ def registry
192
+ @registry ||= {}
193
+ end
194
+
195
+ private
196
+
197
+ def path_for(class_name)
198
+ "#{class_name.gsub("::", "-")}.rb"
199
+ end
200
+
201
+ def write_source(block)
202
+ return unless block
203
+
204
+ RubyVM::AbstractSyntaxTree
205
+ .of(block, keep_script_lines: true)
206
+ .source
207
+ .then { block.is_a?(UnboundMethod) ? _1 : "lambda #{_1}" }
208
+ .strip
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Shared
5
+ module Entities
6
+ class Bus
7
+ InvalidAction = Class.new(StandardError)
8
+ InvalidReceiver = Class.new(StandardError)
9
+
10
+ # event schema:
11
+ # {
12
+ # from: (role),
13
+ # to: (role),
14
+ # action:,
15
+ # payload: {
16
+
17
+ # }
18
+ # }
19
+
20
+ attr_reader :manifest, :entity
21
+ def initialize(entity:)
22
+ @manifest = entity.class.manifest
23
+ @entity = entity
24
+ end
25
+
26
+ def serialize(to:, action:, nesting:)
27
+ assert_valid_action(action:, to:, nesting:)
28
+
29
+ {
30
+ id: SecureRandom.uuid,
31
+ from: entity.role,
32
+ to: to,
33
+ nesting:,
34
+ action:,
35
+ payload: {
36
+ attributes: serialize_attributes(manifest:, entity:, to:)
37
+ }
38
+ }
39
+ end
40
+
41
+ def handle(event:)
42
+ assert_valid_receiver(to: event.fetch("to"))
43
+ assert_valid_action(
44
+ action: event.fetch("action"),
45
+ to: entity.role,
46
+ nesting: event.fetch("nesting"),
47
+ )
48
+
49
+ attributes = event.fetch("payload").fetch("attributes", {})
50
+
51
+ merge(entity:, manifest:, attributes:)
52
+
53
+ target_entity = entity
54
+ event.fetch("nesting")[1..-1].each do |assoc_name, id|
55
+ break if target_entity.nil?
56
+
57
+ spec = entity.association(assoc_name)
58
+
59
+ target_entity =(
60
+ if spec.type == :reference
61
+ target_entity = spec.get
62
+ else
63
+ spec.get.find { _1.internal_entity_id == id }
64
+ end
65
+ )
66
+ end
67
+
68
+ raise "No EntityError" if target_entity.nil?
69
+
70
+ target_entity
71
+ .class
72
+ .actions
73
+ .fetch(event.fetch("action"))
74
+ .dispatch(target_entity)
75
+ end
76
+
77
+ private
78
+
79
+ def serialize_attributes(manifest:, entity:, to:)
80
+ return if entity.nil?
81
+
82
+ data = {}
83
+
84
+ manifest
85
+ .accessors
86
+ .each do |name, spec|
87
+ data[name] = entity.send(name)
88
+ end
89
+
90
+ manifest
91
+ .associations
92
+ .each do |name, spec|
93
+ if spec.type == :reference
94
+ data[name] = serialize_attributes(
95
+ manifest: spec.klass,
96
+ entity: entity.send(name),
97
+ to:
98
+ )
99
+ else
100
+ data[name] = (entity.send(name) || []).map {
101
+ serialize_attributes(
102
+ manifest: spec.klass,
103
+ entity: _1,
104
+ to:
105
+ )
106
+ }
107
+ end
108
+ end
109
+
110
+ data
111
+ end
112
+
113
+ def merge(manifest:, entity:, attributes:)
114
+ manifest
115
+ .accessors
116
+ .values
117
+ .select { _1.accepted_by?(entity.role) }
118
+ .each do |attribute|
119
+ next unless attributes.key?(attribute.name)
120
+
121
+ entity.send("#{attribute.name}=", attributes[attribute.name])
122
+ end
123
+
124
+ manifest
125
+ .associations
126
+ .values
127
+ .select { _1.type == :reference }
128
+ .select { _1.accepted_by?(entity.role) }
129
+ .each do |association|
130
+ next unless attributes.key?(association.name)
131
+
132
+ value = attributes.fetch(association.name)
133
+
134
+ if value.nil?
135
+ entity.association(association.name).set(nil)
136
+ next
137
+ end
138
+
139
+ entity
140
+ .association(association.name)
141
+ .get
142
+ .then { _1 || entity.association(association.name).build }
143
+ .tap {
144
+ merge(
145
+ entity: _1,
146
+ manifest: association.klass,
147
+ attributes: value
148
+ )
149
+ }
150
+ .then { entity.association(association.name).set(_1) }
151
+ end
152
+
153
+ manifest
154
+ .associations
155
+ .values
156
+ .select { _1.type == :collection }
157
+ .select { _1.accepted_by?(entity.role) }
158
+ .each do |collection|
159
+ next unless attributes.key?(collection.name)
160
+
161
+ identifier = collection.identifier
162
+
163
+ incoming_coll = attributes.fetch(collection.name) || []
164
+ existing_coll = entity.send(collection.name) || []
165
+
166
+ items = (
167
+ incoming_coll.map do |val|
168
+ found_item = (
169
+ val.fetch(identifier) &&
170
+ existing_coll.find { _1.send(identifier) == val.fetch(identifier) }
171
+ )
172
+
173
+ if found_item
174
+ found_item.tap { |r|
175
+ merge(
176
+ entity: r,
177
+ manifest: collection.klass,
178
+ attributes: val
179
+ )
180
+ }
181
+ else
182
+ entity
183
+ .association(collection.name)
184
+ .build
185
+ .tap { |r|
186
+ merge(
187
+ entity: r,
188
+ manifest: collection.klass,
189
+ attributes: val
190
+ )
191
+ }
192
+ end
193
+ end
194
+ )
195
+ entity.association(collection.name).set(items)
196
+ end
197
+ end
198
+
199
+ def assert_valid_action(action:, to:, nesting:)
200
+ nested_manifest = manifest
201
+ nesting[1..-1].each do |assoc_name, _id|
202
+ nested_manifest = manifest.associations.fetch(assoc_name).klass
203
+ end
204
+
205
+ unless nested_manifest.actions[action]&.accepted_by?(to.to_s)
206
+ raise InvalidAction.new("invalid action: #{action}")
207
+ end
208
+ end
209
+
210
+ def assert_valid_receiver(to:)
211
+ if to != entity.role
212
+ raise InvalidReceiver.new("invalid receiver role: to: #{to}, entity: #{entity.role.inspect}")
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Shared
5
+ module Entities
6
+ class CollectionProxy
7
+ include Enumerable
8
+
9
+ attr_reader :wrap, :unwrap
10
+ def initialize(collection, wrap:, unwrap:)
11
+ @collection = collection
12
+ @wrap = wrap
13
+ @unwrap = unwrap
14
+ end
15
+
16
+ # Read operations - wrap items when accessing
17
+ def first
18
+ val = @collection.first
19
+ val.nil? ? nil : @wrap.call(val)
20
+ end
21
+
22
+ def last
23
+ val = @collection.last
24
+ val.nil? ? nil : @wrap.call(val)
25
+ end
26
+
27
+ def [](index)
28
+ val = @collection[index]
29
+ val.nil? ? nil : @wrap.call(val)
30
+ end
31
+
32
+ def at(index)
33
+ val = @collection.at(index)
34
+ val.nil? ? nil : @wrap.call(val)
35
+ end
36
+
37
+ def each(&block)
38
+ @collection.each { |item| block.call(@wrap.call(item)) }
39
+ end
40
+
41
+ def size
42
+ @collection.size
43
+ end
44
+ alias_method :length, :size
45
+ alias_method :count, :size
46
+
47
+ def empty?
48
+ @collection.empty?
49
+ end
50
+
51
+ # Mutation operations - unwrap items and delegate to underlying collection
52
+ def <<(item)
53
+ @collection << @unwrap.call(item)
54
+ self
55
+ end
56
+
57
+ def push(*items)
58
+ @collection.push(*items.map { |item| @unwrap.call(item) })
59
+ self
60
+ end
61
+
62
+ def pop
63
+ val = @collection.pop
64
+ val.nil? ? nil : @wrap.call(val)
65
+ end
66
+
67
+ def shift
68
+ val = @collection.shift
69
+ val.nil? ? nil : @wrap.call(val)
70
+ end
71
+
72
+ def unshift(*items)
73
+ @collection.unshift(*items.map { |item| @unwrap.call(item) })
74
+ self
75
+ end
76
+
77
+ def []=(index, item)
78
+ @collection[index] = @unwrap.call(item)
79
+ end
80
+
81
+ def delete(item)
82
+ unwrapped = @unwrap.call(item)
83
+ val = @collection.delete(unwrapped)
84
+ val.nil? ? nil : @wrap.call(val)
85
+ end
86
+
87
+ def delete_at(index)
88
+ val = @collection.delete_at(index)
89
+ val.nil? ? nil : @wrap.call(val)
90
+ end
91
+
92
+ def clear
93
+ @collection.clear
94
+ self
95
+ end
96
+
97
+ def insert(index, *items)
98
+ @collection.insert(index, *items.map { |item| @unwrap.call(item) })
99
+ self
100
+ end
101
+
102
+ def concat(other_array)
103
+ @collection.concat(other_array.map { |item| @unwrap.call(item) })
104
+ self
105
+ end
106
+
107
+ def replace(other_array)
108
+ @collection.replace(other_array.map { |item| @unwrap.call(item) })
109
+ self
110
+ end
111
+
112
+ # Enumerable methods that return arrays should return CollectionProxy
113
+ def select(&block)
114
+ selected = @collection.select { |item| block.call(@wrap.call(item)) }
115
+ CollectionProxy.new(selected, wrap: @wrap, unwrap: @unwrap)
116
+ end
117
+ alias_method :filter, :select
118
+
119
+ def reject(&block)
120
+ rejected = @collection.reject { |item| block.call(@wrap.call(item)) }
121
+ CollectionProxy.new(rejected, wrap: @wrap, unwrap: @unwrap)
122
+ end
123
+
124
+ def take(n)
125
+ taken = @collection.take(n)
126
+ CollectionProxy.new(taken, wrap: @wrap, unwrap: @unwrap)
127
+ end
128
+
129
+ def take_while(&block)
130
+ taken = @collection.take_while { |item| block.call(@wrap.call(item)) }
131
+ CollectionProxy.new(taken, wrap: @wrap, unwrap: @unwrap)
132
+ end
133
+
134
+ def drop(n)
135
+ dropped = @collection.drop(n)
136
+ CollectionProxy.new(dropped, wrap: @wrap, unwrap: @unwrap)
137
+ end
138
+
139
+ def drop_while(&block)
140
+ dropped = @collection.drop_while { |item| block.call(@wrap.call(item)) }
141
+ CollectionProxy.new(dropped, wrap: @wrap, unwrap: @unwrap)
142
+ end
143
+
144
+ def reverse
145
+ reversed = @collection.reverse
146
+ CollectionProxy.new(reversed, wrap: @wrap, unwrap: @unwrap)
147
+ end
148
+
149
+ def sort(&block)
150
+ if block
151
+ sorted = @collection.sort { |a, b| block.call(@wrap.call(a), @wrap.call(b)) }
152
+ else
153
+ sorted = @collection.sort
154
+ end
155
+ CollectionProxy.new(sorted, wrap: @wrap, unwrap: @unwrap)
156
+ end
157
+
158
+ def sort_by(&block)
159
+ sorted = @collection.sort_by { |item| block.call(@wrap.call(item)) }
160
+ CollectionProxy.new(sorted, wrap: @wrap, unwrap: @unwrap)
161
+ end
162
+
163
+ def uniq(&block)
164
+ if block
165
+ uniqued = @collection.uniq { |item| block.call(@wrap.call(item)) }
166
+ else
167
+ uniqued = @collection.uniq
168
+ end
169
+ CollectionProxy.new(uniqued, wrap: @wrap, unwrap: @unwrap)
170
+ end
171
+
172
+ def compact
173
+ compacted = @collection.compact
174
+ CollectionProxy.new(compacted, wrap: @wrap, unwrap: @unwrap)
175
+ end
176
+
177
+ def slice(*args)
178
+ sliced = @collection.slice(*args)
179
+ if sliced.is_a?(Array)
180
+ CollectionProxy.new(sliced, wrap: @wrap, unwrap: @unwrap)
181
+ else
182
+ # Single element access returns wrapped item
183
+ sliced.nil? ? nil : @wrap.call(sliced)
184
+ end
185
+ end
186
+
187
+ end
188
+ end
189
+ end
190
+ end