dbee 2.1.1 → 3.0.0

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.
@@ -0,0 +1,47 @@
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
+ # Converts a tree based model to a Schema. Note that this results in a
12
+ # different but compatible schema compared to the result of generating a
13
+ # Dbee::Schema by calling to_schema on a Dbee::Base class. This is
14
+ # because converting from a tree based model is a lossy.
15
+ class SchemaFromTreeBasedModel # :nodoc:
16
+ class << self
17
+ def convert(tree_model)
18
+ Schema.new(to_graph_based_models(tree_model))
19
+ end
20
+
21
+ private
22
+
23
+ def to_graph_based_models(tree_model, parent_model = nil, models_attrs_by_name = {})
24
+ models_attrs_by_name[tree_model.name] = {
25
+ name: tree_model.name,
26
+ table: tree_model.table,
27
+ partitioners: tree_model.partitioners,
28
+ relationships: {}
29
+ }
30
+
31
+ parent_model && add_relationship_to_parent_model(
32
+ tree_model, models_attrs_by_name[parent_model.name]
33
+ )
34
+
35
+ tree_model.models.each do |sub_model|
36
+ to_graph_based_models(sub_model, tree_model, models_attrs_by_name)
37
+ end
38
+
39
+ models_attrs_by_name
40
+ end
41
+
42
+ def add_relationship_to_parent_model(model, graph_model_attrs)
43
+ graph_model_attrs[:relationships][model.name] = { constraints: model.constraints }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,50 @@
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 Util
12
+ # Provides a "make_keyed_by" method which extends the Hashable gem's
13
+ # concept of a "make" method.
14
+ module MakeKeyedBy # :nodoc:
15
+ # Given a hash of hashes or a hash of values of instances of this class,
16
+ # a hash is returned where all of the values are instances of this class
17
+ # and the keys are the string versions of the original hash.
18
+ #
19
+ # An ArgumentError is raised if the value's <tt>key_attrib</tt> is not
20
+ # equal to the top level hash key. This ensures that the
21
+ # <tt>key_attrib</tt> is the same in the incoming hash and the value.
22
+ #
23
+ # This is useful for cases where it makes sense in the configuration
24
+ # (YAML) specification to represent certain objects in a hash structure
25
+ # instead of a list.
26
+ def make_keyed_by(key_attrib, spec_hash)
27
+ # Once Ruby 2.5 support is dropped, this can just use the block form of
28
+ # #to_h.
29
+ spec_hash.map do |key, spec|
30
+ string_key = key.to_s
31
+ [string_key, make_value_checking_key_attib!(key_attrib, string_key, spec)]
32
+ end.to_h
33
+ end
34
+
35
+ private
36
+
37
+ def make_value_checking_key_attib!(key_attrib, key, spec)
38
+ if spec.is_a?(self)
39
+ if spec.send(key_attrib).to_s != key
40
+ err_msg = "expected a #{key_attrib} of '#{key}' but got '#{spec.send(key_attrib)}'"
41
+ raise ArgumentError, err_msg
42
+ end
43
+ spec
44
+ else
45
+ make((spec || {}).merge(key_attrib => key))
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/dbee/version.rb CHANGED
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module Dbee
11
- VERSION = '2.1.1'
11
+ VERSION = '3.0.0'
12
12
  end
@@ -11,52 +11,17 @@ require 'spec_helper'
11
11
  require 'fixtures/models'
12
12
 
13
13
  describe Dbee::Base do
14
- describe '#to_model' do
15
- it 'compiles correctly' do
16
- model_name = 'Theaters, Members, and Movies'
17
- expected_config = yaml_fixture('models.yaml')[model_name]
14
+ describe '.to_schema' do
15
+ it 'passes the keychain and model to DslSchemaBuilder' do
16
+ key_chain = Dbee::KeyChain.new
18
17
 
