interfacets 0.1.0 → 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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +173 -1
  3. data/.tmp +5 -0
  4. data/LICENSE +21 -0
  5. data/Rakefile +9 -7
  6. data/lib/interfacets/client/actor.rb +20 -0
  7. data/lib/interfacets/client/assets.rb +210 -0
  8. data/lib/interfacets/client/bus.rb +73 -0
  9. data/lib/interfacets/client/channels/api.rb +102 -0
  10. data/lib/interfacets/client/channels/audio.rb +101 -0
  11. data/lib/interfacets/client/channels/base.rb +28 -0
  12. data/lib/interfacets/client/channels/page_visibility.rb +21 -0
  13. data/lib/interfacets/client/channels/react/builder.rb +203 -0
  14. data/lib/interfacets/client/channels/react/channel.rb +61 -0
  15. data/lib/interfacets/client/channels/react/dom.rb +91 -0
  16. data/lib/interfacets/client/channels/react/evaluator.rb +33 -0
  17. data/lib/interfacets/client/channels/speech_to_text.rb +100 -0
  18. data/lib/interfacets/client/channels/timer.rb +51 -0
  19. data/lib/interfacets/client/channels/url.rb +52 -0
  20. data/lib/interfacets/client/config.rb +22 -0
  21. data/lib/interfacets/client/delegator.rb +37 -0
  22. data/lib/interfacets/client/registry.rb +23 -0
  23. data/lib/interfacets/client/system.rb +88 -0
  24. data/lib/interfacets/client/utils/active_support_concern.rb +220 -0
  25. data/lib/interfacets/client/utils/mruby_patches.rb +81 -0
  26. data/lib/interfacets/client/utils/open_struct.rb +115 -0
  27. data/lib/interfacets/client/utils/securerandom.rb +69 -0
  28. data/lib/interfacets/client.rb +13 -0
  29. data/lib/interfacets/component_registry.rb +115 -0
  30. data/lib/interfacets/component_schema_parser.rb +84 -0
  31. data/lib/interfacets/mruby/build.dockerfile +66 -0
  32. data/lib/interfacets/mruby/build_config.rb +20 -0
  33. data/lib/interfacets/mruby/entrypoint.rb +23 -0
  34. data/lib/interfacets/mruby/init.c +66 -0
  35. data/lib/interfacets/server/api.rb +44 -0
  36. data/lib/interfacets/server/assets/facet.rb +63 -0
  37. data/lib/interfacets/server/assets.rb +216 -0
  38. data/lib/interfacets/server/basic_router.rb +79 -0
  39. data/lib/interfacets/server/bus.rb +34 -0
  40. data/lib/interfacets/server/config.rb +87 -0
  41. data/lib/interfacets/server/facets/deserializer.rb +25 -0
  42. data/lib/interfacets/server/facets/schema/serializer.rb +54 -0
  43. data/lib/interfacets/server/facets/serializer.rb +50 -0
  44. data/lib/interfacets/server/registry.rb +51 -0
  45. data/lib/interfacets/shared/basic_routable.rb +45 -0
  46. data/lib/interfacets/shared/entities/bus.rb +230 -0
  47. data/lib/interfacets/shared/entities/collection_proxy.rb +190 -0
  48. data/lib/interfacets/shared/entities/specs/handlers.rb +133 -0
  49. data/lib/interfacets/shared/entities/specs.rb +161 -0
  50. data/lib/interfacets/shared/entity.rb +102 -0
  51. data/lib/interfacets/shared/entity_dsl.rb +154 -0
  52. data/lib/interfacets/shared/facet.rb +200 -0
  53. data/lib/interfacets/shared/generated_store.rb +149 -0
  54. data/lib/interfacets/shared/utils.rb +54 -0
  55. data/lib/interfacets/shared/validations.rb +75 -0
  56. data/lib/interfacets/shared/view.rb +74 -0
  57. data/lib/interfacets/test/component_registry.rb +63 -0
  58. data/lib/interfacets/test/js/inline_bus.rb +100 -0
  59. data/lib/interfacets/test/js/nodo_bus.rb +98 -0
  60. data/lib/interfacets/test/js/receivers/api.rb +48 -0
  61. data/lib/interfacets/test/js/receivers/react/node/xml_parser.rb +75 -0
  62. data/lib/interfacets/test/js/receivers/react/node.rb +133 -0
  63. data/lib/interfacets/test/js/receivers/react.rb +32 -0
  64. data/lib/interfacets/test/js/receivers/timer.rb +77 -0
  65. data/lib/interfacets/test/js/receivers/url.rb +60 -0
  66. data/lib/interfacets/test/standard_elements.yml +173 -0
  67. data/lib/interfacets/test/ui_simulator.rb +75 -0
  68. data/lib/interfacets/test/validation_engine.rb +151 -0
  69. data/lib/interfacets/test.rb +13 -0
  70. data/lib/interfacets/version.rb +1 -1
  71. data/lib/interfacets.rb +29 -2
  72. metadata +114 -6
  73. data/README.md +0 -35
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Interfacets
6
+ module Server
7
+ module Assets
8
+ Const =
9
+ Data.define(:nesting, :type, :name, :parent) do
10
+ def full_name
11
+ [nesting.last&.full_name, name].compact.join("::")
12
+ end
13
+ end
14
+
15
+ class Classifier
16
+ class Visitor < Prism::Visitor
17
+ # This class just gathers up all modules and classes defined
18
+ attr_reader :mods
19
+ def initialize
20
+ @stack = []
21
+ @mods = []
22
+ super
23
+ end
24
+
25
+ def visit_module_node(node)
26
+ push_mod(node)
27
+ end
28
+
29
+ def visit_class_node(node)
30
+ push_mod(node)
31
+ end
32
+
33
+ private
34
+
35
+ def push_mod(node)
36
+ type = node.type == :module_node ? :module : :class
37
+
38
+ parent = (
39
+ if type == :class && node.child_nodes[1]
40
+ node.child_nodes[1].full_name
41
+ end
42
+ )
43
+
44
+ const = Const.new(
45
+ nesting: @stack.dup,
46
+ name: node.constant_path.full_name,
47
+ type:,
48
+ parent:
49
+ )
50
+ @stack << const
51
+ mods << @stack.dup
52
+ visit_child_nodes(node)
53
+ @stack.pop
54
+ end
55
+ end
56
+
57
+ attr_reader :code
58
+ def initialize(code)
59
+ @code = code
60
+ end
61
+
62
+ def consts
63
+ @consts ||= (
64
+ Prism.parse(code).value.accept(visitor)
65
+ visitor.mods.map(&:last)
66
+ )
67
+ end
68
+
69
+ def visitor
70
+ @visitor ||= Visitor.new
71
+ end
72
+ end
73
+
74
+ class Serializer
75
+ attr_reader :paths, :file
76
+ def initialize(paths:, file: File)
77
+ @paths = paths
78
+ @file = file
79
+ end
80
+
81
+ def as_json
82
+ {
83
+ fs_map:,
84
+ const_map:,
85
+ scaffolding:,
86
+ }
87
+ end
88
+
89
+ private
90
+
91
+ def const_map
92
+ @const_map ||= (
93
+ full_map = Hash.new { |h, k| h[k] = [] }
94
+
95
+ paths
96
+ .each { |path|
97
+ consts_by_path.fetch(path).each { |c| full_map[c.full_name] << path }
98
+ }
99
+
100
+ full_map.transform_values(&:uniq!)
101
+ full_map
102
+ )
103
+ end
104
+
105
+ def scaffolding
106
+ serialized = {}
107
+ results = []
108
+
109
+ consts.each { |const|
110
+ push_bootstrapper(const, results, serialized)
111
+ }
112
+
113
+ results
114
+ end
115
+
116
+ def push_bootstrapper(const, results, serialized)
117
+ return if serialized.key?(const.full_name)
118
+
119
+ serialized[const.full_name] = true
120
+
121
+ const.nesting.each do |nesting_const|
122
+ push_bootstrapper(nesting_const, results, serialized)
123
+ end
124
+
125
+ parent = (
126
+ if const.parent
127
+ resolve_const_name(const.nesting.last&.full_name, const.parent)
128
+ end
129
+ )
130
+
131
+ if parent.is_a?(Const)
132
+ push_bootstrapper(parent, results, serialized)
133
+ end
134
+
135
+ parent_name = parent.is_a?(Const) ? parent.full_name : parent
136
+
137
+ parent_str = "< #{parent_name}" if parent
138
+ results << <<~TXT
139
+ #{const.type} #{const.full_name} #{parent_str}
140
+ class << self
141
+ include Interfacets::Client::Assets::AutoloadHook
142
+ end
143
+ end
144
+ TXT
145
+ end
146
+
147
+ def resolve_const_name(nesting, name)
148
+ full_name = [nesting, name].compact.reject(&:empty?).join("::")
149
+ return consts_by_name[full_name] if consts_by_name.key?(full_name)
150
+
151
+ # maybe a constant that we don't handle loading for (eg, StandardError)
152
+ return name if nesting.nil? || nesting.empty?
153
+
154
+ resolve_const_name(nesting.split("::")[0...-1].join("::"), name)
155
+ end
156
+
157
+ def consts_by_name
158
+ @consts_by_name ||= consts.group_by(&:full_name).transform_values(&:first)
159
+ end
160
+
161
+ def fs_map
162
+ @fs_map ||= paths.map { [_1, file.read(_1)] }.to_h
163
+ end
164
+
165
+ def consts
166
+ @consts ||= consts_by_path.values.flatten
167
+ end
168
+
169
+ def consts_by_path
170
+ @consts_by_path ||= (
171
+ fs_map
172
+ .transform_values { |code| Classifier.new(code).consts }
173
+ )
174
+ end
175
+ end
176
+
177
+ GEM_DIRS = [
178
+ File.expand_path(File.join(__dir__, "../shared")),
179
+ File.expand_path(File.join(__dir__, "../client")),
180
+ File.expand_path(File.join(__dir__, "../client.rb")),
181
+ ].freeze
182
+
183
+ class << self
184
+ def bundle(dirs:, registry:, only_facets: false)
185
+
186
+ target_dirs = (
187
+ if only_facets
188
+ dirs
189
+ else
190
+ GEM_DIRS + dirs
191
+ end
192
+ )
193
+
194
+ target_dirs
195
+ .flat_map { paths(_1) }
196
+ .flatten
197
+ .map(&:to_s)
198
+ .then { Serializer.new(paths: _1).as_json }
199
+ end
200
+
201
+ private
202
+
203
+ def paths(path)
204
+ if File.file?(path)
205
+ [path]
206
+ else
207
+ [
208
+ Dir.glob(File.join(path, "*.rb")),
209
+ Dir.glob(File.join(path, "**/*.rb")),
210
+ ].flatten
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Server
5
+ class BasicRouter
6
+ class Evaluator
7
+ attr_reader :bus
8
+ def initialize(bus:)
9
+ @bus = bus
10
+ end
11
+
12
+ def call(klass:, id:, query:, resolved_path:)
13
+ klass.server_entity_class
14
+ .find_for_basic_routing(id, query:, bus: bus)
15
+ .tap { _1.entity.api_path = resolved_path }
16
+ end
17
+ end
18
+
19
+ attr_reader :paths, :bus, :default
20
+ def initialize(bus:, paths:, default: nil)
21
+ @bus = bus
22
+ @paths = paths
23
+ @default = default
24
+ end
25
+
26
+ def call(url, query: {})
27
+ path = normalize(url)
28
+
29
+ if path.nil? && default
30
+ klass = klass_for(default)
31
+ return evaluator.call(klass:, id: nil, query:, resolved_path: default)
32
+ end
33
+
34
+ if normalized_paths.key?(path)
35
+ klass = klass_for(path)
36
+
37
+ evaluator.call(klass:, id: nil, query:, resolved_path: path)
38
+ else
39
+ *parts, id = path.split("/")
40
+ resolved_base_path = parts.join("/")
41
+
42
+ if normalized_paths.key?(resolved_base_path)
43
+ klass = klass_for(resolved_base_path)
44
+ evaluator.call(klass:, id:, query:, resolved_path: path)
45
+ elsif default
46
+ klass = klass_for(default)
47
+ evaluator.call(klass:, id: nil, query:, resolved_path: default)
48
+ else
49
+ raise "unhandled route: #{path}"
50
+ end
51
+ end
52
+ end
53
+
54
+ def evaluator
55
+ @evaluator ||= Evaluator.new(bus:)
56
+ end
57
+
58
+ def klass_for(path)
59
+ val = normalized_paths.fetch(normalize(path))
60
+
61
+ (val.is_a?(String) ? Object.const_get(val) : val)
62
+ end
63
+
64
+ def normalized_paths
65
+ @normalized_paths ||= (
66
+ paths
67
+ .transform_keys { normalize(_1) }
68
+ )
69
+ end
70
+
71
+ def normalize(path)
72
+ path
73
+ &.sub(/^#{bus.root_url}/, "/")
74
+ &.sub(/^/, "/")
75
+ &.sub(%r{^/*}, "/")
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Server
5
+ class Bus
6
+ attr_reader(
7
+ :root_url,
8
+ :asset_paths,
9
+ :registry,
10
+ )
11
+
12
+ def initialize(root_url:, asset_paths:)
13
+ @registry = Registry.new
14
+ @root_url = root_url
15
+ @asset_paths = asset_paths
16
+ end
17
+
18
+ def client_system_json(only_facets: false)
19
+ {
20
+ assets: Assets.bundle(dirs: asset_paths, registry:, only_facets:),
21
+ root_url:
22
+ }
23
+ end
24
+
25
+ def build(name:, store:)
26
+ registry.build(name, store)
27
+ end
28
+
29
+ def handle(name:, id:, event:)
30
+ registry.load(name, id).handle(event)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Server
5
+ class Config
6
+ attr_reader :registry
7
+ attr_accessor(
8
+ :mount_point,
9
+ :host,
10
+ )
11
+
12
+ def initialize(
13
+ mount_point:,
14
+ registry:,
15
+ assets:
16
+ )
17
+ @mount_point = mount_point
18
+ @registry = registry
19
+ @assets = assets
20
+ end
21
+
22
+ def bundled_assets
23
+ dirs = @assets.map(&:to_s)
24
+
25
+ facets = (
26
+ @registry
27
+ .values
28
+ .map { Object.const_get(_1) }
29
+ )
30
+
31
+ Interfacets::Server::Assets.bundle(dirs:, facets:)
32
+ end
33
+
34
+ def schema
35
+ @facet_registry
36
+ .values
37
+ .map { Object.const_get(_1) }
38
+ .then {
39
+ Interfacets::Shared::Facets::Schema::Serializer
40
+ .call(facets: _1)
41
+ }
42
+ end
43
+
44
+ def register_facet(name, class_or_class_name)
45
+ # if klass.name is nil, then just use the class reference
46
+ @facet_registry[name] = class_or_class_name
47
+ end
48
+
49
+ def all_facets
50
+ @facet_registry.keys.map { facet(_1) }
51
+ end
52
+
53
+ def default_facet
54
+ build_facet(facet_uid: @default_facet)
55
+ end
56
+
57
+ def facet(name)
58
+ klass_name = (
59
+ if @facet_registry.key?(name)
60
+ @facet_registry.fetch(name)
61
+ elsif @facet_registry.values.include?(name)
62
+ name
63
+ else
64
+ raise("unknown facet_class: #{name}")
65
+ end
66
+ )
67
+
68
+ klass_name.constantize
69
+ end
70
+
71
+ def ingest_facet(data)
72
+ Facets::Deserializer.call(data:, config: self)
73
+ end
74
+
75
+ def build_facet(facet_uid: nil, name: nil, id: nil, query: {})
76
+ if name
77
+ facet(name).build(id, query:)
78
+ elsif @facet_registry.key?(facet_uid)
79
+ facet(facet_uid).build(nil, query:)
80
+ else
81
+ *name, id = facet_uid.split("/")
82
+ facet(name.join("/")).build(id, query:)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Server
5
+ module Facets
6
+ class Deserializer
7
+ class << self
8
+ def call(data:, config:)
9
+ facet_class_name = data.fetch("facet_class")
10
+ attributes = data.fetch("attributes")
11
+
12
+ facet = (
13
+ config
14
+ .facet(facet_class_name)
15
+ .build(attributes.fetch("id"))
16
+ )
17
+
18
+ facet.entity.ingest(attributes)
19
+ facet
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Server
5
+ module Facets
6
+ module Schema
7
+ class Serializer
8
+ def self.call(...)
9
+ new(...).call
10
+ end
11
+
12
+ private attr_reader(:facets, :schema)
13
+ def initialize(facets:)
14
+ @facets = facets
15
+ @schema = { views: {}, entities: {}, apis: {} }
16
+ end
17
+
18
+ def call
19
+ facets.each do |facet|
20
+ config = facet.client_config
21
+
22
+ schema[:views][config.view.name] = {
23
+ body: write_source(config.view.block),
24
+ }
25
+
26
+ schema[:entities][config.entity.name] = {
27
+ api_type: config.api.name,
28
+ body: write_source(config.entity.block),
29
+ }
30
+
31
+ schema[:apis][config.api.name] = {
32
+ body: write_source(config.api.block),
33
+ }
34
+ end
35
+
36
+ schema
37
+ end
38
+
39
+ private
40
+
41
+ def write_source(block)
42
+ return unless block
43
+
44
+ RubyVM::AbstractSyntaxTree
45
+ .of(block, keep_script_lines: true)
46
+ .source
47
+ .then { block.is_a?(UnboundMethod) ? _1 : "lambda #{_1}" }
48
+ .strip
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -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,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+ require "ostruct"
5
+
6
+ module Interfacets
7
+ module Server
8
+ class Registry
9
+ class Channel
10
+ def initialize(registry:)
11
+ @registry = registry
12
+ end
13
+
14
+ def build(...)
15
+ @registry.build(...)
16
+ end
17
+
18
+ def render_facet
19
+ @facet.render
20
+ end
21
+
22
+ def rendered?
23
+ @facet
24
+ end
25
+
26
+ def render(klass, store)
27
+ @facet = @registry.build(klass, store)
28
+ end
29
+ end
30
+
31
+ def build(name, store)
32
+ facet = name.is_a?(Module) ? name : Object.const_get(name)
33
+
34
+ Api.new(
35
+ registry: self,
36
+ name:,
37
+ entity: (
38
+ facet
39
+ .server_entity_class
40
+ .new(
41
+ store: store.is_a?(Hash) ? OpenStruct.new(store) : store,
42
+ nesting: "root",
43
+ parent: nil,
44
+ channel: Channel.new(registry: self)
45
+ )
46
+ )
47
+ )
48
+ end
49
+ end
50
+ end
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