interfacets 0.9.9 → 0.9.99

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.tmp +5 -0
  3. data/Rakefile +4 -0
  4. data/lib/interfacets/client/assets.rb +1 -0
  5. data/lib/interfacets/client/bus.rb +5 -1
  6. data/lib/interfacets/client/channels/api.rb +10 -3
  7. data/lib/interfacets/client/registry.rb +9 -70
  8. data/lib/interfacets/client/utils/mruby_patches.rb +1 -1
  9. data/lib/interfacets/client/utils/open_struct.rb +90 -77
  10. data/lib/interfacets/component_registry.rb +115 -0
  11. data/lib/interfacets/component_schema_parser.rb +84 -0
  12. data/lib/interfacets/server/api.rb +5 -25
  13. data/lib/interfacets/server/assets/facet.rb +5 -3
  14. data/lib/interfacets/server/assets.rb +13 -7
  15. data/lib/interfacets/server/basic_router.rb +15 -10
  16. data/lib/interfacets/server/bus.rb +4 -9
  17. data/lib/interfacets/server/config.rb +1 -1
  18. data/lib/interfacets/server/registry.rb +23 -184
  19. data/lib/interfacets/shared/basic_routable.rb +45 -0
  20. data/lib/interfacets/shared/entities/bus.rb +41 -29
  21. data/lib/interfacets/shared/entities/specs/handlers.rb +16 -0
  22. data/lib/interfacets/shared/entities/specs.rb +46 -9
  23. data/lib/interfacets/shared/entity.rb +23 -99
  24. data/lib/interfacets/shared/entity_dsl.rb +154 -0
  25. data/lib/interfacets/shared/facet.rb +200 -0
  26. data/lib/interfacets/shared/generated_store.rb +6 -2
  27. data/lib/interfacets/shared/utils.rb +1 -1
  28. data/lib/interfacets/shared/validations.rb +5 -1
  29. data/lib/interfacets/{client → shared}/view.rb +33 -6
  30. data/lib/interfacets/test/component_registry.rb +63 -0
  31. data/lib/interfacets/test/js/inline_bus.rb +21 -12
  32. data/lib/interfacets/test/js/nodo_bus.rb +18 -1
  33. data/lib/interfacets/test/js/receivers/api.rb +14 -3
  34. data/lib/interfacets/test/js/receivers/react/node/xml_parser.rb +75 -0
  35. data/lib/interfacets/test/js/receivers/react/node.rb +29 -63
  36. data/lib/interfacets/test/js/receivers/react.rb +4 -3
  37. data/lib/interfacets/test/js/receivers/timer.rb +77 -0
  38. data/lib/interfacets/test/js/receivers/url.rb +5 -0
  39. data/lib/interfacets/test/standard_elements.yml +173 -0
  40. data/lib/interfacets/test/{browser.rb → ui_simulator.rb} +18 -6
  41. data/lib/interfacets/test/validation_engine.rb +151 -0
  42. data/lib/interfacets/test.rb +0 -4
  43. data/lib/interfacets/version.rb +1 -1
  44. data/lib/interfacets.rb +3 -0
  45. metadata +29 -18
  46. data/lib/interfacets/client/facet.rb +0 -26
  47. data/lib/interfacets/client/facet2.rb +0 -15
  48. data/lib/interfacets/client/facets/attributes/accessor.rb +0 -28
  49. data/lib/interfacets/client/facets/attributes/association.rb +0 -50
  50. data/lib/interfacets/client/facets/attributes/bind.rb +0 -25
  51. data/lib/interfacets/client/facets/attributes/collection.rb +0 -47
  52. data/lib/interfacets/client/facets/attributes/readonly.rb +0 -19
  53. data/lib/interfacets/client/facets/deserializer.rb +0 -30
  54. data/lib/interfacets/client/facets/schema/deserializer.rb +0 -63
  55. data/lib/interfacets/client/facets/schema.rb +0 -63
  56. data/lib/interfacets/client/facets/serializer.rb +0 -18
  57. data/lib/interfacets/server/basic_routable.rb +0 -40
  58. data/lib/interfacets/server/facet.rb +0 -51
  59. data/lib/interfacets/shared/entity_collection.rb +0 -88
