roar-extensions 0.0.2
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 +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
|