19
- expected_model = Dbee::Model.make(expected_config)
18
+ schema_builder_double = double(Dbee::DslSchemaBuilder)
19
+ expect(Dbee::DslSchemaBuilder).to receive(:new).with(Models::A, key_chain).and_return(
20
+ schema_builder_double
21
+ )
22
+ expect(schema_builder_double).to receive(:to_schema)
20
23
 
21
- key_paths = %w[
22
- members.demos.phone_numbers.a
23
- members.movies.b
24
- members.favorite_comic_movies.c
25
- members.favorite_mystery_movies.d
26
- members.favorite_comedy_movies.e
27
- parent_theater.members.demos.phone_numbers.f
28
- parent_theater.members.movies.g
29
- parent_theater.members.favorite_comic_movies.h
30
- parent_theater.members.favorite_mystery_movies.i
31
- parent_theater.members.favorite_comedy_movies.j
32
- ]
33
-
34
- key_chain = Dbee::KeyChain.new(key_paths)
35
-
36
- actual_model = Models::Theater.to_model(key_chain)
37
-
38
- expect(actual_model).to eq(expected_model)
39
- end
40
-
41
- it 'honors key_chain to flatten cyclic references' do
42
- model_name = 'Cycle Example'
43
- expected_config = yaml_fixture('models.yaml')[model_name]
44
-
45
- expected_model = Dbee::Model.make(expected_config)
46
-
47
- key_paths = %w[
48
- b1.c.a.z
49
- b1.d.a.y
50
- b2.c.a.x
51
- b2.d.a.w
52
- b2.d.a.b1.c.z
53
- ]
54
-
55
- key_chain = Dbee::KeyChain.new(key_paths)
56
-
57
- actual_model = Cycles::A.to_model(key_chain)
58
-
59
- expect(actual_model).to eq(expected_model)
24
+ Models::A.to_schema(key_chain)
60
25
  end
61
26
  end
62
27
 
@@ -72,32 +37,4 @@ describe Dbee::Base do
72
37
  expect(Models::E.inherited_table_name).to eq('table_set_to_e')
73
38
  end
74
39
  end
75
-
76
- describe 'partitioners' do
77
- it 'honors partitioners on a root model' do
78
- model_name = 'Partitioner Example 1'
79
- expected_config = yaml_fixture('models.yaml')[model_name]
80
- expected_model = Dbee::Model.make(expected_config)
81
-
82
- key_paths = %w[id]
83
- key_chain = Dbee::KeyChain.new(key_paths)
84
-
85
- actual_model = PartitionerExamples::Dog.to_model(key_chain)
86
-
87
- expect(actual_model).to eq(expected_model)
88
- end
89
-
90
- it 'honors partitioners on a child model' do
91
- model_name = 'Partitioner Example 2'
92
- expected_config = yaml_fixture('models.yaml')[model_name]
93
- expected_model = Dbee::Model.make(expected_config)
94
-
95
- key_paths = %w[id dogs.id]
96
- key_chain = Dbee::KeyChain.new(key_paths)
97
-
98
- actual_model = PartitionerExamples::Owner.to_model(key_chain)
99
-
100
- expect(actual_model).to eq(expected_model)
101
- end
102
- end
103
40
  end
@@ -9,23 +9,28 @@
9
9
 
10
10
  require 'spec_helper'
11
11
 
12
- describe Dbee::ConstantResolver do
13
- module A
14
- class B; end
15
- class E; end
16
- end
17
-
12
+ # rubocop:disable Lint/EmptyClass
13
+ # These are intentionally empty to keep the tests focused.
14
+ module A
18
15
  class B; end
19
16
 
20
- module C
21
- class D; end
22
- class E; end
17
+ class E; end
18
+ end
19
+
20
+ class B; end
23
21
 
24
- module F
25
- class G; end
26
- end
22
+ module C
23
+ class D; end
24
+
25
+ class E; end
26
+
27
+ module F
28
+ class G; end
27
29
  end
30
+ end
31
+ # rubocop:enable Lint/EmptyClass
28
32
 
33
+ describe Dbee::ConstantResolver do
29
34
  it 'resolves nested constant with the same as an ancestor constants sibling' do
