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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +8 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +273 -0
  8. data/Rakefile +10 -0
  9. data/examples/basic.rb +43 -0
  10. data/examples/migration.rb +65 -0
  11. data/lib/yadm.rb +39 -0
  12. data/lib/yadm/adapters.rb +32 -0
  13. data/lib/yadm/adapters/common_sql.rb +120 -0
  14. data/lib/yadm/adapters/memory.rb +175 -0
  15. data/lib/yadm/adapters/mysql.rb +17 -0
  16. data/lib/yadm/adapters/postgresql.rb +17 -0
  17. data/lib/yadm/adapters/sqlite.rb +17 -0
  18. data/lib/yadm/criteria.rb +32 -0
  19. data/lib/yadm/criteria/argument.rb +22 -0
  20. data/lib/yadm/criteria/attribute.rb +15 -0
  21. data/lib/yadm/criteria/condition.rb +31 -0
  22. data/lib/yadm/criteria/expression.rb +19 -0
  23. data/lib/yadm/criteria/limit.rb +25 -0
  24. data/lib/yadm/criteria/order.rb +48 -0
  25. data/lib/yadm/criteria_parser.rb +62 -0
  26. data/lib/yadm/criteria_parser/expression_parser.rb +77 -0
  27. data/lib/yadm/entity.rb +53 -0
  28. data/lib/yadm/identity_map.rb +51 -0
  29. data/lib/yadm/mapper.rb +16 -0
  30. data/lib/yadm/mapping.rb +71 -0
  31. data/lib/yadm/mapping/attribute.rb +31 -0
  32. data/lib/yadm/query.rb +28 -0
  33. data/lib/yadm/repository.rb +103 -0
  34. data/lib/yadm/version.rb +3 -0
  35. data/spec/spec_helper.rb +26 -0
  36. data/spec/support/criteria_helpers.rb +33 -0
  37. data/spec/support/sequel_helpers.rb +25 -0
  38. data/spec/support/shared_examples_for_a_sequel_adapter.rb +173 -0
  39. data/spec/yadm/adapters/common_sql_spec.rb +89 -0
  40. data/spec/yadm/adapters/memory_spec.rb +230 -0
  41. data/spec/yadm/adapters/mysql_spec.rb +9 -0
  42. data/spec/yadm/adapters/postgresql_spec.rb +9 -0
  43. data/spec/yadm/adapters/sqlite_spec.rb +5 -0
  44. data/spec/yadm/adapters_spec.rb +32 -0
  45. data/spec/yadm/criteria/condition_spec.rb +50 -0
  46. data/spec/yadm/criteria/limit_spec.rb +45 -0
  47. data/spec/yadm/criteria/order_spec.rb +50 -0
  48. data/spec/yadm/criteria_parser/expression_parser_spec.rb +47 -0
  49. data/spec/yadm/criteria_parser_spec.rb +55 -0
  50. data/spec/yadm/criteria_spec.rb +40 -0
  51. data/spec/yadm/entity_spec.rb +76 -0
  52. data/spec/yadm/identity_map_spec.rb +128 -0
  53. data/spec/yadm/mapper_spec.rb +23 -0
  54. data/spec/yadm/mapping/attribute_spec.rb +35 -0
  55. data/spec/yadm/mapping_spec.rb +122 -0
  56. data/spec/yadm/query_spec.rb +45 -0
  57. data/spec/yadm/repository_spec.rb +175 -0
  58. data/spec/yadm_spec.rb +45 -0
  59. data/yadm.gemspec +33 -0
  60. 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
@@ -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
@@ -0,0 +1,3 @@
1
+ module YADM
2
+ VERSION = '0.1'
3
+ end
@@ -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