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,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