30
35
  expect(subject.constantize('A::B')).to eq(::A::B)
31
36
  end
@@ -0,0 +1,106 @@
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
+ require 'spec_helper'
11
+ require 'fixtures/models'
12
+
13
+ describe Dbee::DslSchemaBuilder do
14
+ specify 'theaters example' do
15
+ model_name = 'Theaters, Members, and Movies from DSL'
16
+ expected_config = yaml_fixture('models.yaml')[model_name]
17
+
18
+ expected_schema = Dbee::Schema.new(expected_config)
19
+
20
+ key_paths = %w[
21
+ members.a
22
+ members.demos.phone_numbers.a
23
+ members.movies.b
24
+ members.favorite_comic_movies.c
25
+ members.favorite_mystery_movies.d
26
+ members.favorite_comedy_movies.e
27
+ parent_theater.members.demos.phone_numbers.f
28
+ parent_theater.members.movies.g
29
+ parent_theater.members.favorite_comic_movies.h
30
+ parent_theater.members.favorite_mystery_movies.i
31
+ parent_theater.members.favorite_comedy_movies.j
32
+ ]
33
+
34
+ key_chain = Dbee::KeyChain.new(key_paths)
35
+
36
+ actual_schema = Models::Theater.to_schema(key_chain)
37
+ expect(actual_schema).to eq(expected_schema)
38
+ end
39
+
40
+ it 'honors key_chain to flatten cyclic references' do
41
+ model_name = 'Cycle Example'
42
+ expected_config = yaml_fixture('models.yaml')[model_name]
43
+
44
+ expected_schema = Dbee::Schema.new(expected_config)
45
+
46
+ key_paths = %w[
47
+ b1.c.a.z
48
+ b1.d.a.y
49
+ b2.c.a.x
50
+ b2.d.a.w
51
+ b2.d.a.b1.c.z
52
+ ]
53
+
54
+ key_chain = Dbee::KeyChain.new(key_paths)
55
+
56
+ actual_schema = Cycles::A.to_schema(key_chain)
57
+ expect(actual_schema).to eq(expected_schema)
58
+ end
59
+
60
+ describe 'partitioners' do
61
+ it 'honors partitioners on a root model' do
62
+ model_name = 'Partitioner Example 1'
63
+ expected_config = yaml_fixture('models.yaml')[model_name]
64
+ expected_schema = Dbee::Schema.new(expected_config)
65
+
66
+ key_paths = %w[id]
67
+ key_chain = Dbee::KeyChain.new(key_paths)
68
+
69
+ actual_schema = PartitionerExamples::Dog.to_schema(key_chain)
70
+
71
+ expect(actual_schema).to eq(expected_schema)
72
+ end
73
+
74
+ it 'honors partitioners on a child model' do
75
+ model_name = 'Partitioner Example 2'
76
+ expected_config = yaml_fixture('models.yaml')[model_name]
77
+ expected_schema = Dbee::Schema.new(expected_config)
78
+
79
+ key_paths = %w[id dogs.id]
80
+ key_chain = Dbee::KeyChain.new(key_paths)
81
+
82
+ actual_schema = PartitionerExamples::Owner.to_schema(key_chain)
83
+
84
+ expect(actual_schema).to eq(expected_schema)
85
+ end
86
+ end
87
+
88
+ context 'README examples' do
89
+ specify 'code-first and configuration-first models are equal' do
90
+ config = yaml_fixture('models.yaml')['Readme']
91
+ config_schema = Dbee::Schema.new(config)
92
+
93
+ key_chain = Dbee::KeyChain.new(%w[
94
+ patients.a
95
+ patients.notes.b
96
+ patients.work_phone_number.c
97
+ patients.cell_phone_number.d
98
+ patients.fax_phone_number.e
99
+ ])
100
+
101
+ code_schema = ReadmeDataModels::Practice.to_schema(key_chain)
102
+
103
+ expect(config_schema).to eq(code_schema)
104
+ end
105
+ end
106
+ end
@@ -78,4 +78,28 @@ describe Dbee::KeyChain do
78
78
  end
