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
@@ -0,0 +1,31 @@
|
|
1
|
+
module YADM
|
2
|
+
class Mapping
|
3
|
+
class Attribute
|
4
|
+
COERCIONS = {
|
5
|
+
Integer => -> (value) { value.to_i },
|
6
|
+
String => -> (value) { value.to_s }
|
7
|
+
}.tap do |hash|
|
8
|
+
hash.default = -> (value) { value }
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :type
|
12
|
+
protected :type
|
13
|
+
|
14
|
+
def initialize(type)
|
15
|
+
@type = type
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
type == other.type
|
20
|
+
end
|
21
|
+
|
22
|
+
def coerce(value)
|
23
|
+
if value.nil?
|
24
|
+
nil
|
25
|
+
else
|
26
|
+
COERCIONS[type].call(value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/yadm/query.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module YADM
|
2
|
+
class Query
|
3
|
+
attr_reader :criteria, :arguments
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(criteria = Criteria.new, arguments = {})
|
8
|
+
@criteria = criteria
|
9
|
+
@arguments = arguments
|
10
|
+
end
|
11
|
+
|
12
|
+
def merge(new_criteria, new_arguments)
|
13
|
+
self.class.new(criteria.merge(new_criteria), arguments.merge(new_arguments))
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_a
|
17
|
+
repository.send_query(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def each(&block)
|
21
|
+
to_a.each(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def repository
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module YADM
|
2
|
+
module Repository
|
3
|
+
class << self
|
4
|
+
def included(including_class)
|
5
|
+
including_class.const_set(:Query, query_class_for(including_class))
|
6
|
+
|
7
|
+
including_class.extend(ClassMethods)
|
8
|
+
including_class.extend(DSL)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def query_class_for(repository)
|
13
|
+
Class.new(YADM::Query) do
|
14
|
+
define_method :repository do
|
15
|
+
repository
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
include Enumerable
|
23
|
+
|
24
|
+
def find(id)
|
25
|
+
wrap_object(mapping.get(id))
|
26
|
+
end
|
27
|
+
|
28
|
+
def persist(entity)
|
29
|
+
if entity.id.nil?
|
30
|
+
entity.id = mapping.add(entity.attributes)
|
31
|
+
else
|
32
|
+
new_attributes = entity.attributes
|
33
|
+
mapping.change(new_attributes.delete(:id), new_attributes)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete(entity)
|
38
|
+
mapping.remove(entity.id)
|
39
|
+
end
|
40
|
+
|
41
|
+
def each(&block)
|
42
|
+
default_query.each(&block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def count
|
46
|
+
mapping.count
|
47
|
+
end
|
48
|
+
|
49
|
+
def send_query(query)
|
50
|
+
mapping.send_query(query).map do |attributes|
|
51
|
+
wrap_object(attributes)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def default_query
|
56
|
+
query_class.new
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def wrap_object(attributes)
|
61
|
+
entity_class.new(attributes)
|
62
|
+
end
|
63
|
+
|
64
|
+
def mapping
|
65
|
+
YADM.mapper.mapping_for(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
def query_class
|
69
|
+
const_get(:Query)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
module DSL
|
74
|
+
|
75
|
+
private
|
76
|
+
def criteria(name, &block)
|
77
|
+
criteria = CriteriaParser.parse(block, name)
|
78
|
+
|
79
|
+
query_class.class_eval do
|
80
|
+
define_method(name) do |*args|
|
81
|
+
merge(criteria, name => args)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
define_singleton_method(name) do |*args|
|
86
|
+
default_query.public_send(name, *args)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def entity(entity_class)
|
91
|
+
@entity_class = entity_class
|
92
|
+
end
|
93
|
+
|
94
|
+
def entity_class
|
95
|
+
if @entity_class.nil?
|
96
|
+
raise ArgumentError, "Entity is not declared for repository #{self.name}"
|
97
|
+
else
|
98
|
+
@entity_class
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/yadm/version.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'yadm'
|
2
|
+
|
3
|
+
support_glob = Pathname.new('../support/*.rb').expand_path(__FILE__)
|
4
|
+
Dir.glob(support_glob) { |path| require(path) }
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.expect_with :rspec do |expectations|
|
8
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
9
|
+
# # => "be bigger than 2 and smaller than 4"
|
10
|
+
# ...rather than:
|
11
|
+
# # => "be bigger than 2"
|
12
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
13
|
+
end
|
14
|
+
|
15
|
+
config.filter_run :focus
|
16
|
+
config.run_all_when_everything_filtered = true
|
17
|
+
|
18
|
+
config.disable_monkey_patching!
|
19
|
+
config.profile_examples = 5
|
20
|
+
|
21
|
+
config.order = :random
|
22
|
+
Kernel.srand config.seed
|
23
|
+
|
24
|
+
config.include CriteriaHelpers
|
25
|
+
config.include SequelHelpers
|
26
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module CriteriaHelpers
|
2
|
+
def build_criteria(components)
|
3
|
+
YADM::Criteria.new(components)
|
4
|
+
end
|
5
|
+
|
6
|
+
def build_condition(expression)
|
7
|
+
YADM::Criteria::Condition.new(expression)
|
8
|
+
end
|
9
|
+
|
10
|
+
def build_order(clauses)
|
11
|
+
YADM::Criteria::Order.new(clauses)
|
12
|
+
end
|
13
|
+
|
14
|
+
def build_order_clause(type, expression)
|
15
|
+
YADM::Criteria::Order::Clause.new(type, expression)
|
16
|
+
end
|
17
|
+
|
18
|
+
def build_limit(limit)
|
19
|
+
YADM::Criteria::Limit.new(limit)
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_expression(receiver, method, argument)
|
23
|
+
YADM::Criteria::Expression.new(receiver, method, [argument])
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_attribute(name)
|
27
|
+
YADM::Criteria::Attribute.new(name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_argument(group, index)
|
31
|
+
YADM::Criteria::Argument.new(group, index)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SequelHelpers
|
2
|
+
def setup_table
|
3
|
+
connection.create_table! :posts do
|
4
|
+
primary_key :id
|
5
|
+
|
6
|
+
String :title
|
7
|
+
Integer :comments_count
|
8
|
+
DateTime :created_at
|
9
|
+
end
|
10
|
+
|
11
|
+
[
|
12
|
+
['First', 7, now - 10],
|
13
|
+
['Second', 10, now - 20],
|
14
|
+
['Third', 4, now - 10],
|
15
|
+
['Fourth', 13, now]
|
16
|
+
].each do |title, comments_count, created_at|
|
17
|
+
subject.add(
|
18
|
+
:posts,
|
19
|
+
title: title,
|
20
|
+
comments_count: comments_count,
|
21
|
+
created_at: created_at
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
RSpec.shared_examples 'a sequel adapter' do
|
2
|
+
let(:connection) { subject.send(:connection) }
|
3
|
+
let(:now) { Time.now }
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
connection.create_table! :people do
|
7
|
+
primary_key :id
|
8
|
+
|
9
|
+
String :name
|
10
|
+
String :email
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:person_attributes) do
|
15
|
+
{ name: 'John', email: 'john@example.com' }
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#get' do
|
19
|
+
before(:each) do
|
20
|
+
subject.add(:people, person_attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns the record with the specified id' do
|
24
|
+
expect(subject.get(:people, 1)).to eq(person_attributes.merge(id: 1))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#add' do
|
29
|
+
it 'adds a new record' do
|
30
|
+
expect {
|
31
|
+
subject.add(:people, person_attributes)
|
32
|
+
}.to change { subject.count(:people) }.by(1)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'returns the id of the created record' do
|
36
|
+
expect(subject.add(:people, person_attributes)).to eq(1)
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'with a nil id attribute' do
|
40
|
+
it 'ignores the id' do
|
41
|
+
expect {
|
42
|
+
subject.add(:people, person_attributes.merge(id: nil))
|
43
|
+
}.to change { subject.count(:people) }.by(1)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#change' do
|
49
|
+
before(:each) do
|
50
|
+
subject.add(:people, person_attributes)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'changes an existing record' do
|
54
|
+
expect {
|
55
|
+
subject.change(:people, 1, name: 'Jack')
|
56
|
+
}.to change { subject.get(:people, 1)[:name] }.to('Jack')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#remove' do
|
61
|
+
before(:each) do
|
62
|
+
subject.add(:people, person_attributes)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'removes the record with the specified id' do
|
66
|
+
expect {
|
67
|
+
subject.remove(:people, 1)
|
68
|
+
}.to change { subject.count(:people) }.by(-1)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#send_query' do
|
73
|
+
before(:each) do
|
74
|
+
setup_table
|
75
|
+
end
|
76
|
+
|
77
|
+
let(:criteria) do
|
78
|
+
build_criteria(
|
79
|
+
condition: build_condition(
|
80
|
+
build_expression(build_attribute(:comments_count), :<, 10)
|
81
|
+
),
|
82
|
+
order: build_order(
|
83
|
+
[
|
84
|
+
build_order_clause(:desc, build_attribute(:comments_count))
|
85
|
+
]
|
86
|
+
),
|
87
|
+
limit: build_limit(1)
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
let(:query) { YADM::Query.new(criteria, {}) }
|
92
|
+
|
93
|
+
it 'filters, orders and limits the records' do
|
94
|
+
data = subject.send_query(:posts, query)
|
95
|
+
|
96
|
+
expect(data.count).to eq(1)
|
97
|
+
expect(data.first[:id]).to eq(1)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe '#filter' do
|
102
|
+
before(:each) do
|
103
|
+
setup_table
|
104
|
+
end
|
105
|
+
|
106
|
+
let(:condition) do
|
107
|
+
build_condition(
|
108
|
+
build_expression(build_attribute(:comments_count), :<, 10)
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'returns a dataset with the condition' do
|
113
|
+
result = subject.filter(subject.from(:posts), condition, {})
|
114
|
+
|
115
|
+
expect(result.count).to eq(2)
|
116
|
+
expect(result.to_a.first[:title]).to eq('First')
|
117
|
+
expect(result.to_a.last[:title]).to eq('Third')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe '#order' do
|
122
|
+
before(:each) do
|
123
|
+
setup_table
|
124
|
+
end
|
125
|
+
|
126
|
+
let(:order) do
|
127
|
+
build_order(
|
128
|
+
[
|
129
|
+
build_order_clause(:desc, build_attribute(:created_at)),
|
130
|
+
build_order_clause(:asc, build_attribute(:comments_count))
|
131
|
+
]
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'returns a dataset with the specified order' do
|
136
|
+
result = subject.order(subject.from(:posts), order, {})
|
137
|
+
titles = result.map { |record| record[:title] }
|
138
|
+
|
139
|
+
expect(titles).to eq(%w(Fourth Third First Second))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe '#limit' do
|
144
|
+
before(:each) do
|
145
|
+
setup_table
|
146
|
+
end
|
147
|
+
|
148
|
+
let(:limit) { build_limit(2) }
|
149
|
+
|
150
|
+
it 'returns a dataset with the specified limit' do
|
151
|
+
result = subject.limit(subject.from(:posts), limit, {})
|
152
|
+
expect(result.count).to eq(2)
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'with arguments' do
|
156
|
+
let(:limit) { build_limit(build_argument(:first, 0)) }
|
157
|
+
|
158
|
+
it 'returns a dataset with the specified limit' do
|
159
|
+
result = subject.limit(subject.from(:posts), limit, first: [3])
|
160
|
+
expect(result.count).to eq(3)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe '#migrate' do
|
166
|
+
let(:block) { double('Block') }
|
167
|
+
|
168
|
+
it 'calls the given block passing it the database connection' do
|
169
|
+
expect(block).to receive(:call).with(connection)
|
170
|
+
subject.migrate(block)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'yadm/adapters/common_sql'
|
2
|
+
|
3
|
+
RSpec.describe YADM::Adapters::CommonSQL do
|
4
|
+
describe '.sequelize' do
|
5
|
+
%i(< > <= >= + - * / !=).each do |operator|
|
6
|
+
context "with '#{operator}' operator" do
|
7
|
+
let(:expression) do
|
8
|
+
build_expression(
|
9
|
+
build_attribute(:comments_count),
|
10
|
+
operator,
|
11
|
+
1
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "creates an expression with '#{operator}' operator" do
|
16
|
+
result = described_class.sequelize(expression, {})
|
17
|
+
|
18
|
+
expect(result.args.first).to eq(Sequel::SQL::Identifier.new(:comments_count))
|
19
|
+
expect(result.op).to eq(operator)
|
20
|
+
expect(result.args.last).to eq(1)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "with '==' operator" do
|
26
|
+
let(:expression) do
|
27
|
+
build_expression(build_attribute(:comments_count), :==, 10)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "creates an expression with '=' operator" do
|
31
|
+
result = described_class.sequelize(expression, {})
|
32
|
+
expect(result.op).to eq(:'=')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "with '&' operator" do
|
37
|
+
let(:subexpression1) do
|
38
|
+
build_expression(build_attribute(:comments_count), :>, 25)
|
39
|
+
end
|
40
|
+
|
41
|
+
let(:subexpression2) do
|
42
|
+
build_expression(build_attribute(:comments_count), :<=, 30)
|
43
|
+
end
|
44
|
+
|
45
|
+
let(:expression) do
|
46
|
+
build_expression(subexpression1, :&, subexpression2)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "creates an expression with 'AND' operator" do
|
50
|
+
result = described_class.sequelize(expression, {})
|
51
|
+
expect(result.op).to eq(:AND)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context "with '|' operator" do
|
56
|
+
let(:subexpression1) do
|
57
|
+
build_expression(build_attribute(:comments_count), :<, 25)
|
58
|
+
end
|
59
|
+
|
60
|
+
let(:subexpression2) do
|
61
|
+
build_expression(build_attribute(:comments_count), :>, 40)
|
62
|
+
end
|
63
|
+
|
64
|
+
let(:expression) do
|
65
|
+
build_expression(subexpression1, :|, subexpression2)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "creates an expression with 'OR' operator" do
|
69
|
+
result = described_class.sequelize(expression, {})
|
70
|
+
expect(result.op).to eq(:OR)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'with arguments' do
|
75
|
+
let(:expression) do
|
76
|
+
build_expression(
|
77
|
+
build_attribute(:comments_count),
|
78
|
+
:>,
|
79
|
+
build_argument(:first, 0)
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "creates an expression with argument replaced with it's value" do
|
84
|
+
result = described_class.sequelize(expression, first: [20])
|
85
|
+
expect(result.args.last).to eq(20)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|