spyke 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +84 -0
- data/Rakefile +9 -0
- data/lib/spyke.rb +6 -0
- data/lib/spyke/associations.rb +55 -0
- data/lib/spyke/associations/association.rb +62 -0
- data/lib/spyke/associations/belongs_to.rb +11 -0
- data/lib/spyke/associations/has_many.rb +39 -0
- data/lib/spyke/associations/has_one.rb +11 -0
- data/lib/spyke/attributes.rb +130 -0
- data/lib/spyke/base.rb +25 -0
- data/lib/spyke/collection.rb +10 -0
- data/lib/spyke/config.rb +5 -0
- data/lib/spyke/exceptions.rb +3 -0
- data/lib/spyke/http.rb +78 -0
- data/lib/spyke/orm.rb +83 -0
- data/lib/spyke/path.rb +28 -0
- data/lib/spyke/relation.rb +57 -0
- data/lib/spyke/result.rb +25 -0
- data/lib/spyke/scope_registry.rb +17 -0
- data/lib/spyke/scopes.rb +30 -0
- data/lib/spyke/version.rb +3 -0
- data/spyke.gemspec +36 -0
- data/test/associations_test.rb +282 -0
- data/test/attributes_test.rb +102 -0
- data/test/callbacks_test.rb +24 -0
- data/test/custom_request_test.rb +53 -0
- data/test/orm_test.rb +136 -0
- data/test/path_test.rb +23 -0
- data/test/relation_test.rb +76 -0
- data/test/support/api.rb +6 -0
- data/test/support/fixtures.rb +65 -0
- data/test/support/webmock.rb +6 -0
- data/test/test_helper.rb +14 -0
- metadata +287 -0
data/lib/spyke/base.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'spyke/associations'
|
3
|
+
require 'spyke/attributes'
|
4
|
+
require 'spyke/orm'
|
5
|
+
require 'spyke/http'
|
6
|
+
require 'spyke/scopes'
|
7
|
+
|
8
|
+
module Spyke
|
9
|
+
class Base
|
10
|
+
# ActiveModel
|
11
|
+
include ActiveModel::Conversion
|
12
|
+
include ActiveModel::Model
|
13
|
+
include ActiveModel::Validations
|
14
|
+
include ActiveModel::Validations::Callbacks
|
15
|
+
extend ActiveModel::Translation
|
16
|
+
extend ActiveModel::Callbacks
|
17
|
+
|
18
|
+
# Spyke
|
19
|
+
include Associations
|
20
|
+
include Attributes
|
21
|
+
include Http
|
22
|
+
include Orm
|
23
|
+
include Scopes
|
24
|
+
end
|
25
|
+
end
|
data/lib/spyke/config.rb
ADDED
data/lib/spyke/http.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'spyke/config'
|
3
|
+
require 'spyke/path'
|
4
|
+
require 'spyke/result'
|
5
|
+
|
6
|
+
module Spyke
|
7
|
+
module Http
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
METHODS = %i{ get post put patch delete }
|
10
|
+
|
11
|
+
included do
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
METHODS.each do |method|
|
16
|
+
define_method(method) do |path, params = {}|
|
17
|
+
new_or_collection_from_result send("#{method}_raw", path, params)
|
18
|
+
end
|
19
|
+
|
20
|
+
define_method("#{method}_raw") do |path, params = {}|
|
21
|
+
request(method, path, params)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def request(method, path, params = {})
|
26
|
+
response = connection.send(method) do |request|
|
27
|
+
if method == :get
|
28
|
+
request.url path.to_s, params
|
29
|
+
else
|
30
|
+
request.url path.to_s
|
31
|
+
request.body = params
|
32
|
+
end
|
33
|
+
end
|
34
|
+
Result.new_from_response(response)
|
35
|
+
end
|
36
|
+
|
37
|
+
def new_or_collection_from_result(result)
|
38
|
+
if result.data.is_a?(Array)
|
39
|
+
new_collection_from_result(result)
|
40
|
+
else
|
41
|
+
new_from_result(result)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def new_from_result(result)
|
46
|
+
new result.data if result.data
|
47
|
+
end
|
48
|
+
|
49
|
+
def new_collection_from_result(result)
|
50
|
+
Collection.new Array(result.data).map { |record| new(record) }, result.metadata
|
51
|
+
end
|
52
|
+
|
53
|
+
def uri(uri_template = "/#{model_name.plural}/:id")
|
54
|
+
@uri ||= uri_template
|
55
|
+
end
|
56
|
+
|
57
|
+
def connection
|
58
|
+
Config.connection
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
METHODS.each do |method|
|
63
|
+
define_method(method) do |action = nil, params = {}|
|
64
|
+
params = action if action.is_a?(Hash)
|
65
|
+
path = case action
|
66
|
+
when Symbol then uri.join(action)
|
67
|
+
when String then Path.new(action, attributes)
|
68
|
+
else uri
|
69
|
+
end
|
70
|
+
self.attributes = self.class.send("#{method}_raw", path, params).data
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def uri
|
75
|
+
Path.new(@uri_template, attributes)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/spyke/orm.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
module Spyke
|
2
|
+
module Orm
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
define_model_callbacks :create, :update, :save
|
7
|
+
|
8
|
+
class_attribute :include_root
|
9
|
+
self.include_root = true
|
10
|
+
|
11
|
+
class_attribute :callback_methods
|
12
|
+
self.callback_methods = { create: :post, update: :put }.freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def include_root_in_json(value)
|
17
|
+
self.include_root = value
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_for(callback, value = nil)
|
21
|
+
self.callback_methods = callback_methods.merge(callback => value) if value
|
22
|
+
callback_methods[callback]
|
23
|
+
end
|
24
|
+
|
25
|
+
def find(id)
|
26
|
+
id = strip_slug(id)
|
27
|
+
where(id: id).find_one || raise(ResourceNotFound)
|
28
|
+
end
|
29
|
+
|
30
|
+
def fetch
|
31
|
+
uri = new.uri
|
32
|
+
get_raw uri, current_scope.params.except(*uri.variables)
|
33
|
+
end
|
34
|
+
|
35
|
+
def create(attributes = {})
|
36
|
+
record = new(attributes)
|
37
|
+
record.save
|
38
|
+
record
|
39
|
+
end
|
40
|
+
|
41
|
+
def destroy(id = nil)
|
42
|
+
new(id: id).destroy
|
43
|
+
end
|
44
|
+
|
45
|
+
def build(attributes = {})
|
46
|
+
new(attributes)
|
47
|
+
end
|
48
|
+
|
49
|
+
def strip_slug(id)
|
50
|
+
id.to_s.split('-').first
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_params
|
55
|
+
if include_root?
|
56
|
+
{ self.class.model_name.param_key => attributes.except(*uri.variables) }
|
57
|
+
else
|
58
|
+
attributes.except(*uri.variables)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def persisted?
|
63
|
+
id?
|
64
|
+
end
|
65
|
+
|
66
|
+
def save
|
67
|
+
run_callbacks :save do
|
68
|
+
callback = persisted? ? :update : :create
|
69
|
+
run_callbacks(callback) do
|
70
|
+
send self.class.method_for(callback), to_params
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def destroy
|
76
|
+
delete
|
77
|
+
end
|
78
|
+
|
79
|
+
def reload
|
80
|
+
self.attributes = self.class.find(id).attributes
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/spyke/path.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'uri_template'
|
2
|
+
|
3
|
+
module Spyke
|
4
|
+
class Path
|
5
|
+
def initialize(uri_template, params = {})
|
6
|
+
@uri_template = URITemplate.new(:colon, uri_template)
|
7
|
+
@params = params
|
8
|
+
end
|
9
|
+
|
10
|
+
def join(other_path)
|
11
|
+
self.class.new File.join(path, other_path.to_s), @params
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
path
|
16
|
+
end
|
17
|
+
|
18
|
+
def variables
|
19
|
+
@variables ||= @uri_template.variables.map(&:to_sym)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def path
|
25
|
+
@uri_template.expand(@params).chomp('/')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spyke/exceptions'
|
2
|
+
|
3
|
+
module Spyke
|
4
|
+
class Relation
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :klass, :params
|
8
|
+
delegate :to_ary, :[], :empty?, :last, :size, :metadata, to: :find_some
|
9
|
+
|
10
|
+
def initialize(klass, options = {})
|
11
|
+
@klass, @options, @params = klass, options, {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def all
|
15
|
+
where
|
16
|
+
end
|
17
|
+
|
18
|
+
def where(conditions = {})
|
19
|
+
@params.merge!(conditions)
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_one
|
24
|
+
@find_one ||= klass.new_from_result(fetch)
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_some
|
28
|
+
@find_some ||= klass.new_collection_from_result(fetch)
|
29
|
+
end
|
30
|
+
|
31
|
+
def each
|
32
|
+
find_some.each { |record| yield record }
|
33
|
+
end
|
34
|
+
|
35
|
+
def uri
|
36
|
+
@options[:uri]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def method_missing(name, *args, &block)
|
42
|
+
if klass.respond_to?(name)
|
43
|
+
scoping { klass.send(name, *args) }
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Keep hold of current scope while running a method on the class
|
50
|
+
def scoping
|
51
|
+
klass.current_scope = self
|
52
|
+
yield
|
53
|
+
ensure
|
54
|
+
klass.current_scope = nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/spyke/result.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Spyke
|
2
|
+
class Result
|
3
|
+
attr_reader :body
|
4
|
+
|
5
|
+
def self.new_from_response(response)
|
6
|
+
new(response.body)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(body)
|
10
|
+
@body = HashWithIndifferentAccess.new(body)
|
11
|
+
end
|
12
|
+
|
13
|
+
def data
|
14
|
+
body[:data]
|
15
|
+
end
|
16
|
+
|
17
|
+
def metadata
|
18
|
+
body[:metadata] || {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def errors
|
22
|
+
body[:errors]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Spyke
|
2
|
+
class ScopeRegistry
|
3
|
+
extend ActiveSupport::PerThreadRegistry
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@registry = Hash.new { |hash, key| hash[key] = {} }
|
7
|
+
end
|
8
|
+
|
9
|
+
def value_for(scope_type, variable_name)
|
10
|
+
@registry[scope_type][variable_name]
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_value_for(scope_type, variable_name, value)
|
14
|
+
@registry[scope_type][variable_name] = value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/spyke/scopes.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spyke/relation'
|
2
|
+
require 'spyke/scope_registry'
|
3
|
+
|
4
|
+
module Spyke
|
5
|
+
module Scopes
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
delegate :all, :where, to: :current_scope
|
10
|
+
|
11
|
+
def scope(name, code)
|
12
|
+
self.class.send :define_method, name, code
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_scope=(scope)
|
16
|
+
ScopeRegistry.set_value_for(:current_scope, name, scope)
|
17
|
+
end
|
18
|
+
|
19
|
+
def current_scope
|
20
|
+
ScopeRegistry.value_for(:current_scope, name) || Relation.new(self, uri: uri)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def current_scope
|
27
|
+
self.class.current_scope
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spyke.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'spyke/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "spyke"
|
8
|
+
spec.version = Spyke::VERSION
|
9
|
+
spec.authors = ["Jens Balvig"]
|
10
|
+
spec.email = ["jens@balvig.com"]
|
11
|
+
spec.summary = %q{Interact with REST services in an ActiveRecord-like manner}
|
12
|
+
spec.description = %q{Interact with REST services in an ActiveRecord-like manner}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activesupport"
|
22
|
+
spec.add_dependency "activemodel"
|
23
|
+
spec.add_dependency "faraday"
|
24
|
+
spec.add_dependency "uri_template"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
27
|
+
spec.add_development_dependency "faraday_middleware"
|
28
|
+
spec.add_development_dependency "minitest"
|
29
|
+
spec.add_development_dependency "minitest-line"
|
30
|
+
spec.add_development_dependency "minitest-reporters"
|
31
|
+
spec.add_development_dependency "mocha"
|
32
|
+
spec.add_development_dependency "multi_json"
|
33
|
+
spec.add_development_dependency "pry"
|
34
|
+
spec.add_development_dependency "rake"
|
35
|
+
spec.add_development_dependency "webmock"
|
36
|
+
end
|
@@ -0,0 +1,282 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Spyke
|
4
|
+
class AssociationsTest < MiniTest::Test
|
5
|
+
def test_association_independence
|
6
|
+
assert_kind_of Associations::HasMany, Recipe.new.groups
|
7
|
+
assert_raises NoMethodError do
|
8
|
+
Recipe.new.recipes
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_initializing_with_has_many_association
|
13
|
+
group = Group.new(ingredients: [Ingredient.new(title: 'Water'), Ingredient.new(title: 'Flour')])
|
14
|
+
assert_equal %w{ Water Flour }, group.ingredients.map(&:title)
|
15
|
+
assert_equal({ 'group' => { 'ingredients' => [{ 'title' => 'Water' }, { 'title' => 'Flour' }] } }, group.to_params)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_initializing_with_has_one_association
|
19
|
+
recipe = Recipe.new(image: Image.new(url: 'bob.jpg'))
|
20
|
+
assert_equal 'bob.jpg', recipe.image.url
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_initializing_with_blank_has_one_association
|
24
|
+
recipe = Recipe.new(image: Image.new)
|
25
|
+
assert_kind_of Image, recipe.image
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_embedded_associations
|
29
|
+
stub_request(:get, 'http://sushi.com/recipes/1').to_return_json(data: { groups: [{ id: 1, name: 'Fish' }] })
|
30
|
+
|
31
|
+
recipe = Recipe.find(1)
|
32
|
+
|
33
|
+
assert_equal %w{ Fish }, recipe.groups.map(&:name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_nested_embedded_associtations
|
37
|
+
json = { data: { groups: [{ ingredients: [{ id: 1, name: 'Fish' }] }, { ingredients: [] }] } }
|
38
|
+
stub_request(:get, 'http://sushi.com/recipes/1').to_return_json(json)
|
39
|
+
|
40
|
+
recipe = Recipe.find(1)
|
41
|
+
|
42
|
+
assert_equal %w{ Fish }, recipe.ingredients.map(&:name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_singular_associtations
|
46
|
+
stub_request(:get, 'http://sushi.com/recipes/1').to_return_json(data: { image: { url: 'bob.jpg' } })
|
47
|
+
|
48
|
+
recipe = Recipe.find(1)
|
49
|
+
|
50
|
+
assert_equal 'bob.jpg', recipe.image.url
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_unloaded_has_many_association
|
54
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes/1/groups?public=true').to_return_json(data: [{ id: 1 }])
|
55
|
+
|
56
|
+
groups = Recipe.new(id: 1).groups.where(public: true).to_a
|
57
|
+
|
58
|
+
assert_equal 1, groups.first.id
|
59
|
+
assert_requested endpoint
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_unloaded_has_one_association
|
63
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes/1/image').to_return_json(data: { url: 'bob.jpg' })
|
64
|
+
|
65
|
+
image = Recipe.new(id: 1).image
|
66
|
+
|
67
|
+
assert_equal 'bob.jpg', image.url
|
68
|
+
assert_requested endpoint
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_array_like_behavior
|
72
|
+
stub_request(:get, 'http://sushi.com/recipes/1/groups').to_return_json(data: [{ name: 'Fish' }, { name: 'Fruit' }, { name: 'Bread' }])
|
73
|
+
|
74
|
+
recipe = Recipe.new(id: 1)
|
75
|
+
|
76
|
+
assert_equal %w{ Fish Fruit }, recipe.groups[0..1].map(&:name)
|
77
|
+
assert_equal 'Bread', recipe.groups.last.name
|
78
|
+
assert_equal 'Fish', recipe.groups.first.name
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_nil_has_one_association
|
82
|
+
stub_request(:get, 'http://sushi.com/recipes/1/image')
|
83
|
+
|
84
|
+
image = Recipe.new(id: 1).image
|
85
|
+
|
86
|
+
assert_nil image
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_unloaded_belongs_to_association
|
90
|
+
endpoint = stub_request(:get, 'http://sushi.com/users/1')
|
91
|
+
|
92
|
+
recipe = Recipe.new(user_id: 1)
|
93
|
+
recipe.user
|
94
|
+
|
95
|
+
assert_requested endpoint
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_scopes_on_assocations
|
99
|
+
endpoint = stub_request(:get, 'http://sushi.com/users/1/recipes?page=2')
|
100
|
+
|
101
|
+
User.new(id: 1).recipes.page(2).to_a
|
102
|
+
|
103
|
+
assert_requested endpoint
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_build_has_many_association
|
107
|
+
recipe = Recipe.new(id: 1)
|
108
|
+
recipe.groups.build
|
109
|
+
assert_equal 1, recipe.groups.first.recipe_id
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_new_has_many_association
|
113
|
+
recipe = Recipe.new(id: 1)
|
114
|
+
recipe.groups.new
|
115
|
+
assert_equal 1, recipe.groups.first.recipe_id
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_deep_build_has_many_association
|
119
|
+
recipe = Recipe.new(id: 1)
|
120
|
+
recipe.groups.build(ingredients: [Ingredient.new(name: 'Salt')])
|
121
|
+
|
122
|
+
assert_equal %w{ Salt }, recipe.ingredients.map(&:name)
|
123
|
+
assert_equal({ 'recipe' => { 'groups' => [{ 'recipe_id' => 1, 'ingredients' => [{ 'name' => 'Salt' }] }] } }, recipe.to_params)
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_deep_build_has_many_association_with_scope
|
127
|
+
recipe = User.new(id: 1).recipes.published.build
|
128
|
+
|
129
|
+
assert_equal({ 'recipe' => { 'status' => 'published' } }, recipe.to_params)
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_build_association_with_ids
|
133
|
+
user = User.new(id: 1, recipes: [{ id: 1 }])
|
134
|
+
user.recipe_ids = ['', 2]
|
135
|
+
|
136
|
+
assert_equal [2], user.recipes.map(&:id)
|
137
|
+
assert_equal({ 'user' => { 'recipes' => [{ 'user_id' => 1, 'id' => 2 }] } }, user.to_params)
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_converting_association_to_ids
|
141
|
+
stub_request(:get, 'http://sushi.com/users/1/recipes').to_return_json(data: [{ id: 2 }])
|
142
|
+
user = User.new(id: 1)
|
143
|
+
assert_equal [2], user.recipe_ids
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_build_has_one_association
|
147
|
+
recipe = Recipe.new(id: 1)
|
148
|
+
image = recipe.build_image
|
149
|
+
assert_equal 1, image.recipe_id
|
150
|
+
assert_equal 1, recipe.image.recipe_id
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_custom_class_name
|
154
|
+
stub_request(:get, 'http://sushi.com/recipes/1').to_return_json(data: { background_image: { url: 'bob.jpg' } })
|
155
|
+
|
156
|
+
recipe = Recipe.find(1)
|
157
|
+
|
158
|
+
assert_equal 'bob.jpg', recipe.background_image.url
|
159
|
+
assert_equal Image, recipe.background_image.class
|
160
|
+
end
|
161
|
+
|
162
|
+
def test_cached_result
|
163
|
+
endpoint_1 = stub_request(:get, 'http://sushi.com/recipes/1/groups?per_page=3')
|
164
|
+
endpoint_2 = stub_request(:get, 'http://sushi.com/recipes/1/groups')
|
165
|
+
|
166
|
+
recipe = Recipe.new(id: 1)
|
167
|
+
groups = recipe.groups.where(per_page: 3)
|
168
|
+
groups.any?
|
169
|
+
groups.to_a
|
170
|
+
assert_requested endpoint_1, times: 1
|
171
|
+
|
172
|
+
recipe.groups.to_a
|
173
|
+
assert_requested endpoint_2, times: 1
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_custom_uri_template
|
177
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes/1/alternates/recipe')
|
178
|
+
Recipe.new(id: 1).alternate
|
179
|
+
assert_requested endpoint
|
180
|
+
end
|
181
|
+
|
182
|
+
def test_create_association
|
183
|
+
endpoint = stub_request(:post, 'http://sushi.com/recipes/1/groups').with(body: { group: { title: 'Topping' } }).to_return_json(data: { title: 'Topping', id: 1, recipe_id: 1 })
|
184
|
+
|
185
|
+
recipe = Recipe.new(id: 1)
|
186
|
+
group = recipe.groups.create(title: 'Topping')
|
187
|
+
|
188
|
+
assert_equal 'Topping', group.title
|
189
|
+
assert_equal 1, group.id
|
190
|
+
assert_equal 'Topping', recipe.groups.last.title
|
191
|
+
assert_equal 1, recipe.groups.last.id
|
192
|
+
assert_requested endpoint
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_create_association_with_no_params
|
196
|
+
endpoint = stub_request(:post, 'http://sushi.com/recipes/1/groups').with(body: { group: {} })
|
197
|
+
|
198
|
+
Recipe.new(id: 1).groups.create
|
199
|
+
|
200
|
+
assert_requested endpoint
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_create_scoped_association
|
204
|
+
endpoint = stub_request(:post, 'http://sushi.com/users/1/recipes').with(body: { recipe: { title: 'Sushi', status: 'published' } })
|
205
|
+
|
206
|
+
User.new(id: 1).recipes.published.create(title: 'Sushi')
|
207
|
+
|
208
|
+
assert_requested endpoint
|
209
|
+
end
|
210
|
+
|
211
|
+
def test_save_association
|
212
|
+
endpoint = stub_request(:post, 'http://sushi.com/recipes/1/groups').with(body: { group: { title: 'Topping' } }).to_return_json(data: { title: 'Topping', recipe_id: 1 })
|
213
|
+
|
214
|
+
group = Recipe.new(id: 1).groups.build(title: 'Topping')
|
215
|
+
group.save
|
216
|
+
|
217
|
+
assert_equal 'Topping', group.title
|
218
|
+
assert_requested endpoint
|
219
|
+
end
|
220
|
+
|
221
|
+
def test_nested_attributes_has_one
|
222
|
+
recipe = Recipe.new(image_attributes: { file: 'bob.jpg' })
|
223
|
+
assert_equal 'bob.jpg', recipe.image.file
|
224
|
+
end
|
225
|
+
|
226
|
+
def test_nested_attributes_belongs_to
|
227
|
+
recipe = Recipe.new(user_attributes: { name: 'Bob' })
|
228
|
+
assert_equal 'Bob', recipe.user.name
|
229
|
+
end
|
230
|
+
|
231
|
+
def test_nested_attributes_has_many
|
232
|
+
recipe = Recipe.new(groups_attributes: [{ title: 'starter' }, { title: 'sauce' }])
|
233
|
+
assert_equal %w{ starter sauce }, recipe.groups.map(&:title)
|
234
|
+
end
|
235
|
+
|
236
|
+
def test_nested_attributes_overwriting_existing
|
237
|
+
recipe = Recipe.new(groups_attributes: [{ title: 'starter' }, { title: 'sauce' }])
|
238
|
+
recipe.attributes = { groups_attributes: [{ title: 'flavor' }] }
|
239
|
+
assert_equal %w{ starter sauce flavor }, recipe.groups.map(&:title)
|
240
|
+
end
|
241
|
+
|
242
|
+
def test_nested_attributes_merging_with_existing
|
243
|
+
recipe = Recipe.new(groups_attributes: [{ id: 1, title: 'starter', description: 'nice' }, { id: 2, title: 'sauce', description: 'spicy' }])
|
244
|
+
recipe.attributes = { groups_attributes: [{ 'id' => '2', 'title' => 'flavor' }] }
|
245
|
+
assert_equal %w{ starter flavor }, recipe.groups.map(&:title)
|
246
|
+
assert_equal %w{ nice spicy }, recipe.groups.map(&:description)
|
247
|
+
end
|
248
|
+
|
249
|
+
def test_deeply_nested_attributes
|
250
|
+
params = { groups_attributes: [{ id: 1, ingredient_attributes: [{ id: 1, title: 'fish' }]}] }
|
251
|
+
recipe = Recipe.new(params)
|
252
|
+
recipe.attributes = params
|
253
|
+
assert_equal 1, recipe.groups.size
|
254
|
+
end
|
255
|
+
|
256
|
+
def test_nested_attributes_has_many_using_hash_syntax
|
257
|
+
recipe = Recipe.new(groups_attributes: { '0' => { title: 'starter' }, '1' => { title: 'sauce' } })
|
258
|
+
assert_equal %w{ starter sauce }, recipe.groups.map(&:title)
|
259
|
+
end
|
260
|
+
|
261
|
+
def test_nested_nested_attributes
|
262
|
+
recipe = Recipe.new(groups_attributes: { '0' => { ingredients_attributes: { '0' => { name: 'Salt' } } } })
|
263
|
+
assert_equal %w{ Salt }, recipe.ingredients.map(&:name)
|
264
|
+
end
|
265
|
+
|
266
|
+
def test_reflect_on_association
|
267
|
+
assert_equal Group, Recipe.reflect_on_association(:group).klass
|
268
|
+
skip 'wishlisted'
|
269
|
+
assert_equal Recipe, Recipe.reflect_on_association(:alternate).klass
|
270
|
+
end
|
271
|
+
|
272
|
+
def test_embed_only_singular_associations
|
273
|
+
assert_nil Recipe.new.background_image
|
274
|
+
assert_equal 'photo.jpg', Recipe.new(background_image: { url: 'photo.jpg' }).background_image.url
|
275
|
+
end
|
276
|
+
|
277
|
+
def test_embed_only_plural_associations
|
278
|
+
assert_equal [], Group.new.ingredients.to_a
|
279
|
+
assert_equal [1], Group.new(ingredients: [{ id: 1 }]).ingredients.map(&:id)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|