ruby-object-framework 1.0.0

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/API.md +78 -0
  3. data/README.md +250 -0
  4. data/THIRD_PARTY_NOTICES.md +45 -0
  5. data/examples/guides/01_plain_use_case.rb +13 -0
  6. data/examples/guides/02_composed_application.rb +25 -0
  7. data/examples/guides/03_order_domain.rb +14 -0
  8. data/examples/guides/04_scoped_resources.rb +38 -0
  9. data/examples/guides/05_mapping.rb +39 -0
  10. data/examples/guides/06_associations.rb +49 -0
  11. data/examples/guides/07_http.rb +17 -0
  12. data/examples/guides/08_errors.rb +20 -0
  13. data/examples/guides/09_sqlite.rb +38 -0
  14. data/examples/guides/10_grpc.rb +26 -0
  15. data/examples/guides/11_ractor.rb +15 -0
  16. data/examples/guides/12_box.rb +11 -0
  17. data/examples/guides/13_testing.rb +24 -0
  18. data/examples/guides/manifest.yml +62 -0
  19. data/examples/orders/application.rb +51 -0
  20. data/examples/orders/contracts.rb +19 -0
  21. data/examples/orders/domain.rb +35 -0
  22. data/examples/orders/export_proto.rb +5 -0
  23. data/examples/orders/grpc_client.py +24 -0
  24. data/examples/orders/grpc_server.rb +23 -0
  25. data/examples/orders/server.rb +18 -0
  26. data/lib/object_framework/application.rb +136 -0
  27. data/lib/object_framework/capabilities.rb +59 -0
  28. data/lib/object_framework/errors.rb +85 -0
  29. data/lib/object_framework/execution.rb +184 -0
  30. data/lib/object_framework/grpc/messages.rb +30 -0
  31. data/lib/object_framework/grpc/server.rb +264 -0
  32. data/lib/object_framework/grpc/service.rb +55 -0
  33. data/lib/object_framework/http/io.rb +127 -0
  34. data/lib/object_framework/http/presentation.rb +55 -0
  35. data/lib/object_framework/http/protocol.rb +209 -0
  36. data/lib/object_framework/http/router.rb +80 -0
  37. data/lib/object_framework/http/server.rb +279 -0
  38. data/lib/object_framework/http2/connection.rb +399 -0
  39. data/lib/object_framework/http2/frame.rb +21 -0
  40. data/lib/object_framework/http2/hpack.rb +206 -0
  41. data/lib/object_framework/http2/hpack_tables.rb +333 -0
  42. data/lib/object_framework/http2/inbound.rb +75 -0
  43. data/lib/object_framework/memory/storage.rb +97 -0
  44. data/lib/object_framework/persistence/mapping.rb +150 -0
  45. data/lib/object_framework/persistence/query.rb +97 -0
  46. data/lib/object_framework/persistence/session.rb +173 -0
  47. data/lib/object_framework/persistence/transaction.rb +54 -0
  48. data/lib/object_framework/protobuf.rb +268 -0
  49. data/lib/object_framework/sqlite/compiler.rb +77 -0
  50. data/lib/object_framework/sqlite/schema.rb +95 -0
  51. data/lib/object_framework/sqlite/storage.rb +220 -0
  52. data/lib/object_framework/sqlite.rb +6 -0
  53. data/lib/object_framework/version.rb +5 -0
  54. data/lib/object_framework.rb +31 -0
  55. metadata +105 -0
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Protocol tables from RFC 7541, Appendices A and B.
4
+ # Copyright (c) 2015 IETF Trust and the persons identified as the document authors.
5
+ # See THIRD_PARTY_NOTICES.md for the applicable Simplified BSD license.
6
+ module ObjectFramework
7
+ module HTTP2
8
+ module HPACK
9
+ STATIC_TABLE = [
10
+ [":authority", ""].freeze,
11
+ [":method", "GET"].freeze,
12
+ [":method", "POST"].freeze,
13
+ [":path", "/"].freeze,
14
+ [":path", "/index.html"].freeze,
15
+ [":scheme", "http"].freeze,
16
+ [":scheme", "https"].freeze,
17
+ [":status", "200"].freeze,
18
+ [":status", "204"].freeze,
19
+ [":status", "206"].freeze,
20
+ [":status", "304"].freeze,
21
+ [":status", "400"].freeze,
22
+ [":status", "404"].freeze,
23
+ [":status", "500"].freeze,
24
+ ["accept-charset", ""].freeze,
25
+ ["accept-encoding", "gzip, deflate"].freeze,
26
+ ["accept-language", ""].freeze,
27
+ ["accept-ranges", ""].freeze,
28
+ ["accept", ""].freeze,
29
+ ["access-control-allow-origin", ""].freeze,
30
+ ["age", ""].freeze,
31
+ ["allow", ""].freeze,
32
+ ["authorization", ""].freeze,
33
+ ["cache-control", ""].freeze,
34
+ ["content-disposition", ""].freeze,
35
+ ["content-encoding", ""].freeze,
36
+ ["content-language", ""].freeze,
37
+ ["content-length", ""].freeze,
38
+ ["content-location", ""].freeze,
39
+ ["content-range", ""].freeze,
40
+ ["content-type", ""].freeze,
41
+ ["cookie", ""].freeze,
42
+ ["date", ""].freeze,
43
+ ["etag", ""].freeze,
44
+ ["expect", ""].freeze,
45
+ ["expires", ""].freeze,
46
+ ["from", ""].freeze,
47
+ ["host", ""].freeze,
48
+ ["if-match", ""].freeze,
49
+ ["if-modified-since", ""].freeze,
50
+ ["if-none-match", ""].freeze,
51
+ ["if-range", ""].freeze,
52
+ ["if-unmodified-since", ""].freeze,
53
+ ["last-modified", ""].freeze,
54
+ ["link", ""].freeze,
55
+ ["location", ""].freeze,
56
+ ["max-forwards", ""].freeze,
57
+ ["proxy-authenticate", ""].freeze,
58
+ ["proxy-authorization", ""].freeze,
59
+ ["range", ""].freeze,
60
+ ["referer", ""].freeze,
61
+ ["refresh", ""].freeze,
62
+ ["retry-after", ""].freeze,
63
+ ["server", ""].freeze,
64
+ ["set-cookie", ""].freeze,
65
+ ["strict-transport-security", ""].freeze,
66
+ ["transfer-encoding", ""].freeze,
67
+ ["user-agent", ""].freeze,
68
+ ["vary", ""].freeze,
69
+ ["via", ""].freeze,
70
+ ["www-authenticate", ""].freeze,
71
+ ].freeze
72
+ HUFFMAN_CODES = [
73
+ [0x1ff8, 13].freeze,
74
+ [0x7fffd8, 23].freeze,
75
+ [0xfffffe2, 28].freeze,
76
+ [0xfffffe3, 28].freeze,
77
+ [0xfffffe4, 28].freeze,
78
+ [0xfffffe5, 28].freeze,
79
+ [0xfffffe6, 28].freeze,
80
+ [0xfffffe7, 28].freeze,
81
+ [0xfffffe8, 28].freeze,
82
+ [0xffffea, 24].freeze,
83
+ [0x3ffffffc, 30].freeze,
84
+ [0xfffffe9, 28].freeze,
85
+ [0xfffffea, 28].freeze,
86
+ [0x3ffffffd, 30].freeze,
87
+ [0xfffffeb, 28].freeze,
88
+ [0xfffffec, 28].freeze,
89
+ [0xfffffed, 28].freeze,
90
+ [0xfffffee, 28].freeze,
91
+ [0xfffffef, 28].freeze,
92
+ [0xffffff0, 28].freeze,
93
+ [0xffffff1, 28].freeze,
94
+ [0xffffff2, 28].freeze,
95
+ [0x3ffffffe, 30].freeze,
96
+ [0xffffff3, 28].freeze,
97
+ [0xffffff4, 28].freeze,
98
+ [0xffffff5, 28].freeze,
99
+ [0xffffff6, 28].freeze,
100
+ [0xffffff7, 28].freeze,
101
+ [0xffffff8, 28].freeze,
102
+ [0xffffff9, 28].freeze,
103
+ [0xffffffa, 28].freeze,
104
+ [0xffffffb, 28].freeze,
105
+ [0x14, 6].freeze,
106
+ [0x3f8, 10].freeze,
107
+ [0x3f9, 10].freeze,
108
+ [0xffa, 12].freeze,
109
+ [0x1ff9, 13].freeze,
110
+ [0x15, 6].freeze,
111
+ [0xf8, 8].freeze,
112
+ [0x7fa, 11].freeze,
113
+ [0x3fa, 10].freeze,
114
+ [0x3fb, 10].freeze,
115
+ [0xf9, 8].freeze,
116
+ [0x7fb, 11].freeze,
117
+ [0xfa, 8].freeze,
118
+ [0x16, 6].freeze,
119
+ [0x17, 6].freeze,
120
+ [0x18, 6].freeze,
121
+ [0x0, 5].freeze,
122
+ [0x1, 5].freeze,
123
+ [0x2, 5].freeze,
124
+ [0x19, 6].freeze,
125
+ [0x1a, 6].freeze,
126
+ [0x1b, 6].freeze,
127
+ [0x1c, 6].freeze,
128
+ [0x1d, 6].freeze,
129
+ [0x1e, 6].freeze,
130
+ [0x1f, 6].freeze,
131
+ [0x5c, 7].freeze,
132
+ [0xfb, 8].freeze,
133
+ [0x7ffc, 15].freeze,
134
+ [0x20, 6].freeze,
135
+ [0xffb, 12].freeze,
136
+ [0x3fc, 10].freeze,
137
+ [0x1ffa, 13].freeze,
138
+ [0x21, 6].freeze,
139
+ [0x5d, 7].freeze,
140
+ [0x5e, 7].freeze,
141
+ [0x5f, 7].freeze,
142
+ [0x60, 7].freeze,
143
+ [0x61, 7].freeze,
144
+ [0x62, 7].freeze,
145
+ [0x63, 7].freeze,
146
+ [0x64, 7].freeze,
147
+ [0x65, 7].freeze,
148
+ [0x66, 7].freeze,
149
+ [0x67, 7].freeze,
150
+ [0x68, 7].freeze,
151
+ [0x69, 7].freeze,
152
+ [0x6a, 7].freeze,
153
+ [0x6b, 7].freeze,
154
+ [0x6c, 7].freeze,
155
+ [0x6d, 7].freeze,
156
+ [0x6e, 7].freeze,
157
+ [0x6f, 7].freeze,
158
+ [0x70, 7].freeze,
159
+ [0x71, 7].freeze,
160
+ [0x72, 7].freeze,
161
+ [0xfc, 8].freeze,
162
+ [0x73, 7].freeze,
163
+ [0xfd, 8].freeze,
164
+ [0x1ffb, 13].freeze,
165
+ [0x7fff0, 19].freeze,
166
+ [0x1ffc, 13].freeze,
167
+ [0x3ffc, 14].freeze,
168
+ [0x22, 6].freeze,
169
+ [0x7ffd, 15].freeze,
170
+ [0x3, 5].freeze,
171
+ [0x23, 6].freeze,
172
+ [0x4, 5].freeze,
173
+ [0x24, 6].freeze,
174
+ [0x5, 5].freeze,
175
+ [0x25, 6].freeze,
176
+ [0x26, 6].freeze,
177
+ [0x27, 6].freeze,
178
+ [0x6, 5].freeze,
179
+ [0x74, 7].freeze,
180
+ [0x75, 7].freeze,
181
+ [0x28, 6].freeze,
182
+ [0x29, 6].freeze,
183
+ [0x2a, 6].freeze,
184
+ [0x7, 5].freeze,
185
+ [0x2b, 6].freeze,
186
+ [0x76, 7].freeze,
187
+ [0x2c, 6].freeze,
188
+ [0x8, 5].freeze,
189
+ [0x9, 5].freeze,
190
+ [0x2d, 6].freeze,
191
+ [0x77, 7].freeze,
192
+ [0x78, 7].freeze,
193
+ [0x79, 7].freeze,
194
+ [0x7a, 7].freeze,
195
+ [0x7b, 7].freeze,
196
+ [0x7ffe, 15].freeze,
197
+ [0x7fc, 11].freeze,
198
+ [0x3ffd, 14].freeze,
199
+ [0x1ffd, 13].freeze,
200
+ [0xffffffc, 28].freeze,
201
+ [0xfffe6, 20].freeze,
202
+ [0x3fffd2, 22].freeze,
203
+ [0xfffe7, 20].freeze,
204
+ [0xfffe8, 20].freeze,
205
+ [0x3fffd3, 22].freeze,
206
+ [0x3fffd4, 22].freeze,
207
+ [0x3fffd5, 22].freeze,
208
+ [0x7fffd9, 23].freeze,
209
+ [0x3fffd6, 22].freeze,
210
+ [0x7fffda, 23].freeze,
211
+ [0x7fffdb, 23].freeze,
212
+ [0x7fffdc, 23].freeze,
213
+ [0x7fffdd, 23].freeze,
214
+ [0x7fffde, 23].freeze,
215
+ [0xffffeb, 24].freeze,
216
+ [0x7fffdf, 23].freeze,
217
+ [0xffffec, 24].freeze,
218
+ [0xffffed, 24].freeze,
219
+ [0x3fffd7, 22].freeze,
220
+ [0x7fffe0, 23].freeze,
221
+ [0xffffee, 24].freeze,
222
+ [0x7fffe1, 23].freeze,
223
+ [0x7fffe2, 23].freeze,
224
+ [0x7fffe3, 23].freeze,
225
+ [0x7fffe4, 23].freeze,
226
+ [0x1fffdc, 21].freeze,
227
+ [0x3fffd8, 22].freeze,
228
+ [0x7fffe5, 23].freeze,
229
+ [0x3fffd9, 22].freeze,
230
+ [0x7fffe6, 23].freeze,
231
+ [0x7fffe7, 23].freeze,
232
+ [0xffffef, 24].freeze,
233
+ [0x3fffda, 22].freeze,
234
+ [0x1fffdd, 21].freeze,
235
+ [0xfffe9, 20].freeze,
236
+ [0x3fffdb, 22].freeze,
237
+ [0x3fffdc, 22].freeze,
238
+ [0x7fffe8, 23].freeze,
239
+ [0x7fffe9, 23].freeze,
240
+ [0x1fffde, 21].freeze,
241
+ [0x7fffea, 23].freeze,
242
+ [0x3fffdd, 22].freeze,
243
+ [0x3fffde, 22].freeze,
244
+ [0xfffff0, 24].freeze,
245
+ [0x1fffdf, 21].freeze,
246
+ [0x3fffdf, 22].freeze,
247
+ [0x7fffeb, 23].freeze,
248
+ [0x7fffec, 23].freeze,
249
+ [0x1fffe0, 21].freeze,
250
+ [0x1fffe1, 21].freeze,
251
+ [0x3fffe0, 22].freeze,
252
+ [0x1fffe2, 21].freeze,
253
+ [0x7fffed, 23].freeze,
254
+ [0x3fffe1, 22].freeze,
255
+ [0x7fffee, 23].freeze,
256
+ [0x7fffef, 23].freeze,
257
+ [0xfffea, 20].freeze,
258
+ [0x3fffe2, 22].freeze,
259
+ [0x3fffe3, 22].freeze,
260
+ [0x3fffe4, 22].freeze,
261
+ [0x7ffff0, 23].freeze,
262
+ [0x3fffe5, 22].freeze,
263
+ [0x3fffe6, 22].freeze,
264
+ [0x7ffff1, 23].freeze,
265
+ [0x3ffffe0, 26].freeze,
266
+ [0x3ffffe1, 26].freeze,
267
+ [0xfffeb, 20].freeze,
268
+ [0x7fff1, 19].freeze,
269
+ [0x3fffe7, 22].freeze,
270
+ [0x7ffff2, 23].freeze,
271
+ [0x3fffe8, 22].freeze,
272
+ [0x1ffffec, 25].freeze,
273
+ [0x3ffffe2, 26].freeze,
274
+ [0x3ffffe3, 26].freeze,
275
+ [0x3ffffe4, 26].freeze,
276
+ [0x7ffffde, 27].freeze,
277
+ [0x7ffffdf, 27].freeze,
278
+ [0x3ffffe5, 26].freeze,
279
+ [0xfffff1, 24].freeze,
280
+ [0x1ffffed, 25].freeze,
281
+ [0x7fff2, 19].freeze,
282
+ [0x1fffe3, 21].freeze,
283
+ [0x3ffffe6, 26].freeze,
284
+ [0x7ffffe0, 27].freeze,
285
+ [0x7ffffe1, 27].freeze,
286
+ [0x3ffffe7, 26].freeze,
287
+ [0x7ffffe2, 27].freeze,
288
+ [0xfffff2, 24].freeze,
289
+ [0x1fffe4, 21].freeze,
290
+ [0x1fffe5, 21].freeze,
291
+ [0x3ffffe8, 26].freeze,
292
+ [0x3ffffe9, 26].freeze,
293
+ [0xffffffd, 28].freeze,
294
+ [0x7ffffe3, 27].freeze,
295
+ [0x7ffffe4, 27].freeze,
296
+ [0x7ffffe5, 27].freeze,
297
+ [0xfffec, 20].freeze,
298
+ [0xfffff3, 24].freeze,
299
+ [0xfffed, 20].freeze,
300
+ [0x1fffe6, 21].freeze,
301
+ [0x3fffe9, 22].freeze,
302
+ [0x1fffe7, 21].freeze,
303
+ [0x1fffe8, 21].freeze,
304
+ [0x7ffff3, 23].freeze,
305
+ [0x3fffea, 22].freeze,
306
+ [0x3fffeb, 22].freeze,
307
+ [0x1ffffee, 25].freeze,
308
+ [0x1ffffef, 25].freeze,
309
+ [0xfffff4, 24].freeze,
310
+ [0xfffff5, 24].freeze,
311
+ [0x3ffffea, 26].freeze,
312
+ [0x7ffff4, 23].freeze,
313
+ [0x3ffffeb, 26].freeze,
314
+ [0x7ffffe6, 27].freeze,
315
+ [0x3ffffec, 26].freeze,
316
+ [0x3ffffed, 26].freeze,
317
+ [0x7ffffe7, 27].freeze,
318
+ [0x7ffffe8, 27].freeze,
319
+ [0x7ffffe9, 27].freeze,
320
+ [0x7ffffea, 27].freeze,
321
+ [0x7ffffeb, 27].freeze,
322
+ [0xffffffe, 28].freeze,
323
+ [0x7ffffec, 27].freeze,
324
+ [0x7ffffed, 27].freeze,
325
+ [0x7ffffee, 27].freeze,
326
+ [0x7ffffef, 27].freeze,
327
+ [0x7fffff0, 27].freeze,
328
+ [0x3ffffee, 26].freeze,
329
+ [0x3fffffff, 30].freeze,
330
+ ].freeze
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module HTTP2
5
+ class Inbound
6
+ def initialize(cancellation:, on_consume: ->(_) {}, buffer_limit: 65_535)
7
+ @cancellation, @on_consume, @limit = cancellation, on_consume, buffer_limit
8
+ @mutex, @changed, @queue = Mutex.new, ConditionVariable.new, []
9
+ @bytes, @finished, @discard = 0, false, false
10
+ end
11
+
12
+ def push(bytes)
13
+ discarded = @mutex.synchronize do
14
+ if @discard
15
+ bytes.bytesize
16
+ else
17
+ raise ProtocolError.new("Inbound stream exceeds flow control", code: 3) if @bytes + bytes.bytesize > @limit
18
+ @queue << bytes.b.freeze unless bytes.empty?
19
+ @bytes += bytes.bytesize
20
+ @changed.broadcast
21
+ 0
22
+ end
23
+ end
24
+ @on_consume.call(discarded) if discarded.positive?
25
+ end
26
+
27
+ def finish
28
+ @mutex.synchronize { @finished = true; @changed.broadcast }
29
+ end
30
+
31
+ def discard
32
+ bytes = @mutex.synchronize do
33
+ @discard, @finished = true, true
34
+ discarded, @bytes = @bytes, 0
35
+ @queue.clear
36
+ @changed.broadcast
37
+ discarded
38
+ end
39
+ @on_consume.call(bytes) if bytes.positive?
40
+ end
41
+
42
+ def cancel
43
+ @cancellation.cancel
44
+ @mutex.synchronize { @changed.broadcast }
45
+ end
46
+
47
+ def read_exact(count, allow_eof: false)
48
+ result = +"".b
49
+ while result.bytesize < count
50
+ part = @mutex.synchronize do
51
+ loop do
52
+ @cancellation.check!
53
+ unless @queue.empty?
54
+ first = @queue.shift
55
+ length = [count - result.bytesize, first.bytesize].min
56
+ @queue.unshift(first.byteslice(length..)) if length < first.bytesize
57
+ @bytes -= length
58
+ break first.byteslice(0, length)
59
+ end
60
+ if @finished
61
+ return nil if allow_eof && result.empty?
62
+ raise InvalidInput, "Truncated gRPC message"
63
+ end
64
+ remaining = @cancellation.deadline ? @cancellation.deadline - HTTP::Timing.now : 0.1
65
+ @changed.wait(@mutex, [[remaining, 0].max, 0.1].min)
66
+ end
67
+ end
68
+ result << part
69
+ @on_consume.call(part.bytesize)
70
+ end
71
+ result
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module Memory
5
+ class Storage
6
+ def initialize
7
+ @mutex, @tables, @revision, @closed = Mutex.new, {}, 0, false
8
+ end
9
+
10
+ def begin_transaction(mode:)
11
+ @mutex.synchronize do
12
+ raise Unavailable, "Storage is closed" if @closed
13
+ Transaction.new(storage: self, tables: @tables.transform_values(&:dup), revision: @revision, mode:)
14
+ end
15
+ end
16
+
17
+ def publish(tables, revision:)
18
+ @mutex.synchronize do
19
+ raise Unavailable, "Storage is closed" if @closed
20
+ raise Conflict, "Snapshot revision changed" unless revision == @revision
21
+ # Publishing the whole table set under one lock makes multi-row commits atomic.
22
+ @tables = tables.transform_values { |rows| rows.dup.freeze }.freeze
23
+ @revision += 1
24
+ end
25
+ end
26
+
27
+ def close
28
+ @mutex.synchronize { @closed = true; @tables = {} }
29
+ end
30
+ end
31
+
32
+ class Transaction < Persistence::Transaction
33
+ def initialize(storage:, tables:, revision:, mode:)
34
+ super(mode:)
35
+ @storage, @tables, @revision, @dirty = storage, tables, revision, false
36
+ end
37
+
38
+ def find(table, id)
39
+ active!
40
+ row = rows(table)[id]
41
+ Values.detach(row) if row
42
+ end
43
+
44
+ def query(query)
45
+ active!
46
+ Values.detach(query.apply(rows(query.table).values))
47
+ end
48
+
49
+ def insert(table, row)
50
+ writable!
51
+ value = row_value(row)
52
+ raise Duplicate, "Duplicate identity" if rows(table).key?(value[:id])
53
+ rows(table)[value[:id]] = value
54
+ @dirty = true
55
+ value[:id]
56
+ end
57
+
58
+ def update(table, id, row)
59
+ writable!
60
+ raise NotFound, "Unknown identity" unless rows(table).key?(id)
61
+ rows(table)[id] = row_value(row, id:)
62
+ @dirty = true
63
+ end
64
+
65
+ def delete(table, id)
66
+ writable!
67
+ raise NotFound, "Unknown identity" unless rows(table).key?(id)
68
+ rows(table).delete(id)
69
+ @dirty = true
70
+ end
71
+
72
+ def commit
73
+ active!
74
+ @storage.publish(@tables, revision: @revision) if @dirty
75
+ @state = :committed
76
+ rescue Conflict
77
+ @state = :rolled_back
78
+ @tables.clear
79
+ raise
80
+ end
81
+
82
+ private
83
+
84
+ def rows(table)
85
+ @tables[Persistence::Query.identifier(table)] ||= {}
86
+ end
87
+
88
+ def rollback_storage
89
+ @tables.clear
90
+ end
91
+
92
+ def close_storage
93
+ @tables.clear
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module Persistence
5
+ class Converter
6
+ def initialize(dump: ->(value) { value }, load: ->(value) { value })
7
+ @dump, @load = dump, load
8
+ freeze
9
+ end
10
+
11
+ def dump(value); @dump.call(value); end
12
+ def load(value); @load.call(value); end
13
+ end
14
+
15
+ Field = Data.define(:name, :column, :type, :converter, :reader, :writer)
16
+ Association = Data.define(:name, :kind, :target, :foreign_key, :reader, :writer)
17
+
18
+ module AssociationDSL
19
+ %i[belongs_to has_many].each do |kind|
20
+ define_method(kind) do |name, target:, foreign_key:, read: nil, write: nil|
21
+ mutable!
22
+ name = Query.identifier(name)
23
+ raise ConfigurationError, "Duplicate mapped attribute" if (@fields + @associations).any? { |item| item.name == name }
24
+ foreign_key = Query.identifier(foreign_key)
25
+ if kind == :belongs_to && columns.include?(foreign_key)
26
+ raise ConfigurationError, "Duplicate storage column"
27
+ end
28
+ @associations << Association.new(name:, kind:, target:, foreign_key:,
29
+ reader: read || ->(entity) { entity.public_send(name) },
30
+ writer: write || ->(entity, value) { entity.public_send("#{name}=", value) })
31
+ self
32
+ end
33
+ end
34
+ end
35
+
36
+ class Mapping
37
+ include AssociationDSL
38
+ TYPES = %i[string integer float boolean blob].freeze
39
+ attr_reader :entity_class, :table, :fields, :associations
40
+
41
+ def initialize(entity_class, table:)
42
+ @entity_class, @table = entity_class, Query.identifier(table)
43
+ @fields, @associations = [], []
44
+ @builder = ->(attributes) { entity_class.new(**attributes) }
45
+ end
46
+
47
+ def field(name, column: name, type: :string, converter: Converter.new, read: nil, write: nil)
48
+ mutable!
49
+ name, column = Query.identifier(name), Query.identifier(column)
50
+ raise ConfigurationError, "Unsupported column type" unless TYPES.include?(type)
51
+ raise ConfigurationError, "Duplicate mapped column or attribute" if columns.include?(column) || (@fields + @associations).any? { |item| item.name == name }
52
+ @fields << Field.new(name:, column:, type:, converter:,
53
+ reader: read || ->(entity) { entity.public_send(name) },
54
+ writer: write || ->(entity, value) { entity.public_send("#{name}=", value) })
55
+ self
56
+ end
57
+
58
+ def build(&builder)
59
+ mutable!
60
+ @builder = builder
61
+ self
62
+ end
63
+
64
+ def identity
65
+ fields.find { |field| field.name == :id } || raise(ConfigurationError, "Mapping requires an id field")
66
+ end
67
+
68
+ def columns
69
+ fields.map(&:column) + associations.select { |item| item.kind == :belongs_to }.map(&:foreign_key)
70
+ end
71
+
72
+ def schema
73
+ fields.to_h { |field| [field.column, field.type] }.merge(
74
+ associations.select { |item| item.kind == :belongs_to }.to_h { |item| [item.foreign_key, :string] }
75
+ ).freeze
76
+ end
77
+
78
+ def hydrate(row)
79
+ @builder.call(fields.to_h { |field| [field.name, field.converter.load(Values.mutable_copy(row[field.column]))] })
80
+ end
81
+
82
+ def dump(entity, baseline: nil, loaded: [])
83
+ row = fields.to_h { |field| [field.column, field.converter.dump(field.reader.call(entity))] }
84
+ associations.select { |association| association.kind == :belongs_to }.each do |association|
85
+ target = association.reader.call(entity)
86
+ row[association.foreign_key] = if target
87
+ target.public_send(:id) || raise(InvalidInput, "Associated entities must be registered before flush")
88
+ elsif baseline && !loaded.include?(association.name)
89
+ baseline[association.foreign_key]
90
+ end
91
+ end
92
+ Values.detach(baseline ? baseline.merge(row) : row)
93
+ end
94
+
95
+ def finalize!
96
+ identity
97
+ raise ConfigurationError, "Identity must map to the id column" unless identity.column == :id
98
+ fields.freeze
99
+ associations.freeze
100
+ freeze
101
+ end
102
+
103
+ private
104
+
105
+ def mutable!
106
+ raise ConfigurationError, "Mapping is finalized" if frozen?
107
+ end
108
+ end
109
+
110
+ class Mappings
111
+ include Enumerable
112
+
113
+ def initialize
114
+ @mappings = {}
115
+ end
116
+
117
+ def define(entity_class, table:, &description)
118
+ raise ConfigurationError, "Mappings are finalized" if @mappings.frozen?
119
+ raise ConfigurationError, "Duplicate entity mapping" if @mappings.key?(entity_class)
120
+ raise ConfigurationError, "Duplicate table mapping" if @mappings.values.any? { |mapping| mapping.table == table.to_sym }
121
+ mapping = Mapping.new(entity_class, table:)
122
+ mapping.instance_eval(&description) if description
123
+ @mappings[entity_class] = mapping
124
+ end
125
+
126
+ def each(&block)
127
+ @mappings.values.each(&block)
128
+ end
129
+
130
+ def fetch(entity_class)
131
+ @mappings.fetch(entity_class) { raise ConfigurationError, "Unmapped entity: #{entity_class}" }
132
+ end
133
+
134
+ def validate!
135
+ each do |mapping|
136
+ mapping.identity
137
+ mapping.associations.each do |association|
138
+ target = fetch(association.target)
139
+ if association.kind == :has_many && !target.columns.include?(association.foreign_key)
140
+ raise ConfigurationError, "Collection foreign key is not mapped"
141
+ end
142
+ end
143
+ end
144
+ each(&:finalize!)
145
+ @mappings.freeze
146
+ self
147
+ end
148
+ end
149
+ end
150
+ end