clean_model 0.0.1
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/.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: []
|