mince 2.0.0.pre → 2.0.0.pre.2

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.
data/lib/mince/version.rb CHANGED
@@ -9,7 +9,7 @@ module Mince
9
9
  end
10
10
 
11
11
  def self.patch
12
- '0.pre'
12
+ '0.pre.2'
13
13
  end
14
14
  end
15
15
 
data/lib/mince.rb CHANGED
@@ -1,2 +1,130 @@
1
- require_relative 'mince/version'
2
- require_relative 'mince/config'
1
+ # = Mince
2
+ #
3
+ # Mince is focused to provide support to write database agnostic applications
4
+ # and to write applications with an architecture that follows the Single
5
+ # Responsibility principle.
6
+ #
7
+ # The highest level difference between using mince and using something
8
+ # like Rails' ActiveRecord library is that mince does not encourage
9
+ # the "Active Record Pattern" in general. Look "Active Record Pattern"
10
+ # up in wikipedia to get a good understanding of what that means.
11
+ #
12
+ # Mince encourages separating your data objects and behavior from your
13
+ # business objects and behavior. These are two separate classes of
14
+ # objects with two complete separate responsibilities.
15
+ #
16
+ # * The data object provides access to the data store for a particular
17
+ # collection of data.
18
+ # * The business object adds behavior to the data object in order for the
19
+ # application to provide business value
20
+ #
21
+ # Creating an architecture like this provides more flexibility and more
22
+ # extensibility for your application. While you are developing your application
23
+ # you shoud be asking, what is the responsibility of this object? And make clear
24
+ # separations of behavior and responsibility
25
+ #
26
+ # Given that being said. The standard way to structure your directories while
27
+ # using mince is the following
28
+ #
29
+ # ./app
30
+ # ./models
31
+ # - authenticator.rb
32
+ # - user.rb
33
+ # - project.rb
34
+ # ./data_models
35
+ # - user_data_model.rb
36
+ # - project_data_model.rb
37
+ #
38
+ # 1. `Authenticator` is a standard ruby class that uses the user class
39
+ # to provide strong and clean authentication.
40
+ # 2. `User` and `Project` are ruby classes with the Mince::Model mixed in to
41
+ # provide some standard behavior to interact with a mince data model.
42
+ # 3. `UserDataModel` and `ProjectDataModel` are classes with the Mince::DataModel
43
+ # mixed in to provide standard behavior to interact with a mince data interface.
44
+ #
45
+ # A quick example of the extensibility of this would be if you needed to get users
46
+ # from LDAP or Active Directory rather than from MongoDB. This is a pretty common
47
+ # request and similar to other requirements for enterprise applications.
48
+ #
49
+ # With this architecture you could easily expand it to the following:
50
+ #
51
+ # ./app
52
+ # ./models
53
+ # - authenticator.rb
54
+ # - user.rb
55
+ # - project.rb
56
+ # ./data_models
57
+ # - ldap_user_data_model.rb
58
+ # - project_data_model.rb
59
+ #
60
+ # The only difference is now, instead of using a mince user data model, you are using
61
+ # a custom ldap user data model. As long as the API you wrote for the ldap user data
62
+ # model requires the same input and provides the same output as the mince user data
63
+ # model, then you did not have to change any other code in your application.
64
+ #
65
+ # Let's take this a little further. Say you have LDAP auth now, and you've continued
66
+ # developing on your application. You start feeling some pain with having your
67
+ # development environment depend on so many external systems in order to be up and
68
+ # running. Think about it, right now you are dependent on MongoDB and a deve LDAP
69
+ # environment to be running and seeded with data. That was the position I was in
70
+ # and one of the reasons I wrote mince.
71
+ #
72
+ # Let's extend the architecture like so:
73
+ #
74
+ # - app/
75
+ # - models/
76
+ # - authenticator.rb
77
+ # - user.rb
78
+ # - user_data_provider.rb
79
+ # - project.rb
80
+ # ./data_models
81
+ # - ldap_user_data_model.rb
82
+ # - user_data_model.rb
83
+ # - project_data_model.rb
84
+ #
85
+ # I added a user data model and user data provider. The user data provider determines
86
+ # which user data source to use for the specific environment that you are in.
87
+ #
88
+ # If you are running in a development environment, then you would configure it to
89
+ # use the user data model, if you are in staging, or production environments, you
90
+ # would use the ldap user data model.
91
+ #
92
+ # This removes one external system dependency from our development environment, let's
93
+ # remove MongoDB from being a development dependency as well so that we can focus
94
+ # on the business logic of our application, and not waste time on database maintenance
95
+ # issues.
96
+ #
97
+ # Configure mince to use hashy_db when you are running in Development mode like so:
98
+ # # require mince, if you haven't already
99
+ # require 'mince'
100
+ #
101
+ # # require the gem (needs to be installed via `gem install`, bundler, gemspec,
102
+ # # or some other way)
103
+ # require 'hashy_db'
104
+ #
105
+ # # Tell mince to use hashy_db
106
+ # Mince::Config.interface = Mince::HashyDb::Interface
107
+ #
108
+ # Now, all of mince related data interactions will use an in-memory hash.
109
+ #
110
+ # Configure mince to use mingo (a mince MongoDB interface) when you are running in Staging, or Production mode:
111
+ # # require mince, if you haven't already
112
+ # require 'mince'
113
+ #
114
+ # # require the gem (needs to be installed via `gem install`, bundler, gemspec,
115
+ # # or some other way)
116
+ # require 'mingo'
117
+ #
118
+ # # Tell mince to use mingo
119
+ # Mince::Config.interface = Mince::Mingo::Interface
120
+ #
121
+ # Done. Now we can simply start the ruby server and develop away.
122
+ #
123
+ # @author Matt Simpson
124
+ module Mince
125
+ # Load all mince libraries
126
+ require_relative 'mince/version'
127
+ require_relative 'mince/config'
128
+ require_relative 'mince/data_model'
129
+ require_relative 'mince/model'
130
+ end
@@ -0,0 +1,55 @@
1
+ require 'mince'
2
+ require 'hashy_db'
3
+
4
+ describe 'A simple mince data model integration spec' do
5
+ before do
6
+ Mince::Config.interface = Mince::HashyDb::Interface
7
+ Mince::HashyDb::Interface.clear
8
+ end
9
+
10
+ after do
11
+ Mince::HashyDb::Interface.clear
12
+ end
13
+
14
+ describe 'a model' do
15
+ subject { model_klass.new attributes }
16
+
17
+ let(:model_klass) do
18
+ Class.new do
19
+ include Mince::Model
20
+
21
+ data_model(
22
+ Class.new do
23
+ include Mince::DataModel
24
+
25
+ data_collection :guitars
26
+ data_fields :brand, :price, :type, :color
27
+ end
28
+ )
29
+ fields :brand, :price, :type, :color
30
+ end
31
+ end
32
+
33
+ let(:attributes) { { brand: brand, price: price, type: type, color: color } }
34
+ let(:brand) { mock }
35
+ let(:price) { mock }
36
+ let(:type) { mock }
37
+ let(:color) { mock }
38
+
39
+ it 'is initialized with the correct data' do
40
+ subject.brand.should == brand
41
+ subject.price.should == price
42
+ subject.type.should == type
43
+ subject.color.should == color
44
+ end
45
+
46
+ it 'can be persisted to the mince data interface' do
47
+ subject.save
48
+
49
+ raw_record = Mince::Config.interface.find(:guitars, Mince::HashyDb::Interface.primary_key, subject.id)
50
+ model_record = model_klass.find(subject.id)
51
+ raw_record[:brand].should == brand
52
+ model_record.brand.should == brand
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,228 @@
1
+ require_relative '../../../lib/mince/data_model'
2
+ require 'digest'
3
+
4
+ describe Mince::DataModel, 'Mixin' do
5
+ let(:described_class) { klass }
6
+
7
+ let(:collection_name) { :guitars }
8
+ let(:data_field_attributes) do
9
+ {
10
+ brand: 'a brand everyone knows',
11
+ price: 'a price you save up for',
12
+ type: 'the kind you want',
13
+ color: 'should be your favorite'
14
+ }
15
+ end
16
+
17
+ let(:klass) do
18
+ Class.new do
19
+ include Mince::DataModel
20
+
21
+ data_collection :guitars
22
+ data_fields :brand, :price, :type, :color
23
+ end
24
+ end
25
+
26
+ let(:interface) { mock 'mince data interface class', generate_unique_id: unique_id, primary_key: primary_key }
27
+ let(:unique_id) { mock 'id' }
28
+ let(:primary_key) { "custom_id" }
29
+
30
+ before do
31
+ Mince::Config.stub(:interface => interface)
32
+ end
33
+
34
+ describe "storing a data model" do
35
+ let(:model) { mock 'a model', instance_values: data_field_attributes }
36
+
37
+ before do
38
+ interface.stub(:add)
39
+ end
40
+
41
+ it 'generates a unique id using the model as a salt' do
42
+ interface.should_receive(:generate_unique_id).with(model).and_return(unique_id)
43
+
44
+ described_class.store(model)
45
+ end
46
+
47
+ it 'adds the data model to the db store' do
48
+ interface.should_receive(:add).with(collection_name, HashWithIndifferentAccess.new({primary_key => unique_id}).merge(data_field_attributes))
49
+
50
+ described_class.store(model)
51
+ end
52
+ end
53
+
54
+ it 'can delete the collection' do
55
+ interface.should_receive(:delete_collection).with(collection_name)
56
+
57
+ described_class.delete_collection
58
+ end
59
+
60
+ it 'can delete a field' do
61
+ field = mock 'field to delete from the collection'
62
+
63
+ interface.should_receive(:delete_field).with(collection_name, field)
64
+
65
+ described_class.delete_field(field)
66
+ end
67
+
68
+ it 'can delete records by a given set of params' do
69
+ params = mock 'params that provide a condition of what records to delete from the collection'
70
+
71
+ interface.should_receive(:delete_by_params).with(collection_name, params)
72
+
73
+ described_class.delete_by_params(params)
74
+ end
75
+
76
+ describe "updating a data model" do
77
+ let(:data_model_id) { '1234567' }
78
+ let(:model) { mock 'a model', id: data_model_id, instance_values: data_field_attributes }
79
+
80
+ before do
81
+ interface.stub(:replace)
82
+ end
83
+
84
+ it 'replaces the data model in the db store' do
85
+ interface.should_receive(:replace).with(collection_name, HashWithIndifferentAccess.new({primary_key => data_model_id}).merge(data_field_attributes))
86
+
87
+ described_class.update(model)
88
+ end
89
+ end
90
+
91
+ describe 'updating a specific field for a data model' do
92
+ let(:data_model_id) { '1234567' }
93
+
94
+ it 'has the data store update the field' do
95
+ interface.should_receive(:update_field_with_value).with(collection_name, data_model_id, :some_field, 'some value')
96
+
97
+ described_class.update_field_with_value(data_model_id, :some_field, 'some value')
98
+ end
99
+ end
100
+
101
+ describe 'incrementing a specific field by a given an amount' do
102
+ let(:data_model_id) { mock 'id' }
103
+
104
+ it 'has the data store update the field' do
105
+ interface.should_receive(:increment_field_by_amount).with(collection_name, data_model_id, :some_field, 4)
106
+
107
+ described_class.increment_field_by_amount(data_model_id, :some_field, 4)
108
+ end
109
+ end
110
+
111
+ describe "pushing a value to an array for a data model" do
112
+ let(:data_model_id) { '1234567' }
113
+
114
+ it 'replaces the data model in the db store' do
115
+ interface.should_receive(:push_to_array).with(collection_name, primary_key, data_model_id, :array_field, 'some value')
116
+
117
+ described_class.push_to_array(data_model_id, :array_field, 'some value')
118
+ end
119
+ end
120
+
121
+ describe "getting all data models with a specific value for a field" do
122
+ let(:data_model) { {primary_key => 'some id'} }
123
+ let(:expected_data_models) { [HashWithIndifferentAccess.new({:id => 'some id', primary_key => 'some id'})] }
124
+ let(:data_models) { [data_model] }
125
+ subject { described_class.array_contains(:some_field, 'some value') }
126
+
127
+ it 'returns the stored data models with the requested field / value' do
128
+ interface.should_receive(:array_contains).with(collection_name, :some_field, 'some value').and_return(data_models)
129
+
130
+ subject.should == expected_data_models
131
+ end
132
+ end
133
+
134
+ describe "removing a value from an array for a data model" do
135
+ let(:data_model_id) { '1234567' }
136
+
137
+ it 'removes the value from the array' do
138
+ interface.should_receive(:remove_from_array).with(collection_name, primary_key, data_model_id, :array_field, 'some value')
139
+
140
+ described_class.remove_from_array(data_model_id, :array_field, 'some value')
141
+ end
142
+ end
143
+
144
+ describe 'getting all of the data models' do
145
+ let(:data_model) { {primary_key => 'some id'} }
146
+ let(:expected_data_models) { [HashWithIndifferentAccess.new({:id => 'some id', primary_key => 'some id'})] }
147
+ let(:data_models) { [data_model] }
148
+
149
+ it 'returns the stored data models' do
150
+ interface.should_receive(:find_all).with(collection_name).and_return(data_models)
151
+
152
+ described_class.all.should == expected_data_models
153
+ end
154
+ end
155
+
156
+ describe "getting all the data fields by a parameter hash" do
157
+ let(:data_model) { {primary_key => 'some id'} }
158
+ let(:expected_data_models) { [{:id => 'some id', primary_key => 'some id'}] }
159
+ let(:data_models) { [data_model] }
160
+ let(:expected_data_models) { [HashWithIndifferentAccess.new(data_model)] }
161
+
162
+ it 'passes the hash to the interface_class' do
163
+ interface.should_receive(:get_all_for_key_with_value).with(collection_name, :field2, 'not nil').and_return(data_models)
164
+
165
+ described_class.all_by_field(:field2, 'not nil').should == expected_data_models
166
+ end
167
+ end
168
+
169
+ describe "getting a record by a set of key values" do
170
+ let(:data_model) { {primary_key => 'some id'} }
171
+ let(:data_models) { [data_model] }
172
+ let(:expected_data_models) { [{:id => 'some id', primary_key => 'some id'}] }
173
+
174
+ let(:sample_hash) { {field1: nil, field2: 'not nil'} }
175
+
176
+ it 'passes the hash to the interface_class' do
177
+ interface.should_receive(:get_by_params).with(collection_name, sample_hash).and_return(data_models)
178
+
179
+ described_class.find_by_fields(sample_hash).should == HashWithIndifferentAccess.new(expected_data_models.first)
180
+ end
181
+ end
182
+
183
+ describe "getting all of the data models for a where a field contains any value of a given array of values" do
184
+ let(:data_models) { [{primary_key => 'some id'}, {primary_key => 'some id 2'}] }
185
+ let(:expected_data_models) { [{"id" => 'some id', primary_key => 'some id'}, {"id" => 'some id 2', primary_key => 'some id 2'}] }
186
+
187
+ subject { described_class.containing_any(:some_field, ['value 1', 'value 2']) }
188
+
189
+ it 'returns the stored data models' do
190
+ interface.should_receive(:containing_any).with(collection_name, :some_field, ['value 1', 'value 2']).and_return(data_models)
191
+
192
+ subject.should == expected_data_models
193
+ end
194
+ end
195
+
196
+ describe "getting a record by a key and value" do
197
+ let(:data_model) { {primary_key => 'some id'} }
198
+ let(:expected_data_model) { {:id => 'some id', primary_key => 'some id'} }
199
+
200
+ it 'returns the correct data model' do
201
+ interface.should_receive(:get_for_key_with_value).with(collection_name, :field2, 'not nil').and_return(data_model)
202
+
203
+ described_class.find_by_field(:field2, 'not nil').should == HashWithIndifferentAccess.new(expected_data_model)
204
+ end
205
+ end
206
+
207
+ describe "getting all data models with a specific value for a field" do
208
+ let(:data_models) { [{primary_key => 'some id'}, {primary_key => 'some id 2'}] }
209
+ let(:expected_data_models) { [HashWithIndifferentAccess.new({:id => 'some id', primary_key => 'some id'}), HashWithIndifferentAccess.new({id: 'some id 2', primary_key => 'some id 2'})] }
210
+ subject { described_class.all_by_field(:some_field, 'some value') }
211
+
212
+ it 'returns the stored data models with the requested field / value' do
213
+ interface.should_receive(:get_all_for_key_with_value).with(collection_name, :some_field, 'some value').and_return(data_models)
214
+
215
+ subject.should == expected_data_models
216
+ end
217
+ end
218
+
219
+ describe 'getting a specific data model' do
220
+ let(:data_model) { {primary_key => 'id', :id => 'id' } }
221
+
222
+ it 'returns the data model from the data store' do
223
+ interface.should_receive(:find).with(collection_name, primary_key, 'id').and_return(data_model)
224
+
225
+ described_class.find(data_model[:id]).should == HashWithIndifferentAccess.new(data_model)
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,128 @@
1
+ require_relative '../../../lib/mince/model'
2
+
3
+ describe Mince::Model do
4
+ let(:klass) do
5
+ Class.new do
6
+ include Mince::Model
7
+
8
+ data_model Class.new
9
+
10
+ field :meh
11
+ field :foo, assignable: true
12
+ field :bar, assignable: false
13
+ fields :baz, :qaaz
14
+ end
15
+ end
16
+
17
+ let(:meh) { mock 'meh' }
18
+ let(:foo) { mock 'foo' }
19
+ let(:bar) { mock 'bar' }
20
+ let(:baz) { mock 'baz' }
21
+ let(:qaaz) { mock 'qaaz' }
22
+
23
+ subject { klass.new(meh: meh, foo: foo, bar: bar, baz: baz, qaaz: qaaz) }
24
+
25
+ it 'initializes the object and assigns values to the fields' do
26
+ subject.meh.should == meh
27
+ subject.foo.should == foo
28
+ subject.bar.should == bar
29
+ subject.baz.should == baz
30
+ subject.qaaz.should == qaaz
31
+ end
32
+
33
+ it 'can set the assignable fields outside of the initilizer' do
34
+ subject.foo = 'foo1'
35
+ subject.foo.should == 'foo1'
36
+ subject.attributes = { foo: 'foo2' }
37
+ subject.foo.should == 'foo2'
38
+ end
39
+
40
+ it 'cannot set the readonly field outside of the initilizer' do
41
+ subject.attributes = { bar: 'bar1' }
42
+ subject.bar.should == bar
43
+ end
44
+
45
+ it 'fields are readonly by default' do
46
+ subject.attributes = { meh: 'meh1' }
47
+ subject.meh.should == meh
48
+ end
49
+
50
+ describe 'saving' do
51
+ let(:id) { mock 'id' }
52
+ let(:data_fields) { subject.fields }
53
+
54
+ before do
55
+ subject.data_model.stub(:data_fields).and_return(data_fields)
56
+ end
57
+
58
+ context 'when the model has fields that are not defined in the data model' do
59
+ let(:data_fields) { subject.fields - extra_fields }
60
+ let(:extra_fields) { subject.fields[0..-2] }
61
+
62
+ it 'raises an exception with a message' do
63
+ expect { subject.save }.to raise_error("Tried to save a #{subject.class.name} with fields not specified in #{subject.data_model.name}: #{extra_fields.join(', ')}")
64
+ end
65
+ end
66
+
67
+ context 'when it has not yet been persisted to the mince data model' do
68
+ before do
69
+ subject.data_model.stub(:store => id)
70
+ end
71
+
72
+ it 'stores the model' do
73
+ subject.data_model.should_receive(:store).with(subject).and_return(id)
74
+
75
+ subject.save
76
+ end
77
+
78
+ it 'assigns the id' do
79
+ subject.save
80
+
81
+ subject.id.should == id
82
+ end
83
+ end
84
+
85
+ context 'when it has already been persisted to the mince data model' do
86
+ let(:subject) { klass.new(id: id) }
87
+
88
+ it 'updates the model' do
89
+ subject.data_model.should_receive(:update).with(subject)
90
+
91
+ subject.save
92
+ end
93
+ end
94
+ end
95
+
96
+ describe "Query Methods:" do
97
+ describe 'finding a model by id' do
98
+ subject { klass.find(id) }
99
+
100
+ let(:id) { mock 'id' }
101
+
102
+ before do
103
+ klass.data_model.stub(:find).with(id).and_return(data)
104
+ end
105
+
106
+ context 'when it exists' do
107
+ let(:data) { mock 'data' }
108
+ let(:model) { mock 'model' }
109
+
110
+ before do
111
+ klass.stub(:new).with(data).and_return(model)
112
+ end
113
+
114
+ it 'returns the model' do
115
+ subject.should == model
116
+ end
117
+ end
118
+
119
+ context 'when it does not exist' do
120
+ let(:data) { nil }
121
+
122
+ it 'returns nothing' do
123
+ subject.should be_nil
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end