roar-extensions 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Guardfile +24 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +10 -0
- data/lib/roar-extensions.rb +1 -0
- data/lib/roar_extensions.rb +30 -0
- data/lib/roar_extensions/destroyed_record_presenter.rb +14 -0
- data/lib/roar_extensions/helpers/embedded_parameter_parsing.rb +37 -0
- data/lib/roar_extensions/json_hal_extensions.rb +6 -0
- data/lib/roar_extensions/link_presenter.rb +26 -0
- data/lib/roar_extensions/money_presenter.rb +13 -0
- data/lib/roar_extensions/paginated_collection_presenter.rb +58 -0
- data/lib/roar_extensions/presenter.rb +65 -0
- data/lib/roar_extensions/representable_json_extensions.rb +13 -0
- data/lib/roar_extensions/representer.rb +61 -0
- data/lib/roar_extensions/resource_links.rb +16 -0
- data/lib/roar_extensions/version.rb +3 -0
- data/roar-extensions.gemspec +27 -0
- data/spec/destroyed_record_presenter_spec.rb +17 -0
- data/spec/helpers/embedded_parameter_parsing_spec.rb +52 -0
- data/spec/link_presenter_spec.rb +48 -0
- data/spec/money_presenter_spec.rb +19 -0
- data/spec/paginated_collection_presenter_spec.rb +176 -0
- data/spec/presenter_spec.rb +167 -0
- data/spec/representable_spec.rb +66 -0
- data/spec/resource_links_spec.rb +42 -0
- data/spec/roar/representer/feature/hypermedia_spec.rb +25 -0
- data/spec/roar/representer/json/hal_spec.rb +47 -0
- data/spec/roar_extensions_spec.rb +23 -0
- data/spec/spec_helper.rb +10 -0
- metadata +220 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
module RoarExtensions
|
2
|
+
module ResourceLinks
|
3
|
+
private
|
4
|
+
def merge_links(collection, &presenter_generator)
|
5
|
+
collection.inject({}) do |acc, element|
|
6
|
+
acc.merge(presenter_generator.call(element).to_hash)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def resource_link_json(link_hash)
|
11
|
+
link_hash.inject({}) do |acc, (rel, href)|
|
12
|
+
acc.merge(LinkPresenter.new(rel, href).to_hash)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/roar_extensions/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Donald Plummer", "Michael Xavier"]
|
6
|
+
gem.email = ["developers@crystalcommerce.com"]
|
7
|
+
gem.description = %q{Useful extensions to roar}
|
8
|
+
gem.summary = %q{Useful extensions to roar}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "roar-extensions"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = RoarExtensions::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency("roar", "0.11.3")
|
19
|
+
gem.add_dependency("activesupport", ">= 2.3.14")
|
20
|
+
|
21
|
+
gem.add_development_dependency "pry"
|
22
|
+
gem.add_development_dependency "pry-nav"
|
23
|
+
gem.add_development_dependency "rake", "~>0.9.2"
|
24
|
+
gem.add_development_dependency "rspec", "~>2.10.0"
|
25
|
+
gem.add_development_dependency "guard", "~>1.2.1"
|
26
|
+
gem.add_development_dependency "guard-rspec", "~>1.1.0"
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'roar_extensions'
|
3
|
+
|
4
|
+
module RoarExtensions
|
5
|
+
describe DestroyedRecordPresenter do
|
6
|
+
class DestroyedRecordPresenterTest
|
7
|
+
include DestroyedRecordPresenter
|
8
|
+
root_element :foo
|
9
|
+
end
|
10
|
+
|
11
|
+
subject { DestroyedRecordPresenterTest.new(123) }
|
12
|
+
|
13
|
+
it "has just the id" do
|
14
|
+
subject.to_hash.should == {'foo' => {'id' => 123}}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'roar_extensions'
|
3
|
+
|
4
|
+
module RoarExtensions::Helpers
|
5
|
+
describe EmbeddedParameterParsing do
|
6
|
+
class MyTestController
|
7
|
+
def self.before_filter(*args)
|
8
|
+
# yup
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
subject { MyTestController.new }
|
13
|
+
|
14
|
+
describe "including into a class" do
|
15
|
+
it "calls before_filter" do
|
16
|
+
MyTestController.should_receive(:before_filter).with(:parse_embedded_params_filter)
|
17
|
+
MyTestController.send(:include, EmbeddedParameterParsing)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "parse_embedded_params" do
|
22
|
+
before(:each) do
|
23
|
+
MyTestController.send(:include, EmbeddedParameterParsing)
|
24
|
+
end
|
25
|
+
it "returns empty array for blank string" do
|
26
|
+
subject.parse_embedded_params("").should == []
|
27
|
+
end
|
28
|
+
|
29
|
+
it "returns empty array for nil" do
|
30
|
+
subject.parse_embedded_params(nil).should == []
|
31
|
+
end
|
32
|
+
|
33
|
+
it "returns the given list if comma-separated as symbols" do
|
34
|
+
subject.parse_embedded_params("foo,bar").should == [:foo, :bar]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "recursively parses nested embeddings" do
|
38
|
+
subject.parse_embedded_params("line_items:product,line_items:variant,customer,address:ip_address").should == [
|
39
|
+
:customer,
|
40
|
+
{:line_items => [:product, :variant], :address => [:ip_address]}
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
it "recurses multiple levels" do
|
45
|
+
subject.parse_embedded_params("customer,line_items:product:category").should == [
|
46
|
+
:customer,
|
47
|
+
{:line_items => [{:product => [:category]}]}
|
48
|
+
]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'roar_extensions'
|
3
|
+
|
4
|
+
module RoarExtensions
|
5
|
+
describe LinkPresenter do
|
6
|
+
subject { LinkPresenter.new('search_engine', 'http://google.com') }
|
7
|
+
|
8
|
+
describe "#==" do
|
9
|
+
it "returns true if href, rel, and title match" do
|
10
|
+
subject.should == LinkPresenter.new('search_engine', 'http://google.com')
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns false if href, rel, or title differ" do
|
14
|
+
subject.should_not == LinkPresenter.new('search_engine', 'http://bing.com')
|
15
|
+
subject.should_not == LinkPresenter.new('weee', 'http://google.com')
|
16
|
+
subject.should_not == LinkPresenter.new('search_engine', 'http://google.com', 'cows')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "no title" do
|
21
|
+
its(:as_json) do
|
22
|
+
should == { 'search_engine' => {:href => 'http://google.com'} }
|
23
|
+
end
|
24
|
+
|
25
|
+
it "aliases to_hash to as_json" do
|
26
|
+
subject.to_hash.should == subject.as_json
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "title given" do
|
31
|
+
subject { LinkPresenter.new('search_engine',
|
32
|
+
'http://google.com',
|
33
|
+
'Cool Search') }
|
34
|
+
its(:as_json) do
|
35
|
+
should == {
|
36
|
+
'search_engine' => {
|
37
|
+
:href => 'http://google.com',
|
38
|
+
:title => 'Cool Search'
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
it "aliases to_hash to as_json" do
|
44
|
+
subject.to_hash.should == subject.as_json
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'roar_extensions'
|
3
|
+
|
4
|
+
module RoarExtensions
|
5
|
+
describe MoneyPresenter do
|
6
|
+
let(:currency) { stub(:iso_code => "USD") }
|
7
|
+
let(:money) { stub(:currency => currency, :cents => 100) }
|
8
|
+
|
9
|
+
subject { MoneyPresenter.new(money) }
|
10
|
+
|
11
|
+
it "has cents" do
|
12
|
+
subject.to_hash['money']['cents'].should == 100
|
13
|
+
end
|
14
|
+
|
15
|
+
it "has currency" do
|
16
|
+
subject.to_hash['money']['currency'].should == "USD"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require "roar_extensions"
|
3
|
+
|
4
|
+
module RoarExtensions
|
5
|
+
describe PaginatedCollectionPresenter do
|
6
|
+
class TestEntry
|
7
|
+
include RoarExtensions::Presenter
|
8
|
+
|
9
|
+
attr_reader :name, :age
|
10
|
+
|
11
|
+
def initialize(name, age)
|
12
|
+
@name = name
|
13
|
+
@age = age
|
14
|
+
end
|
15
|
+
|
16
|
+
property :name
|
17
|
+
property :age
|
18
|
+
property :lowercased_name, :from => :name_downcase
|
19
|
+
|
20
|
+
def name_downcase
|
21
|
+
name.downcase
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:current_page) { 1 }
|
27
|
+
let(:next_page) { 2 }
|
28
|
+
let(:previous_page) { nil }
|
29
|
+
let(:entries) {[
|
30
|
+
TestEntry.new("Bob", 41)
|
31
|
+
]}
|
32
|
+
let(:paginated_result) do
|
33
|
+
mock("Paginated Result", :total_pages => 3,
|
34
|
+
:current_page => current_page,
|
35
|
+
:next_page => next_page,
|
36
|
+
:previous_page => previous_page,
|
37
|
+
:total_entries => 3,
|
38
|
+
:collect => entries,
|
39
|
+
:per_page => 1)
|
40
|
+
end
|
41
|
+
let(:base_path) { "/things" }
|
42
|
+
let(:json_options) {{}}
|
43
|
+
|
44
|
+
describe "#to_hash" do
|
45
|
+
let(:presenter) { PaginatedCollectionPresenter.new(paginated_result,
|
46
|
+
base_path)}
|
47
|
+
subject { JSON.parse(presenter.to_json(json_options))['paginated_collection'] }
|
48
|
+
|
49
|
+
it "includes the total pages" do
|
50
|
+
subject['total_pages'].should == 3
|
51
|
+
end
|
52
|
+
|
53
|
+
it "includes the total entries" do
|
54
|
+
subject['total_entries'].should == 3
|
55
|
+
end
|
56
|
+
|
57
|
+
it "includes the per page" do
|
58
|
+
subject['per_page'].should == 1
|
59
|
+
end
|
60
|
+
|
61
|
+
it "includes the self link" do
|
62
|
+
subject['_links']['self'].should == {"href" => "/things"}
|
63
|
+
end
|
64
|
+
|
65
|
+
it "includes the next_page link" do
|
66
|
+
subject['_links']['next_page'].should == {"href" => "/things?page=2"}
|
67
|
+
end
|
68
|
+
|
69
|
+
it "does not include the previous_page link on the first page" do
|
70
|
+
subject['_links'].should_not have_key('previous_page')
|
71
|
+
end
|
72
|
+
|
73
|
+
it "includes the paginated results under the entries key" do
|
74
|
+
subject['entries'].should == [{'name' => 'Bob',
|
75
|
+
'age' => 41,
|
76
|
+
'lowercased_name' => 'bob'}]
|
77
|
+
end
|
78
|
+
|
79
|
+
it "includes current_page" do
|
80
|
+
subject["current_page"].should == 1
|
81
|
+
end
|
82
|
+
|
83
|
+
it "includes next_page" do
|
84
|
+
subject["next_page"].should == 2
|
85
|
+
end
|
86
|
+
|
87
|
+
it "includes previous_page" do
|
88
|
+
subject["previous_page"].should == nil
|
89
|
+
end
|
90
|
+
|
91
|
+
context "on last page" do
|
92
|
+
let(:previous_page) { 2 }
|
93
|
+
let(:current_page) { 3 }
|
94
|
+
let(:next_page) { nil }
|
95
|
+
|
96
|
+
it "includes the self link" do
|
97
|
+
subject['_links']['self'].should == {"href" => "/things?page=3"}
|
98
|
+
end
|
99
|
+
|
100
|
+
it "does not include the next_page link" do
|
101
|
+
subject['_links'].should_not have_key('next_page')
|
102
|
+
end
|
103
|
+
|
104
|
+
it "includes the previous_page link" do
|
105
|
+
subject['_links']['previous_page'].should == {"href" => "/things?page=2"}
|
106
|
+
end
|
107
|
+
|
108
|
+
it "includes current_page" do
|
109
|
+
subject["current_page"].should == 3
|
110
|
+
end
|
111
|
+
|
112
|
+
it "includes next_page" do
|
113
|
+
subject["next_page"].should == nil
|
114
|
+
end
|
115
|
+
|
116
|
+
it "includes previous_page" do
|
117
|
+
subject["previous_page"].should == 2
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "middle page" do
|
122
|
+
let(:previous_page) { 1 }
|
123
|
+
let(:current_page) { 2 }
|
124
|
+
let(:next_page) { 3 }
|
125
|
+
|
126
|
+
it "includes the self link" do
|
127
|
+
subject['_links']['self'].should == {"href" => "/things?page=2"}
|
128
|
+
end
|
129
|
+
|
130
|
+
it "includes the next_page link" do
|
131
|
+
subject['_links']['next_page'].should == {"href" => "/things?page=3"}
|
132
|
+
end
|
133
|
+
|
134
|
+
it "includes the previous_page link" do
|
135
|
+
subject['_links']['previous_page'].should == {"href" => "/things"}
|
136
|
+
end
|
137
|
+
|
138
|
+
it "includes current_page" do
|
139
|
+
subject["current_page"].should == 2
|
140
|
+
end
|
141
|
+
|
142
|
+
it "includes next_page" do
|
143
|
+
subject["next_page"].should == 3
|
144
|
+
end
|
145
|
+
|
146
|
+
it "includes previous_page" do
|
147
|
+
subject["previous_page"].should == 1
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
context "attribute whitelisting" do
|
152
|
+
let(:json_options) {{:include => [:name]}}
|
153
|
+
|
154
|
+
it "the include option is passed to the entries" do
|
155
|
+
subject['entries'].should == [{'name' => 'Bob'}]
|
156
|
+
end
|
157
|
+
|
158
|
+
context "using the :from property option" do
|
159
|
+
let(:json_options) {{:include => ["lowercased_name"]}}
|
160
|
+
|
161
|
+
it "the include option is passed to the entries" do
|
162
|
+
subject['entries'].should == [{'lowercased_name' => 'bob'}]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context "attribute blacklisting" do
|
168
|
+
let(:json_options) {{:exclude => [:name]}}
|
169
|
+
|
170
|
+
it "the include option is passed to the entries" do
|
171
|
+
subject['entries'].should == [{'age' => 41, 'lowercased_name' => 'bob'}]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'roar_extensions'
|
3
|
+
|
4
|
+
module RoarExtensions
|
5
|
+
describe Presenter do
|
6
|
+
|
7
|
+
class TestPhotoPresenter
|
8
|
+
include Presenter
|
9
|
+
|
10
|
+
root_element :photo
|
11
|
+
|
12
|
+
delegated_property :position
|
13
|
+
end
|
14
|
+
|
15
|
+
class TestProductPresenter
|
16
|
+
include Presenter
|
17
|
+
|
18
|
+
root_element :product
|
19
|
+
|
20
|
+
delegated_property :id, :always_include => true
|
21
|
+
delegated_property :name
|
22
|
+
delegated_property :is_buying, :from => :buying?
|
23
|
+
delegated_property :msrp, :as => MoneyPresenter
|
24
|
+
property :possible_variants_count
|
25
|
+
property :min_sell_price, :as => MoneyPresenter
|
26
|
+
property :catalog_links, :from => :catalog_links_as_json
|
27
|
+
delegated_collection :photos, :as => TestPhotoPresenter, :embedded => true
|
28
|
+
|
29
|
+
link(:rel => "self") { "/v1/products/#{record.id}" }
|
30
|
+
link(:rel => "category") { "/v1/categories/#{record.category_id}" }
|
31
|
+
link(:rel => "related_products") { "/v1/products/#{record.id}/related" }
|
32
|
+
|
33
|
+
def initialize(record, options = {})
|
34
|
+
super(record)
|
35
|
+
|
36
|
+
@embedded = options.fetch(:embedded, [])
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def min_sell_price
|
42
|
+
OpenStruct.new(:cents => 499,
|
43
|
+
:currency => OpenStruct.new(:iso_code => 'USD'))
|
44
|
+
end
|
45
|
+
|
46
|
+
def possible_variants_count
|
47
|
+
3
|
48
|
+
end
|
49
|
+
|
50
|
+
def catalog_links_as_json
|
51
|
+
{'omg' => 'wtf'}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
let(:product) {
|
56
|
+
mock("Product Record",
|
57
|
+
:id => 9001,
|
58
|
+
:category_id => 800,
|
59
|
+
:name => "Worship",
|
60
|
+
:photos => [photo],
|
61
|
+
:msrp => stub(:cents => 300, :currency => stub(:iso_code => 'USD')),
|
62
|
+
:buying? => true)
|
63
|
+
}
|
64
|
+
|
65
|
+
let(:photo) {
|
66
|
+
mock("Photo", :position => 1)
|
67
|
+
}
|
68
|
+
|
69
|
+
let(:options) { {} }
|
70
|
+
let(:json_options) { {} }
|
71
|
+
|
72
|
+
let(:presenter) { TestProductPresenter.new(product, options) }
|
73
|
+
|
74
|
+
subject { presenter }
|
75
|
+
|
76
|
+
it "aliases to_hash to as_json" do
|
77
|
+
subject.to_hash.should == subject.as_json
|
78
|
+
end
|
79
|
+
|
80
|
+
context "as_json" do
|
81
|
+
subject { presenter.as_json(json_options)['product'] }
|
82
|
+
|
83
|
+
it "has a name" do
|
84
|
+
subject['name'].should == 'Worship'
|
85
|
+
end
|
86
|
+
|
87
|
+
it "has a possible_variants_count" do
|
88
|
+
subject['possible_variants_count'].should == 3
|
89
|
+
end
|
90
|
+
|
91
|
+
it "has a catalog_links" do
|
92
|
+
subject['catalog_links'].should == {'omg' => 'wtf'}
|
93
|
+
end
|
94
|
+
|
95
|
+
it "has an msrp as money" do
|
96
|
+
subject['msrp'].should == {
|
97
|
+
'money' => {
|
98
|
+
'cents' => 300,
|
99
|
+
'currency' => 'USD'
|
100
|
+
}
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
it "has a min sell price as money" do
|
105
|
+
subject['min_sell_price'].should == {
|
106
|
+
'money' => {
|
107
|
+
'cents' => 499,
|
108
|
+
'currency' => 'USD'
|
109
|
+
}
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
it "has an id" do
|
114
|
+
subject['id'].should == 9001
|
115
|
+
end
|
116
|
+
|
117
|
+
it "has the buyingness" do
|
118
|
+
subject['is_buying'].should == true
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
it "has api _links" do
|
123
|
+
subject['_links'].should == {
|
124
|
+
'self' => { :href => "/v1/products/9001" },
|
125
|
+
'related_products' => { :href => "/v1/products/9001/related" },
|
126
|
+
'category' => { :href => '/v1/categories/800'}
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
it "does not embed" do
|
131
|
+
subject.should_not have_key('_embedded')
|
132
|
+
end
|
133
|
+
|
134
|
+
context "embedding of photos enabled" do
|
135
|
+
let(:options) { {:embedded => [:photos]} }
|
136
|
+
|
137
|
+
it "has embedded photos" do
|
138
|
+
subject['_embedded']['photos'].should == [
|
139
|
+
{
|
140
|
+
'photo' => {
|
141
|
+
'position' => 1
|
142
|
+
}
|
143
|
+
}
|
144
|
+
]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "always renders nil attributes" do
|
149
|
+
before(:each) do
|
150
|
+
product.stub(:name).and_return(nil)
|
151
|
+
end
|
152
|
+
|
153
|
+
it "has a null name" do
|
154
|
+
subject.fetch('name').should == nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
context "limiting attributes returned" do
|
159
|
+
let(:json_options) {{ :include => [:name] }}
|
160
|
+
|
161
|
+
it "limits to the attributes requested, plus required attributes" do
|
162
|
+
subject.keys.should == ['id', 'name']
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|