spyke 1.0.0
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.
- 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
|