dbee 1.2.1 → 2.0.0.pre.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -10
- data/README.md +81 -26
- data/dbee.gemspec +1 -0
- data/lib/dbee/base.rb +20 -40
- data/lib/dbee/dsl/association.rb +53 -0
- data/lib/dbee/dsl/association_builder.rb +65 -0
- data/lib/dbee/dsl/methods.rb +74 -0
- data/lib/dbee/version.rb +1 -1
- data/lib/dbee.rb +10 -0
- data/spec/dbee/base_spec.rb +3 -3
- data/spec/dbee/model_spec.rb +1 -1
- data/spec/dbee_spec.rb +3 -3
- data/spec/fixtures/models.rb +41 -65
- data/spec/fixtures/models.yaml +19 -12
- metadata +21 -5
- data/lib/dbee/dsl/inflectable.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ccd40ea06b62b782bc525c069d38b01fc4c486b42507dd8f70e915ba197f01b6
|
4
|
+
data.tar.gz: a2f2fac76927357e5187d922c3da1ffd8e102430a878f8dedbec3aeb654ad0be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea4bfb0f14b33d70b7704ba8006189d5d725f90cf7de89d321d7147ab75c7b068644dd4d483b2aa7c1ee187080ae438096d05856b4c9d2b46265b5548a481d4a
|
7
|
+
data.tar.gz: ee4ecf4505be3b1911662de5553a3b38606af539a0bfadbc243fb2d38bf6d11310945bf2664d10795c97d0e6a82bc49262ae9550c0c7a867b25a4c1d9f3a4503
|
data/CHANGELOG.md
CHANGED
@@ -1,55 +1,71 @@
|
|
1
|
+
# 2.0.0 (September 3rd, 2019)
|
2
|
+
|
3
|
+
### Additions:
|
4
|
+
|
5
|
+
* New DSL method for declaring child associations: `child`.
|
6
|
+
* New DSL method for declaring parent associations: `parent`.
|
7
|
+
* Added global inflection using Dry::Inflector, can be overridden using Dbee.inflector = Dry::Inflector.new # or some other instance.
|
8
|
+
|
9
|
+
### Breaking changes:
|
10
|
+
|
11
|
+
Inflection was introduced with this release and hence changes some of the default naming inference behavior:
|
12
|
+
|
13
|
+
* Table name is now inferred to be the pluralized and de-modulized class name (unless explicitly declared.)
|
14
|
+
* Top level root model name is inferred to be the underscored and de-modulized class name.
|
15
|
+
* Model is now inferred to be the relative, singular, and camelized association name (unless explicitly declared.)
|
16
|
+
|
1
17
|
# 1.2.1 (September 1st, 2019)
|
2
18
|
|
3
|
-
Fixes:
|
19
|
+
### Fixes:
|
4
20
|
|
5
21
|
* Dbee::Base table name should default to the most-parent table name (if none are explicitly defined.) Overriding still takes most child sub-class precedence.
|
6
22
|
|
7
23
|
# 1.2.0 (August 29th, 2019)
|
8
24
|
|
9
|
-
Additions:
|
25
|
+
### Additions:
|
10
26
|
|
11
27
|
* Added partitioners to a model specification. A Partitioner is essentially a way of explicitly specifying that a data model must meet a specific equality for a column.
|
12
28
|
|
13
|
-
Changes:
|
29
|
+
### Changes:
|
14
30
|
|
15
31
|
* Model#ancestors renamed to Model#ancestors!
|
16
32
|
* Model#ancestor now returns a hash where the key is an array of strings and not just a pre-concatenated string.
|
17
33
|
|
18
34
|
# 1.1.0 (August 28th, 2019)
|
19
35
|
|
20
|
-
Additions:
|
36
|
+
### Additions:
|
21
37
|
|
22
38
|
* Added better equality and sort methods for Ruby objects (added class type checks where needed.)
|
23
39
|
|
24
|
-
Changes:
|
40
|
+
### Changes:
|
25
41
|
|
26
42
|
* Removed Sorter in favor of Sorter subclasses.
|
27
43
|
* Duplicate filters and sorters will be ignored in a Query.
|
28
44
|
|
29
45
|
# 1.0.3 (August 27th, 2019)
|
30
46
|
|
31
|
-
Additions:
|
47
|
+
### Additions:
|
32
48
|
|
33
49
|
* Added 'parent' keyword for static constraints. This makes it possible to have a static constraint apply to a parent and not just a child relationship.
|
34
50
|
|
35
51
|
# 1.0.2 (August 26th, 2019)
|
36
52
|
|
37
|
-
Fixes:
|
53
|
+
### Fixes:
|
38
54
|
|
39
55
|
* Dbee::Base subclasses can now support cycles to N-depth, where N is limited by the Query. The recursion will go as far as the Query specifies it has to go.
|
40
56
|
|
41
|
-
Additions:
|
57
|
+
### Additions:
|
42
58
|
|
43
59
|
* Equals filter is now the default type when type is omitted.
|
44
60
|
* model can now be a string so it can be lazy evaluated at run-time instead of at script evaluation time.
|
45
61
|
|
46
62
|
# 1.0.1 (August 26th, 2019)
|
47
63
|
|
48
|
-
Fixes:
|
64
|
+
### Fixes:
|
49
65
|
|
50
66
|
* Dbee::Base subclasses can now declare self-referential associations. Note that it will stop the hierarchy once the cycle is detected.
|
51
67
|
|
52
|
-
Additions:
|
68
|
+
### Additions:
|
53
69
|
|
54
70
|
* Static constraint is now the default type when type is omitted.
|
55
71
|
|
data/README.md
CHANGED
@@ -80,58 +80,116 @@ There are two ways to model this schema using Dbee:
|
|
80
80
|
1. code-first
|
81
81
|
2. configuration-first
|
82
82
|
|
83
|
-
#### Code-First Data Modeling
|
83
|
+
#### Code-First Data Modeling (With Inflection)
|
84
84
|
|
85
85
|
Code-first data modeling involves creating sub-classes of Dbee::Base that describes the tables and associations. We could model the above example as:
|
86
86
|
|
87
87
|
````ruby
|
88
88
|
module ReadmeDataModels
|
89
|
-
class
|
89
|
+
class PhoneNumber < Dbee::Base
|
90
90
|
table :phones
|
91
|
+
|
92
|
+
parent :patient
|
93
|
+
end
|
94
|
+
|
95
|
+
class Note < Dbee::Base
|
96
|
+
parent :patient
|
97
|
+
end
|
98
|
+
|
99
|
+
class Patient < Dbee::Base
|
100
|
+
child :notes
|
101
|
+
|
102
|
+
child :work_phone_number, model: 'ReadmeDataModels::PhoneNumber',
|
103
|
+
static: { name: :phone_number_type, value: 'work' }
|
104
|
+
|
105
|
+
child :cell_phone_number, model: 'ReadmeDataModels::PhoneNumber',
|
106
|
+
static: { name: :phone_number_type, value: 'cell' }
|
107
|
+
|
108
|
+
child :fax_phone_number, model: 'ReadmeDataModels::PhoneNumber',
|
109
|
+
static: { name: :phone_number_type, value: 'fax' }
|
110
|
+
end
|
111
|
+
|
112
|
+
class Practice < Dbee::Base
|
113
|
+
child :patients
|
114
|
+
end
|
115
|
+
end
|
116
|
+
````
|
117
|
+
|
118
|
+
The two DSL methods: parent/child are very similar to ActiveRecord's belongs_to/has_many, respectively. Options for these methods are:
|
119
|
+
|
120
|
+
* **model**: class constant, string, or symbol to use as associated data model. If omitted, the model name will be the relative, singular, and camelized version of the association name.
|
121
|
+
* **foreign_key**: name of the key on the child table. If omitted for child then it will resolve as singular, underscored, de-modulized class name suffixed with '\_id'. If omitted for parent it will resolve to 'id'.
|
122
|
+
* **primary_key**: name of the key on the parent table. If omitted for child then it will resolve as 'id'. If omitted for parent then it will resolve as the singular, underscored, de-modulized name of the relationship suffixed with '\_id'
|
123
|
+
|
124
|
+
##### Customizing Inflection Rules
|
125
|
+
|
126
|
+
Inflection is provided via the (Dry::Inflector gem)[https://github.com/dry-rb/dry-inflector]. There are options to add custom grammar rules which you can then pass into Dbee. For example:
|
127
|
+
|
128
|
+
````ruby
|
129
|
+
Dbee.inflector = Dry::Inflector.new do |inflections|
|
130
|
+
inflections.plural 'virus', 'viruses' # specify a rule for #pluralize
|
131
|
+
inflections.singular 'thieves', 'thief' # specify a rule for #singularize
|
132
|
+
inflections.uncountable 'dry-inflector' # add an exception for an uncountable word
|
133
|
+
end
|
134
|
+
````
|
135
|
+
|
136
|
+
#### Code-First Data Modeling (Without Inflection)
|
137
|
+
|
138
|
+
You can use the raw `association` method you wish to fully control the entire referencing configuration.
|
139
|
+
|
140
|
+
````ruby
|
141
|
+
module ReadmeDataModels
|
142
|
+
class PhoneNumber < Dbee::Base
|
143
|
+
table :phones
|
144
|
+
|
145
|
+
association :patient, model: 'ReadmeDataModels::Patient', constraints: {
|
146
|
+
type: :reference, name: :patient_id, parent: :id
|
147
|
+
}
|
91
148
|
end
|
92
149
|
|
93
|
-
class
|
150
|
+
class Note < Dbee::Base
|
151
|
+
association :patient, model: 'ReadmeDataModels::Patient', constraints: {
|
152
|
+
type: :reference, name: :patient_id, parent: :id
|
153
|
+
}
|
94
154
|
end
|
95
155
|
|
96
|
-
class
|
97
|
-
association :notes, model:
|
156
|
+
class Patient < Dbee::Base
|
157
|
+
association :notes, model: 'ReadmeDataModels::Note', constraints: {
|
98
158
|
type: :reference, name: :patient_id, parent: :id
|
99
159
|
}
|
100
160
|
|
101
|
-
association :work_phone_number, model:
|
161
|
+
association :work_phone_number, model: 'ReadmeDataModels::PhoneNumber', constraints: [
|
102
162
|
{ type: :reference, name: :patient_id, parent: :id },
|
103
163
|
{ type: :static, name: :phone_number_type, value: 'work' }
|
104
164
|
]
|
105
165
|
|
106
|
-
association :cell_phone_number, model:
|
166
|
+
association :cell_phone_number, model: 'ReadmeDataModels::PhoneNumber', constraints: [
|
107
167
|
{ type: :reference, name: :patient_id, parent: :id },
|
108
168
|
{ type: :static, name: :phone_number_type, value: 'cell' }
|
109
169
|
]
|
110
170
|
|
111
|
-
association :fax_phone_number, model:
|
171
|
+
association :fax_phone_number, model: 'ReadmeDataModels::PhoneNumber', constraints: [
|
112
172
|
{ type: :reference, name: :patient_id, parent: :id },
|
113
173
|
{ type: :static, name: :phone_number_type, value: 'fax' }
|
114
174
|
]
|
115
175
|
end
|
116
176
|
|
117
|
-
class
|
118
|
-
association :patients, model:
|
177
|
+
class Practice < Dbee::Base
|
178
|
+
association :patients, model: Patient, constraints: {
|
119
179
|
type: :reference, name: :practice_id, parent: :id
|
120
180
|
}
|
121
181
|
end
|
122
182
|
end
|
123
|
-
|
124
183
|
````
|
125
184
|
|
126
|
-
|
127
|
-
|
185
|
+
The two code-first examples above should be technically equivalent.
|
128
186
|
|
129
187
|
#### Configuration-First Data Modeling
|
130
188
|
|
131
189
|
You can choose to alternatively describe your data model using configuration. The YAML below is equivalent to the Ruby sub-classes above:
|
132
190
|
|
133
191
|
````yaml
|
134
|
-
name:
|
192
|
+
name: practice
|
135
193
|
models:
|
136
194
|
- name: patients
|
137
195
|
constraints:
|
@@ -182,15 +240,14 @@ You can leverage the model partitioners for hard-coding partitioning by column=v
|
|
182
240
|
##### Code-first:
|
183
241
|
|
184
242
|
````ruby
|
185
|
-
class
|
186
|
-
|
243
|
+
class Animal < Dbee::Base
|
244
|
+
end
|
187
245
|
|
246
|
+
class Dog < Animal
|
188
247
|
partitioner :type, 'Dog'
|
189
248
|
end
|
190
249
|
|
191
|
-
class
|
192
|
-
table 'animals'
|
193
|
-
|
250
|
+
class Cat < Animal
|
194
251
|
partitioner :type, 'Cat'
|
195
252
|
end
|
196
253
|
````
|
@@ -199,14 +256,14 @@ end
|
|
199
256
|
|
200
257
|
````yaml
|
201
258
|
Dogs:
|
202
|
-
name:
|
259
|
+
name: dog
|
203
260
|
table: animals
|
204
261
|
partitioners:
|
205
262
|
- name: type
|
206
263
|
value: Dog
|
207
264
|
|
208
265
|
Cats:
|
209
|
-
name:
|
266
|
+
name: cat
|
210
267
|
table: animals
|
211
268
|
partitioners:
|
212
269
|
- name: type
|
@@ -309,10 +366,9 @@ Here are some sample executions based off the preceding examples:
|
|
309
366
|
##### Code-First Execution
|
310
367
|
|
311
368
|
````ruby
|
312
|
-
require 'dbee'
|
313
369
|
require 'dbee/providers/active_record_provider'
|
314
370
|
|
315
|
-
class
|
371
|
+
class Practice < Dbee::Base; end
|
316
372
|
|
317
373
|
provider = Dbee::Providers::ActiveRecordProvider.new
|
318
374
|
|
@@ -324,19 +380,18 @@ query = {
|
|
324
380
|
]
|
325
381
|
}
|
326
382
|
|
327
|
-
sql = Dbee.sql(
|
383
|
+
sql = Dbee.sql(Practice, query, provider)
|
328
384
|
````
|
329
385
|
|
330
386
|
##### Configuration-First Execution
|
331
387
|
|
332
388
|
````ruby
|
333
|
-
require 'dbee'
|
334
389
|
require 'dbee/providers/active_record_provider'
|
335
390
|
|
336
391
|
provider = Dbee::Providers::ActiveRecordProvider.new
|
337
392
|
|
338
393
|
model = {
|
339
|
-
name: :
|
394
|
+
name: :practice
|
340
395
|
}
|
341
396
|
|
342
397
|
query = {
|
data/dbee.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |s|
|
|
22
22
|
s.required_ruby_version = '>= 2.4.6'
|
23
23
|
|
24
24
|
s.add_dependency('acts_as_hashable', '~>1', '>=1.1.0')
|
25
|
+
s.add_dependency('dry-inflector', '~>0')
|
25
26
|
|
26
27
|
s.add_development_dependency('guard-rspec', '~>4.7')
|
27
28
|
s.add_development_dependency('pry', '~>0')
|
data/lib/dbee/base.rb
CHANGED
@@ -7,38 +7,28 @@
|
|
7
7
|
# LICENSE file in the root directory of this source tree.
|
8
8
|
#
|
9
9
|
|
10
|
-
require_relative 'dsl/
|
10
|
+
require_relative 'dsl/association'
|
11
|
+
require_relative 'dsl/association_builder'
|
12
|
+
require_relative 'dsl/methods'
|
11
13
|
require_relative 'dsl/reflectable'
|
12
14
|
|
13
15
|
module Dbee
|
14
16
|
# Instead of using the configuration-first approach, you could use this super class for
|
15
17
|
# Model declaration.
|
16
18
|
class Base
|
17
|
-
extend Dsl::Inflectable
|
18
19
|
extend Dsl::Reflectable
|
20
|
+
extend Dsl::Methods
|
19
21
|
|
20
22
|
BASE_CLASS_CONSTANT = Dbee::Base
|
21
23
|
|
22
24
|
class << self
|
23
|
-
def partitioner(name, value)
|
24
|
-
partitioners << { name: name, value: value }
|
25
|
-
end
|
26
|
-
|
27
|
-
def table(name)
|
28
|
-
tap { @table_name = name.to_s }
|
29
|
-
end
|
30
|
-
|
31
|
-
def association(name, opts = {})
|
32
|
-
tap { associations_by_name[name.to_s] = opts.merge(name: name) }
|
33
|
-
end
|
34
|
-
|
35
25
|
# This method is cycle-resistant due to the fact that it is a requirement to send in a
|
36
26
|
# key_chain. That means each model produced using to_model is specific to a set of desired
|
37
27
|
# fields. Basically, you cannot derive a Model from a Base subclass without the context
|
38
28
|
# of a Query. This is not true for configuration-first Model definitions because, in that
|
39
29
|
# case, cycles do not exist since the nature of the configuration is flat.
|
40
30
|
def to_model(key_chain, name = nil, constraints = [], path_parts = [])
|
41
|
-
derived_name = name.to_s.empty? ?
|
31
|
+
derived_name = name.to_s.empty? ? inflected_class_name(self.name) : name.to_s
|
42
32
|
key = [key_chain, derived_name, constraints, path_parts]
|
43
33
|
|
44
34
|
to_models[key] ||= Model.make(
|
@@ -51,25 +41,9 @@ module Dbee
|
|
51
41
|
)
|
52
42
|
end
|
53
43
|
|
54
|
-
def table_name
|
55
|
-
@table_name || ''
|
56
|
-
end
|
57
|
-
|
58
|
-
def associations_by_name
|
59
|
-
@associations_by_name ||= {}
|
60
|
-
end
|
61
|
-
|
62
|
-
def partitioners
|
63
|
-
@partitioners ||= []
|
64
|
-
end
|
65
|
-
|
66
|
-
def table_name?
|
67
|
-
!table_name.empty?
|
68
|
-
end
|
69
|
-
|
70
44
|
def inherited_table_name
|
71
45
|
subclasses(BASE_CLASS_CONSTANT).find(&:table_name?)&.table_name ||
|
72
|
-
|
46
|
+
inflected_table_name(reversed_subclasses(BASE_CLASS_CONSTANT).first.name)
|
73
47
|
end
|
74
48
|
|
75
49
|
def inherited_associations
|
@@ -97,16 +71,14 @@ module Dbee
|
|
97
71
|
end
|
98
72
|
|
99
73
|
def associations(key_chain, path_parts)
|
100
|
-
inherited_associations.select { |c| key_chain.ancestor_path?(path_parts, c
|
101
|
-
.
|
102
|
-
model_constant
|
103
|
-
associated_constraints = config[:constraints]
|
104
|
-
name = config[:name]
|
74
|
+
inherited_associations.select { |c| key_chain.ancestor_path?(path_parts, c.name) }
|
75
|
+
.map do |association|
|
76
|
+
model_constant = association.model_constant
|
105
77
|
|
106
|
-
|
78
|
+
model_constant.to_model(
|
107
79
|
key_chain,
|
108
|
-
name,
|
109
|
-
|
80
|
+
association.name,
|
81
|
+
association.constraints,
|
110
82
|
path_parts
|
111
83
|
)
|
112
84
|
end
|
@@ -115,6 +87,14 @@ module Dbee
|
|
115
87
|
def to_models
|
116
88
|
@to_models ||= {}
|
117
89
|
end
|
90
|
+
|
91
|
+
def inflected_table_name(name)
|
92
|
+
inflector.pluralize(inflector.underscore(inflector.demodulize(name)))
|
93
|
+
end
|
94
|
+
|
95
|
+
def inflected_class_name(name)
|
96
|
+
inflector.underscore(inflector.demodulize(name))
|
97
|
+
end
|
118
98
|
end
|
119
99
|
end
|
120
100
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
module Dbee
|
11
|
+
module Dsl
|
12
|
+
# The main logic that can take input from model association declaration and turn it into
|
13
|
+
# usable information.
|
14
|
+
class Association
|
15
|
+
attr_reader :on_class_name, :inflector, :name, :opts
|
16
|
+
|
17
|
+
def initialize(on_class_name, inflector, name, opts = {})
|
18
|
+
raise ArgumentError, 'on_class_name is required' if on_class_name.to_s.empty?
|
19
|
+
raise ArgumentError, 'inflector is required' unless inflector
|
20
|
+
raise ArgumentError, 'name is required' if name.to_s.empty?
|
21
|
+
|
22
|
+
@on_class_name = on_class_name
|
23
|
+
@inflector = inflector
|
24
|
+
@name = name.to_s
|
25
|
+
@opts = opts || {}
|
26
|
+
|
27
|
+
freeze
|
28
|
+
end
|
29
|
+
|
30
|
+
def model_constant
|
31
|
+
constantize(class_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def constraints
|
35
|
+
opts[:constraints] || []
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def class_name
|
41
|
+
opts[:model] || relative_class_name
|
42
|
+
end
|
43
|
+
|
44
|
+
def relative_class_name
|
45
|
+
(on_class_name.split('::')[0...-1] + [inflector.classify(name)]).join('::')
|
46
|
+
end
|
47
|
+
|
48
|
+
def constantize(value)
|
49
|
+
value.is_a?(String) || value.is_a?(Symbol) ? Object.const_get(value) : value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
module Dbee
|
11
|
+
module Dsl
|
12
|
+
# This is really syntactic sugar built on top of Association. It can handle two main
|
13
|
+
# use-cases when declaring associations:
|
14
|
+
# - parent: create an association to a parent model (foreign key is on immediate table)
|
15
|
+
# - child: create an association to a child model (foreign key is on child table)
|
16
|
+
class AssociationBuilder
|
17
|
+
attr_reader :inflector
|
18
|
+
|
19
|
+
def initialize(inflector)
|
20
|
+
raise ArgumentError, 'inflector is required' unless inflector
|
21
|
+
|
22
|
+
@inflector = inflector
|
23
|
+
|
24
|
+
freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
def parent_association(on_class_name, name, opts = {})
|
28
|
+
reference_constraint = {
|
29
|
+
name: opts[:foreign_key] || :id,
|
30
|
+
parent: opts[:primary_key] || inflector.foreign_key(name)
|
31
|
+
}
|
32
|
+
|
33
|
+
association(on_class_name, name, opts, reference_constraint)
|
34
|
+
end
|
35
|
+
|
36
|
+
def child_association(on_class_name, name, opts = {})
|
37
|
+
reference_constraint = {
|
38
|
+
name: opts[:foreign_key] || inflector.foreign_key(on_class_name),
|
39
|
+
parent: opts[:primary_key] || :id
|
40
|
+
}
|
41
|
+
|
42
|
+
association(on_class_name, name, opts, reference_constraint)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def association(on_class_name, name, opts, reference_constraint)
|
48
|
+
association_opts = {
|
49
|
+
model: opts[:model],
|
50
|
+
constraints: [reference_constraint] + make_constraints(opts)
|
51
|
+
}
|
52
|
+
|
53
|
+
Association.new(on_class_name, inflector, name, association_opts)
|
54
|
+
end
|
55
|
+
|
56
|
+
def make_constraints(opts = {})
|
57
|
+
array(opts[:constraints]) + array(opts[:static]).map { |c| c.merge(type: :static) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def array(value)
|
61
|
+
value.is_a?(Hash) ? [value] : Array(value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
module Dbee
|
11
|
+
module Dsl
|
12
|
+
# This mixin contains all the reader/writers for model meta-data declared through the DSL.
|
13
|
+
module Methods
|
14
|
+
def partitioner(name, value)
|
15
|
+
partitioners << { name: name, value: value }
|
16
|
+
end
|
17
|
+
|
18
|
+
def table(name)
|
19
|
+
tap { @table_name = name.to_s }
|
20
|
+
end
|
21
|
+
|
22
|
+
def parent(name, opts = {})
|
23
|
+
association = association_builder.parent_association(self.name, name, opts)
|
24
|
+
|
25
|
+
association(name, association)
|
26
|
+
end
|
27
|
+
|
28
|
+
def child(name, opts = {})
|
29
|
+
association = association_builder.child_association(self.name, name, opts)
|
30
|
+
|
31
|
+
association(name, association)
|
32
|
+
end
|
33
|
+
|
34
|
+
def association(name, opts = {})
|
35
|
+
value =
|
36
|
+
if opts.is_a?(Dsl::Association)
|
37
|
+
opts
|
38
|
+
else
|
39
|
+
Dsl::Association.new(self.name, inflector, name, opts)
|
40
|
+
end
|
41
|
+
|
42
|
+
tap do
|
43
|
+
associations_by_name[name.to_s] = value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def table_name
|
48
|
+
@table_name || ''
|
49
|
+
end
|
50
|
+
|
51
|
+
def associations_by_name
|
52
|
+
@associations_by_name ||= {}
|
53
|
+
end
|
54
|
+
|
55
|
+
def partitioners
|
56
|
+
@partitioners ||= []
|
57
|
+
end
|
58
|
+
|
59
|
+
def table_name?
|
60
|
+
!table_name.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def inflector
|
66
|
+
Dbee.inflector
|
67
|
+
end
|
68
|
+
|
69
|
+
def association_builder
|
70
|
+
@association_builder ||= Dsl::AssociationBuilder.new(inflector)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/dbee/version.rb
CHANGED
data/lib/dbee.rb
CHANGED
@@ -8,6 +8,7 @@
|
|
8
8
|
#
|
9
9
|
|
10
10
|
require 'acts_as_hashable'
|
11
|
+
require 'dry/inflector'
|
11
12
|
require 'forwardable'
|
12
13
|
|
13
14
|
require_relative 'dbee/base'
|
@@ -20,6 +21,15 @@ require_relative 'dbee/providers'
|
|
20
21
|
# Top-level namespace that provides the main public API.
|
21
22
|
module Dbee
|
22
23
|
class << self
|
24
|
+
# Use this to override the built in Dry::Inflector instance.
|
25
|
+
# This is useful is you have your own grammar/overrides you need to load.
|
26
|
+
# See the referenced gem here: https://github.com/dry-rb/dry-inflector
|
27
|
+
attr_writer :inflector
|
28
|
+
|
29
|
+
def inflector
|
30
|
+
@inflector ||= Dry::Inflector.new
|
31
|
+
end
|
32
|
+
|
23
33
|
def sql(model, query, provider)
|
24
34
|
query = Query.make(query)
|
25
35
|
model =
|
data/spec/dbee/base_spec.rb
CHANGED
@@ -33,7 +33,7 @@ describe Dbee::Base do
|
|
33
33
|
|
34
34
|
key_chain = Dbee::KeyChain.new(key_paths)
|
35
35
|
|
36
|
-
actual_model = Models::
|
36
|
+
actual_model = Models::Theater.to_model(key_chain)
|
37
37
|
|
38
38
|
expect(actual_model).to eq(expected_model)
|
39
39
|
end
|
@@ -82,7 +82,7 @@ describe Dbee::Base do
|
|
82
82
|
key_paths = %w[id]
|
83
83
|
key_chain = Dbee::KeyChain.new(key_paths)
|
84
84
|
|
85
|
-
actual_model = PartitionerExamples::
|
85
|
+
actual_model = PartitionerExamples::Dog.to_model(key_chain)
|
86
86
|
|
87
87
|
expect(actual_model).to eq(expected_model)
|
88
88
|
end
|
@@ -95,7 +95,7 @@ describe Dbee::Base do
|
|
95
95
|
key_paths = %w[id dogs.id]
|
96
96
|
key_chain = Dbee::KeyChain.new(key_paths)
|
97
97
|
|
98
|
-
actual_model = PartitionerExamples::
|
98
|
+
actual_model = PartitionerExamples::Owner.to_model(key_chain)
|
99
99
|
|
100
100
|
expect(actual_model).to eq(expected_model)
|
101
101
|
end
|
data/spec/dbee/model_spec.rb
CHANGED
data/spec/dbee_spec.rb
CHANGED
@@ -45,7 +45,7 @@ describe Dbee do
|
|
45
45
|
end
|
46
46
|
|
47
47
|
it 'accepts a Dbee::Base constant as a model and passes a Model instance to provider#sql' do
|
48
|
-
model_constant = Models::
|
48
|
+
model_constant = Models::Theater
|
49
49
|
|
50
50
|
expect(provider).to receive(:sql).with(model_constant.to_model(query.key_chain), query)
|
51
51
|
|
@@ -53,7 +53,7 @@ describe Dbee do
|
|
53
53
|
end
|
54
54
|
|
55
55
|
it 'accepts a Dbee::Query instance as a query and passes a Query instance to provider#sql' do
|
56
|
-
model = Models::
|
56
|
+
model = Models::Theater.to_model(query.key_chain)
|
57
57
|
|
58
58
|
expect(provider).to receive(:sql).with(model, query)
|
59
59
|
|
@@ -61,7 +61,7 @@ describe Dbee do
|
|
61
61
|
end
|
62
62
|
|
63
63
|
it 'accepts a hash as a query and passes a Query instance to provider#sql' do
|
64
|
-
model = Models::
|
64
|
+
model = Models::Theater.to_model(query.key_chain)
|
65
65
|
|
66
66
|
expect(provider).to receive(:sql).with(model, query)
|
67
67
|
|
data/spec/fixtures/models.rb
CHANGED
@@ -8,60 +8,44 @@
|
|
8
8
|
#
|
9
9
|
|
10
10
|
module Models
|
11
|
-
class
|
12
|
-
end
|
11
|
+
class Movie < Dbee::Base; end
|
13
12
|
|
14
|
-
class
|
15
|
-
end
|
13
|
+
class PhoneNumber < Dbee::Base; end
|
16
14
|
|
17
|
-
class
|
18
|
-
|
19
|
-
constraints: {
|
20
|
-
name: :demographic_id,
|
21
|
-
parent: :id
|
22
|
-
}
|
15
|
+
class Demographic < Dbee::Base
|
16
|
+
child :phone_numbers
|
23
17
|
end
|
24
18
|
|
25
|
-
class
|
26
|
-
|
19
|
+
class MemberBase < Dbee::Base
|
20
|
+
child :demos, model: 'Models::Demographic', foreign_key: :member_id
|
27
21
|
|
28
|
-
|
29
|
-
constraints: { type: :reference, name: :member_id, parent: :id }
|
30
|
-
|
31
|
-
association :movies, model: Movies,
|
32
|
-
constraints: { name: :member_id, parent: :id }
|
22
|
+
child :movies, foreign_key: :member_id
|
33
23
|
end
|
34
24
|
|
35
|
-
class
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
]
|
25
|
+
class Member < MemberBase
|
26
|
+
table 'members'
|
27
|
+
|
28
|
+
child :favorite_comic_movies, model: Movie, static: { name: :genre, value: 'comic' }
|
40
29
|
|
41
|
-
|
42
|
-
{ type: :reference, name: :member_id, parent: :id },
|
30
|
+
child :favorite_mystery_movies, model: Movie, constraints: [
|
43
31
|
{ type: :static, name: :genre, value: 'mystery' }
|
44
32
|
]
|
45
33
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
]
|
34
|
+
child :favorite_comedy_movies, model: Movie, constraints: {
|
35
|
+
type: :static, name: :genre, value: 'comedy'
|
36
|
+
}
|
50
37
|
end
|
51
38
|
|
52
|
-
class
|
53
|
-
|
54
|
-
{ type: :reference, name: :tid, parent: :id },
|
39
|
+
class TheaterBase < Dbee::Base
|
40
|
+
child :members, foreign_key: :tid, constraints: [
|
55
41
|
{ type: :reference, name: :partition, parent: :partition }
|
56
42
|
]
|
57
43
|
end
|
58
44
|
|
59
|
-
class
|
45
|
+
class Theater < TheaterBase
|
60
46
|
table 'theaters'
|
61
47
|
|
62
|
-
|
63
|
-
{ type: :reference, name: :id, parent: :parent_theater_id }
|
64
|
-
]
|
48
|
+
parent :parent_theater, model: self
|
65
49
|
end
|
66
50
|
|
67
51
|
class A < Dbee::Base
|
@@ -72,8 +56,7 @@ module Models
|
|
72
56
|
table 'table_set_to_b'
|
73
57
|
end
|
74
58
|
|
75
|
-
class C < A
|
76
|
-
end
|
59
|
+
class C < A; end
|
77
60
|
|
78
61
|
class D < A
|
79
62
|
table ''
|
@@ -85,36 +68,30 @@ module Models
|
|
85
68
|
end
|
86
69
|
|
87
70
|
module ReadmeDataModels
|
88
|
-
class
|
71
|
+
class PhoneNumber < Dbee::Base
|
89
72
|
table :phones
|
90
73
|
end
|
91
74
|
|
92
|
-
class
|
93
|
-
end
|
75
|
+
class Note < Dbee::Base; end
|
94
76
|
|
95
|
-
class
|
96
|
-
|
97
|
-
type: :reference, name: :patient_id, parent: :id
|
98
|
-
}
|
77
|
+
class Patient < Dbee::Base
|
78
|
+
child :notes
|
99
79
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
]
|
80
|
+
child :work_phone_number, model: PhoneNumber, static: {
|
81
|
+
name: :phone_number_type, value: 'work'
|
82
|
+
}
|
104
83
|
|
105
|
-
|
106
|
-
{ type: :reference, name: :patient_id, parent: :id },
|
84
|
+
child :cell_phone_number, model: PhoneNumber, constraints: [
|
107
85
|
{ type: :static, name: :phone_number_type, value: 'cell' }
|
108
86
|
]
|
109
87
|
|
110
|
-
|
111
|
-
{ type: :reference, name: :patient_id, parent: :id },
|
88
|
+
child :fax_phone_number, model: PhoneNumber, constraints: [
|
112
89
|
{ type: :static, name: :phone_number_type, value: 'fax' }
|
113
90
|
]
|
114
91
|
end
|
115
92
|
|
116
|
-
class
|
117
|
-
association :patients,
|
93
|
+
class Practice < Dbee::Base
|
94
|
+
association :patients, constraints: {
|
118
95
|
type: :reference, name: :practice_id, parent: :id
|
119
96
|
}
|
120
97
|
end
|
@@ -126,36 +103,35 @@ module Cycles
|
|
126
103
|
end
|
127
104
|
|
128
105
|
class A < BaseA
|
106
|
+
table :as
|
107
|
+
|
129
108
|
association :b2, model: 'Cycles::B'
|
130
109
|
end
|
131
110
|
|
132
111
|
class B < Dbee::Base
|
133
|
-
association :c
|
112
|
+
association :c
|
134
113
|
|
135
|
-
association :d
|
114
|
+
association :d
|
136
115
|
end
|
137
116
|
|
138
117
|
class C < Dbee::Base
|
139
|
-
association :a
|
118
|
+
association :a
|
140
119
|
end
|
141
120
|
|
142
121
|
class D < Dbee::Base
|
143
|
-
association :a
|
122
|
+
association :a
|
144
123
|
end
|
145
124
|
end
|
146
125
|
|
147
126
|
# Examples of Rails Single Table Inheritance.
|
148
127
|
module PartitionerExamples
|
149
|
-
class
|
150
|
-
|
151
|
-
name: :owner_id, parent: :id
|
152
|
-
}
|
128
|
+
class Owner < Dbee::Base
|
129
|
+
child :dogs
|
153
130
|
end
|
154
131
|
|
155
|
-
class
|
156
|
-
end
|
132
|
+
class Animal < Dbee::Base; end
|
157
133
|
|
158
|
-
class
|
134
|
+
class Dog < Animal
|
159
135
|
partitioner :type, 'Dog'
|
160
136
|
|
161
137
|
partitioner :deleted, false
|
data/spec/fixtures/models.yaml
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
Theaters, Members, and Movies:
|
2
|
-
name:
|
2
|
+
name: theater
|
3
3
|
table: theaters
|
4
4
|
models:
|
5
5
|
- name: members
|
@@ -120,7 +120,8 @@ Theaters, Members, and Movies:
|
|
120
120
|
name: genre
|
121
121
|
value: comedy
|
122
122
|
Readme:
|
123
|
-
name:
|
123
|
+
name: practice
|
124
|
+
table: practices
|
124
125
|
models:
|
125
126
|
- name: patients
|
126
127
|
constraints:
|
@@ -162,38 +163,43 @@ Readme:
|
|
162
163
|
value: fax
|
163
164
|
Cycle Example:
|
164
165
|
name: a
|
165
|
-
table:
|
166
|
+
table: as
|
166
167
|
models:
|
167
168
|
- name: b1
|
168
|
-
table:
|
169
|
+
table: bs
|
169
170
|
models:
|
170
171
|
- name: c
|
172
|
+
table: cs
|
171
173
|
models:
|
172
174
|
- name: a
|
173
|
-
table:
|
175
|
+
table: as
|
174
176
|
- name: d
|
177
|
+
table: ds
|
175
178
|
models:
|
176
179
|
- name: a
|
177
|
-
table:
|
180
|
+
table: as
|
178
181
|
- name: b2
|
179
|
-
table:
|
182
|
+
table: bs
|
180
183
|
models:
|
181
184
|
- name: c
|
185
|
+
table: cs
|
182
186
|
models:
|
183
187
|
- name: a
|
184
|
-
table:
|
188
|
+
table: as
|
185
189
|
- name: d
|
190
|
+
table: ds
|
186
191
|
models:
|
187
192
|
- name: a
|
188
|
-
table:
|
193
|
+
table: as
|
189
194
|
models:
|
190
195
|
- name: b1
|
191
|
-
table:
|
196
|
+
table: bs
|
192
197
|
models:
|
193
198
|
- name: c
|
199
|
+
table: cs
|
194
200
|
|
195
201
|
Partitioner Example 1:
|
196
|
-
name:
|
202
|
+
name: dog
|
197
203
|
table: animals
|
198
204
|
partitioners:
|
199
205
|
- name: type
|
@@ -202,7 +208,8 @@ Partitioner Example 1:
|
|
202
208
|
value: false
|
203
209
|
|
204
210
|
Partitioner Example 2:
|
205
|
-
name:
|
211
|
+
name: owner
|
212
|
+
table: owners
|
206
213
|
models:
|
207
214
|
- name: dogs
|
208
215
|
table: animals
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dbee
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0.pre.alpha
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Ruggio
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-09-
|
11
|
+
date: 2019-09-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: acts_as_hashable
|
@@ -30,6 +30,20 @@ dependencies:
|
|
30
30
|
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 1.1.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: dry-inflector
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
33
47
|
- !ruby/object:Gem::Dependency
|
34
48
|
name: guard-rspec
|
35
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -155,7 +169,9 @@ files:
|
|
155
169
|
- dbee.gemspec
|
156
170
|
- lib/dbee.rb
|
157
171
|
- lib/dbee/base.rb
|
158
|
-
- lib/dbee/dsl/
|
172
|
+
- lib/dbee/dsl/association.rb
|
173
|
+
- lib/dbee/dsl/association_builder.rb
|
174
|
+
- lib/dbee/dsl/methods.rb
|
159
175
|
- lib/dbee/dsl/reflectable.rb
|
160
176
|
- lib/dbee/key_chain.rb
|
161
177
|
- lib/dbee/key_path.rb
|
@@ -221,9 +237,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
221
237
|
version: 2.4.6
|
222
238
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
223
239
|
requirements:
|
224
|
-
- - "
|
240
|
+
- - ">"
|
225
241
|
- !ruby/object:Gem::Version
|
226
|
-
version:
|
242
|
+
version: 1.3.1
|
227
243
|
requirements: []
|
228
244
|
rubygems_version: 3.0.3
|
229
245
|
signing_key:
|
data/lib/dbee/dsl/inflectable.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
#
|
4
|
-
# Copyright (c) 2019-present, Blue Marble Payroll, LLC
|
5
|
-
#
|
6
|
-
# This source code is licensed under the MIT license found in the
|
7
|
-
# LICENSE file in the root directory of this source tree.
|
8
|
-
#
|
9
|
-
|
10
|
-
module Dbee
|
11
|
-
module Dsl
|
12
|
-
# Provides methods for dealing with naming grammar.
|
13
|
-
module Inflectable
|
14
|
-
def constantize(value)
|
15
|
-
value.is_a?(String) || value.is_a?(Symbol) ? Object.const_get(value) : value
|
16
|
-
end
|
17
|
-
|
18
|
-
def tableize(value)
|
19
|
-
value.split('::')
|
20
|
-
.last
|
21
|
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
22
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
23
|
-
.tr('-', '_')
|
24
|
-
.downcase
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|