dbee 2.1.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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