@@ -118,8 +118,8 @@ module Interfacets
118
118
 
119
119
  serialized[const.full_name] = true
120
120
 
121
- const.nesting.each do |_nesting_const|
122
- push_bootstrapper(const, results, serialized)
121
+ const.nesting.each do |nesting_const|
122
+ push_bootstrapper(nesting_const, results, serialized)
123
123
  end
124
124
 
125
125
  parent = (
@@ -145,7 +145,7 @@ module Interfacets
145
145
  end
146
146
 
147
147
  def resolve_const_name(nesting, name)
148
- full_name = [nesting, name].compact.join("::")
148
+ full_name = [nesting, name].compact.reject(&:empty?).join("::")
149
149
  return consts_by_name[full_name] if consts_by_name.key?(full_name)
150
150
 
151
151
  # maybe a constant that we don't handle loading for (eg, StandardError)
@@ -159,7 +159,7 @@ module Interfacets
159
159
  end
160
160
 
161
161
  def fs_map
162
- paths.map { [_1, file.read(_1)] }.to_h
162
+ @fs_map ||= paths.map { [_1, file.read(_1)] }.to_h
163
163
  end
164
164
 
165
165
  def consts
@@ -181,11 +181,17 @@ module Interfacets
181
181
  ].freeze
182
182
 
183
183
  class << self
184
- def bundle(dirs:, registry:)
184
+ def bundle(dirs:, registry:, only_facets: false)
185
185
 
186
- registry.serialize
186
+ target_dirs = (
187
+ if only_facets
188
+ dirs
189
+ else
190
+ GEM_DIRS + dirs
191
+ end
192
+ )
187
193
 
188
- (dirs + GEM_DIRS + [registry.build_dir])
194
+ target_dirs
189
195
  .flat_map { paths(_1) }
190
196
  .flatten
191
197
  .map(&:to_s)
@@ -10,10 +10,8 @@ module Interfacets
10
10
  end
11
11
 
12
12
  def call(klass:, id:, query:, resolved_path:)
13
- klass.bus = bus
14
- klass
15
- .find
16
- .call(id, query:)
13
+ klass.server_entity_class
14
+ .find_for_basic_routing(id, query:, bus: bus)
17
15
  .tap { _1.entity.api_path = resolved_path }
18
16
  end
19
17
  end
@@ -26,13 +24,13 @@ module Interfacets
26
24
  end
27
25
 
28
26
  def call(url, query: {})
29
- if url.nil? && default
27
+ path = normalize(url)
28
+
29
+ if path.nil? && default
30
30
  klass = klass_for(default)
31
- evaluator.call(klass:, id: nil, query:, resolved_path: default)
31
+ return evaluator.call(klass:, id: nil, query:, resolved_path: default)
32
32
  end
33
33
 