79
79
  end
80
80
  end
81
+
82
+ describe '#to_unique_ancestors collapses the KeyChain' do
83
+ let(:collapsed_key_chain) do
84
+ described_class.new([
85
+ 'z.b.c.d.any_column',
86
+ 'b.c.any_column',
87
+ 'any_column',
88
+ '44.55.any_column'
89
+ ])
90
+ end
91
+
92
+ specify 'the empty case' do
93
+ expect(described_class.new.to_unique_ancestors).to eq described_class.new
94
+ end
95
+
96
+ specify 'when there are no ancestors, one key path is returned' do
97
+ no_query_depth = described_class.new(%w[column1 column2 column3])
98
+ expect(no_query_depth.to_unique_ancestors).to eq described_class.new(['any_column'])
99
+ end
100
+
101
+ it 'returns a new KeyChain which has one KeyPath per ancestor' do
102
+ expect(subject.to_unique_ancestors).to eq collapsed_key_chain
103
+ end
104
+ end
81
105
  end
@@ -9,14 +9,13 @@
9
9
 
10
10
  require 'spec_helper'
11
11
 
12
- describe Dbee::Model::Constraints do
13
- CONSTRAINT_CONFIG = { name: :a }.freeze
14
-
15
- CONSTRAINT_FACTORIES = {
16
- Dbee::Model::Constraints::Reference => CONSTRAINT_CONFIG.merge(parent: :b, type: :reference),
17
- Dbee::Model::Constraints::Static => CONSTRAINT_CONFIG.merge(value: :b, type: :static)
18
- }.freeze
12
+ CONSTRAINT_CONFIG = { name: :a }.freeze
13
+ CONSTRAINT_FACTORIES = {
14
+ Dbee::Model::Constraints::Reference => CONSTRAINT_CONFIG.merge(parent: :b, type: :reference),
15
+ Dbee::Model::Constraints::Static => CONSTRAINT_CONFIG.merge(value: :b, type: :static)
16
+ }.freeze
19
17
 
18
+ describe Dbee::Model::Constraints do
20
19
  CONSTRAINT_FACTORIES.each_pair do |constant, config|
21
20
  it "should instantiate #{constant} objects" do
22
21
  expect(described_class.make(config)).to be_a(constant)
@@ -55,77 +55,80 @@ describe Dbee::Model do
55
55
  end
56
56
  end
57
57
 
