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,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module Persistence
5
+ Criterion = Data.define(:field, :operator, :value)
6
+
7
+ class Query
8
+ OPERATORS = %i[eq ne gt gte lt lte in].freeze
9
+ attr_reader :table, :criteria, :ordering, :row_limit, :row_offset
10
+
11
+ def self.identifier(name)
12
+ raise ConfigurationError, "Invalid storage identifier: #{name.inspect}" unless /\A[a-zA-Z_][a-zA-Z0-9_]*\z/.match?(name.to_s)
13
+ name.to_sym
14
+ end
15
+
16
+ def initialize(table: nil, criteria: [], ordering: [], row_limit: nil, row_offset: 0)
17
+ @table = self.class.identifier(table) if table
18
+ @criteria, @ordering = criteria.freeze, ordering.freeze
19
+ @row_limit, @row_offset = row_limit, row_offset
20
+ freeze
21
+ end
22
+
23
+ def for_table(table)
24
+ copy(table:)
25
+ end
26
+
27
+ def where(**attributes)
28
+ attributes.reduce(self) { |query, (field, value)| query.compare(field, :eq, value) }
29
+ end
30
+
31
+ def compare(field, operator, value)
32
+ raise ConfigurationError, "Unsupported query operator" unless OPERATORS.include?(operator)
33
+ raise ConfigurationError, "IN requires an array" if operator == :in && !value.is_a?(Array)
34
+ criterion = Criterion.new(field: self.class.identifier(field), operator:, value: Values.detach(value))
35
+ copy(criteria: criteria + [criterion])
36
+ end
37
+
38
+ def order(field, direction = :asc)
39
+ raise ConfigurationError, "Invalid ordering direction" unless %i[asc desc].include?(direction)
40
+ copy(ordering: ordering + [[self.class.identifier(field), direction].freeze])
41
+ end
42
+
43
+ def limit(count)
44
+ copy(row_limit: nonnegative(count))
45
+ end
46
+
47
+ def offset(count)
48
+ copy(row_offset: nonnegative(count))
49
+ end
50
+
51
+ def apply(rows)
52
+ selected = rows.select { |row| criteria.all? { |criterion| matches?(row, criterion) } }
53
+ ordering = @ordering.any? { |field, _| field == :id } ? @ordering : @ordering + [[:id, :asc]]
54
+ selected.sort! do |left, right|
55
+ ordering.each do |field, direction|
56
+ a, b = left[field], right[field]
57
+ comparison = if a.nil? then b.nil? ? 0 : -1 elsif b.nil? then 1 else a <=> b end
58
+ raise InvalidInput, "Incomparable query values" unless comparison
59
+ break(direction == :desc ? -comparison : comparison) unless comparison.zero?
60
+ end.then { |result| result.is_a?(Integer) ? result : 0 }
61
+ end
62
+ selected = selected.drop(row_offset)
63
+ row_limit ? selected.take(row_limit) : selected
64
+ end
65
+
66
+ private
67
+
68
+ def copy(**changes)
69
+ self.class.new(**{ table:, criteria:, ordering:, row_limit:, row_offset: }.merge(changes))
70
+ end
71
+
72
+ def nonnegative(count)
73
+ raise ConfigurationError, "Expected a nonnegative integer" unless count.is_a?(Integer) && count >= 0
74
+ count
75
+ end
76
+
77
+ def matches?(row, criterion)
78
+ actual, expected = row[criterion.field], criterion.value
79
+ case criterion.operator
80
+ when :eq then actual == expected
81
+ when :ne then !actual.nil? && actual != expected
82
+ when :in then criterion.value.include?(actual)
83
+ else
84
+ return false if actual.nil? || expected.nil?
85
+ comparison = actual <=> expected
86
+ return false unless comparison
87
+ case criterion.operator
88
+ when :gt then comparison > 0
89
+ when :gte then comparison >= 0
90
+ when :lt then comparison < 0
91
+ when :lte then comparison <= 0
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module Persistence
5
+ class IdentityMap
6
+ def initialize
7
+ @entities = {}
8
+ end
9
+
10
+ def find(type, id); @entities[[type, id]]; end
11
+
12
+ def add(type, id, entity)
13
+ existing = find(type, id)
14
+ raise Duplicate, "Identity already belongs to another object" if existing && !existing.equal?(entity)
15
+ @entities[[type, Values.detach(id)]] = entity
16
+ end
17
+
18
+ def remove(type, id); @entities.delete([type, id]); end
19
+ def clear; @entities.clear; end
20
+ end
21
+
22
+ Entry = Struct.new(:entity, :identity, :mapping, :baseline, :state, :loaded, keyword_init: true)
23
+
24
+ class UnitOfWork
25
+ def initialize(transaction:)
26
+ @transaction, @entries = transaction, {}.compare_by_identity
27
+ end
28
+
29
+ def entry(entity); @entries.fetch(entity); end
30
+ def tracked?(entity); @entries.key?(entity); end
31
+
32
+ def track(entity, mapping:, baseline:, state: :clean)
33
+ @entries[entity] ||= Entry.new(entity:, identity: Values.detach(mapping.identity.reader.call(entity)), mapping:, baseline:, state:, loaded: [])
34
+ end
35
+
36
+ def remove(entity)
37
+ entry = @entries.fetch(entity) { raise InvalidInput, "Entity is not managed by this session" }
38
+ if entry.state == :new
39
+ @entries.delete(entity)
40
+ else
41
+ entry.state = :deleted
42
+ end
43
+ end
44
+
45
+ def flush
46
+ @entries.values.each do |entry|
47
+ entity, mapping = entry.entity, entry.mapping
48
+ id = mapping.identity.reader.call(entity)
49
+ raise InvalidInput, "Managed identity cannot change" unless entry.identity == id
50
+ if entry.state == :deleted
51
+ @transaction.delete(mapping.table, id)
52
+ @entries.delete(entity)
53
+ next
54
+ end
55
+ row = mapping.dump(entity, baseline: entry.baseline, loaded: entry.loaded)
56
+ if entry.state == :new
57
+ @transaction.insert(mapping.table, row)
58
+ elsif row != entry.baseline
59
+ @transaction.update(mapping.table, id, row)
60
+ end
61
+ entry.baseline, entry.state = row, :clean
62
+ end
63
+ end
64
+
65
+ def clear; @entries.clear; end
66
+ end
67
+
68
+ class Session
69
+ attr_reader :transaction
70
+
71
+ def initialize(transaction:, mappings:)
72
+ @transaction, @mappings = transaction, mappings
73
+ @identities, @work = IdentityMap.new, UnitOfWork.new(transaction:)
74
+ @closed, @removed = false, {}
75
+ end
76
+
77
+ def add(entity)
78
+ active!
79
+ mapping = @mappings.fetch(entity.class)
80
+ unless mapping.identity.reader.call(entity)
81
+ mapping.identity.writer.call(entity, SecureRandom.uuid)
82
+ end
83
+ if @removed.key?([entity.class, mapping.identity.reader.call(entity)])
84
+ raise InvalidInput, "Removed entities cannot be registered again"
85
+ end
86
+ @identities.add(entity.class, mapping.identity.reader.call(entity), entity)
87
+ @work.track(entity, mapping:, baseline: nil, state: :new)
88
+ entity
89
+ end
90
+
91
+ def find(type, id, load: [])
92
+ active!
93
+ raise NotFound, "Entity was removed" if @removed.key?([type, id])
94
+ entity = @identities.find(type, id)
95
+ unless entity
96
+ mapping = @mappings.fetch(type)
97
+ row = transaction.find(mapping.table, id)
98
+ raise NotFound, "Entity not found" unless row
99
+ entity = attach(mapping, row)
100
+ end
101
+ load_associations(entity, load)
102
+ entity
103
+ end
104
+
105
+ def query(type, query = Query.new, load: [])
106
+ active!
107
+ mapping = @mappings.fetch(type)
108
+ transaction.query(query.for_table(mapping.table)).reject { |row| @removed.key?([type, row[:id]]) }.map do |row|
109
+ entity = @identities.find(type, row[:id]) || attach(mapping, row)
110
+ load_associations(entity, load)
111
+ entity
112
+ end
113
+ end
114
+
115
+ def remove(entity)
116
+ active!
117
+ id = @work.entry(entity).identity
118
+ @work.remove(entity)
119
+ @removed[[entity.class, id]] = true
120
+ @identities.remove(entity.class, id)
121
+ end
122
+
123
+ def flush
124
+ active!
125
+ @work.flush
126
+ end
127
+
128
+ def close
129
+ @closed = true
130
+ @identities.clear
131
+ @work.clear
132
+ @removed.clear
133
+ end
134
+
135
+ private
136
+
137
+ def active!
138
+ raise ClosedSession, "Session is closed" if @closed
139
+ raise ClosedSession, "Session transaction is no longer active" unless transaction.state == :active
140
+ end
141
+
142
+ def attach(mapping, row)
143
+ entity = mapping.hydrate(row)
144
+ @identities.add(mapping.entity_class, row[:id], entity)
145
+ @work.track(entity, mapping:, baseline: row)
146
+ entity
147
+ end
148
+
149
+ def load_associations(entity, plan)
150
+ entry = @work.entry(entity)
151
+ plan.each do |item|
152
+ name, nested = item.is_a?(Hash) ? item.first : [item, []]
153
+ association = entry.mapping.associations.find { |candidate| candidate.name == name }
154
+ raise ConfigurationError, "Unknown association: #{name}" unless association
155
+ unless entry.loaded.include?(name)
156
+ entry.loaded << name
157
+ value = if association.kind == :belongs_to
158
+ id = entry.baseline&.fetch(association.foreign_key, nil)
159
+ find(association.target, id) if id
160
+ else
161
+ id = entry.mapping.identity.reader.call(entity)
162
+ query(association.target, Query.new.where(**{ association.foreign_key => id }))
163
+ end
164
+ association.writer.call(entity, value)
165
+ end
166
+ value = association.reader.call(entity)
167
+ children = association.kind == :has_many ? value : [value].compact
168
+ children.each { |child| load_associations(child, nested) } unless nested.empty?
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module Persistence
5
+ class ClosedTransaction < StandardError; end
6
+ class ReadOnlyTransaction < StandardError; end
7
+
8
+ class Transaction
9
+ attr_reader :state, :mode
10
+
11
+ def initialize(mode:)
12
+ raise ConfigurationError, "Invalid transaction mode" unless %i[read write].include?(mode)
13
+ @mode, @state, @closed = mode, :active, false
14
+ end
15
+
16
+ def rollback
17
+ active!
18
+ rollback_storage
19
+ @state = :rolled_back
20
+ end
21
+
22
+ def close
23
+ return if @closed
24
+ begin
25
+ rollback if @state == :active
26
+ ensure
27
+ @closed = true
28
+ close_storage
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def active!
35
+ raise ClosedTransaction, "Transaction is no longer active" if @closed || @state != :active
36
+ end
37
+
38
+ def writable!
39
+ active!
40
+ raise ReadOnlyTransaction, "Cannot modify a read transaction" unless mode == :write
41
+ end
42
+
43
+ def row_value(row, id: row[:id])
44
+ raise InvalidInput, "A row identity is required" unless id.is_a?(String) && !id.empty?
45
+ raise InvalidInput, "Row identity cannot change" unless row[:id] == id
46
+ row.each_key { |field| Query.identifier(field) }
47
+ Values.detach(row)
48
+ end
49
+
50
+ def rollback_storage; end
51
+ def close_storage; end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module Protobuf
5
+ SCALARS = {
6
+ int32: 0, int64: 0, uint32: 0, uint64: 0, sint32: 0, sint64: 0, bool: 0,
7
+ fixed64: 1, sfixed64: 1, double: 1, string: 2, bytes: 2,
8
+ fixed32: 5, sfixed32: 5, float: 5
9
+ }.freeze
10
+
11
+ def self.name(value)
12
+ raise ConfigurationError, "Invalid protobuf name" unless /\A[A-Za-z_][A-Za-z_0-9]*\z/.match?(value.to_s)
13
+ value.to_s.dup.freeze
14
+ end
15
+
16
+ module Wire
17
+ def self.varint(value)
18
+ raise InvalidInput, "Varint is out of range" unless value.is_a?(Integer) && value.between?(0, 2**64 - 1)
19
+ bytes = +"".b
20
+ while value >= 128
21
+ bytes << ((value & 127) | 128)
22
+ value >>= 7
23
+ end
24
+ bytes << value
25
+ end
26
+
27
+ class Cursor
28
+ attr_reader :position
29
+ def initialize(bytes); @bytes, @position = bytes.b, 0; end
30
+ def done?; @position == @bytes.bytesize; end
31
+
32
+ def take(count)
33
+ raise InvalidInput, "Truncated protobuf field" if count.negative? || @position + count > @bytes.bytesize
34
+ result = @bytes.byteslice(@position, count)
35
+ @position += count
36
+ result
37
+ end
38
+
39
+ def varint
40
+ result = 0
41
+ 10.times do |index|
42
+ byte = take(1).getbyte(0)
43
+ raise InvalidInput, "Varint overflow" if index == 9 && byte > 1
44
+ result |= (byte & 127) << (index * 7)
45
+ return result if byte < 128
46
+ end
47
+ raise InvalidInput, "Varint overflow"
48
+ end
49
+
50
+ def skip(wire)
51
+ case wire
52
+ when 0 then varint
53
+ when 1 then take(8)
54
+ when 2 then take(varint)
55
+ when 5 then take(4)
56
+ else raise InvalidInput, "Unsupported protobuf wire type"
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ class Enum
63
+ attr_reader :name, :values
64
+ def initialize(name, **values)
65
+ @name, @values = Protobuf.name(name), values.dup.freeze
66
+ raise ConfigurationError, "A proto3 enum must begin with zero" unless values.values.first == 0
67
+ raise ConfigurationError, "Duplicate enum value" unless values.values.uniq.size == values.size
68
+ values.each { |key, value| Protobuf.name(key); raise ConfigurationError, "Invalid enum value" unless value.is_a?(Integer) && value.between?(-(2**31), 2**31 - 1) }
69
+ freeze
70
+ end
71
+
72
+ def definition
73
+ "enum #{name} {\n" + values.map { |key, value| " #{key} = #{value};\n" }.join + "}\n"
74
+ end
75
+ end
76
+
77
+ Field = Data.define(:name, :number, :type, :repeated, :packed)
78
+
79
+ class Message
80
+ attr_reader :name, :fields
81
+
82
+ def initialize(name, &declaration)
83
+ @name, @fields = Protobuf.name(name), []
84
+ instance_eval(&declaration) if declaration
85
+ @numbers = fields.to_h { |field| [field.number, field] }.freeze
86
+ @fields.freeze
87
+ freeze
88
+ end
89
+
90
+ def field(name, number, type, repeated: false, packed: true)
91
+ name = Protobuf.name(name).to_sym
92
+ unless number.is_a?(Integer) && number.between?(1, 2**29 - 1) && !number.between?(19_000, 19_999)
93
+ raise ConfigurationError, "Invalid protobuf field number"
94
+ end
95
+ raise ConfigurationError, "Duplicate protobuf field" if fields.any? { |field| field.name == name || field.number == number }
96
+ raise ConfigurationError, "Unknown protobuf type" unless SCALARS.key?(type) || type.is_a?(Message) || type.is_a?(Enum)
97
+ @fields << Field.new(name:, number:, type:, repeated:, packed:)
98
+ end
99
+
100
+ def encode(values = nil, depth: 0, **attributes)
101
+ raise InvalidInput, "Protobuf nesting limit exceeded" if depth >= 64
102
+ raise InvalidInput, "Pass message values once" if values && !attributes.empty?
103
+ values ||= attributes
104
+ values = values.to_h if values.is_a?(Data)
105
+ raise InvalidInput, "Expected protobuf message values" unless values.is_a?(Hash)
106
+ values = values.transform_keys { |key| key.to_s.to_sym }
107
+ raise InvalidInput, "Unknown protobuf field" unless (values.keys - fields.map(&:name)).empty?
108
+ fields.each_with_object(+"".b) do |field, output|
109
+ next unless values.key?(field.name)
110
+ value = values[field.name]
111
+ if field.repeated
112
+ raise InvalidInput, "Repeated fields require arrays" unless value.is_a?(Array)
113
+ if packable?(field) && field.packed && !value.empty?
114
+ payload = value.map { |item| encode_value(field.type, item, depth:) }.join.b
115
+ output << Wire.varint((field.number << 3) | 2) << Wire.varint(payload.bytesize) << payload
116
+ else
117
+ value.each { |item| output << encode_field(field, item, depth:) }
118
+ end
119
+ elsif !default?(field.type, value)
120
+ output << encode_field(field, value, depth:)
121
+ end
122
+ end
123
+ end
124
+
125
+ def decode(bytes, depth: 0, initial: nil)
126
+ raise InvalidInput, "Protobuf nesting limit exceeded" if depth >= 64
127
+ cursor = Wire::Cursor.new(bytes)
128
+ result = initial ? Values.mutable_copy(initial) : fields.to_h { |field| [field.name, default_value(field)] }
129
+ until cursor.done?
130
+ tag = cursor.varint
131
+ number, wire = tag >> 3, tag & 7
132
+ raise InvalidInput, "Invalid protobuf field tag" unless number.between?(1, 2**29 - 1)
133
+ field = @numbers[number]
134
+ if field && field.repeated && packable?(field) && wire == 2
135
+ packed = Wire::Cursor.new(cursor.take(cursor.varint))
136
+ result[field.name] << decode_value(field.type, packed, depth:) until packed.done?
137
+ elsif field && wire == wire_type(field.type)
138
+ value = decode_value(field.type, cursor, depth:, initial: field.type.is_a?(Message) && !field.repeated ? result[field.name] : nil)
139
+ field.repeated ? result[field.name] << value : result[field.name] = value
140
+ else
141
+ cursor.skip(wire)
142
+ end
143
+ end
144
+ Values.detach(result)
145
+ end
146
+
147
+ def dependencies
148
+ fields.map(&:type).select { |type| type.is_a?(Message) || type.is_a?(Enum) }
149
+ end
150
+
151
+ def definition
152
+ "message #{name} {\n" + fields.map do |field|
153
+ type = field.type.respond_to?(:name) ? field.type.name : field.type.to_s
154
+ option = field.repeated && packable?(field) && !field.packed ? " [packed = false]" : ""
155
+ " #{field.repeated ? 'repeated ' : ''}#{type} #{field.name} = #{field.number}#{option};\n"
156
+ end.join + "}\n"
157
+ end
158
+
159
+ private
160
+
161
+ def wire_type(type)
162
+ return 2 if type.is_a?(Message)
163
+ return 0 if type.is_a?(Enum)
164
+ SCALARS.fetch(type)
165
+ end
166
+
167
+ def packable?(field); wire_type(field.type) != 2; end
168
+
169
+ def default_value(field)
170
+ return [] if field.repeated
171
+ return nil if field.type.is_a?(Message)
172
+ return 0 if field.type.is_a?(Enum)
173
+ case field.type
174
+ when :string then ""
175
+ when :bytes then "".b
176
+ when :bool then false
177
+ when :float, :double then 0.0
178
+ else 0
179
+ end
180
+ end
181
+
182
+ def default?(type, value)
183
+ return value.nil? if type.is_a?(Message)
184
+ return value.is_a?(Integer) && value.zero? if type.is_a?(Enum)
185
+ case type
186
+ when :string, :bytes then value == ""
187
+ when :bool then value == false
188
+ when :float, :double then value == 0 && !value.to_s.start_with?("-")
189
+ else value.is_a?(Integer) && value.zero?
190
+ end
191
+ end
192
+
193
+ def encode_field(field, value, depth:)
194
+ payload = encode_value(field.type, value, depth:)
195
+ tag = Wire.varint((field.number << 3) | wire_type(field.type))
196
+ wire_type(field.type) == 2 ? tag + Wire.varint(payload.bytesize) + payload : tag + payload
197
+ end
198
+
199
+ def bounded(value, bits:, signed:)
200
+ minimum, maximum = signed ? [-(2**(bits - 1)), 2**(bits - 1) - 1] : [0, 2**bits - 1]
201
+ raise InvalidInput, "Protobuf integer is out of range" unless value.is_a?(Integer) && value.between?(minimum, maximum)
202
+ value
203
+ end
204
+
205
+ def encode_value(type, value, depth:)
206
+ return type.encode(value, depth: depth + 1) if type.is_a?(Message)
207
+ if type.is_a?(Enum)
208
+ value = type.values.fetch(value) { raise InvalidInput, "Unknown enum symbol" } if value.is_a?(Symbol)
209
+ return Wire.varint(bounded(value, bits: 32, signed: true) & (2**64 - 1))
210
+ end
211
+ case type
212
+ when :string, :bytes
213
+ raise InvalidInput, "Expected a protobuf string" unless value.is_a?(String)
214
+ raise InvalidInput, "Invalid UTF-8 string" if type == :string && !value.dup.force_encoding(Encoding::UTF_8).valid_encoding?
215
+ value.b
216
+ when :bool
217
+ raise InvalidInput, "Expected a protobuf boolean" unless value == true || value == false
218
+ Wire.varint(value ? 1 : 0)
219
+ when :float, :double
220
+ raise InvalidInput, "Expected a protobuf number" unless value.is_a?(Numeric)
221
+ [value].pack(type == :float ? "e" : "E")
222
+ else
223
+ bits = type.to_s.end_with?("32") ? 32 : 64
224
+ signed = !%i[uint32 uint64 fixed32 fixed64].include?(type)
225
+ value = bounded(value, bits:, signed:)
226
+ case type
227
+ when :sint32, :sint64 then Wire.varint((value << 1) ^ (value >> (bits - 1)))
228
+ when :fixed32 then [value].pack("V")
229
+ when :sfixed32 then [value].pack("l<")
230
+ when :fixed64 then [value].pack("Q<")
231
+ when :sfixed64 then [value].pack("q<")
232
+ else Wire.varint(value & (2**64 - 1))
233
+ end
234
+ end
235
+ end
236
+
237
+ def decode_value(type, cursor, depth:, initial: nil)
238
+ return type.decode(cursor.take(cursor.varint), depth: depth + 1, initial:) if type.is_a?(Message)
239
+ if type.is_a?(Enum)
240
+ value = cursor.varint & (2**32 - 1)
241
+ return value >= 2**31 ? value - 2**32 : value
242
+ end
243
+ case type
244
+ when :string, :bytes
245
+ value = cursor.take(cursor.varint)
246
+ if type == :string
247
+ value.force_encoding(Encoding::UTF_8)
248
+ raise InvalidInput, "Invalid UTF-8 string" unless value.valid_encoding?
249
+ end
250
+ value
251
+ when :bool then !cursor.varint.zero?
252
+ when :float then cursor.take(4).unpack1("e")
253
+ when :double then cursor.take(8).unpack1("E")
254
+ when :fixed32 then cursor.take(4).unpack1("V")
255
+ when :sfixed32 then cursor.take(4).unpack1("l<")
256
+ when :fixed64 then cursor.take(8).unpack1("Q<")
257
+ when :sfixed64 then cursor.take(8).unpack1("q<")
258
+ else
259
+ bits = type.to_s.end_with?("32") ? 32 : 64
260
+ value = cursor.varint & (2**bits - 1)
261
+ return (value >> 1) ^ -(value & 1) if %i[sint32 sint64].include?(type)
262
+ return value - 2**bits if %i[int32 int64].include?(type) && value >= 2**(bits - 1)
263
+ value
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module SQLite
5
+ class Compiler
6
+ OPERATORS = { eq: "=", ne: "!=", gt: ">", gte: ">=", lt: "<", lte: "<=" }.freeze
7
+
8
+ def initialize(schema:)
9
+ @schema = schema
10
+ end
11
+
12
+ def query(query)
13
+ @schema.table(query.table)
14
+ bindings = []
15
+ sql = "SELECT * FROM #{@schema.quote(query.table)}"
16
+ unless query.criteria.empty?
17
+ sql += " WHERE " + query.criteria.map { |criterion| predicate(query.table, criterion, bindings) }.join(" AND ")
18
+ end
19
+ ordering = query.ordering.any? { |field, _| field == :id } ? query.ordering : query.ordering + [[:id, :asc]]
20
+ sql += " ORDER BY " + ordering.map do |field, direction|
21
+ @schema.type(query.table, field)
22
+ "#{@schema.quote(field)} #{direction == :desc ? 'DESC' : 'ASC'}"
23
+ end.join(", ")
24
+ if query.row_limit || query.row_offset.positive?
25
+ sql += " LIMIT ? OFFSET ?"
26
+ bindings += [query.row_limit || -1, query.row_offset]
27
+ end
28
+ [sql, bindings]
29
+ end
30
+
31
+ def encode(table, column, value)
32
+ type = @schema.type(table, column)
33
+ return nil if value.nil?
34
+ valid = case type
35
+ when :string, :blob then value.is_a?(String)
36
+ when :integer then value.is_a?(Integer) && value.between?(-(2**63), 2**63 - 1)
37
+ when :float then value.is_a?(Numeric) && value.to_f.finite?
38
+ when :boolean then value == true || value == false
39
+ end
40
+ raise InvalidInput, "Invalid storage field type" unless valid
41
+ return value ? 1 : 0 if type == :boolean
42
+ return ::SQLite3::Blob.new(value) if type == :blob
43
+ value
44
+ end
45
+
46
+ def decode(table, row)
47
+ return nil unless row
48
+ Values.detach(row.to_h do |column, value|
49
+ type = @schema.type(table, column)
50
+ [column.to_sym, type == :boolean && !value.nil? ? value != 0 : value]
51
+ end)
52
+ end
53
+
54
+ private
55
+
56
+ def predicate(table, criterion, bindings)
57
+ field = @schema.quote(criterion.field)
58
+ @schema.type(table, criterion.field)
59
+ if criterion.operator == :in
60
+ values = criterion.value.reject(&:nil?)
61
+ parts = []
62
+ unless values.empty?
63
+ bindings.concat(values.map { |value| encode(table, criterion.field, value) })
64
+ parts << "#{field} IN (#{(['?'] * values.size).join(', ')})"
65
+ end
66
+ parts << "#{field} IS NULL" if criterion.value.include?(nil)
67
+ return parts.empty? ? "0" : "(#{parts.join(' OR ')})"
68
+ end
69
+ if criterion.value.nil? && %i[eq ne].include?(criterion.operator)
70
+ return "#{field} IS #{criterion.operator == :ne ? 'NOT ' : ''}NULL"
71
+ end
72
+ bindings << encode(table, criterion.field, criterion.value)
73
+ "#{field} #{OPERATORS.fetch(criterion.operator)} ?"
74
+ end
75
+ end
76
+ end
77
+ end