clean_model 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +198 -0
- data/Rakefile +1 -0
- data/clean_model.gemspec +25 -0
- data/lib/clean_model.rb +11 -0
- data/lib/clean_model/attribute.rb +47 -0
- data/lib/clean_model/base.rb +62 -0
- data/lib/clean_model/exceptions.rb +30 -0
- data/lib/clean_model/persistent.rb +76 -0
- data/lib/clean_model/remote.rb +87 -0
- data/lib/clean_model/version.rb +3 -0
- data/spec/base_model_spec.rb +145 -0
- data/spec/persistent_model_spec.rb +112 -0
- data/spec/remote_models_spec.rb +148 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/models/base_models.rb +35 -0
- data/spec/support/models/persistent_models.rb +10 -0
- data/spec/support/models/remote_models.rb +13 -0
- metadata +119 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
# CleanModel
|
2
|
+
|
3
|
+
Extensions for ActiveModel to implement multiple types of models
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'clean_model'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install clean_model
|
18
|
+
|
19
|
+
## Basic models
|
20
|
+
|
21
|
+
### Class definitions
|
22
|
+
|
23
|
+
class Person
|
24
|
+
include CleanModel::Base
|
25
|
+
|
26
|
+
attribute :first_name
|
27
|
+
attribute :last_name
|
28
|
+
end
|
29
|
+
|
30
|
+
### Usage
|
31
|
+
|
32
|
+
person = Person.new first_name: 'John', last_name: 'Doe'
|
33
|
+
|
34
|
+
person.first_name -> 'John'
|
35
|
+
person.last_name -> 'Doe'
|
36
|
+
|
37
|
+
person.attributes -> {first_name: 'John', last_name: 'Doe'}
|
38
|
+
|
39
|
+
person.assign_attributes first_name: 'Jorge'
|
40
|
+
|
41
|
+
person.attributes -> {first_name: 'Jorge', last_name: 'Doe'}
|
42
|
+
|
43
|
+
### Active Model validations
|
44
|
+
|
45
|
+
class Person
|
46
|
+
include CleanModel::Base
|
47
|
+
|
48
|
+
attribute :first_name
|
49
|
+
attribute :last_name
|
50
|
+
|
51
|
+
validates_presence_of :first_name, :last_name
|
52
|
+
end
|
53
|
+
|
54
|
+
person = Person.new
|
55
|
+
person.valid? -> false
|
56
|
+
|
57
|
+
### Strong typing
|
58
|
+
|
59
|
+
class Engine
|
60
|
+
include CleanModel::Base
|
61
|
+
|
62
|
+
attribute :power, class_name: :numeric
|
63
|
+
attribute :cylinders, class_name: :integer
|
64
|
+
attribute :valves, class_name: 'Integer'
|
65
|
+
end
|
66
|
+
|
67
|
+
engine = Engine.new
|
68
|
+
engine.power = 130
|
69
|
+
engine.cylinders = 6.1 -> Raise error CleanModel::InvalidTypeAssignment
|
70
|
+
|
71
|
+
### Transformations
|
72
|
+
|
73
|
+
class Car
|
74
|
+
include CleanModel::Base
|
75
|
+
|
76
|
+
attribute :brand
|
77
|
+
attribute :model
|
78
|
+
attribute :engine, class_name: 'Engine'
|
79
|
+
attribute :comfort, transformation: lambda { |v| v.is_a?(String) ? v.split(',').map(&:strip) : v }
|
80
|
+
end
|
81
|
+
|
82
|
+
car = Car.new do |c|
|
83
|
+
c.engine = {power: 110, cylinders: 16, valves: 6}
|
84
|
+
end
|
85
|
+
car.engine -> <Engine @power=110, @cylinders=16, @valves=6>
|
86
|
+
|
87
|
+
car = Car.new do |c|
|
88
|
+
c.comfort = 'bluetooth, gps, electric pack'
|
89
|
+
end
|
90
|
+
car.comfort -> ['bluetooth', 'gps', 'electric pack']
|
91
|
+
|
92
|
+
### Collections
|
93
|
+
|
94
|
+
class Factory
|
95
|
+
include CleanModel::Base
|
96
|
+
|
97
|
+
attribute :cars, collection: 'Car'
|
98
|
+
end
|
99
|
+
|
100
|
+
factory = Factory.new do |f|
|
101
|
+
f.cars = [
|
102
|
+
{brand: 'Honda', model: 'Civic'},
|
103
|
+
{brand: 'Toyota', model: 'Corolla'},
|
104
|
+
]
|
105
|
+
end
|
106
|
+
|
107
|
+
factory.cars -> [<Car @brand=Honda, @model=Civic>, <Car @brand=Toyota, @model=Corolla>]
|
108
|
+
|
109
|
+
## Models with custom persistence
|
110
|
+
|
111
|
+
### Definition
|
112
|
+
|
113
|
+
class Post
|
114
|
+
include CleanModel::Persistent
|
115
|
+
|
116
|
+
attribute :subject
|
117
|
+
attribute :content
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def create
|
122
|
+
...
|
123
|
+
end
|
124
|
+
|
125
|
+
def update
|
126
|
+
...
|
127
|
+
end
|
128
|
+
|
129
|
+
def delete
|
130
|
+
...
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
### Usage
|
135
|
+
|
136
|
+
Post.create(subject: 'Title', content: 'Some text')
|
137
|
+
or
|
138
|
+
post = Post.new subject: 'Title', content: 'Some text'
|
139
|
+
post.save
|
140
|
+
|
141
|
+
post.content = 'Another text'
|
142
|
+
post.save
|
143
|
+
|
144
|
+
post.update_attributes(title: 'Another title')
|
145
|
+
|
146
|
+
post.destroy
|
147
|
+
|
148
|
+
## Remote models (for REST APIs)
|
149
|
+
|
150
|
+
### Definition
|
151
|
+
|
152
|
+
class User
|
153
|
+
include CleanModel::Remote
|
154
|
+
|
155
|
+
connection host: 'localhost', port: 9999
|
156
|
+
|
157
|
+
attribute :first_name
|
158
|
+
attribute :last_name
|
159
|
+
attribute :email
|
160
|
+
|
161
|
+
def self.find(id)
|
162
|
+
http_get "/users/#{id}.json" do |response|
|
163
|
+
new JSON.parse(response.body)
|
164
|
+
emd
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def create
|
170
|
+
http.post '/users/create.json', wrapped_attributes
|
171
|
+
end
|
172
|
+
|
173
|
+
def update
|
174
|
+
http.put "/users/#{id}.json", wrapped_attributes(except: :id)
|
175
|
+
end
|
176
|
+
|
177
|
+
def delete
|
178
|
+
http.delete("/users/#{id}.json")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
### Usage
|
183
|
+
|
184
|
+
User.create first_name: 'John', last_name: 'Doe'
|
185
|
+
|
186
|
+
user = User.find(1)
|
187
|
+
|
188
|
+
user.update_attributes(first_name: 'Jorge')
|
189
|
+
|
190
|
+
user.destroy
|
191
|
+
|
192
|
+
## Contributing
|
193
|
+
|
194
|
+
1. Fork it
|
195
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
196
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
197
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
198
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/clean_model.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require 'clean_model/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'clean_model'
|
7
|
+
s.version = CleanModel::VERSION
|
8
|
+
s.authors = ['Gabriel Naiman']
|
9
|
+
s.email = ['gabynaiman@gmail.com']
|
10
|
+
s.homepage = 'https://github.com/gabynaiman/clean_model'
|
11
|
+
s.summary = 'Extensions for ActiveModel to implement multiple types of models'
|
12
|
+
s.description = 'Extensions for ActiveModel to implement multiple types of models'
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_dependency 'activesupport', '>= 3.0.0'
|
20
|
+
s.add_dependency 'activemodel', '>= 3.0.0'
|
21
|
+
s.add_dependency 'web_client'
|
22
|
+
|
23
|
+
s.add_development_dependency 'rspec'
|
24
|
+
s.add_development_dependency 'webmock'
|
25
|
+
end
|
data/lib/clean_model.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'web_client'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require 'clean_model/version'
|
7
|
+
require 'clean_model/attribute'
|
8
|
+
require 'clean_model/exceptions'
|
9
|
+
require 'clean_model/base'
|
10
|
+
require 'clean_model/persistent'
|
11
|
+
require 'clean_model/remote'
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module CleanModel
|
2
|
+
class Attribute
|
3
|
+
attr_reader :name, :options
|
4
|
+
|
5
|
+
def initialize(name, options={})
|
6
|
+
@name = symbolize(name)
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate!(value)
|
11
|
+
raise InvalidTypeAssignment.new(name, value) unless value.is_a? klass
|
12
|
+
end
|
13
|
+
|
14
|
+
def transform(value)
|
15
|
+
if @options[:transformation]
|
16
|
+
@options[:transformation].call(value)
|
17
|
+
elsif value.is_a?(Hash) && klass.new.respond_to?(:assign_attributes)
|
18
|
+
obj = klass.new
|
19
|
+
obj.assign_attributes value
|
20
|
+
obj
|
21
|
+
elsif value.is_a?(Array) && collection_class.new.respond_to?(:assign_attributes)
|
22
|
+
value.map do |v|
|
23
|
+
obj = collection_class.new
|
24
|
+
obj.assign_attributes v
|
25
|
+
obj
|
26
|
+
end
|
27
|
+
else
|
28
|
+
value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def klass
|
35
|
+
@options[:class_name].to_s.classify.constantize
|
36
|
+
end
|
37
|
+
|
38
|
+
def collection_class
|
39
|
+
@options[:collection].to_s.classify.constantize
|
40
|
+
end
|
41
|
+
|
42
|
+
def symbolize(text)
|
43
|
+
text.is_a?(String) ? text.to_s.underscore.parameterize('_').to_sym : text
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module CleanModel
|
2
|
+
module Base
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ActiveModel::Translation
|
8
|
+
base.send :include, ActiveModel::Validations
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
def attribute(name, options={})
|
14
|
+
attr = Attribute.new(name, options)
|
15
|
+
attributes << attr
|
16
|
+
|
17
|
+
define_method name do
|
18
|
+
instance_variable_get "@#{name}"
|
19
|
+
end
|
20
|
+
|
21
|
+
define_method "#{name}=" do |value|
|
22
|
+
value = attr.transform(value)
|
23
|
+
attr.validate!(value)
|
24
|
+
instance_variable_set "@#{name}", value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def attributes
|
29
|
+
@attributes ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
def attribute_names
|
33
|
+
attributes.map(&:name)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
module InstanceMethods
|
39
|
+
|
40
|
+
def initialize(attributes={})
|
41
|
+
if block_given?
|
42
|
+
yield(self)
|
43
|
+
else
|
44
|
+
assign_attributes attributes
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def assign_attributes(attributes)
|
49
|
+
return nil unless attributes
|
50
|
+
attributes.each do |name, value|
|
51
|
+
send("#{name}=", value) if respond_to?("#{name}=")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def attributes
|
56
|
+
Hash[self.class.attribute_names.map { |a| [a, send(a)] }]
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CleanModel
|
2
|
+
|
3
|
+
class Error < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class InvalidTypeAssignment < Error
|
7
|
+
def initialize(attribute, value)
|
8
|
+
super "#{value} is not valid for #{attribute}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class UndefinedPersistenceMethod < Error
|
13
|
+
def initialize(klass, method)
|
14
|
+
super "#{klass} must define method [#{method}]"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class InvalidResponse < Error
|
19
|
+
def initialize(response)
|
20
|
+
super response.content_type == 'application/json' ? response.body : "#{response.code} - Unexpected error"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ConnectionFail < Error
|
25
|
+
def initialize(exception)
|
26
|
+
super exception.message
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module CleanModel
|
2
|
+
module Persistent
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.send :include, Base
|
6
|
+
base.send :extend, ClassMethods
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
base.send :include, ActiveModel::Conversion
|
9
|
+
|
10
|
+
base.attribute :id
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
def create(attributes={})
|
16
|
+
begin
|
17
|
+
create! attributes
|
18
|
+
rescue
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def create!(attributes={})
|
24
|
+
model = new attributes
|
25
|
+
model.save!
|
26
|
+
model
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
module InstanceMethods
|
32
|
+
|
33
|
+
def new_record?
|
34
|
+
id.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def persisted?
|
38
|
+
!new_record?
|
39
|
+
end
|
40
|
+
|
41
|
+
def save!
|
42
|
+
raise errors.full_messages.join("\n") unless save
|
43
|
+
end
|
44
|
+
|
45
|
+
def save
|
46
|
+
return false unless valid?
|
47
|
+
new_record? ? create : update
|
48
|
+
end
|
49
|
+
|
50
|
+
def update_attributes(attributes)
|
51
|
+
assign_attributes attributes
|
52
|
+
save
|
53
|
+
end
|
54
|
+
|
55
|
+
def destroy
|
56
|
+
delete
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def create
|
62
|
+
raise UndefinedPersistenceMethod.new(self.class, :create)
|
63
|
+
end
|
64
|
+
|
65
|
+
def update
|
66
|
+
raise UndefinedPersistenceMethod.new(self.class, :update)
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete
|
70
|
+
raise UndefinedPersistenceMethod.new(self.class, :delete)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module CleanModel
|
2
|
+
module Remote
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.send :include, Persistent
|
6
|
+
base.send :extend, ClassMethods
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def connection(connection=nil)
|
13
|
+
connection ? @connection = connection : @connection
|
14
|
+
end
|
15
|
+
|
16
|
+
def http
|
17
|
+
WebClient::Base.new(connection)
|
18
|
+
end
|
19
|
+
|
20
|
+
def http_get(path, data={})
|
21
|
+
begin
|
22
|
+
response = http.get(path, data)
|
23
|
+
if response.is_a?(Net::HTTPOK)
|
24
|
+
block_given? ? yield(response) : response
|
25
|
+
else
|
26
|
+
raise InvalidResponse.new(response)
|
27
|
+
end
|
28
|
+
rescue WebClient::Error => ex
|
29
|
+
raise ConnectionFail.new(ex)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
module InstanceMethods
|
36
|
+
|
37
|
+
def http
|
38
|
+
self.class.http
|
39
|
+
end
|
40
|
+
|
41
|
+
def http_get(path, data={}, &block)
|
42
|
+
self.class.http_get(path, data, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def wrapped_attributes(options={})
|
46
|
+
exceptions = options[:except] ? [options[:except]].flatten.map(&:to_sym) : []
|
47
|
+
attributes.reject { |k, v| v.nil? || exceptions.include?(k) }.inject({}) { |h, (k, v)| h["#{options[:wrapper] || self.class.to_s.demodulize.underscore}[#{k}]"] = v; h }
|
48
|
+
end
|
49
|
+
|
50
|
+
def save
|
51
|
+
return false unless valid?
|
52
|
+
begin
|
53
|
+
response = new_record? ? create : update
|
54
|
+
if response.is_a?(Net::HTTPSuccess)
|
55
|
+
assign_attributes JSON.parse(response.body) if response.body
|
56
|
+
else
|
57
|
+
if response.code.to_i == 422 #:unprocessable_entity
|
58
|
+
JSON.parse(response.body).each do |attribute, messages|
|
59
|
+
messages.each { |m| errors[attribute.to_sym] << m }
|
60
|
+
end
|
61
|
+
else
|
62
|
+
errors[:base] = response.content_type == 'application/json' ? response.body : "#{response.code} - Unexpected error"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
rescue WebClient::Error => ex
|
66
|
+
errors[:base] = ex.message
|
67
|
+
end
|
68
|
+
errors.empty?
|
69
|
+
end
|
70
|
+
|
71
|
+
def destroy
|
72
|
+
return true if new_record?
|
73
|
+
begin
|
74
|
+
response = delete
|
75
|
+
unless response.is_a?(Net::HTTPSuccess)
|
76
|
+
errors[:base] = response.content_type == 'application/json' ? response.body : "#{response.code} - Unexpected error"
|
77
|
+
end
|
78
|
+
rescue WebClient::Error => ex
|
79
|
+
errors[:base] = ex.message
|
80
|
+
end
|
81
|
+
errors.empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
include BaseModels
|
4
|
+
|
5
|
+
describe CleanModel::Base do
|
6
|
+
|
7
|
+
context 'Basic attributes access' do
|
8
|
+
|
9
|
+
subject { Person.new }
|
10
|
+
|
11
|
+
it 'Respond to each defined attribute' do
|
12
|
+
should respond_to 'first_name'
|
13
|
+
should respond_to 'first_name='
|
14
|
+
should respond_to 'last_name'
|
15
|
+
should respond_to 'last_name='
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'Keep value of attributes' do
|
19
|
+
person = Person.new
|
20
|
+
|
21
|
+
person.first_name.should be_nil
|
22
|
+
person.first_name = 'John'
|
23
|
+
person.first_name.should eq 'John'
|
24
|
+
|
25
|
+
person.last_name.should be_nil
|
26
|
+
person.last_name = 'Doe'
|
27
|
+
person.last_name.should eq 'Doe'
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'Can assign attributes via constructor hash' do
|
31
|
+
person = Person.new first_name: 'John', last_name: 'Doe'
|
32
|
+
person.first_name.should eq 'John'
|
33
|
+
person.last_name.should eq 'Doe'
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'Can assign attributes via constructor block' do
|
37
|
+
person = Person.new do |p|
|
38
|
+
p.first_name = 'John'
|
39
|
+
p.last_name = 'Doe'
|
40
|
+
end
|
41
|
+
person.first_name.should eq 'John'
|
42
|
+
person.last_name.should eq 'Doe'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'Get attributes hash' do
|
46
|
+
person = Person.new first_name: 'John', last_name: 'Doe'
|
47
|
+
person.attributes.keys.should eq [:first_name, :last_name]
|
48
|
+
person.attributes[:first_name].should eq 'John'
|
49
|
+
person.attributes[:last_name].should eq 'Doe'
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'Strong typed attributes restrictions' do
|
55
|
+
|
56
|
+
it 'Type defined with a symbol' do
|
57
|
+
engine = Engine.new
|
58
|
+
engine.cylinders = 6
|
59
|
+
expect { engine.cylinders = 6.5 }.to raise_error CleanModel::InvalidTypeAssignment
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'Type defined with a string' do
|
63
|
+
engine = Engine.new
|
64
|
+
engine.valves = 16
|
65
|
+
expect { engine.valves = 16.5 }.to raise_error CleanModel::InvalidTypeAssignment
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'Type defined with a super class' do
|
69
|
+
engine = Engine.new
|
70
|
+
engine.power = 130
|
71
|
+
expect { engine.power = '130hp' }.to raise_error CleanModel::InvalidTypeAssignment
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'Attribute type conversions' do
|
77
|
+
|
78
|
+
it 'Transform to model when assign a hash' do
|
79
|
+
car = Car.new do |c|
|
80
|
+
c.engine = {power: 110, cylinders: 16, valves: 6}
|
81
|
+
end
|
82
|
+
car.engine.should be_a Engine
|
83
|
+
car.engine.power.should eq 110
|
84
|
+
car.engine.cylinders.should eq 16
|
85
|
+
car.engine.valves.should eq 6
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'Apply custom transformation' do
|
89
|
+
car = Car.new do |c|
|
90
|
+
c.comfort = 'bluetooth, gps, electric pack'
|
91
|
+
end
|
92
|
+
car.comfort.should be_a Array
|
93
|
+
car.comfort.should eq ['bluetooth', 'gps', 'electric pack']
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'Transform array elements when collection defined' do
|
97
|
+
factory = Factory.new do |f|
|
98
|
+
f.cars = [
|
99
|
+
{brand: 'Honda', model: 'Civic'},
|
100
|
+
{brand: 'Toyota', model: 'Corolla'},
|
101
|
+
]
|
102
|
+
end
|
103
|
+
|
104
|
+
factory.cars.should be_a Array
|
105
|
+
factory.cars.should have(2).items
|
106
|
+
|
107
|
+
factory.cars[0].should be_a Car
|
108
|
+
factory.cars[0].brand.should eq 'Honda'
|
109
|
+
factory.cars[0].model.should eq 'Civic'
|
110
|
+
|
111
|
+
factory.cars[1].should be_a Car
|
112
|
+
factory.cars[1].brand.should eq 'Toyota'
|
113
|
+
factory.cars[1].model.should eq 'Corolla'
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'Active model naming and translation' do
|
119
|
+
|
120
|
+
it 'Get a model name' do
|
121
|
+
Person.model_name.should eq 'BaseModels::Person'
|
122
|
+
Person.model_name.human.should eq 'Person'
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'Get a human attribute names' do
|
126
|
+
Person.human_attribute_name(:first_name).should eq 'First name'
|
127
|
+
Person.human_attribute_name(:last_name).should eq 'Last name'
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'Active model validations' do
|
133
|
+
|
134
|
+
it 'Validates presence' do
|
135
|
+
Person.new(first_name: 'John', last_name: 'Doe').should be_valid
|
136
|
+
|
137
|
+
person = Person.new
|
138
|
+
person.should_not be_valid
|
139
|
+
person.errors[:first_name].should have(1).items
|
140
|
+
person.errors[:last_name].should have(1).items
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
include PersistentModels
|
4
|
+
|
5
|
+
describe CleanModel::Persistent do
|
6
|
+
|
7
|
+
context 'Respond to persistence methods' do
|
8
|
+
|
9
|
+
subject { Post.new }
|
10
|
+
|
11
|
+
it 'Class methods ' do
|
12
|
+
Post.should respond_to :create
|
13
|
+
Post.should respond_to :create!
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'Instance methods' do
|
17
|
+
should respond_to :id
|
18
|
+
should respond_to :id=
|
19
|
+
should respond_to :save
|
20
|
+
should respond_to :save!
|
21
|
+
should respond_to :update_attributes
|
22
|
+
should respond_to :destroy
|
23
|
+
should respond_to :new_record?
|
24
|
+
should respond_to :persisted?
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'Undefined persistence methods' do
|
30
|
+
|
31
|
+
it 'Can not create' do
|
32
|
+
expect { Post.new.send :create }.to raise_error CleanModel::UndefinedPersistenceMethod
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'Can not update' do
|
36
|
+
expect { Post.new.send :update }.to raise_error CleanModel::UndefinedPersistenceMethod
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'Can not destroy' do
|
40
|
+
expect { Post.new.send :destroy }.to raise_error CleanModel::UndefinedPersistenceMethod
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'Defined persistence methods' do
|
46
|
+
|
47
|
+
it 'Create with class method' do
|
48
|
+
Post.any_instance.stub(:create).and_return(:true)
|
49
|
+
Post.any_instance.should_receive :create
|
50
|
+
|
51
|
+
Post.create(subject: 'Title', content: 'Some text').should be_a Post
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'Create with instance method' do
|
55
|
+
post = Post.new subject: 'Title', content: 'Some text'
|
56
|
+
|
57
|
+
post.stub(:create) { true }
|
58
|
+
post.should_receive :create
|
59
|
+
|
60
|
+
post.save.should be_true
|
61
|
+
|
62
|
+
post.stub(:id) { rand(1000) }
|
63
|
+
|
64
|
+
post.should be_persisted
|
65
|
+
post.should_not be_new_record
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'Save persisted model' do
|
69
|
+
post = Post.new id: 1, subject: 'Title', content: 'Some text'
|
70
|
+
post.should be_persisted
|
71
|
+
post.should_not be_new_record
|
72
|
+
|
73
|
+
post.stub(:update) { true }
|
74
|
+
post.should_receive :update
|
75
|
+
|
76
|
+
post.save.should be_true
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'Update attributes' do
|
80
|
+
post = Post.new id: 1, subject: 'Title', content: 'Some text'
|
81
|
+
|
82
|
+
post.stub(:update) { true }
|
83
|
+
post.should_receive :update
|
84
|
+
|
85
|
+
post.update_attributes(content: 'Other text').should be_true
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'Destroy persisted model' do
|
89
|
+
post = Post.new id: 1, subject: 'Title', content: 'Some text'
|
90
|
+
|
91
|
+
post.stub(:delete) { true }
|
92
|
+
post.should_receive :delete
|
93
|
+
|
94
|
+
post.destroy.should be_true
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'Active model conversion' do
|
100
|
+
|
101
|
+
let(:post) { Post.new id: 1 }
|
102
|
+
|
103
|
+
it 'Respond to instance methods' do
|
104
|
+
post.to_model.should eq post
|
105
|
+
post.to_key.should eq [1]
|
106
|
+
post.to_param.should eq '1'
|
107
|
+
post.to_partial_path.should eq 'persistent_models/posts/post'
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
include RemoteModels
|
4
|
+
|
5
|
+
describe CleanModel::Persistent do
|
6
|
+
|
7
|
+
context 'Successful operations' do
|
8
|
+
|
9
|
+
it 'Create' do
|
10
|
+
user = User.new first_name: 'John', last_name: 'Doe', email: 'john.doe@mail.com'
|
11
|
+
|
12
|
+
user.stub(:create) { user.http.post('/users/create.json', user.wrapped_attributes) }
|
13
|
+
user.should_receive :create
|
14
|
+
|
15
|
+
stub_request(:post, 'http://localhost:9999/users/create.json').
|
16
|
+
with(body: {user: {first_name: 'John', last_name: 'Doe', email: 'john.doe@mail.com'}}).
|
17
|
+
to_return(body: {id: 1}.to_json)
|
18
|
+
|
19
|
+
user.save.should be_true
|
20
|
+
user.should be_persisted
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'Update' do
|
24
|
+
user = User.new id: 1, first_name: 'John', last_name: 'Doe', email: 'john.doe@mail.com'
|
25
|
+
|
26
|
+
user.stub(:update) { user.http.put("/users/#{user.id}.json", user.wrapped_attributes(except: :id)) }
|
27
|
+
user.should_receive :update
|
28
|
+
|
29
|
+
stub_request(:put, 'http://localhost:9999/users/1.json').
|
30
|
+
with(body: {user: {first_name: 'Jorge', last_name: 'Doe', email: 'john.doe@mail.com'}})
|
31
|
+
|
32
|
+
user.update_attributes(first_name: 'Jorge').should be_true
|
33
|
+
user.first_name.should eq 'Jorge'
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'Destroy' do
|
37
|
+
user = User.new id: 1, first_name: 'John', last_name: 'Doe', email: 'john.doe@mail.com'
|
38
|
+
|
39
|
+
user.stub(:delete) { user.http.delete("/users/#{user.id}.json") }
|
40
|
+
user.should_receive :delete
|
41
|
+
|
42
|
+
stub_request(:delete, 'http://localhost:9999/users/1.json')
|
43
|
+
|
44
|
+
user.destroy.should be_true
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'Failed operations' do
|
50
|
+
|
51
|
+
it 'Save validation errors' do
|
52
|
+
user = User.new first_name: 'John', last_name: 'Doe'
|
53
|
+
|
54
|
+
user.stub(:create) { user.http.post('/users/create.json', user.wrapped_attributes) }
|
55
|
+
|
56
|
+
stub_request(:post, 'http://localhost:9999/users/create.json').
|
57
|
+
to_return(status: 422, body: {email: ["can't be blank"]}.to_json)
|
58
|
+
|
59
|
+
user.save.should_not be_true
|
60
|
+
user.errors[:email].should have(1).items
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'Save with unexpected error' do
|
64
|
+
user = User.new first_name: 'John', last_name: 'Doe', email: 'john.doe@mail.com'
|
65
|
+
|
66
|
+
user.stub(:create) { user.http.post('/users/create.json', user.wrapped_attributes) }
|
67
|
+
|
68
|
+
stub_request(:post, 'http://localhost:9999/users/create.json').
|
69
|
+
to_return(status: 500, body: 'Internal Server Error')
|
70
|
+
|
71
|
+
user.save.should_not be_true
|
72
|
+
user.errors[:base].should have(1).items
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'Save with timeout error' do
|
76
|
+
user = User.new first_name: 'John', last_name: 'Doe', email: 'john.doe@mail.com'
|
77
|
+
|
78
|
+
user.stub(:create) { user.http.post('/users/create.json', user.wrapped_attributes) }
|
79
|
+
|
80
|
+
stub_request(:post, 'http://localhost:9999/users/create.json').to_timeout
|
81
|
+
|
82
|
+
user.save.should_not be_true
|
83
|
+
user.errors[:base].should have(1).items
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'Destroy with unexpected error' do
|
87
|
+
user = User.new id: 1, first_name: 'John', last_name: 'Doe', email: 'john.doe@mail.com'
|
88
|
+
|
89
|
+
user.stub(:delete) { user.http.delete("/users/#{user.id}.json") }
|
90
|
+
|
91
|
+
stub_request(:delete, 'http://localhost:9999/users/1.json').
|
92
|
+
to_return(status: 500, body: 'Internal Server Error')
|
93
|
+
|
94
|
+
user.destroy.should_not be_true
|
95
|
+
user.errors[:base].should have(1).items
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'Destroy with timeout error' do
|
99
|
+
user = User.new id: 1, first_name: 'John', last_name: 'Doe', email: 'john.doe@mail.com'
|
100
|
+
|
101
|
+
user.stub(:delete) { user.http.delete("/users/#{user.id}.json") }
|
102
|
+
|
103
|
+
stub_request(:delete, 'http://localhost:9999/users/1.json').to_timeout
|
104
|
+
|
105
|
+
user.destroy.should_not be_true
|
106
|
+
user.errors[:base].should have(1).items
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'Http get safe' do
|
112
|
+
|
113
|
+
before :each do
|
114
|
+
User.stub(:find) do
|
115
|
+
User.http_get '/users/1.json' do |response|
|
116
|
+
User.new JSON.parse(response.body)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'Successful' do
|
122
|
+
stub_request(:get, 'http://localhost:9999/users/1.json').
|
123
|
+
to_return(body: {id: 1, first_name: 'John', last_name: 'Doe', email: 'john.doe@mail.com'}.to_json)
|
124
|
+
|
125
|
+
user = User.find(1)
|
126
|
+
|
127
|
+
user.id.should eq 1
|
128
|
+
user.first_name.should eq 'John'
|
129
|
+
user.last_name.should eq 'Doe'
|
130
|
+
user.email.should eq 'john.doe@mail.com'
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'Invalid response' do
|
134
|
+
stub_request(:get, 'http://localhost:9999/users/1.json').
|
135
|
+
to_return(status: 500, body: 'Internal Server Error')
|
136
|
+
|
137
|
+
expect{User.find(1)}.to raise_error CleanModel::InvalidResponse
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'Connection fail' do
|
141
|
+
stub_request(:get, 'http://localhost:9999/users/1.json').to_timeout
|
142
|
+
|
143
|
+
expect{User.find(1)}.to raise_error CleanModel::ConnectionFail
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module BaseModels
|
2
|
+
|
3
|
+
class Person
|
4
|
+
include CleanModel::Base
|
5
|
+
|
6
|
+
attribute :first_name
|
7
|
+
attribute :last_name
|
8
|
+
|
9
|
+
validates_presence_of :first_name, :last_name
|
10
|
+
end
|
11
|
+
|
12
|
+
class Engine
|
13
|
+
include CleanModel::Base
|
14
|
+
|
15
|
+
attribute :power, class_name: :numeric
|
16
|
+
attribute :cylinders, class_name: :integer
|
17
|
+
attribute :valves, class_name: 'Integer'
|
18
|
+
end
|
19
|
+
|
20
|
+
class Car
|
21
|
+
include CleanModel::Base
|
22
|
+
|
23
|
+
attribute :brand
|
24
|
+
attribute :model
|
25
|
+
attribute :engine, class_name: 'BaseModels::Engine'
|
26
|
+
attribute :comfort, transformation: lambda { |v| v.is_a?(String) ? v.split(',').map(&:strip) : v }
|
27
|
+
end
|
28
|
+
|
29
|
+
class Factory
|
30
|
+
include CleanModel::Base
|
31
|
+
|
32
|
+
attribute :cars, collection: 'BaseModels::Car'
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clean_model
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Gabriel Naiman
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: &24823776 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *24823776
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activemodel
|
27
|
+
requirement: &24823476 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.0.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *24823476
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: web_client
|
38
|
+
requirement: &24823236 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *24823236
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: &24822924 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *24822924
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: webmock
|
60
|
+
requirement: &24822660 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *24822660
|
69
|
+
description: Extensions for ActiveModel to implement multiple types of models
|
70
|
+
email:
|
71
|
+
- gabynaiman@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .gitignore
|
77
|
+
- Gemfile
|
78
|
+
- README.md
|
79
|
+
- Rakefile
|
80
|
+
- clean_model.gemspec
|
81
|
+
- lib/clean_model.rb
|
82
|
+
- lib/clean_model/attribute.rb
|
83
|
+
- lib/clean_model/base.rb
|
84
|
+
- lib/clean_model/exceptions.rb
|
85
|
+
- lib/clean_model/persistent.rb
|
86
|
+
- lib/clean_model/remote.rb
|
87
|
+
- lib/clean_model/version.rb
|
88
|
+
- spec/base_model_spec.rb
|
89
|
+
- spec/persistent_model_spec.rb
|
90
|
+
- spec/remote_models_spec.rb
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
- spec/support/models/base_models.rb
|
93
|
+
- spec/support/models/persistent_models.rb
|
94
|
+
- spec/support/models/remote_models.rb
|
95
|
+
homepage: https://github.com/gabynaiman/clean_model
|
96
|
+
licenses: []
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 1.8.16
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: Extensions for ActiveModel to implement multiple types of models
|
119
|
+
test_files: []
|