58
- describe '#ancestors' do
59
- let(:yaml_entities) { yaml_fixture('models.yaml') }
60
-
61
- let(:entity_hash) { yaml_entities['Theaters, Members, and Movies'] }
62
-
63
- subject { described_class.make(entity_hash) }
64
-
65
- specify 'returns proper models' do
66
- members = subject.models.first
67
-
68
- expected_plan = {
69
- %w[members] => members
58
+ describe '.make_keyed_by' do
59
+ it 'returns a hash of Models where the keys equal the names of the models' do
60
+ input = {
61
+ model1: nil,
62
+ model2: { table: :model2_table }
70
63
  }
71
-
72
- plan = subject.ancestors!(%w[members])
73
-
74
- expect(plan).to eq(expected_plan)
75
- end
76
-
77
- specify 'returns proper multi-level models' do
78
- members = subject.models.first
79
- demos = members.models.first
80
- phone_numbers = demos.models.first
81
-
82
- expected_plan = {
83
- %w[members] => members,
84
- %w[members demos] => demos,
85
- %w[members demos phone_numbers] => phone_numbers
64
+ expected_result = {
65
+ 'model1' => described_class.new(name: 'model1'),
66
+ 'model2' => described_class.new(name: 'model2', table: 'model2_table')
86
67
  }
87
68
 
88
- plan = subject.ancestors!(%w[members demos phone_numbers])
89
-
90
- expect(plan).to eq(expected_plan)
69
+ expect(described_class.make_keyed_by(:name, input)).to eq expected_result
91
70
  end
92
- end
93
-
94
- describe 'equality' do
95
- let(:config) { yaml_fixture('models.yaml')['Theaters, Members, and Movies'] }
96
71
 
97
- subject { described_class.make(config) }
98
-
99
- specify 'equality compares attributes' do
100
- model1 = described_class.make(config)
101
- model2 = described_class.make(config)
72
+ it 'accepts values of Dbee::Model instances' do
73
+ input = { model1: described_class.new(name: :model1) }
74
+ expected_result = { 'model1' => described_class.new(name: :model1) }
75
+ expect(described_class.make_keyed_by(:name, input)).to eq expected_result
76
+ end
102
77
 
103
- expect(model1).to eq(model2)
104
- expect(model1).to eql(model2)
78
+ it 'accepts values of Dbee::Model instances when the key attribute is a string' do
79
+ input = { 'model1' => described_class.new(name: 'model1') }
80
+ expected_result = { 'model1' => described_class.new(name: 'model1') }
81
+ expect(described_class.make_keyed_by(:name, input)).to eq expected_result
105
82
  end
106
83
 
107
- it 'returns false unless comparing same object types' do
108
- expect(subject).not_to eq(config)
109
- expect(subject).not_to eq(nil)
84
+ it 'raises an error when the input hash key is not equal to the name of the model' do
85
+ input = { model1: described_class.new(name: :bogus) }
86
+ expect do
87
+ described_class.make_keyed_by(:name, input)
88
+ end.to raise_error ArgumentError, "expected a name of 'model1' but got 'bogus'"
110
89
  end
111
90
  end
112
91
 
113
- context 'README examples' do
114
- specify 'code-first and configuration-first models are equal' do
115
- config = yaml_fixture('models.yaml')['Readme']
116
- config_model = described_class.make(config)
117
-
118
- key_chain = Dbee::KeyChain.new(%w[
119
- patients.a
120
- patients.notes.b
121
- patients.work_phone_number.c
122
- patients.cell_phone_number.d
123
- patients.fax_phone_number.e
124
- ])
125
-
126
- code_model = ReadmeDataModels::Practice.to_model(key_chain)
92
+ describe '#to_s' do
93
+ it 'is represented by the model name' do
94
+ expect(described_class.new(name: 'foo').to_s).to eq 'foo'
95
+ end
96
+ end
127
97
 
128
- expect(config_model).to eq(code_model)
98
+ equality_cases = {
99
+ tree_based: yaml_fixture('models.yaml')['Theaters, Members, and Movies Tree Based'],
100
+ graph_based: yaml_fixture('models.yaml')['Theaters, Members, and Movies from DSL']['theater']
101
+ }
102
+ equality_cases[:graph_based].merge!(name: 'theaters')
103
+ equality_cases.each do |test_case, config|
104
+ describe "equality for #{test_case}" do
105
+ let(:model1) { described_class.make(config) }
106
+ let(:model2) { described_class.make(config) }
107
+
108
+ subject { described_class.make(config) }
109
+
110
+ specify 'equality compares attributes' do
111
+ expect(model1).to eq(model2)
112
+ expect(model1).to eql(model2)
113
+ end
114
+
115
+ it 'returns false unless comparing same object types' do
116
+ expect(subject).not_to eq(config)
117
+ expect(subject).not_to eq(nil)
118
+ end
119
+
120
+ describe 'hash codes' do
121
+ specify 'are equal when objects are equal' do
122
+ expect(model1).to eq(model2)
123
+ expect(model1.hash).to eq(model2.hash)
124
+ end
125
+
126
+ specify 'are not equal when objects are not equal' do
127
+ different_model = described_class.new(name: :oddball)
128
+ expect(model1).not_to eq(different_model)
129
+ expect(model1.hash).not_to eq(different_model.hash)
130
+ end
131
+ end
129
132
  end
130
133
  end
131
134
  end