34
- path = url.sub(/^#{bus.root_url}/, "/").sub(/^/, "/").sub(%r{^/*}, "/")
35
-
36
34
  if normalized_paths.key?(path)
37
35
  klass = klass_for(path)
38
36
 
@@ -58,7 +56,7 @@ module Interfacets
58
56
  end
59
57
 
60
58
  def klass_for(path)
61
- val = normalized_paths.fetch(path)
59
+ val = normalized_paths.fetch(normalize(path))
62
60
 
63
61
  (val.is_a?(String) ? Object.const_get(val) : val)
64
62
  end
@@ -66,9 +64,16 @@ module Interfacets
66
64
  def normalized_paths
67
65
  @normalized_paths ||= (
68
66
  paths
69
- .transform_keys { _1.start_with?("/") ? _1 : "/#{_1}" }
67
+ .transform_keys { normalize(_1) }
70
68
  )
71
69
  end
70
+
71
+ def normalize(path)
72
+ path
73
+ &.sub(/^#{bus.root_url}/, "/")
74
+ &.sub(/^/, "/")
75
+ &.sub(%r{^/*}, "/")
76
+ end
72
77
  end
73
78
  end
74
79
  end
@@ -9,20 +9,15 @@ module Interfacets
9
9
  :registry,
10
10
  )
11
11
 
12
- def initialize(
13
- root_url:,
14
- asset_paths:,
15
- facets:,
16
- build_dir:
17
- )
18
- @registry = Registry.new(facets:, build_dir:)
12
+ def initialize(root_url:, asset_paths:)
13
+ @registry = Registry.new
19
14
  @root_url = root_url
20
15
  @asset_paths = asset_paths
21
16
  end
22
17
 
23
- def client_system_json
18
+ def client_system_json(only_facets: false)
24
19
  {
25
- assets: Assets.bundle(dirs: asset_paths, registry:),
20
+ assets: Assets.bundle(dirs: asset_paths, registry:, only_facets:),
26
21
  root_url:
27
22
  }
28
23
  end
@@ -36,7 +36,7 @@ module Interfacets
36
36
  .values
37
37
  .map { Object.const_get(_1) }
38
38
  .then {
39
- Interfacets::Server::Facets::Schema::Serializer
39
+ Interfacets::Shared::Facets::Schema::Serializer
40
40
  .call(facets: _1)
41
41
  }
42
42
  end
@@ -1,212 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/all"
4
+ require "ostruct"
4
5
 
5
6
  module Interfacets
6
7
  module Server
7
8
  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
9
+ class Channel
10
+ def initialize(registry:)
11
+ @registry = registry
12
+ end
36
13
 
37
- entity = Class.new(Shared::Entity) do
38
- self.manifest = shared
39
- role("server")
14
+ def build(...)
15
+ @registry.build(...)
16
+ end
40
17
 
41
- klass.shareds.each { class_exec(&_1) }
42
- klass.servers.each { class_exec(&_1) }
18
+ def render_facet
19
+ @facet.render
20
+ end
43
21
 
44
- attr_writer :channel
45
- def channel
46
- @channel || parent.channel
47
- end
48
- end
22
+ def rendered?
23
+ @facet
24
+ end
49
25
 
50
- registry[klass.name] = { klass:, entity:}
51
- end
26
+ def render(klass, store)
27
+ @facet = @registry.build(klass, store)
28
+ end
52
29
  end
53
30
 
54
31
  def build(name, store)
55
- register
56
- entry = registry.fetch(name.is_a?(Class) ? name.name : name)
57
-
58
-
32
+ facet = name.is_a?(Module) ? name : Object.const_get(name)
59
33
 
60
34
  Api.new(
61
35
  registry: self,
62
36
  name:,
63
37
  entity: (
64
- entry
65
- .fetch(:entity)
38
+ facet
39
+ .server_entity_class
66
40
  .new(
67
41
  store: store.is_a?(Hash) ? OpenStruct.new(store) : store,
68
- nesting: ["root"],
69
- parent: nil
42
+ nesting: "root",
43
+ parent: nil,
44
+ channel: Channel.new(registry: self)
70
45
  )
71
46
  )
72
47
  )
73
48
  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
49
  end
211
50
  end
212
51
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Shared
5
+ module BasicRoutable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class << self
10
+ attr_accessor :bus
11
+
12
+ def build(klass, store)
13
+ bus.registry.build(klass, store)
14
+ end
15
+ end
16
+
17
+ # This is unfortunate, but in order to be able to do:
18
+
19
+ # build(self, ...)
20
+ # we need the self to be the facet.
21
+ # We could also update the registry to handle being passed a server
22
+ # entity, but I'm currently tweaking the registry. so this is a TODO.
23
+ facet = self
24
+
25
+ entity_base do
26
+ accessor(:api_path, accepted_by: :client)
27
+ end
28
+
29
+ server_entity(unshift: true) do
30
+ attr_accessor :api_path
31
+
32
+ define_singleton_method(:find) do |&block|
33
+ @find = block if block
34
+ @find
35
+ end
36
+
37
+ define_singleton_method(:find_for_basic_routing) do |id, query:, bus:|
38
+ facet.bus = bus
39
+ facet.instance_exec(id, query:, &find)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -39,39 +39,26 @@ class Bus
39
39
  end
40
40
 
41
41
  def handle(event:)
42
+ action = event.fetch("action")
43
+
42
44
  assert_valid_receiver(to: event.fetch("to"))
43
45
  assert_valid_action(
44
- action: event.fetch("action"),
46
+ action:,
45
47
  to: entity.role,
46
48
  nesting: event.fetch("nesting"),
47
49
  )
48
50
 
49
51
  attributes = event.fetch("payload").fetch("attributes", {})
50
52
 
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
53
+ merge(entity:, manifest:, attributes:, action:)
67
54
 
68
- raise "No EntityError" if target_entity.nil?
55
+ target_entity = entity.entity_at(event.fetch("nesting"))
69
56
 
70
57
  target_entity
71
- .class
72
- .actions
73
- .fetch(event.fetch("action"))
74
- .dispatch(target_entity)
58
+ &.class
59
+ &.actions
60
+ &.fetch(event.fetch("action"))
61
+ &.dispatch(target_entity)
75
62
  end
76
63
 
77
64
  private
@@ -81,6 +68,8 @@ class Bus
81
68
 
82
69
  data = {}
83
70
 
71
+ data[:errors] = entity.errors.to_h if entity.respond_to?(:errors)
72
+
84
73
  manifest
85
74
  .accessors
86
75
  .each do |name, spec|
@@ -110,7 +99,14 @@ class Bus
110
99
  data
111
100
  end
112
101
 
113
- def merge(manifest:, entity:, attributes:)
102
+ def merge(manifest:, entity:, attributes:, action:)
103
+ if (errs = attributes[:errors] || attributes["errors"])
104
+ entity.errors.clear if entity.errors.respond_to?(:clear)
105
+ errs.each do |k, vs|
106
+ Array(vs).each { |v| entity.errors.add(k.to_sym, v) }
107
+ end
108
+ end
109
+
114
110
  manifest
115
111
  .accessors
116
112
  .values
@@ -118,7 +114,19 @@ class Bus
118
114
  .each do |attribute|
119
115
  next unless attributes.key?(attribute.name)
120
116
 
121
- entity.send("#{attribute.name}=", attributes[attribute.name])
117
+ attr_mergers = (
118
+ entity.class.mergers[attribute.name]
119
+ )
120
+
121
+ merger = (
122
+ if attr_mergers.key?(action)
123
+ attr_mergers.fetch(action)
124
+ else
125
+ attr_mergers.fetch(:default)
126
+ end
127
+ )
128
+
129
+ merger.call(entity, attributes[attribute.name])
122
130
  end
123
131
 
124
132
  manifest
@@ -140,11 +148,12 @@ class Bus
140
148
  .association(association.name)
141
149
  .get
142
150
  .then { _1 || entity.association(association.name).build }
143
- .tap {
151
+ .tap { |nested_entity|
144
152
  merge(
145
- entity: _1,
153
+ entity: nested_entity,
146
154
  manifest: association.klass,
147
- attributes: value
155
+ attributes: value,
156
+ action:
148
157
  )
149
158
  }
150
159
  .then { entity.association(association.name).set(_1) }
@@ -175,7 +184,8 @@ class Bus
175
184
  merge(
176
185
  entity: r,
177
186
  manifest: collection.klass,
178
- attributes: val
187
+ attributes: val,
188
+ action:
179
189
  )
180
190
  }
181
191
  else
@@ -186,7 +196,8 @@ class Bus
186
196
  merge(
187
197
  entity: r,
188
198
  manifest: collection.klass,
189
- attributes: val
199
+ attributes: val,
200
+ action:
190
201
  )
191
202
  }
192
203
  end
@@ -198,6 +209,7 @@ class Bus
198
209
 
199
210
  def assert_valid_action(action:, to:, nesting:)
200
211
  nested_manifest = manifest
212
+
201
213
  nesting[1..-1].each do |assoc_name, _id|
202
214
  nested_manifest = manifest.associations.fetch(assoc_name).klass
203
215
  end
@@ -43,6 +43,14 @@ module Interfacets
43
43
  end
44
44
 
45
45
  class Reference < Handler
46
+ def collection?
47
+ false
48
+ end
49
+
50
+ def reference?
51
+ true
52
+ end
53
+
46
54
  def get
47
55
  wrap(entity.instance_exec(&spec.getter))
48
56
  .tap {
@@ -70,6 +78,14 @@ module Interfacets
70
78
  end
71
79
 
72
80
  class Collection < Handler
81
+ def collection?
82
+ true
83
+ end
84
+
85
+ def reference?
86
+ false
87
+ end
88
+
73
89
  def get
74
90
  values = entity.instance_exec(&spec.getter) || []
75
91
 
@@ -72,6 +72,7 @@ module Interfacets
72
72
  :builder,
73
73
  :parent,
74
74
  :identifier,
75
+ :type,
75
76
  )
76
77
  def initialize(parent:, name:)
77
78
  @parent = parent
@@ -81,6 +82,7 @@ module Interfacets
81
82
  @setter = ->(val) { store.send("#{name}=", val) }
82
83
  @builder = ->() { store.association(name).build }
83
84
  @identifier = "id"
85
+
84
86
  @klass = Class.new(Entity) do
85
87
  define_singleton_method(:name) { "#{parent.name}.#{name}" }
86
88
  end
@@ -92,6 +94,8 @@ module Interfacets
92
94
  setter: NOT_PASSED,
93
95
  builder: NOT_PASSED,
94
96
  identifier: NOT_PASSED,
97
+ mod: nil,
98
+ type: NOT_PASSED,
95
99
  &block
96
100
  )
97
101
  @accepted_by = Array(accepted_by).map(&:to_s) unless accepted_by == NOT_PASSED
@@ -99,23 +103,56 @@ module Interfacets
99
103
  @setter = setter unless setter == NOT_PASSED
100
104
  @builder = builder unless builder == NOT_PASSED
101
105
  @identifier = identifier.to_s unless identifier == NOT_PASSED
102
- klass.class_exec(&block) if block_given?
106
+
107
+ if type && type != NOT_PASSED
108
+ type = type.to_sym
109
+ unless [:reference, :collection].include?(type)
110
+ raise ArgumentError.new("invalid association type: #{type.inspect}")
111
+ end
112
+
113
+ if @type && type != @type
114
+ raise ArgumentError.new("invalid association cannot change type")
115
+ end
116
+
117
+ @type = type
118
+ end
119
+
120
+ @klass.include(mod) if mod
121
+
122
+ @klass.class_exec(&block) if block_given?
103
123
  end
104
- end
105
124
 
106
- class Reference < Association
107
- def type = :reference
125
+ def dup_for(new_parent)
126
+ dup.tap do |new_spec|
127
+ new_spec.instance_variable_set(:@parent, new_parent)
128
+ new_spec.instance_variable_set(:@klass, Class.new(@klass))
129
+ end
130
+ end
108
131
 
109
132
  def handler(entity)
110
- Handlers::Reference.new(entity:, spec: self)
133
+ klass = (
134
+ if type == :reference
135
+ Handlers::Reference
136
+ elsif type == :collection
137
+ Handlers::Collection
138
+ else
139
+ raise "Type was never set for association #{parent.name}.#{name}"
140
+ end
141
+ )
142
+
143
+ klass.new(entity:, spec: self)
111
144
  end
112
145
  end
113
146
 
114
- class Collection < Association
115
- def type = :collection
147
+ class Merger
148
+ attr_reader(:name, :block)
149
+ def initialize(name:, block:)
150
+ @name = name
151
+ @block = block
152
+ end
116
153
 
117
- def handler(entity)
118
- Handlers::Collection.new(entity:, spec: self)
154
+ def call(entity, value)
155
+ block.call(entity, value)
119
156
  end
120
157
  end
121
158
  end