muve 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile +11 -1
- data/Rakefile +6 -0
- data/lib/muve/errors.rb +14 -2
- data/lib/muve/helpers.rb +7 -0
- data/lib/muve/model.rb +176 -12
- data/lib/muve/store/mongo.rb +50 -0
- data/lib/muve/store.rb +81 -0
- data/lib/muve/version.rb +1 -1
- data/lib/muve.rb +39 -3
- data/muve.gemspec +1 -5
- data/spec/adaptor/mongo_spec.rb +52 -0
- data/spec/helper_spec.rb +11 -0
- data/spec/model_spec.rb +251 -0
- data/spec/movement_spec.rb +4 -4
- data/spec/muve_spec.rb +29 -0
- data/spec/store_spec.rb +62 -0
- metadata +16 -73
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07fd84a42ff9d48763eb97e0991f3b29c1922a5a
|
4
|
+
data.tar.gz: bae3ee756eb66eff4539fed0fcd717ca1bde469a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c864989ed5a970291c4a794e503553def1dcf813c92d7d8700858434a8170e51c3183cbd9ad0e0fa43651299e6f77a4887e74948bc04704a10140af0c1bbb0f4
|
7
|
+
data.tar.gz: 0bb9e70a4e32a2ae7c44d07689b433f67074353cf508201cc16f45b1c9aec7861cf785c54fa61ccd6e8fb7f6f094bea11d9e96100e0f8a3b261213c5a6247b10
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -1,4 +1,14 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
# Specify your gem's dependencies in muve.gemspec
|
4
3
|
gemspec
|
4
|
+
|
5
|
+
gem "rake", "~> 10.3.2"
|
6
|
+
gem "bundler", "~> 1.6"
|
7
|
+
|
8
|
+
group :test do
|
9
|
+
gem "mongo", "~> 1.10.2"
|
10
|
+
gem "bson_ext"
|
11
|
+
gem "ffaker", "~> 1.24.0"
|
12
|
+
gem "factory_girl", "~> 4.0.0"
|
13
|
+
gem "rspec", "~> 3.0.0"
|
14
|
+
end
|
data/Rakefile
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
require "rspec/core/rake_task"
|
2
2
|
require "rake/testtask"
|
3
3
|
require "bundler/gem_tasks"
|
4
|
+
require "rdoc/task"
|
4
5
|
|
5
6
|
RSpec::Core::RakeTask.new('spec') do |t|
|
6
7
|
t.verbose = false
|
7
8
|
end
|
8
9
|
|
10
|
+
RDoc::Task.new do |rdoc|
|
11
|
+
rdoc.main = "README.rdoc"
|
12
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
13
|
+
end
|
14
|
+
|
9
15
|
task default: :spec
|
data/lib/muve/errors.rb
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
module MuveError
|
2
|
-
class
|
2
|
+
class MuveStandardError < StandardError
|
3
3
|
end
|
4
4
|
|
5
|
-
class MuveSaveError <
|
5
|
+
class MuveSaveError < MuveStandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
class MuveInvalidQuery < MuveStandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
class MuveInvalidAttributes < MuveStandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
class MuveIncompleteImplementation < MuveStandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class MuveNotConfigured < MuveStandardError
|
6
18
|
end
|
7
19
|
end
|
data/lib/muve/helpers.rb
ADDED
data/lib/muve/model.rb
CHANGED
@@ -1,40 +1,204 @@
|
|
1
1
|
module Muve
|
2
|
+
# Muve models imitate some behavior that one may commonly expect of typical
|
3
|
+
# models. There are mechanisms in place for creation, modification, retrieval
|
4
|
+
# and the removal of resources.
|
5
|
+
#
|
6
|
+
# In order to make this a flexible solution, models never take care of
|
7
|
+
# handling the persisting of the resources they manange. That part of the
|
8
|
+
# lifing is dispatched to the +Model::Store+ object which may be though of
|
9
|
+
# as an adaptor.
|
10
|
+
#
|
11
|
+
# --
|
12
|
+
# TODO: Include ActiveRecord::Model instead of using this tedious
|
13
|
+
# implementation
|
14
|
+
# ++
|
2
15
|
module Model
|
3
16
|
include MuveError
|
4
|
-
|
5
|
-
|
17
|
+
include Muve::Helper
|
18
|
+
|
19
|
+
def initialize(params={})
|
20
|
+
params = {} unless params
|
21
|
+
params.each do |attr, value|
|
22
|
+
next if invalid_attributes.include? attr.to_s
|
23
|
+
self.public_send "#{attr}=", value
|
24
|
+
end
|
25
|
+
|
6
26
|
@new_record = true
|
27
|
+
@destroyed = false
|
7
28
|
end
|
8
|
-
|
29
|
+
|
30
|
+
def self.included(base)
|
31
|
+
base.extend ClassMethods
|
32
|
+
end
|
33
|
+
|
34
|
+
# Initializes the +Muve::Model+ class. Use the +Muve::Model::init+ method
|
35
|
+
# to set a adaptor to take care of the retrieval and storage of resources.
|
36
|
+
def self.init(adaptor=nil)
|
37
|
+
@@adaptor = adaptor
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the adaptor set to handle retrieval and storage of resources
|
41
|
+
def self.handler
|
42
|
+
@@adaptor
|
43
|
+
end
|
44
|
+
|
45
|
+
# Save a resource and raises an MuveSaveError on failure
|
9
46
|
def save!
|
10
|
-
create_or_update
|
47
|
+
create_or_update
|
48
|
+
rescue => e
|
49
|
+
raise MuveSaveError, "Save failed because #{e} was raised"
|
11
50
|
end
|
12
51
|
|
52
|
+
# Save a resource
|
13
53
|
def save
|
14
54
|
create_or_update
|
55
|
+
rescue => e
|
56
|
+
false
|
15
57
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
58
|
+
|
59
|
+
# Destroy a resource
|
60
|
+
def destroy
|
61
|
+
if adaptor.delete(container, id) == true
|
62
|
+
@destroyed = true
|
63
|
+
end
|
21
64
|
end
|
22
|
-
|
65
|
+
|
66
|
+
# Returns true if the resource has recently been instantiated but not yet
|
67
|
+
# written to the data store.
|
23
68
|
def new_record?
|
24
|
-
@new_record
|
69
|
+
@new_record = true if @new_record.nil?
|
70
|
+
@new_record
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns true if the resource is not newly instantiated or recently
|
74
|
+
# destroyed.
|
75
|
+
def persisted?
|
76
|
+
!(new_record? || destroyed?)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns true if the resource in question has been destroyed
|
80
|
+
def destroyed?
|
81
|
+
@destroyed
|
25
82
|
end
|
26
83
|
|
84
|
+
# Returns a true if the resource passes all validations
|
27
85
|
def valid?
|
28
86
|
false
|
29
87
|
end
|
30
88
|
|
89
|
+
# Returns true if the resource fails any validation
|
31
90
|
def invalid?
|
32
91
|
!valid?
|
33
92
|
end
|
93
|
+
|
94
|
+
def id
|
95
|
+
@id
|
96
|
+
end
|
34
97
|
|
35
98
|
private
|
99
|
+
def invalid_attributes
|
100
|
+
%w(id adaptor)
|
101
|
+
end
|
102
|
+
|
103
|
+
def populate(details)
|
104
|
+
details = {} unless details
|
105
|
+
|
106
|
+
@id = details[:id] if details.key? :id
|
107
|
+
|
108
|
+
details.each do |attr, value|
|
109
|
+
next if invalid_attributes.include? attr.to_s
|
110
|
+
self.public_send "#{attr}=", value
|
111
|
+
end
|
112
|
+
|
113
|
+
@new_record = false if details.key? :id
|
114
|
+
end
|
115
|
+
|
36
116
|
def create_or_update
|
37
|
-
result = new_record? ? create : update
|
117
|
+
result = new_record? ? create(attributes) : update(attributes)
|
118
|
+
self
|
119
|
+
end
|
120
|
+
|
121
|
+
# NOTE: not sure we need this
|
122
|
+
def attributes
|
123
|
+
data = {}
|
124
|
+
fields.each { |k| data[k.to_sym] = self.public_send(k) }
|
125
|
+
data
|
126
|
+
end
|
127
|
+
|
128
|
+
# Creates the record and performs the necessary housekeeping (e.g.: setting
|
129
|
+
# the new id and un-marking the new_record?
|
130
|
+
def create(attr)
|
131
|
+
@id = adaptor.create(container, attr)
|
132
|
+
@new_record = false
|
133
|
+
end
|
134
|
+
|
135
|
+
# TODO: Update the record and return the number of modified rows
|
136
|
+
def update(attr)
|
137
|
+
adaptor.update(container, id, attr)
|
138
|
+
end
|
139
|
+
|
140
|
+
def adaptor
|
141
|
+
self.class.adaptor
|
142
|
+
end
|
143
|
+
|
144
|
+
def container
|
145
|
+
end
|
146
|
+
|
147
|
+
def details
|
148
|
+
end
|
149
|
+
|
150
|
+
# Class methods exposed to all Muve models
|
151
|
+
module ClassMethods
|
152
|
+
# Configure the adaptor to take care of handling persistence for this
|
153
|
+
# model. The adaptor should extend +Muve::Store+.
|
154
|
+
#
|
155
|
+
# Adaptors provide an abstraction layer between Muve models and the actual
|
156
|
+
# datastore. This provides some flexibility in design as one may exchange
|
157
|
+
# an adaptor for another in order to support another database technology
|
158
|
+
# (.e.g: swithing between document databases or relational databases)
|
159
|
+
def adaptor=(adaptor)
|
160
|
+
@@adaptor = adaptor
|
161
|
+
end
|
162
|
+
|
163
|
+
# The adaptor currently set to handle persistence for all Muve::Model
|
164
|
+
# classes and instances
|
165
|
+
def adaptor
|
166
|
+
@@adaptor
|
167
|
+
end
|
168
|
+
|
169
|
+
def connection
|
170
|
+
Muve::Model.connection
|
171
|
+
end
|
172
|
+
|
173
|
+
def database
|
174
|
+
Muve::Model.database
|
175
|
+
end
|
176
|
+
|
177
|
+
# The container (e.g.: collection, tablename or anything that is analogous
|
178
|
+
# to this construct) of the resource
|
179
|
+
def container
|
180
|
+
raise MuveError::MuveNotConfigured, "container not defined for #{self}"
|
181
|
+
end
|
182
|
+
|
183
|
+
# Finds a resource by id
|
184
|
+
def find(id)
|
185
|
+
result = self.new()
|
186
|
+
result.send(:populate, self.adaptor.get(self, id))
|
187
|
+
result
|
188
|
+
end
|
189
|
+
|
190
|
+
# Querries the resource repository for all resources that match the
|
191
|
+
# specified parameters.
|
192
|
+
def where(params)
|
193
|
+
Enumerator.new do |item|
|
194
|
+
(self.adaptor.get(self, nil, params) or []).each do |details|
|
195
|
+
details
|
196
|
+
result = self.new()
|
197
|
+
result.send(:populate, details)
|
198
|
+
item << result
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
38
202
|
end
|
39
203
|
end
|
40
204
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Muve
|
2
|
+
module Store
|
3
|
+
class Mongo
|
4
|
+
require 'mongo'
|
5
|
+
|
6
|
+
extend Muve::Store
|
7
|
+
|
8
|
+
def self.create(resource, details)
|
9
|
+
raise MuveInvalidAttributes, "invalid update data" unless details.kind_of? Hash
|
10
|
+
resource.database[resource.container].insert(details)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.fetch(resource, id, details={})
|
14
|
+
# TODO: discover a solution that works for situations where database
|
15
|
+
# driver returns string keys as well as symbol keys
|
16
|
+
details = {} unless details.kind_of? Hash
|
17
|
+
result = resource.database[resource.container].find_one(details.merge(_id: id))
|
18
|
+
result = Helper.symbolize_keys(result)
|
19
|
+
result[:id] = result.delete(:_id)
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.find(resource, details)
|
24
|
+
details = {} unless details.kind_of? Hash
|
25
|
+
Enumerator.new do |result|
|
26
|
+
resource.database[resource.container].find(details).each do |item|
|
27
|
+
item = Helper.symbolize_keys(item)
|
28
|
+
item[:id] = item.delete(:_id)
|
29
|
+
result << item
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.update(resource, id, details)
|
35
|
+
raise MuveInvalidAttributes, "invalid update data" unless details.kind_of? Hash
|
36
|
+
# TODO: raise error if details is not valid
|
37
|
+
resource.database[resource.container].find_and_modify(
|
38
|
+
query: { _id: id },
|
39
|
+
update: details
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.delete(resource, id, details=nil)
|
44
|
+
details = {} unless details.kind_of? Hash
|
45
|
+
details = details.merge(_id: id) if id
|
46
|
+
resource.database[resource.container].remove(details)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/muve/store.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'muve/store/mongo'
|
2
|
+
|
3
|
+
module Muve
|
4
|
+
# Muve::Store takes care of resource persistence and retrieval. Use stores
|
5
|
+
# as adaptors to connect your implementation of Muve to whichever datastore
|
6
|
+
# you please.
|
7
|
+
#
|
8
|
+
# Adaptors or Stores only take of the interaction with the datastore but
|
9
|
+
# leave the model's housekeeping up the respective model. Make sure to
|
10
|
+
# conform to the expected return formats for the different adaptor methods.
|
11
|
+
#
|
12
|
+
# Take a look at +Muve::Store::Mongo+ to find an implementation of a store
|
13
|
+
# adaptor.
|
14
|
+
module Store
|
15
|
+
include MuveError
|
16
|
+
include Muve::Helper
|
17
|
+
|
18
|
+
# gets data from the given container matching the provided details
|
19
|
+
#
|
20
|
+
# Given a +Place+ resource the following calls may be acceptable
|
21
|
+
# - +Adaptor.get(Place, 1232) # find one resource where id = 1232+
|
22
|
+
# - +Adaptor.get(Place, nil, { city: 'NYC', rating: 5 })+ # find more
|
23
|
+
def get(resource, id=nil, details=nil)
|
24
|
+
raise MuveInvalidQuery unless id || details
|
25
|
+
|
26
|
+
if details
|
27
|
+
find(resource, details)
|
28
|
+
else
|
29
|
+
fetch(resource, id, {})
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# creates a resource containing the specified details in the repository.
|
34
|
+
# Returns the id of the created object on success, raises an error otherwise
|
35
|
+
def create(resource, details)
|
36
|
+
raise MuveIncompleteImplementation, "implement a create handler for #{self}"
|
37
|
+
end
|
38
|
+
|
39
|
+
# removes a resource matching the optional +id+ and +details+
|
40
|
+
# from the repository.
|
41
|
+
# A successful removal operation should returns +true+ while any other
|
42
|
+
# value is considered an error.
|
43
|
+
def delete(resource, id, details=nil)
|
44
|
+
raise MuveIncompleteImplementation, "implement a delete handler for #{self}"
|
45
|
+
end
|
46
|
+
|
47
|
+
# update a resource with the identified by +id+ with the given +details+
|
48
|
+
def update(resource, id, details)
|
49
|
+
raise MuveIncompleteImplementation, "implement a update handler for #{self}"
|
50
|
+
end
|
51
|
+
|
52
|
+
# collect a single resource from the repository that matches the given id
|
53
|
+
# and details. Upon the successful retrieval of a resource the id of the
|
54
|
+
# resource is presented under the key +id+ while other attributes of the
|
55
|
+
# resource bear arbitrary names.
|
56
|
+
#
|
57
|
+
# { id: 12, name: 'Spock', organization: 'The Enterprise' }
|
58
|
+
def fetch(resource, id, details={})
|
59
|
+
raise MuveIncompleteImplementation, "implement a fetch handler for #{self}"
|
60
|
+
end
|
61
|
+
|
62
|
+
# find resources from its repository that match the given id and details
|
63
|
+
# Returns an +Enumerator+ that returns a hash with the key +id+ containing
|
64
|
+
# the primary key for the respective resource.
|
65
|
+
#
|
66
|
+
# def find(resource, details)
|
67
|
+
# details = {} unless details.kind_of? Hash
|
68
|
+
# Enumerator.new do |item|
|
69
|
+
# fetched_result_from_datastore.each do |data|
|
70
|
+
# item << format_data(data) # format_data composes the required hash
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
def find(resource, details)
|
75
|
+
raise MuveIncompleteImplementation, "implement a find handler for #{self}"
|
76
|
+
end
|
77
|
+
|
78
|
+
alias_method :destroy, :delete
|
79
|
+
alias_method :remove, :delete
|
80
|
+
end
|
81
|
+
end
|
data/lib/muve/version.rb
CHANGED
data/lib/muve.rb
CHANGED
@@ -15,22 +15,58 @@ require "muve/version"
|
|
15
15
|
# times to improve its usability and implementation.
|
16
16
|
module Muve
|
17
17
|
require "muve/errors"
|
18
|
+
require "muve/helpers"
|
18
19
|
require "muve/model"
|
20
|
+
require "muve/store"
|
19
21
|
require "muve/location"
|
20
22
|
require "muve/traveller"
|
21
23
|
require "muve/movement"
|
22
24
|
|
23
25
|
module Model
|
26
|
+
# Connection to the datastore
|
24
27
|
def connection
|
25
28
|
Model.connection
|
26
29
|
end
|
30
|
+
|
31
|
+
# Database instance for the model
|
32
|
+
def database
|
33
|
+
Model.database
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set the connection to the datastore
|
37
|
+
def self.connection=connection
|
38
|
+
(@@conn = connection) if (connection)
|
39
|
+
end
|
27
40
|
|
41
|
+
def self.database=database
|
42
|
+
(@@db = database) if (database)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Connection to the datastore
|
28
46
|
def self.connection
|
29
|
-
|
47
|
+
begin
|
48
|
+
@@conn
|
49
|
+
rescue => e
|
50
|
+
raise MuveNotConfigured, "the connection has not been defined"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Database instance to be used by the adaptor
|
55
|
+
def self.database
|
56
|
+
begin
|
57
|
+
@@db
|
58
|
+
rescue => e
|
59
|
+
raise MuveNotConfigured, "the database has not been defined"
|
60
|
+
end
|
30
61
|
end
|
31
62
|
end
|
32
63
|
|
33
|
-
|
34
|
-
|
64
|
+
# Initialize Muve with an optional connection to the datastore.
|
65
|
+
# This could be a MongoDB or PostgreSQL connection for instance. Besides a
|
66
|
+
# connection, an adaptor will be needed to actually handle the interaction
|
67
|
+
# between the models and the datastore through the given connection.
|
68
|
+
def self.init(connection=nil, database=nil)
|
69
|
+
Model.connection =connection
|
70
|
+
Model.database = database # can't automatically infer the db as this may differ among adaptors
|
35
71
|
end
|
36
72
|
end
|
data/muve.gemspec
CHANGED
@@ -18,11 +18,7 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
|
21
|
-
s.add_development_dependency "bundler", "~> 1.6"
|
22
|
-
s.add_development_dependency "rake"
|
23
|
-
s.add_development_dependency "rspec", "~> 3.0.0"
|
24
|
-
s.add_development_dependency "ffaker"
|
25
|
-
s.add_development_dependency "factory_girl", "~> 4.0"
|
21
|
+
#s.add_development_dependency "bundler", "~> 1.6"
|
26
22
|
|
27
23
|
s.required_ruby_version = '>= 1.9.2'
|
28
24
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Mongo Adaptor' do
|
4
|
+
let(:connection) { Mongo::MongoClient.new }
|
5
|
+
let(:database) { connection.db('muve_test') }
|
6
|
+
before do
|
7
|
+
class Place
|
8
|
+
include Muve::Model
|
9
|
+
|
10
|
+
def self.container
|
11
|
+
'places'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Muve.init(connection, database)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'writes model data to the store' do
|
19
|
+
expect{
|
20
|
+
Muve::Store::Mongo.create(Place, {
|
21
|
+
city: Faker::Address.city,
|
22
|
+
street: Faker::Address.street_name,
|
23
|
+
building: Faker::Address.building_number
|
24
|
+
})
|
25
|
+
}.to change{database['places'].count}.by(1)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'writes modifications to the store' do
|
29
|
+
id = database['places'].insert(name: Faker::Venue.name)
|
30
|
+
new_name = Faker::Venue.name
|
31
|
+
|
32
|
+
expect{
|
33
|
+
Muve::Store::Mongo.update(Place, id, { name: new_name })
|
34
|
+
}.to change{database['places'].find_one(_id: id)['name']}.to(new_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'finds a resource from store' do
|
38
|
+
id = database['places'].insert(name: Faker::Venue.name)
|
39
|
+
expect(Muve::Store::Mongo.get(Place, id)[:id]).to eq(id)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'finds multiple resources from store' do
|
43
|
+
expect(Muve::Store::Mongo.find(Place, {})).to be_a(Enumerable)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'extracts a resource from every result in a multiple resource set' do
|
47
|
+
Muve::Store::Mongo.find(Place, {}).take(3).each do |result|
|
48
|
+
attributes = result.keys.map{ |i| i.to_s }
|
49
|
+
expect(attributes).to include('id', 'city', 'street', 'building')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/spec/helper_spec.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
describe Muve::Helper do
|
2
|
+
describe '#symbolize_keys' do
|
3
|
+
it 'converts all keys to symbols' do
|
4
|
+
hash = { 'name' => 'Stewie Griffin', :skill => 'Physics' }
|
5
|
+
expect(Muve::Helper.symbolize_keys(hash)).to eq(
|
6
|
+
name: 'Stewie Griffin',
|
7
|
+
skill: 'Physics'
|
8
|
+
)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/spec/model_spec.rb
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
describe 'Model' do
|
2
|
+
before do
|
3
|
+
class Resource
|
4
|
+
include Muve::Model
|
5
|
+
attr_accessor :name, :version
|
6
|
+
|
7
|
+
def self.container
|
8
|
+
'resources'
|
9
|
+
end
|
10
|
+
|
11
|
+
def fields
|
12
|
+
[:name, :version]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class AnotherResource
|
17
|
+
include Muve::Model
|
18
|
+
attr_accessor :name, :version, :description
|
19
|
+
|
20
|
+
def self.container
|
21
|
+
'other_resources'
|
22
|
+
end
|
23
|
+
|
24
|
+
def fields
|
25
|
+
[:name, :version, :description]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'remembers its connection' do
|
31
|
+
object = Object.new
|
32
|
+
Muve::Model.connection = object
|
33
|
+
expect(Muve::Model.connection).to be(object)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'raises a not configured exception when connection is not set' do
|
37
|
+
skip # TODO: get rid of Singleton-like pattern?
|
38
|
+
expect {
|
39
|
+
Muve::Model.connection
|
40
|
+
}.to raise_error(MuveError::MuveNotConfigured)
|
41
|
+
|
42
|
+
expect {
|
43
|
+
Resource.connection
|
44
|
+
}.to raise_error(MuveError::MuveNotConfigured)
|
45
|
+
|
46
|
+
expect {
|
47
|
+
AnotherResource.connection
|
48
|
+
}.to raise_error(MuveError::MuveNotConfigured)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'knows the identifier of its repository' do
|
52
|
+
expect(Resource).to respond_to(:container)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'allows the setting of the adaptor' do
|
56
|
+
adaptor = Object.new
|
57
|
+
Resource.adaptor = adaptor
|
58
|
+
expect(Resource.adaptor).to be(adaptor)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'allows the setting of the adaptor through init' do
|
62
|
+
adaptor = Object.new
|
63
|
+
Muve::Model.init(adaptor = adaptor)
|
64
|
+
expect(Muve::Model::handler).to be(adaptor)
|
65
|
+
expect(Muve::Model.handler).to be(adaptor)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'sets the attributes of the resource at initialization' do
|
69
|
+
resource = Resource.new(name: 'muve-resource', version: 0)
|
70
|
+
expect(resource.name).to eq('muve-resource')
|
71
|
+
expect(resource.version).to eq(0)
|
72
|
+
|
73
|
+
another = AnotherResource.new(name: 'muve-something', version: nil, description: 'blah')
|
74
|
+
expect(another.name).to eq('muve-something')
|
75
|
+
expect(another.version).to eq(nil)
|
76
|
+
expect(another.description).to eq('blah')
|
77
|
+
end
|
78
|
+
|
79
|
+
# TODO: Study if this is desirable perhaps one would rather prefer setting
|
80
|
+
# seperate adaptors for different models
|
81
|
+
it 'shares the adaptor amongst all its instances' do
|
82
|
+
generic_adaptor = Object.new
|
83
|
+
Resource.adaptor = generic_adaptor
|
84
|
+
|
85
|
+
expect(Resource.new.send(:adaptor)).to be(generic_adaptor)
|
86
|
+
expect(AnotherResource.new.send(:adaptor)).to be(generic_adaptor)
|
87
|
+
end
|
88
|
+
|
89
|
+
describe 'equiped with an adaptor' do
|
90
|
+
before do
|
91
|
+
class Resource
|
92
|
+
include Muve::Model
|
93
|
+
|
94
|
+
def valid?
|
95
|
+
true
|
96
|
+
end
|
97
|
+
|
98
|
+
def set_as_new_record(bool)
|
99
|
+
@new_record = bool
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class GenericAdaptor
|
104
|
+
extend Muve::Store
|
105
|
+
end
|
106
|
+
|
107
|
+
Resource.adaptor = GenericAdaptor
|
108
|
+
|
109
|
+
@res = Resource.new
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'calls the store create handler upon save' do
|
113
|
+
expect(GenericAdaptor).to receive(:create).once
|
114
|
+
@res.save
|
115
|
+
end
|
116
|
+
|
117
|
+
describe '#find' do
|
118
|
+
before(:each) do
|
119
|
+
@id = SecureRandom.hex
|
120
|
+
allow(GenericAdaptor).to receive(:fetch).and_return({
|
121
|
+
id: @id,
|
122
|
+
name: 'Smile',
|
123
|
+
version: '0',
|
124
|
+
description: 'Supermodel smile... at least that is what they said'
|
125
|
+
})
|
126
|
+
allow(GenericAdaptor).to receive(:find).and_return(Enumerator.new { |y|
|
127
|
+
5.times {
|
128
|
+
y << { id: 12, name: 'Something', version: 1, description: 'haha' }
|
129
|
+
}
|
130
|
+
})
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'returns an instance of the model' do
|
134
|
+
expect(AnotherResource.find(@id)).to be_a(AnotherResource)
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'returns an object containing the record data' do
|
138
|
+
result = AnotherResource.find(@id)
|
139
|
+
expect(result.name).to eq('Smile')
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'returns a record that is not a new record' do
|
143
|
+
expect(AnotherResource.find(@id).new_record?).to be(false)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe '#where' do
|
148
|
+
before(:each) do
|
149
|
+
allow(GenericAdaptor).to receive(:find).and_return(Enumerator.new { |y|
|
150
|
+
5.times {
|
151
|
+
y << { id: 12, name: 'Something', version: 1, description: 'ahha' }
|
152
|
+
}
|
153
|
+
})
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'returns the complete result set' do
|
157
|
+
expect(AnotherResource.where({ name: 'all' }).count).to eq(5)
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'returns a instance of the resource for each item' do
|
161
|
+
AnotherResource.where({ name: 'all' }).each do |item|
|
162
|
+
expect(item).to be_a(AnotherResource)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe '#save' do
|
168
|
+
before(:each) do
|
169
|
+
@res.name = 'first'
|
170
|
+
allow(GenericAdaptor).to receive(:create).and_return(@id = SecureRandom.hex)
|
171
|
+
end
|
172
|
+
|
173
|
+
describe 'on a new record' do
|
174
|
+
it 'returns an instance of itself' do
|
175
|
+
expect(@res.save).to be_a(Resource)
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'obtains an id' do
|
179
|
+
expect { @res.save }.to change{ @res.id }.to(@id)
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'is no longer a new record' do
|
183
|
+
expect{ @res.save }.to change{ @res.new_record? }.to(false)
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'is persisted' do
|
187
|
+
expect{ @res.save }.to change{ @res.persisted? }.to(true)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
describe 'on a existing record' do
|
192
|
+
before(:each) do
|
193
|
+
allow(GenericAdaptor).to receive(:update).and_return(true)
|
194
|
+
@res.save
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'returns persist the resource' do
|
198
|
+
expect(@res).to receive(:update).once
|
199
|
+
@res.name = 'second'
|
200
|
+
@res.save
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe '#destroy' do
|
206
|
+
before(:each) do
|
207
|
+
@id = SecureRandom.hex
|
208
|
+
allow(GenericAdaptor).to receive(:fetch).and_return({
|
209
|
+
id: @id,
|
210
|
+
name: 'Laugh',
|
211
|
+
version: 28,
|
212
|
+
description: 'The best laugh ever... makes me laugh'
|
213
|
+
})
|
214
|
+
allow(GenericAdaptor).to receive(:delete).and_return(true)
|
215
|
+
@res = AnotherResource.find(@id)
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'is marked as removed' do
|
219
|
+
expect { @res.destroy }.to change{ @res.destroyed? }.to(true)
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'is not a new record' do
|
223
|
+
expect { @res.destroy }.to change{ @res.destroyed? }.to(true)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'calls the delete handler upon remove' do
|
228
|
+
expect(GenericAdaptor).to receive(:delete).once
|
229
|
+
@res.destroy
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'calls the update handler upon save on a resource with an id' do
|
233
|
+
id = SecureRandom.uuid
|
234
|
+
expect(GenericAdaptor).to receive(:update).once
|
235
|
+
@res.set_as_new_record false
|
236
|
+
expect(@res.new_record?).to be(false)
|
237
|
+
@res.save
|
238
|
+
end
|
239
|
+
|
240
|
+
it 'calls the find handler upon a request to find resources' do
|
241
|
+
expect(GenericAdaptor).to receive(:find).with(Resource, { name: 'bogus' })
|
242
|
+
Resource.where(name: 'bogus').take(1)
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'calls the fetcher to get a resource' do
|
246
|
+
id = SecureRandom.uuid
|
247
|
+
expect(GenericAdaptor).to receive(:fetch).with(Resource, id, anything)
|
248
|
+
Resource.find(id)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
data/spec/movement_spec.rb
CHANGED
@@ -32,11 +32,11 @@ describe Muve::Movement do
|
|
32
32
|
expect(Muve::Movement.new).to respond_to(:connection)
|
33
33
|
end
|
34
34
|
|
35
|
-
it 'shares the connection among
|
36
|
-
|
35
|
+
it 'shares the connection among other models' do
|
36
|
+
connection = Object.new
|
37
|
+
Muve.init(connection)
|
37
38
|
|
38
|
-
|
39
|
-
expect(connection).not_to eq(nil)
|
39
|
+
expect(Muve::Model.connection).to eq(connection)
|
40
40
|
|
41
41
|
expect(Muve::Movement.new.connection).to be(connection)
|
42
42
|
expect(Muve::Location.new.connection).to be(connection)
|
data/spec/muve_spec.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Muve do
|
4
|
+
it 'includes the model module' do
|
5
|
+
expect{ Muve::Model }.not_to raise_error
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'includes the location class' do
|
9
|
+
expect{ Muve::Location }.not_to raise_error
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'includes the traveller class' do
|
13
|
+
expect{ Muve::Traveller }.not_to raise_error
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'includes the movement class' do
|
17
|
+
expect{ Muve::Movement }.not_to raise_error
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'contains a model that behaves like one' do
|
21
|
+
class TestModel
|
22
|
+
include Muve::Model
|
23
|
+
end
|
24
|
+
|
25
|
+
expect(TestModel.new).to respond_to(:save)
|
26
|
+
expect(TestModel.new).to respond_to(:valid?)
|
27
|
+
expect(TestModel.new).to respond_to(:connection)
|
28
|
+
end
|
29
|
+
end
|
data/spec/store_spec.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
describe Muve::Store do
|
2
|
+
before do
|
3
|
+
class Resource
|
4
|
+
include Muve::Model
|
5
|
+
|
6
|
+
def self.container
|
7
|
+
'resources'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class GenericStore
|
12
|
+
extend Muve::Store
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'provides methods to get a resource' do
|
17
|
+
expect(GenericStore).to respond_to(:get)
|
18
|
+
expect(GenericStore).to respond_to(:fetch)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'provides methods to create a resource' do
|
22
|
+
expect(GenericStore).to respond_to(:create)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'provides methods to destroy a resource' do
|
26
|
+
expect(GenericStore).to respond_to(:delete)
|
27
|
+
expect(GenericStore).to respond_to(:destroy)
|
28
|
+
expect(GenericStore).to respond_to(:remove)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'provides methods to update a resource' do
|
32
|
+
expect(GenericStore).to respond_to(:update)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'raises incomplete implementation errors on non-implemented methods' do
|
36
|
+
%w(create delete update fetch find).each do |method|
|
37
|
+
expect{
|
38
|
+
if method == 'update'
|
39
|
+
GenericStore.send(method, 'resource', nil, {})
|
40
|
+
else
|
41
|
+
GenericStore.send(method, 'resource', nil)
|
42
|
+
end
|
43
|
+
}.to raise_error(MuveError::MuveIncompleteImplementation)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'attempts to fetch a resource if the id is given' do
|
48
|
+
expect(GenericStore).to receive(:fetch)
|
49
|
+
GenericStore.get(Resource, SecureRandom.uuid)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'attempts to find a resource if the id is not given but the details are' do
|
53
|
+
expect(GenericStore).to receive(:find)
|
54
|
+
GenericStore.get(Resource, nil, { name: 'bogus' })
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'raises an error if neither id nor details are given' do
|
58
|
+
expect{
|
59
|
+
GenericStore.get(Resource)
|
60
|
+
}.to raise_error
|
61
|
+
end
|
62
|
+
end
|
metadata
CHANGED
@@ -1,85 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: muve
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Asabina
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-07-
|
12
|
-
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: bundler
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '1.6'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '1.6'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: rake
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: rspec
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: 3.0.0
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: 3.0.0
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: ffaker
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: factory_girl
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '4.0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '4.0'
|
11
|
+
date: 2014-07-29 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
83
13
|
description: Basic helpers to be used with Muvement
|
84
14
|
email:
|
85
15
|
- david@supr.nu
|
@@ -93,16 +23,24 @@ files:
|
|
93
23
|
- Rakefile
|
94
24
|
- lib/muve.rb
|
95
25
|
- lib/muve/errors.rb
|
26
|
+
- lib/muve/helpers.rb
|
96
27
|
- lib/muve/location.rb
|
97
28
|
- lib/muve/model.rb
|
98
29
|
- lib/muve/movement.rb
|
30
|
+
- lib/muve/store.rb
|
31
|
+
- lib/muve/store/mongo.rb
|
99
32
|
- lib/muve/traveller.rb
|
100
33
|
- lib/muve/version.rb
|
101
34
|
- muve.gemspec
|
35
|
+
- spec/adaptor/mongo_spec.rb
|
102
36
|
- spec/factories.rb
|
37
|
+
- spec/helper_spec.rb
|
103
38
|
- spec/location_spec.rb
|
39
|
+
- spec/model_spec.rb
|
104
40
|
- spec/movement_spec.rb
|
41
|
+
- spec/muve_spec.rb
|
105
42
|
- spec/spec_helper.rb
|
43
|
+
- spec/store_spec.rb
|
106
44
|
- spec/traveller_spec.rb
|
107
45
|
homepage: ''
|
108
46
|
licenses:
|
@@ -129,8 +67,13 @@ signing_key:
|
|
129
67
|
specification_version: 4
|
130
68
|
summary: muve gem
|
131
69
|
test_files:
|
70
|
+
- spec/adaptor/mongo_spec.rb
|
132
71
|
- spec/factories.rb
|
72
|
+
- spec/helper_spec.rb
|
133
73
|
- spec/location_spec.rb
|
74
|
+
- spec/model_spec.rb
|
134
75
|
- spec/movement_spec.rb
|
76
|
+
- spec/muve_spec.rb
|
135
77
|
- spec/spec_helper.rb
|
78
|
+
- spec/store_spec.rb
|
136
79
|
- spec/traveller_spec.rb
|