yeti 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/yeti/context.rb CHANGED
@@ -4,12 +4,15 @@ module Yeti
4
4
  attr_reader :id
5
5
  end
6
6
 
7
+ delegate :id, to: :account, prefix: :account
8
+
7
9
  def initialize(hash)
8
10
  @given_account_id = hash.fetch(:account_id)
9
11
  end
10
12
 
11
13
  def account
12
- @account ||= find_account_by_id(given_id) || no_account
14
+ @account ||= find_account_by_id given_account_id if given_account_id
15
+ @account ||= no_account
13
16
  end
14
17
 
15
18
  def find_account_by_id(id)
data/lib/yeti/editor.rb CHANGED
@@ -74,16 +74,27 @@ module Yeti
74
74
  attr_reader :given_id
75
75
 
76
76
  def self.attribute(name, opts={})
77
- self.attributes << name
77
+ opts[:attribute_name] = name
78
+ opts[:from] = :edited unless opts.has_key? :from
79
+ attribute_options[name.to_sym] = opts
78
80
  define_attribute_methods attributes
79
- from = opts[:from] || :edited
81
+ from = case opts[:from].to_s
82
+ when "" then "nil"
83
+ when /^\./ then "self#{opts[:from]}"
84
+ when /\./ then opts[:from]
85
+ else "#{opts[:from]}.#{name}"
86
+ end
80
87
  class_eval """
81
88
  def #{name}
82
- @#{name} = #{from}.#{name} unless defined? @#{name}
89
+ unless defined? @#{name}
90
+ opts = self.class.attribute_options[:#{name}]
91
+ @#{name} = format_input #{from}, opts
92
+ end
83
93
  @#{name}
84
94
  end
85
95
  def #{name}=(value)
86
- value = (value.to_s.clean.strip if value)
96
+ opts = self.class.attribute_options[:#{name}]
97
+ value = format_output value, opts
87
98
  return if value==#{name}
88
99
  #{name}_will_change!
89
100
  @#{name} = value
@@ -94,7 +105,11 @@ module Yeti
94
105
  end
95
106
 
96
107
  def self.attributes
97
- @attributes ||= []
108
+ attribute_options.keys
109
+ end
110
+
111
+ def self.attribute_options
112
+ @attribute_options ||= {}
98
113
  end
99
114
 
100
115
  def self.dont_translate_error_messages
@@ -105,5 +120,13 @@ module Yeti
105
120
  !!@untranslated
106
121
  end
107
122
 
123
+ def format_input(value, attribute_opts)
124
+ value.to_s if value
125
+ end
126
+
127
+ def format_output(value, attribute_opts)
128
+ value.to_s.clean.strip if value
129
+ end
130
+
108
131
  end
109
132
  end
data/lib/yeti/search.rb CHANGED
@@ -1,18 +1,29 @@
1
1
  module Yeti
2
2
  class Search
3
3
 
4
- attr_reader :search, :page
4
+ attr_reader :context
5
5
  delegate :to_ary, :empty?, :each, :group_by, :size, to: :results
6
+ delegate :page_count, to: :paginated_results
6
7
 
7
8
  def initialize(context, hash)
8
9
  @context = context
9
- @search = hash[:search] || {}.with_indifferent_access
10
- @page = hash[:page] || 1
11
- @per_page = hash[:per_page] || 20
10
+ @hash = hash
12
11
  end
13
12
 
14
- def page_count
15
- paginated_results.page_count
13
+ def search
14
+ @search ||= (hash[:search] || {}).with_indifferent_access
15
+ end
16
+
17
+ def page
18
+ @page ||= [1, (hash[:page] || 1).to_i].max
19
+ end
20
+
21
+ def per_page
22
+ @per_page ||= begin
23
+ per_page = [1, (hash[:per_page] || 20).to_i].max
24
+ max = self.class.max_per_page
25
+ max ? [per_page, max].min : per_page
26
+ end
16
27
  end
17
28
 
18
29
  def count
@@ -38,18 +49,23 @@ module Yeti
38
49
  end
39
50
  end
40
51
 
52
+ def paginated_results
53
+ raise NotImplementedError
54
+ end
55
+
41
56
  private
42
57
 
43
- attr_reader :context, :per_page
58
+ attr_reader :hash
59
+
60
+ # ~~~ private class methods ~~~
61
+ def self.max_per_page(value=nil)
62
+ value ? @max_per_page = value : @max_per_page
63
+ end
44
64
 
45
65
  # ~~~ private instance methods ~~~
46
66
  def delegate_to_search_pattern
47
67
  /(?:_equals|_contains|_gte|_lte)\z/
48
68
  end
49
69
 
50
- def paginated_results
51
- raise NotImplementedError
52
- end
53
-
54
70
  end
55
71
  end
data/lib/yeti/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Yeti
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
data/lib/yeti.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require "active_support/core_ext/module/delegation"
2
+ require "active_support/core_ext/hash/indifferent_access"
1
3
  require "active_model"
2
4
  require "string_cleaner"
3
5
  require "yeti/context"
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "bundler/setup"
2
2
  require "logger"
3
3
  require "yeti"
4
+ require "support/matchers"
4
5
 
5
6
  RSpec.configure do |config|
6
7
  config.before(:all) do
@@ -0,0 +1,35 @@
1
+ RSpec::Matchers.define :delegates do |delegated_method|
2
+ match do |subject|
3
+ stubbed = send(@delegate).stub(@delegate_method)
4
+ stubbed.with @delegate_params if @delegate_params
5
+ stubbed.and_return expected=mock
6
+ subject.send(delegated_method) === expected
7
+ end
8
+
9
+ chain :to do |delegate|
10
+ if delegate.is_a?(String) && delegate.include?("#")
11
+ @delegate, @delegate_method = delegate.split "#"
12
+ else
13
+ @delegate = delegate
14
+ @delegate_method = delegated_method
15
+ end
16
+ end
17
+
18
+ chain :with do |delegate_params|
19
+ @delegate_params = delegate_params
20
+ end
21
+
22
+ description do
23
+ delegate_method = ("##{@delegate_method}" if delegated_method.to_s!=@delegate_method.to_s)
24
+ delegate_params = (" with params #{@delegate_params.inspect}" if @delegate_params)
25
+ "delegates #{delegated_method} to #{@delegate}#{delegate_method}#{delegate_params}"
26
+ end
27
+
28
+ failure_message_for_should do |text|
29
+ "expected delegation of #{delegated_method} to #{@delegate}"
30
+ end
31
+
32
+ failure_message_for_should do |text|
33
+ "do not expected delegation of #{delegated_method} to #{@delegate}"
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ require "spec_helper"
2
+
3
+ describe Yeti::Context do
4
+ context "initialization" do
5
+ it "requires a hash with account_id" do
6
+ lambda{ Yeti::Context.new }.should raise_error ArgumentError, "wrong number of arguments (0 for 1)"
7
+ lambda{ Yeti::Context.new key: nil }.should raise_error KeyError, "key not found: :account_id"
8
+ end
9
+ end
10
+ context "when account_id is nil" do
11
+ subject{ Yeti::Context.new account_id: nil }
12
+ it "#account is an instance of Yeti::Context::NoAccount" do
13
+ subject.account.should be_kind_of Yeti::Context::NoAccount
14
+ end
15
+ it("#account_id is nil"){ subject.account_id.should be_nil }
16
+ it "no account can be overriden by subclasses" do
17
+ subclass = Class.new Yeti::Context do
18
+ def no_account
19
+ :custom_no_account
20
+ end
21
+ end
22
+ subject = subclass.new account_id: nil
23
+ subject.account.should == :custom_no_account
24
+ end
25
+ end
26
+ context "when account_id" do
27
+ subject{ Yeti::Context.new account_id: 1 }
28
+ it "uses find_account_by_id to find account" do
29
+ subject.stub(:find_account_by_id).with(1).and_return(expected = mock)
30
+ subject.account.should be expected
31
+ end
32
+ it "#find_account_by_id is virtual" do
33
+ lambda do
34
+ subject.find_account_by_id 1
35
+ end.should raise_error NotImplementedError
36
+ end
37
+ it "#account_id returns account.id" do
38
+ subject.stub(:find_account_by_id).with(1).and_return mock(id: 2)
39
+ subject.account_id.should be 2
40
+ end
41
+ end
42
+ end
@@ -1,50 +1,50 @@
1
1
  require "spec_helper"
2
2
 
3
- describe ::Yeti::Editor do
3
+ describe Yeti::Editor do
4
4
  let(:context){ mock :context }
5
- subject{ ::Yeti::Editor.new context, nil }
6
- it "should keep given context" do
5
+ subject{ Yeti::Editor.new context, nil }
6
+ it "keeps given context" do
7
7
  subject.context.should be context
8
8
  end
9
- it "#find_by_id should be virtual" do
9
+ it "#find_by_id is virtual" do
10
10
  lambda{ subject.find_by_id 1 }.should raise_error NotImplementedError
11
11
  end
12
- it "#new_object should be virtual" do
12
+ it "#new_object is virtual" do
13
13
  lambda{ subject.new_object }.should raise_error NotImplementedError
14
14
  end
15
- it "#persist! should be virtual" do
15
+ it "#persist! is virtual" do
16
16
  lambda{ subject.persist! }.should raise_error NotImplementedError
17
17
  end
18
18
  context "with a given id" do
19
- subject{ ::Yeti::Editor.new context, 1 }
19
+ subject{ Yeti::Editor.new context, 1 }
20
20
  it{ should be_persisted }
21
- it "#edited should use #find_by_id" do
21
+ it "uses #find_by_id to find the main object being edited" do
22
22
  subject.stub(:find_by_id).with(1).and_return(expected = mock)
23
23
  subject.edited.should be expected
24
24
  end
25
25
  end
26
26
  context "without id" do
27
27
  it{ should_not be_persisted }
28
- it "#edited should use #new_object" do
28
+ it "uses #new_object to initialize main object being edited" do
29
29
  subject.stub(:new_object).and_return(expected = mock)
30
30
  subject.edited.should be expected
31
31
  end
32
32
  end
33
33
  context "when not valid" do
34
34
  before{ subject.stub(:valid?).and_return false }
35
- it "#save should return false" do
35
+ it "#save returns false" do
36
36
  subject.save.should be false
37
37
  end
38
38
  end
39
39
  context "when valid" do
40
- it "#save should call persist! then return true" do
40
+ it "#save calls persist! then returns true" do
41
41
  subject.should_receive :persist!
42
42
  subject.save.should be true
43
43
  end
44
44
  end
45
45
  context "editor of one record" do
46
- let :object_editor do
47
- Class.new ::Yeti::Editor do
46
+ let :editor_class do
47
+ Class.new Yeti::Editor do
48
48
  attribute :name
49
49
  validates_presence_of :name
50
50
  def self.name
@@ -53,45 +53,45 @@ describe ::Yeti::Editor do
53
53
  end
54
54
  end
55
55
  context "new record" do
56
- subject{ object_editor.new context, nil }
56
+ subject{ editor_class.new context, nil }
57
57
  let(:new_record){ mock :new_record, name: nil, id: nil }
58
58
  before{ subject.stub(:new_object).and_return new_record }
59
59
  its(:id){ should be_nil }
60
60
  its(:name){ should be_nil }
61
- it "#name= should convert input to string" do
61
+ it "#name= converts input to string" do
62
62
  subject.name = ["test"]
63
63
  subject.name.should == "[\"test\"]"
64
64
  end
65
- it "#name= should clean the value" do
65
+ it "#name= cleans the value of any harmful content" do
66
66
  subject.name = "\tInfected\210\004"
67
67
  subject.name.should == "Infected"
68
68
  end
69
- it "#name= should accept nil" do
69
+ it "#name= accepts nil" do
70
70
  subject.name = "Valid"
71
71
  subject.name = nil
72
72
  subject.name.should be_nil
73
73
  end
74
- it "#attributes should return a hash" do
74
+ it "#attributes returns a hash" do
75
75
  subject.attributes.should == {name: nil}
76
76
  end
77
- it "#attributes= should assign each attribute" do
77
+ it "#attributes= assigns each attribute" do
78
78
  subject.should_receive(:name=).with "Anthony"
79
79
  subject.attributes = {name: "Anthony"}
80
80
  end
81
- it "#attributes= should skip unknown attributes" do
81
+ it "#attributes= skips unknown attributes" do
82
82
  subject.attributes = {unknown: "Anthony"}
83
83
  end
84
84
  context "before validation" do
85
85
  its(:errors){ should be_empty }
86
86
  end
87
87
  context "after validation" do
88
- it "should have an error on name" do
88
+ it "has an error on name" do
89
89
  subject.valid?
90
90
  subject.errors[:name].should have(1).item
91
91
  subject.errors[:name].should == ["can't be blank"]
92
92
  end
93
- it "should be able to return untranslated error messages" do
94
- object_editor.class_eval do
93
+ it "can return untranslated error messages" do
94
+ editor_class.class_eval do
95
95
  dont_translate_error_messages
96
96
  end
97
97
  subject.valid?
@@ -104,12 +104,12 @@ describe ::Yeti::Editor do
104
104
  context "when name is changed" do
105
105
  before{ subject.name = "Anthony" }
106
106
  it{ should be_valid }
107
- it "#attributes should be updated" do
107
+ it "#attributes is updated" do
108
108
  subject.attributes.should == {name: "Anthony"}
109
109
  end
110
- it("name should be updated"){ subject.name.should == "Anthony" }
111
- it("name should be dirty"){ subject.name_changed?.should be true }
112
- it "name should not be dirty anymore if original value is reset" do
110
+ it("name is updated"){ subject.name.should == "Anthony" }
111
+ it("name is dirty"){ subject.name_changed?.should be true }
112
+ it "name isn't dirty anymore if original value is set back" do
113
113
  subject.name = nil
114
114
  subject.name_changed?.should be false
115
115
  end
@@ -120,32 +120,88 @@ describe ::Yeti::Editor do
120
120
  subject.stub :persist!
121
121
  subject.save
122
122
  end
123
- it "should reset dirty attributes" do
123
+ it "resets dirty attributes" do
124
124
  subject.name_changed?.should be false
125
125
  end
126
- it "should still know previous changes" do
126
+ it "still knows previous changes" do
127
127
  subject.previous_changes.should == {"name"=>[nil, "Anthony"]}
128
128
  end
129
129
  end
130
130
  end
131
131
  context "existing record" do
132
- subject{ object_editor.new context, 1 }
132
+ subject{ editor_class.new context, 1 }
133
133
  let(:existing_record){ mock :existing_record, name: "Anthony", id: 1 }
134
134
  before{ subject.stub(:find_by_id).with(1).and_return existing_record }
135
- it("should get id from record"){ subject.id.should be 1 }
136
- it "should get name from record" do
135
+ it("gets id from record"){ subject.id.should be 1 }
136
+ it "gets name from record" do
137
137
  subject.name.should == "Anthony"
138
138
  end
139
139
  it{ should be_valid }
140
+ it "input formatting can be customized" do
141
+ subject.stub(:format_input).with("Anthony", {
142
+ attribute_name: :name,
143
+ from: :edited,
144
+ }).and_return(expected = mock)
145
+ subject.name.should be expected
146
+ end
147
+ it "output formatting can be customized" do
148
+ subject.stub(:format_output).with("Tony", {
149
+ attribute_name: :name,
150
+ from: :edited,
151
+ }).and_return(expected = mock)
152
+ subject.name = "Tony"
153
+ subject.name.should be expected
154
+ end
140
155
  context "when name is changed" do
141
156
  before{ subject.name = nil }
142
- it("name should be updated"){ subject.name.should be_nil }
143
- it("name should be dirty"){ subject.name_changed?.should be true }
144
- it "name should not be dirty anymore if original value is reset" do
157
+ it("name is updated"){ subject.name.should be_nil }
158
+ it("name is dirty"){ subject.name_changed?.should be true }
159
+ it "name isn't dirty anymore if original value is set back" do
145
160
  subject.name = "Anthony"
146
161
  subject.name_changed?.should be false
147
162
  end
148
163
  end
149
164
  end
150
165
  end
166
+ context "editor of multiple records" do
167
+ let :editor_class do
168
+ Class.new Yeti::Editor do
169
+ attribute :name
170
+ attribute :description, from: :related
171
+ attribute :password, from: nil
172
+ attribute :timestamp, from: ".timestamp_str"
173
+ attribute :related_id, from: "related.id"
174
+ attribute :invalid
175
+ def find_by_id(id)
176
+ Struct.new(:id, :name).new(id, "Anthony")
177
+ end
178
+ def related
179
+ Struct.new(:id, :description).new 2, "Business man"
180
+ end
181
+ def timestamp_str
182
+ "2001-01-01"
183
+ end
184
+ end
185
+ end
186
+ subject{ editor_class.new context, 1 }
187
+ it "attribute default value comes from edited" do
188
+ subject.id.should == 1
189
+ subject.name.should == "Anthony"
190
+ end
191
+ it "attribute value can come from another object" do
192
+ subject.description.should == "Business man"
193
+ end
194
+ it "attribute value can come from nowhere" do
195
+ subject.password.should be_nil
196
+ end
197
+ it "attribute value can come from specified method on self" do
198
+ subject.timestamp.should == "2001-01-01"
199
+ end
200
+ it "attribute value can come from specified method on another object" do
201
+ subject.related_id.should == "2"
202
+ end
203
+ it "attribute raises if value cannot be found in source" do
204
+ lambda{ subject.invalid }.should raise_error NoMethodError
205
+ end
206
+ end
151
207
  end
@@ -0,0 +1,101 @@
1
+ require "spec_helper"
2
+
3
+ describe Yeti::Search do
4
+ let(:context){ mock :context }
5
+ context "initialization" do
6
+ it "does require a context and a hash" do
7
+ message = "wrong number of arguments (1 for 2)"
8
+ lambda do
9
+ Yeti::Search.new context
10
+ end.should raise_error ArgumentError, message
11
+ end
12
+ end
13
+ context "given context and an empty hash" do
14
+ subject{ Yeti::Search.new context, {} }
15
+ it "keeps given context" do
16
+ subject.context.should be context
17
+ end
18
+ it "#search defaults to {}" do
19
+ subject.search.should == {}
20
+ end
21
+ it "#page defaults to 1" do
22
+ subject.page.should == 1
23
+ end
24
+ it "#per_page defaults to 20" do
25
+ subject.per_page.should == 20
26
+ end
27
+ end
28
+ context "given context and params" do
29
+ let :search do
30
+ {
31
+ "name_contains" => "tony",
32
+ "popular_equals" => "1",
33
+ "created_at_gte" => "2001-01-01",
34
+ "created_at_lte" => "2002-01-01",
35
+ "uncommon_filter" => "1",
36
+ }
37
+ end
38
+ let(:results){ mock :results }
39
+ subject{ Yeti::Search.new context, search: search }
40
+ before{ subject.stub(:results).and_return results }
41
+ it "#search comes from hash" do
42
+ subject.search.should == search
43
+ end
44
+ it "gets common filters from search" do
45
+ subject.should respond_to(:name_contains)
46
+ subject.should respond_to(:popular_equals)
47
+ subject.should respond_to(:created_at_gte)
48
+ subject.should respond_to(:created_at_lte)
49
+ subject.name_contains.should == "tony"
50
+ subject.popular_equals.should == "1"
51
+ subject.created_at_gte.should == "2001-01-01"
52
+ subject.created_at_lte.should == "2002-01-01"
53
+ end
54
+ it "doesn't get everything from search" do
55
+ subject.should_not respond_to(:uncommon_filter)
56
+ lambda{ subject.invalid_method }.should raise_error NoMethodError
57
+ lambda{ subject.uncommon_filter }.should raise_error NoMethodError
58
+ end
59
+ it "#page comes from hash" do
60
+ Yeti::Search.new(context, page: "2").page.should be 2
61
+ end
62
+ it "doesn't accept page to be lower than 1" do
63
+ Yeti::Search.new(context, page: "0").page.should be 1
64
+ end
65
+ it "#per_page comes from hash" do
66
+ Yeti::Search.new(context, per_page: "10").per_page.should be 10
67
+ end
68
+ it "doesn't accept per_page to be lower than 1" do
69
+ Yeti::Search.new(context, per_page: "0").per_page.should be 1
70
+ end
71
+ it "by default per_page has no limit" do
72
+ Yeti::Search.max_per_page.should be_nil
73
+ Yeti::Search.new(context, per_page: "9999").per_page.should be 9999
74
+ end
75
+ it "per_page can be limited" do
76
+ search_class = Class.new Yeti::Search do
77
+ max_per_page 50
78
+ end
79
+ search_class.max_per_page.should be 50
80
+ search_class.new(context, per_page: "9999").per_page.should be 50
81
+ end
82
+ it "#paginated_results is virtual" do
83
+ lambda do
84
+ subject.paginated_results
85
+ end.should raise_error NotImplementedError
86
+ end
87
+ it{ should delegates(:to_ary).to :results }
88
+ it{ should delegates(:empty?).to :results }
89
+ it{ should delegates(:each).to :results }
90
+ it{ should delegates(:group_by).to :results }
91
+ it{ should delegates(:size).to :results }
92
+ end
93
+ context "when paginated_results is defined" do
94
+ let(:paginated_results){ mock :paginated_results }
95
+ subject{ Yeti::Search.new context, {} }
96
+ before{ subject.stub(:paginated_results).and_return paginated_results }
97
+ it{ should delegates(:page_count).to :paginated_results }
98
+ it{ should delegates(:count).to "paginated_results#pagination_record_count" }
99
+ it{ should delegates(:results).to "paginated_results#all" }
100
+ end
101
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeti
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-19 00:00:00.000000000 Z
12
+ date: 2012-10-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activemodel
@@ -94,7 +94,10 @@ files:
94
94
  - lib/yeti/search.rb
95
95
  - lib/yeti/version.rb
96
96
  - spec/spec_helper.rb
97
+ - spec/support/matchers.rb
98
+ - spec/yeti/context_spec.rb
97
99
  - spec/yeti/editor_spec.rb
100
+ - spec/yeti/search_spec.rb
98
101
  - spec/yeti_spec.rb
99
102
  - yeti.gemspec
100
103
  homepage: ''
@@ -111,7 +114,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
111
114
  version: '0'
112
115
  segments:
113
116
  - 0
114
- hash: 955757267782825945
117
+ hash: -488456883026859265
115
118
  required_rubygems_version: !ruby/object:Gem::Requirement
116
119
  none: false
117
120
  requirements:
@@ -120,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
123
  version: '0'
121
124
  segments:
122
125
  - 0
123
- hash: 955757267782825945
126
+ hash: -488456883026859265
124
127
  requirements: []
125
128
  rubyforge_project:
126
129
  rubygems_version: 1.8.24
@@ -129,5 +132,8 @@ specification_version: 3
129
132
  summary: Editor pattern simplifies edition of multiple objects at once using ActiveModel
130
133
  test_files:
131
134
  - spec/spec_helper.rb
135
+ - spec/support/matchers.rb
136
+ - spec/yeti/context_spec.rb
132
137
  - spec/yeti/editor_spec.rb
138
+ - spec/yeti/search_spec.rb
133
139
  - spec/yeti_spec.rb