yeti 0.1.0 → 0.1.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/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