yadm 0.1
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/.gitignore +8 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +273 -0
- data/Rakefile +10 -0
- data/examples/basic.rb +43 -0
- data/examples/migration.rb +65 -0
- data/lib/yadm.rb +39 -0
- data/lib/yadm/adapters.rb +32 -0
- data/lib/yadm/adapters/common_sql.rb +120 -0
- data/lib/yadm/adapters/memory.rb +175 -0
- data/lib/yadm/adapters/mysql.rb +17 -0
- data/lib/yadm/adapters/postgresql.rb +17 -0
- data/lib/yadm/adapters/sqlite.rb +17 -0
- data/lib/yadm/criteria.rb +32 -0
- data/lib/yadm/criteria/argument.rb +22 -0
- data/lib/yadm/criteria/attribute.rb +15 -0
- data/lib/yadm/criteria/condition.rb +31 -0
- data/lib/yadm/criteria/expression.rb +19 -0
- data/lib/yadm/criteria/limit.rb +25 -0
- data/lib/yadm/criteria/order.rb +48 -0
- data/lib/yadm/criteria_parser.rb +62 -0
- data/lib/yadm/criteria_parser/expression_parser.rb +77 -0
- data/lib/yadm/entity.rb +53 -0
- data/lib/yadm/identity_map.rb +51 -0
- data/lib/yadm/mapper.rb +16 -0
- data/lib/yadm/mapping.rb +71 -0
- data/lib/yadm/mapping/attribute.rb +31 -0
- data/lib/yadm/query.rb +28 -0
- data/lib/yadm/repository.rb +103 -0
- data/lib/yadm/version.rb +3 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/criteria_helpers.rb +33 -0
- data/spec/support/sequel_helpers.rb +25 -0
- data/spec/support/shared_examples_for_a_sequel_adapter.rb +173 -0
- data/spec/yadm/adapters/common_sql_spec.rb +89 -0
- data/spec/yadm/adapters/memory_spec.rb +230 -0
- data/spec/yadm/adapters/mysql_spec.rb +9 -0
- data/spec/yadm/adapters/postgresql_spec.rb +9 -0
- data/spec/yadm/adapters/sqlite_spec.rb +5 -0
- data/spec/yadm/adapters_spec.rb +32 -0
- data/spec/yadm/criteria/condition_spec.rb +50 -0
- data/spec/yadm/criteria/limit_spec.rb +45 -0
- data/spec/yadm/criteria/order_spec.rb +50 -0
- data/spec/yadm/criteria_parser/expression_parser_spec.rb +47 -0
- data/spec/yadm/criteria_parser_spec.rb +55 -0
- data/spec/yadm/criteria_spec.rb +40 -0
- data/spec/yadm/entity_spec.rb +76 -0
- data/spec/yadm/identity_map_spec.rb +128 -0
- data/spec/yadm/mapper_spec.rb +23 -0
- data/spec/yadm/mapping/attribute_spec.rb +35 -0
- data/spec/yadm/mapping_spec.rb +122 -0
- data/spec/yadm/query_spec.rb +45 -0
- data/spec/yadm/repository_spec.rb +175 -0
- data/spec/yadm_spec.rb +45 -0
- data/yadm.gemspec +33 -0
- metadata +254 -0
data/lib/yadm.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'yadm/version'
|
2
|
+
require 'yadm/entity'
|
3
|
+
require 'yadm/adapters'
|
4
|
+
require 'yadm/identity_map'
|
5
|
+
require 'yadm/mapping'
|
6
|
+
require 'yadm/mapper'
|
7
|
+
require 'yadm/criteria'
|
8
|
+
require 'yadm/query'
|
9
|
+
require 'yadm/criteria_parser'
|
10
|
+
require 'yadm/repository'
|
11
|
+
|
12
|
+
module YADM
|
13
|
+
class << self
|
14
|
+
def setup(&block)
|
15
|
+
instance_eval(&block) unless block.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def data_source(name, adapter:, **connection_params)
|
19
|
+
data_source = Adapters.fetch(adapter).new(connection_params)
|
20
|
+
data_sources[name] = IdentityMap.new(data_source)
|
21
|
+
end
|
22
|
+
|
23
|
+
def map(&block)
|
24
|
+
mapper.instance_eval(&block) unless block.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def migrate(data_source_name, &block)
|
28
|
+
data_sources.fetch(data_source_name).migrate(block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def data_sources
|
32
|
+
@data_sources ||= {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def mapper
|
36
|
+
@mapper ||= Mapper.new
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module YADM
|
2
|
+
module Adapters
|
3
|
+
class << self
|
4
|
+
def fetch(name)
|
5
|
+
registry.fetch(name)
|
6
|
+
rescue KeyError
|
7
|
+
raise NotImplementedError, "Adapter `#{name.inspect}` isn't registered."
|
8
|
+
end
|
9
|
+
|
10
|
+
def register(name, adapter)
|
11
|
+
registry[name] = adapter
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def registry
|
16
|
+
@registry ||= {}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Base
|
21
|
+
def self.included(including_module)
|
22
|
+
including_module.extend ClassMethods
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
def register(name)
|
27
|
+
Adapters.register(name, self)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
|
3
|
+
module YADM
|
4
|
+
module Adapters
|
5
|
+
module CommonSQL
|
6
|
+
attr_reader :connection
|
7
|
+
private :connection
|
8
|
+
|
9
|
+
def get(table_name, id)
|
10
|
+
connection[table_name][id: id]
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(table_name, new_attributes)
|
14
|
+
new_attributes = new_attributes.dup
|
15
|
+
new_attributes.delete(:id)
|
16
|
+
connection[table_name].insert(new_attributes)
|
17
|
+
end
|
18
|
+
|
19
|
+
def change(table_name, id, new_attributes)
|
20
|
+
connection[table_name].where(id: id).update(new_attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove(table_name, id)
|
24
|
+
connection[table_name].where(id: id).delete
|
25
|
+
end
|
26
|
+
|
27
|
+
def count(table_name)
|
28
|
+
connection[table_name].count
|
29
|
+
end
|
30
|
+
|
31
|
+
def send_query(table_name, query)
|
32
|
+
result = filter(from(table_name), query.criteria.condition, query.arguments)
|
33
|
+
result = order(result, query.criteria.order, query.arguments)
|
34
|
+
result = limit(result, query.criteria.limit, query.arguments)
|
35
|
+
|
36
|
+
result.to_a
|
37
|
+
end
|
38
|
+
|
39
|
+
def from(table_name)
|
40
|
+
connection[table_name]
|
41
|
+
end
|
42
|
+
|
43
|
+
def filter(dataset, condition, arguments)
|
44
|
+
if condition.nil?
|
45
|
+
dataset
|
46
|
+
else
|
47
|
+
sequel_expression = sequelize(condition.expression, arguments)
|
48
|
+
dataset.where(sequel_expression)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def order(dataset, order, arguments)
|
53
|
+
if order.nil?
|
54
|
+
dataset
|
55
|
+
else
|
56
|
+
order.clauses.inject(dataset) do |dataset, clause|
|
57
|
+
sequel_expression = sequelize(clause.expression, arguments)
|
58
|
+
dataset.order_more(sequel_expression.send(clause.type))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def limit(dataset, limit, arguments)
|
64
|
+
if limit.nil? || limit.limit.nil?
|
65
|
+
dataset
|
66
|
+
else
|
67
|
+
take(dataset, limit.limit, arguments)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def migrate(block)
|
72
|
+
block.call(connection)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
def sequelize(node, arguments)
|
77
|
+
CommonSQL.sequelize(node, arguments)
|
78
|
+
end
|
79
|
+
|
80
|
+
def take(dataset, limit, arguments)
|
81
|
+
number = if limit.is_a?(Criteria::Argument)
|
82
|
+
limit.fetch_from(arguments)
|
83
|
+
else
|
84
|
+
limit
|
85
|
+
end
|
86
|
+
|
87
|
+
dataset.limit(number)
|
88
|
+
end
|
89
|
+
|
90
|
+
class << self
|
91
|
+
def sequelize(node, arguments)
|
92
|
+
case node
|
93
|
+
when Criteria::Expression
|
94
|
+
operator = sequelize_operator(node.method_name)
|
95
|
+
receiver = sequelize(node.receiver, arguments)
|
96
|
+
arguments = node.arguments.map { |arg| sequelize(arg, arguments) }
|
97
|
+
|
98
|
+
Sequel::SQL::ComplexExpression.new(operator, receiver, *arguments)
|
99
|
+
when Criteria::Attribute
|
100
|
+
Sequel::SQL::Identifier.new(node.name)
|
101
|
+
when Criteria::Argument
|
102
|
+
node.fetch_from(arguments)
|
103
|
+
else
|
104
|
+
node
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
def sequelize_operator(operator)
|
110
|
+
case operator
|
111
|
+
when :== then :'='
|
112
|
+
when :& then :AND
|
113
|
+
when :| then :OR
|
114
|
+
else operator
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module YADM
|
2
|
+
module Adapters
|
3
|
+
class Memory
|
4
|
+
include Base
|
5
|
+
|
6
|
+
register :memory
|
7
|
+
|
8
|
+
attr_reader :collections
|
9
|
+
private :collections
|
10
|
+
|
11
|
+
def initialize(connection_params = {})
|
12
|
+
# Memory adapter doesn't need any connection.
|
13
|
+
@collections = Hash.new do |hash, collection_name|
|
14
|
+
hash[collection_name] = Collection.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(collection_name, id)
|
19
|
+
collections[collection_name].get(id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def add(collection_name, record)
|
23
|
+
collections[collection_name].add(record)
|
24
|
+
end
|
25
|
+
|
26
|
+
def change(collection_name, id, new_attributes)
|
27
|
+
collections[collection_name].change(id, new_attributes)
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove(collection_name, id)
|
31
|
+
collections[collection_name].remove(id)
|
32
|
+
end
|
33
|
+
|
34
|
+
def count(collection_name)
|
35
|
+
collections[collection_name].count
|
36
|
+
end
|
37
|
+
|
38
|
+
def send_query(collection_name, query)
|
39
|
+
collections[collection_name].send_query(query)
|
40
|
+
end
|
41
|
+
|
42
|
+
def migrate(block)
|
43
|
+
# do nothing here (memory adapter doesn't need migrations)
|
44
|
+
end
|
45
|
+
|
46
|
+
class Collection
|
47
|
+
attr_reader :records
|
48
|
+
private :records
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
@records = {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def get(id)
|
55
|
+
records.fetch(id)
|
56
|
+
end
|
57
|
+
|
58
|
+
def add(record)
|
59
|
+
next_id.tap do |new_id|
|
60
|
+
records[new_id] = record.merge(id: new_id)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def change(id, new_attributes)
|
65
|
+
records[id].update(new_attributes)
|
66
|
+
end
|
67
|
+
|
68
|
+
def remove(id)
|
69
|
+
records.delete(id)
|
70
|
+
end
|
71
|
+
|
72
|
+
def count
|
73
|
+
records.count
|
74
|
+
end
|
75
|
+
|
76
|
+
def send_query(query)
|
77
|
+
result = filter(all, query.criteria.condition, query.arguments)
|
78
|
+
result = order(result, query.criteria.order, query.arguments)
|
79
|
+
result = limit(result, query.criteria.limit, query.arguments)
|
80
|
+
end
|
81
|
+
|
82
|
+
def all
|
83
|
+
records.values.dup
|
84
|
+
end
|
85
|
+
|
86
|
+
def filter(dataset, condition, arguments)
|
87
|
+
if condition.nil?
|
88
|
+
dataset
|
89
|
+
else
|
90
|
+
dataset.select { |record| matches?(record, condition.expression, arguments) }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def order(dataset, order, arguments)
|
95
|
+
if order.nil?
|
96
|
+
dataset
|
97
|
+
else
|
98
|
+
dataset.sort { |*records| compare(records, order.clauses, arguments) }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def limit(dataset, limit, arguments)
|
103
|
+
if limit.nil? || limit.limit.nil?
|
104
|
+
dataset
|
105
|
+
else
|
106
|
+
take(dataset, limit.limit, arguments)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
def next_id
|
112
|
+
id_sequence.next
|
113
|
+
end
|
114
|
+
|
115
|
+
def id_sequence
|
116
|
+
@sequence ||= Enumerator.new do |yielder|
|
117
|
+
id = 0
|
118
|
+
loop do
|
119
|
+
id += 1
|
120
|
+
yielder.yield id
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def matches?(record, expression, arguments)
|
126
|
+
!!record_eval(record, expression, arguments)
|
127
|
+
end
|
128
|
+
|
129
|
+
def compare(records, clauses, arguments)
|
130
|
+
clauses.inject(0) do |comparison, clause|
|
131
|
+
return comparison unless comparison.zero?
|
132
|
+
|
133
|
+
values = records.map do |record|
|
134
|
+
record_eval(record, clause.expression, arguments)
|
135
|
+
end
|
136
|
+
|
137
|
+
if clause.asc?
|
138
|
+
values.first <=> values.last
|
139
|
+
else
|
140
|
+
values.last <=> values.first
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def take(records, limit, arguments)
|
146
|
+
number = if limit.is_a?(Criteria::Argument)
|
147
|
+
limit.fetch_from(arguments)
|
148
|
+
else
|
149
|
+
limit
|
150
|
+
end
|
151
|
+
|
152
|
+
records.take(number)
|
153
|
+
end
|
154
|
+
|
155
|
+
def record_eval(record, node, arguments)
|
156
|
+
case node
|
157
|
+
when Criteria::Expression
|
158
|
+
receiver = record_eval(record, node.receiver, arguments)
|
159
|
+
arguments = node.arguments.map { |arg| record_eval(record, arg, arguments) }
|
160
|
+
|
161
|
+
receiver.send(node.method_name, *arguments)
|
162
|
+
when Criteria::Attribute
|
163
|
+
record.fetch(node.name) do
|
164
|
+
raise ArgumentError, "#{node.name.inspect} attribute not found."
|
165
|
+
end
|
166
|
+
when Criteria::Argument
|
167
|
+
node.fetch_from(arguments)
|
168
|
+
else
|
169
|
+
node
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'yadm/adapters/common_sql'
|
2
|
+
require 'mysql2'
|
3
|
+
|
4
|
+
module YADM
|
5
|
+
module Adapters
|
6
|
+
class MySQL
|
7
|
+
include Base
|
8
|
+
include CommonSQL
|
9
|
+
|
10
|
+
register :mysql
|
11
|
+
|
12
|
+
def initialize(connection_parameters = {})
|
13
|
+
@connection = Sequel.connect(adapter: :mysql2, **connection_parameters)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'yadm/adapters/common_sql'
|
2
|
+
require 'pg'
|
3
|
+
|
4
|
+
module YADM
|
5
|
+
module Adapters
|
6
|
+
class PostgreSQL
|
7
|
+
include Base
|
8
|
+
include CommonSQL
|
9
|
+
|
10
|
+
register :postgresql
|
11
|
+
|
12
|
+
def initialize(connection_parameters = {})
|
13
|
+
@connection = Sequel.connect(adapter: :postgres, **connection_parameters)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'yadm/adapters/common_sql'
|
2
|
+
require 'sqlite3'
|
3
|
+
|
4
|
+
module YADM
|
5
|
+
module Adapters
|
6
|
+
class Sqlite
|
7
|
+
include Base
|
8
|
+
include CommonSQL
|
9
|
+
|
10
|
+
register :sqlite
|
11
|
+
|
12
|
+
def initialize(connection_parameters = {})
|
13
|
+
@connection = Sequel.connect(adapter: :sqlite, **connection_parameters)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'yadm/criteria/expression'
|
2
|
+
require 'yadm/criteria/attribute'
|
3
|
+
require 'yadm/criteria/argument'
|
4
|
+
require 'yadm/criteria/condition'
|
5
|
+
require 'yadm/criteria/order'
|
6
|
+
require 'yadm/criteria/limit'
|
7
|
+
|
8
|
+
module YADM
|
9
|
+
class Criteria
|
10
|
+
attr_reader :condition, :order, :limit
|
11
|
+
|
12
|
+
def initialize(condition: nil, order: nil, limit: nil)
|
13
|
+
@condition = condition
|
14
|
+
@order = order
|
15
|
+
@limit = limit
|
16
|
+
end
|
17
|
+
|
18
|
+
def merge(other_criteria)
|
19
|
+
self.class.new(
|
20
|
+
condition: Condition.merge(condition, other_criteria.condition),
|
21
|
+
order: Order.merge(order, other_criteria.order),
|
22
|
+
limit: Limit.merge(limit, other_criteria.limit)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
%i(condition order limit).all? do |method|
|
28
|
+
other.respond_to?(method) && send(method) == other.send(method)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|