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.
- checksums.yaml +7 -0
- data/API.md +78 -0
- data/README.md +250 -0
- data/THIRD_PARTY_NOTICES.md +45 -0
- data/examples/guides/01_plain_use_case.rb +13 -0
- data/examples/guides/02_composed_application.rb +25 -0
- data/examples/guides/03_order_domain.rb +14 -0
- data/examples/guides/04_scoped_resources.rb +38 -0
- data/examples/guides/05_mapping.rb +39 -0
- data/examples/guides/06_associations.rb +49 -0
- data/examples/guides/07_http.rb +17 -0
- data/examples/guides/08_errors.rb +20 -0
- data/examples/guides/09_sqlite.rb +38 -0
- data/examples/guides/10_grpc.rb +26 -0
- data/examples/guides/11_ractor.rb +15 -0
- data/examples/guides/12_box.rb +11 -0
- data/examples/guides/13_testing.rb +24 -0
- data/examples/guides/manifest.yml +62 -0
- data/examples/orders/application.rb +51 -0
- data/examples/orders/contracts.rb +19 -0
- data/examples/orders/domain.rb +35 -0
- data/examples/orders/export_proto.rb +5 -0
- data/examples/orders/grpc_client.py +24 -0
- data/examples/orders/grpc_server.rb +23 -0
- data/examples/orders/server.rb +18 -0
- data/lib/object_framework/application.rb +136 -0
- data/lib/object_framework/capabilities.rb +59 -0
- data/lib/object_framework/errors.rb +85 -0
- data/lib/object_framework/execution.rb +184 -0
- data/lib/object_framework/grpc/messages.rb +30 -0
- data/lib/object_framework/grpc/server.rb +264 -0
- data/lib/object_framework/grpc/service.rb +55 -0
- data/lib/object_framework/http/io.rb +127 -0
- data/lib/object_framework/http/presentation.rb +55 -0
- data/lib/object_framework/http/protocol.rb +209 -0
- data/lib/object_framework/http/router.rb +80 -0
- data/lib/object_framework/http/server.rb +279 -0
- data/lib/object_framework/http2/connection.rb +399 -0
- data/lib/object_framework/http2/frame.rb +21 -0
- data/lib/object_framework/http2/hpack.rb +206 -0
- data/lib/object_framework/http2/hpack_tables.rb +333 -0
- data/lib/object_framework/http2/inbound.rb +75 -0
- data/lib/object_framework/memory/storage.rb +97 -0
- data/lib/object_framework/persistence/mapping.rb +150 -0
- data/lib/object_framework/persistence/query.rb +97 -0
- data/lib/object_framework/persistence/session.rb +173 -0
- data/lib/object_framework/persistence/transaction.rb +54 -0
- data/lib/object_framework/protobuf.rb +268 -0
- data/lib/object_framework/sqlite/compiler.rb +77 -0
- data/lib/object_framework/sqlite/schema.rb +95 -0
- data/lib/object_framework/sqlite/storage.rb +220 -0
- data/lib/object_framework/sqlite.rb +6 -0
- data/lib/object_framework/version.rb +5 -0
- data/lib/object_framework.rb +31 -0
- 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
|