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,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Server
5
+ class Api
6
+ class Channel
7
+ attr_reader :klass, :store
8
+
9
+ def rendered?
10
+ @rendered
11
+ end
12
+
13
+ def render(klass, store)
14
+ @rendered = true
15
+ @klass = klass
16
+ @store = store
17
+ end
18
+ end
19
+
20
+ attr_reader :entity, :name, :registry
21
+ def initialize(entity:, name:, registry:)
22
+ @entity = entity
23
+ @name = name
24
+ @registry = registry
25
+ end
26
+
27
+ def handle(event)
28
+ channel = Channel.new
29
+
30
+ entity.channel = channel
31
+ Shared::Entities::Bus
32
+ .new(entity:)
33
+ .handle(event:)
34
+
35
+ if entity.channel.rendered?
36
+ registry.build(
37
+ channel.klass,
38
+ channel.store,
39
+ ).render
40
+ else
41
+ emit("after_#{event.fetch("action")}")
42
+ end
43
+ end
44
+
45
+ def render
46
+ emit("after_load")
47
+ end
48
+
49
+ private
50
+
51
+ def emit(action)
52
+ {
53
+ facet: name,
54
+ payload: (
55
+ Shared::Entities::Bus
56
+ .new(entity:)
57
+ .serialize(to: "client", action:, nesting: ["root"])
58
+ )
59
+ }
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Server
5
+ module Assets
6
+ class Facet
7
+ def self.register(klass:, assets:)
8
+ assets[klass.name] = new(klass).code
9
+ end
10
+
11
+ attr_reader :klass
12
+ def initialize(klass)
13
+ @klass = klass
14
+ end
15
+
16
+ def code
17
+ # nest it all so names resolve
18
+ *mods, klass_name = klass.name.split("::")
19
+
20
+ headers = []
21
+ footers = []
22
+
23
+ current_mod = ""
24
+ mods.each do |mod_name|
25
+ current_mod += "::#{mod_name}"
26
+ type = current_mod.constantize.is_a?(Module) ? :module : :class
27
+ headers << "#{type} #{mod_name}"
28
+ footers << "end"
29
+ end
30
+
31
+ headers << "class #{klass_name} < Interfacets::Client::Facet"
32
+ footers << "end"
33
+
34
+ <<~TXT
35
+ #{headers.join("\n")}
36
+
37
+ include Interfacets::Client::Facets::Schema
38
+
39
+ view_spec #{write_source(klass.client_config.view.block)}
40
+
41
+ entity_spec #{write_source(klass.client_config.api.block)}
42
+
43
+ client_spec #{write_source(klass.client_config.entity.block)}
44
+ #{footers.join("\n")}
45
+ TXT
46
+ end
47
+
48
+ private
49
+
50
+ def write_source(block)
51
+ return unless block
52
+
53
+ RubyVM::AbstractSyntaxTree
54
+ .of(block, keep_script_lines: true)
55
+ .source
56
+ .strip
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,210 @@
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(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.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
+ 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:)
185
+
186
+ registry.serialize
187
+
188
+ (dirs + GEM_DIRS + [registry.build_dir])
189
+ .flat_map { paths(_1) }
190
+ .flatten
191
+ .map(&:to_s)
192
+ .then { Serializer.new(paths: _1).as_json }
193
+ end
194
+
195
+ private
196
+
197
+ def paths(path)
198
+ if File.file?(path)
199
+ [path]
200
+ else
201
+ [
202
+ Dir.glob(File.join(path, "*.rb")),
203
+ Dir.glob(File.join(path, "**/*.rb")),
204
+ ].flatten
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Server
5
+ module BasicRoutable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attach(self)
10
+ end
11
+
12
+
13
+ class_methods do
14
+ def attach(mod)
15
+ mod.shared do
16
+ accessor(:api_path, accepted_by: :client)
17
+ end
18
+
19
+ mod.server do
20
+ attr_accessor :api_path
21
+ end
22
+ end
23
+
24
+ def inherited(mod)
25
+ attach(mod)
26
+ end
27
+
28
+ attr_accessor :bus
29
+ def find(&block)
30
+ @find = block if block_given?
31
+ @find
32
+ end
33
+
34
+ def build(klass, store)
35
+ bus.registry.build(klass, store)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,74 @@
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.bus = bus
14
+ klass
15
+ .find
16
+ .call(id, query:)
17
+ .tap { _1.entity.api_path = resolved_path }
18
+ end
19
+ end
20
+
21
+ attr_reader :paths, :bus, :default
22
+ def initialize(bus:, paths:, default: nil)
23
+ @bus = bus
24
+ @paths = paths
25
+ @default = default
26
+ end
27
+
28
+ def call(url, query: {})
29
+ if url.nil? && default
30
+ klass = klass_for(default)
31
+ evaluator.call(klass:, id: nil, query:, resolved_path: default)
32
+ end
33
+
34
+ path = url.sub(/^#{bus.root_url}/, "/").sub(/^/, "/").sub(%r{^/*}, "/")
35
+
36
+ if normalized_paths.key?(path)
37
+ klass = klass_for(path)
38
+
39
+ evaluator.call(klass:, id: nil, query:, resolved_path: path)
40
+ else
41
+ *parts, id = path.split("/")
42
+ resolved_base_path = parts.join("/")
43
+
44
+ if normalized_paths.key?(resolved_base_path)
45
+ klass = klass_for(resolved_base_path)
46
+ evaluator.call(klass:, id:, query:, resolved_path: path)
47
+ elsif default
48
+ klass = klass_for(default)
49
+ evaluator.call(klass:, id: nil, query:, resolved_path: default)
50
+ else
51
+ raise "unhandled route: #{path}"
52
+ end
53
+ end
54
+ end
55
+
56
+ def evaluator
57
+ @evaluator ||= Evaluator.new(bus:)
58
+ end
59
+
60
+ def klass_for(path)
61
+ val = normalized_paths.fetch(path)
62
+
63
+ (val.is_a?(String) ? Object.const_get(val) : val)
64
+ end
65
+
66
+ def normalized_paths
67
+ @normalized_paths ||= (
68
+ paths
69
+ .transform_keys { _1.start_with?("/") ? _1 : "/#{_1}" }
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,39 @@
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(
13
+ root_url:,
14
+ asset_paths:,
15
+ facets:,
16
+ build_dir:
17
+ )
18
+ @registry = Registry.new(facets:, build_dir:)
19
+ @root_url = root_url
20
+ @asset_paths = asset_paths
21
+ end
22
+
23
+ def client_system_json
24
+ {
25
+ assets: Assets.bundle(dirs: asset_paths, registry:),
26
+ root_url:
27
+ }
28
+ end
29
+
30
+ def build(name:, store:)
31
+ registry.build(name, store)
32
+ end
33
+
34
+ def handle(name:, id:, event:)
35
+ registry.load(name, id).handle(event)
36
+ end
37
+ end
38
+ end
39
+ 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::Server::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,51 @@
1
+ # frozen_string_literal: true
2
+ require "active_support/concern"
3
+
4
+ module Interfacets
5
+ module Server
6
+ module Facet
7
+ extend ActiveSupport::Concern
8
+
9
+ class Actor
10
+ def initialize(entity:, facet:)
11
+ @entity = entity
12
+ @facet = facet
13
+ end
14
+ end
15
+
16
+ class_methods do
17
+ def view(&block)
18
+ views << block
19
+ end
20
+
21
+ def views
22
+ @views ||= []
23
+ end
24
+
25
+ def client(&block)
26
+ clients << block
27
+ end
28
+
29
+ def clients
30
+ @clients ||= []
31
+ end
32
+
33
+ def shared(&block)
34
+ shareds << block
35
+ end
36
+
37
+ def shareds
38
+ @shareds ||= []
39
+ end
40
+
41
+ def server(&block)
42
+ servers << block
43
+ end
44
+
45
+ def servers
46
+ @servers ||= []
47
+ end
48
+ end
49
+ end
50
+ end
51
+ 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