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 ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in clean_model.gemspec
4
+ gemspec
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"
@@ -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
@@ -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,3 @@
1
+ module CleanModel
2
+ VERSION = "0.0.1"
3
+ 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
@@ -0,0 +1,8 @@
1
+ require 'webmock/rspec'
2
+ require 'clean_model'
3
+
4
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
5
+
6
+ RSpec.configure do |config|
7
+
8
+ end
@@ -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
@@ -0,0 +1,10 @@
1
+ module PersistentModels
2
+
3
+ class Post
4
+ include CleanModel::Persistent
5
+
6
+ attribute :subject
7
+ attribute :content
8
+ end
9
+
10
+ end
@@ -0,0 +1,13 @@
1
+ module RemoteModels
2
+
3
+ class User
4
+ include CleanModel::Remote
5
+
6
+ connection host: 'localhost', port: 9999
7
+
8
+ attribute :first_name
9
+ attribute :last_name
10
+ attribute :email
11
+ end
12
+
13
+ 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: []