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
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Spyke
|
4
|
+
class AttributesTest < MiniTest::Test
|
5
|
+
def test_predicate_methods
|
6
|
+
stub_request(:get, 'http://sushi.com/recipes/1').to_return_json(data: { id: 1, title: 'Sushi' })
|
7
|
+
|
8
|
+
recipe = Recipe.find(1)
|
9
|
+
|
10
|
+
assert_equal true, recipe.title?
|
11
|
+
assert_equal false, recipe.description?
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_respond_to
|
15
|
+
stub_request(:get, 'http://sushi.com/recipes/1').to_return_json(data: { id: 1, serves: 3 })
|
16
|
+
|
17
|
+
recipe = Recipe.find(1)
|
18
|
+
|
19
|
+
assert_equal true, recipe.respond_to?(:serves)
|
20
|
+
assert_equal false, recipe.respond_to?(:story)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_assigning_attributes
|
24
|
+
recipe = Recipe.new(id: 2)
|
25
|
+
recipe.attributes = { title: 'Pasta' }
|
26
|
+
|
27
|
+
assert_equal 'Pasta', recipe.title
|
28
|
+
assert_equal 2, recipe.id
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_removing_id_if_blank_string
|
32
|
+
recipe = Recipe.new
|
33
|
+
|
34
|
+
assert_nil recipe.id
|
35
|
+
assert_equal({ 'recipe' => {} }, recipe.to_params)
|
36
|
+
|
37
|
+
recipe.id = ''
|
38
|
+
assert_nil recipe.id
|
39
|
+
assert_equal({ 'recipe' => {} }, recipe.to_params)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_setters
|
43
|
+
recipe = Recipe.new
|
44
|
+
recipe.title = 'Sushi'
|
45
|
+
assert_equal 'Sushi', recipe.title
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_equality
|
49
|
+
assert_equal Recipe.new(id: 2, title: 'Fish'), Recipe.new(id: 2, title: 'Fish')
|
50
|
+
refute_equal Recipe.new(id: 2, title: 'Fish'), Recipe.new(id: 1, title: 'Fish')
|
51
|
+
refute_equal Recipe.new(id: 2, title: 'Fish'), 'not_a_spyke_object'
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_explicit_attributes
|
55
|
+
recipe = Recipe.new
|
56
|
+
assert_equal nil, recipe.title
|
57
|
+
assert_raises NoMethodError do
|
58
|
+
recipe.description
|
59
|
+
end
|
60
|
+
|
61
|
+
recipe = Recipe.new(title: 'Fish')
|
62
|
+
assert_equal 'Fish', recipe.title
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_converting_files_to_faraday_io
|
66
|
+
Faraday::UploadIO.stubs(:new).with('/photo.jpg', 'image/jpeg').returns('UploadIO')
|
67
|
+
file = mock
|
68
|
+
file.stubs(:path).returns('/photo.jpg')
|
69
|
+
file.stubs(:content_type).returns('image/jpeg')
|
70
|
+
|
71
|
+
recipe = Recipe.new(image: Image.new(file: file))
|
72
|
+
|
73
|
+
assert_equal 'UploadIO', recipe.image.to_params['image']['file']
|
74
|
+
assert_equal 'UploadIO', recipe.to_params['recipe']['image']['file']
|
75
|
+
|
76
|
+
recipe = Recipe.new(image_attributes: { file: file })
|
77
|
+
|
78
|
+
assert_equal 'UploadIO', recipe.image.to_params['image']['file']
|
79
|
+
assert_equal 'UploadIO', recipe.to_params['recipe']['image']['file']
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_inspect
|
83
|
+
recipe = Recipe.new(id: 2, title: 'Pizza', description: 'Delicious')
|
84
|
+
assert_equal '#<Recipe(/recipes/2) id: 2 title: "Pizza" description: "Delicious">', recipe.inspect
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_rejecting_wrong_number_of_args
|
88
|
+
skip 'wishlisted'
|
89
|
+
stub_request(:any, /.*/)
|
90
|
+
recipe = Recipe.new(description: 'Delicious')
|
91
|
+
assert_raises ArgumentError do
|
92
|
+
recipe.description(2)
|
93
|
+
end
|
94
|
+
assert_raises ArgumentError do
|
95
|
+
recipe.description?(2)
|
96
|
+
end
|
97
|
+
assert_raises ArgumentError do
|
98
|
+
recipe.image(2)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Spyke
|
4
|
+
class CallbacksTest < MiniTest::Test
|
5
|
+
def setup
|
6
|
+
stub_request(:any, /.*/)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_before_save
|
10
|
+
Recipe.any_instance.expects(:before_save_callback)
|
11
|
+
Recipe.create
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_before_create
|
15
|
+
Recipe.any_instance.expects(:before_create_callback)
|
16
|
+
Recipe.create
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_before_update
|
20
|
+
Recipe.any_instance.expects(:before_update_callback)
|
21
|
+
Recipe.new(id: 1).save
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Spyke
|
4
|
+
class CustomRequestTest < MiniTest::Test
|
5
|
+
def test_custom_get_request_from_class
|
6
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes/recent').to_return_json(data: [{ id: 1, title: 'Bread' }])
|
7
|
+
assert_equal %w{ Bread }, Recipe.get('/recipes/recent').map(&:title)
|
8
|
+
assert_requested endpoint
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_get_request_with_prepended_scope
|
12
|
+
skip 'wishlisted'
|
13
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes/recent?status=published')
|
14
|
+
Recipe.published.get('/recipes/recent')
|
15
|
+
assert_requested endpoint
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_get_request_with_appended_scope
|
19
|
+
skip 'wishlisted'
|
20
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes/recent?status=published')
|
21
|
+
Recipe.get('/recipes/recent').published.fetch
|
22
|
+
assert_requested endpoint
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_custom_get_request_from_class
|
26
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes/recent').to_return_json(data: [{ id: 1, title: 'Bread' }])
|
27
|
+
assert_equal %w{ Bread }, Recipe.get('/recipes/recent').map(&:title)
|
28
|
+
assert_requested endpoint
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_custom_put_request_from_class
|
32
|
+
endpoint = stub_request(:put, 'http://sushi.com/recipes/1/publish')
|
33
|
+
Recipe.put('/recipes/1/publish')
|
34
|
+
assert_requested endpoint
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_custom_put_request_from_instance
|
38
|
+
endpoint = stub_request(:put, 'http://sushi.com/recipes/1/publish').to_return_json(data: { id: 1, status: 'published' })
|
39
|
+
recipe = Recipe.new(id: 1, status: 'unpublished')
|
40
|
+
recipe.put('/recipes/:id/publish')
|
41
|
+
|
42
|
+
assert_equal 'published', recipe.status
|
43
|
+
assert_requested endpoint
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_custom_put_request_from_instance_with_symbol
|
47
|
+
endpoint = stub_request(:put, 'http://sushi.com/recipes/1/draft')
|
48
|
+
recipe = Recipe.new(id: 1)
|
49
|
+
recipe.put(:draft)
|
50
|
+
assert_requested endpoint
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/test/orm_test.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Spyke
|
4
|
+
class OrmTest < MiniTest::Test
|
5
|
+
def test_find
|
6
|
+
stub_request(:get, 'http://sushi.com/recipes/1').to_return_json(data: { id: 1, title: 'Sushi' })
|
7
|
+
stub_request(:get, 'http://sushi.com/users/1').to_return_json(data: { id: 1, name: 'Bob' })
|
8
|
+
|
9
|
+
recipe = Recipe.find(1)
|
10
|
+
user = User.find(1)
|
11
|
+
|
12
|
+
assert_equal 1, recipe.id
|
13
|
+
assert_equal 'Sushi', recipe.title
|
14
|
+
assert_equal 'Bob', user.name
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_reload
|
18
|
+
stub_request(:get, 'http://sushi.com/recipes/1').to_return_json(data: { id: 1, title: 'Sushi' })
|
19
|
+
|
20
|
+
recipe = Recipe.find(1)
|
21
|
+
assert_equal 'Sushi', recipe.title
|
22
|
+
|
23
|
+
stub_request(:get, 'http://sushi.com/recipes/1').to_return_json(data: { id: 1, title: 'Sashimi' })
|
24
|
+
recipe.reload
|
25
|
+
assert_equal 'Sashimi', recipe.title
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_find_with_slug
|
29
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes/1').to_return_json(data: { id: 1 })
|
30
|
+
Recipe.find('1-delicious-soup')
|
31
|
+
assert_requested endpoint
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_404
|
35
|
+
stub_request(:get, 'http://sushi.com/recipes/1').to_return(status: 404, body: { message: 'Not found' }.to_json)
|
36
|
+
|
37
|
+
assert_raises ResourceNotFound do
|
38
|
+
Recipe.find(1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_save_new_record
|
43
|
+
endpoint = stub_request(:post, 'http://sushi.com/recipes').with(body: { recipe: { title: 'Sushi' } }).to_return_json(data: { id: 1, title: 'Sushi (created)' })
|
44
|
+
|
45
|
+
recipe = Recipe.new(title: 'Sushi')
|
46
|
+
recipe.save
|
47
|
+
|
48
|
+
assert_equal 'Sushi (created)', recipe.title
|
49
|
+
assert_requested endpoint
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_save_persisted_record
|
53
|
+
stub_request(:put, /.*/)
|
54
|
+
endpoint = stub_request(:put, 'http://sushi.com/recipes/1').with(body: { recipe: { title: 'Sushi' } }).to_return_json(data: { id: 1, title: 'Sushi (saved)' })
|
55
|
+
|
56
|
+
recipe = Recipe.new(id: 1, title: 'Sashimi')
|
57
|
+
recipe.title = 'Sushi'
|
58
|
+
recipe.save
|
59
|
+
|
60
|
+
assert_equal 'Sushi (saved)', recipe.title
|
61
|
+
assert_requested endpoint
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_create
|
65
|
+
endpoint = stub_request(:post, 'http://sushi.com/recipes').with(body: { recipe: { title: 'Sushi' } }).to_return_json(data: { id: 1, title: 'Sushi' })
|
66
|
+
|
67
|
+
recipe = Recipe.create(title: 'Sushi')
|
68
|
+
|
69
|
+
assert_equal 'Sushi', recipe.title
|
70
|
+
assert_requested endpoint
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_find_using_custom_uri_template
|
74
|
+
endpoint = stub_request(:get, 'http://sushi.com/images/photos/1').to_return_json(data: { id: 1 })
|
75
|
+
Photo.find(1)
|
76
|
+
assert_requested endpoint
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_create_using_custom_uri_template
|
80
|
+
endpoint = stub_request(:post, 'http://sushi.com/images/photos')
|
81
|
+
Photo.create
|
82
|
+
assert_requested endpoint
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_create_using_nested_custom_uri_template
|
86
|
+
endpoint = stub_request(:post, 'http://sushi.com/recipes/1/ingredients')
|
87
|
+
Ingredient.new(recipe_id: 1).save
|
88
|
+
assert_requested endpoint
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_create_using_custom_method
|
92
|
+
endpoint = stub_request(:put, 'http://sushi.com/images')
|
93
|
+
Image.create
|
94
|
+
assert_requested endpoint
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_inheritance_using_custom_method
|
98
|
+
endpoint = stub_request(:put, 'http://sushi.com/step_images')
|
99
|
+
StepImage.create
|
100
|
+
assert_requested endpoint
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_non_nested_params
|
104
|
+
assert_equal({ 'url' => 'bob.jpg' }, RecipeImage.new(url: 'bob.jpg').to_params)
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_destroy
|
108
|
+
endpoint = stub_request(:delete, 'http://sushi.com/recipes/1')
|
109
|
+
Recipe.new(id: 1).destroy
|
110
|
+
assert_requested endpoint
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_destroy_class_method
|
114
|
+
endpoint = stub_request(:delete, 'http://sushi.com/recipes/1')
|
115
|
+
Recipe.destroy(1)
|
116
|
+
assert_requested endpoint
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_scoped_destroy_class_method
|
120
|
+
endpoint = stub_request(:delete, 'http://sushi.com/recipes/1/ingredients/2')
|
121
|
+
Ingredient.where(recipe_id: 1).destroy(2)
|
122
|
+
assert_requested endpoint
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_scoped_destroy_class_method_without_param
|
126
|
+
endpoint = stub_request(:delete, 'http://sushi.com/recipes/1/image')
|
127
|
+
RecipeImage.where(recipe_id: 1).destroy
|
128
|
+
assert_requested endpoint
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_validations
|
132
|
+
assert_equal false, RecipeImage.new.valid?
|
133
|
+
assert_equal true, RecipeImage.new(url: 'bob.jpg').valid?
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/test/path_test.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Spyke
|
4
|
+
class PathTest < MiniTest::Test
|
5
|
+
def test_collection_path
|
6
|
+
assert_equal '/recipes', Path.new('/recipes/:id').to_s
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_resource_path
|
10
|
+
assert_equal '/recipes/2', Path.new('/recipes/:id', id: 2).to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_nested_collection_path
|
14
|
+
path = Path.new('/users/:user_id/recipes/:id', user_id: 1, status: 'published')
|
15
|
+
assert_equal [:user_id, :id], path.variables
|
16
|
+
assert_equal '/users/1/recipes', path.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_nested_resource_path
|
20
|
+
assert_equal '/users/1/recipes/2', Path.new('/users/:user_id/recipes/:id', user_id: 1, id: 2).to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Spyke
|
4
|
+
class RelationTest < MiniTest::Test
|
5
|
+
def test_all
|
6
|
+
stub_request(:get, 'http://sushi.com/recipes').to_return_json(data: [{ id: 1, title: 'Sushi' }, { id: 2, title: 'Nigiri' }], metadata: 'meta')
|
7
|
+
|
8
|
+
recipes = Recipe.all
|
9
|
+
|
10
|
+
assert_equal %w{ Sushi Nigiri }, recipes.map(&:title)
|
11
|
+
assert_equal [1, 2], recipes.map(&:id)
|
12
|
+
assert_equal 'meta', recipes.metadata
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_chainable_where
|
16
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes?status=published&per_page=3')
|
17
|
+
|
18
|
+
Recipe.where(status: 'published').where(per_page: 3).to_a
|
19
|
+
|
20
|
+
assert_requested endpoint
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_chainable_class_method
|
24
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes?status=published&per_page=3')
|
25
|
+
|
26
|
+
Recipe.where(per_page: 3).published.to_a
|
27
|
+
|
28
|
+
assert_requested endpoint
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_prepended_chainable_class_method
|
32
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes?status=published&per_page=3')
|
33
|
+
|
34
|
+
Recipe.published.where(per_page: 3).to_a
|
35
|
+
|
36
|
+
assert_requested endpoint
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_scope_class_method
|
40
|
+
endpoint = stub_request(:get, 'http://sushi.com/recipes?status=published&page=3')
|
41
|
+
|
42
|
+
Recipe.published.page(3).to_a
|
43
|
+
assert_requested endpoint
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_scope_doesnt_get_stuck
|
47
|
+
endpoint_1 = stub_request(:get, 'http://sushi.com/recipes?per_page=3&status=published')
|
48
|
+
endpoint_2 = stub_request(:get, 'http://sushi.com/recipes?status=published')
|
49
|
+
|
50
|
+
Recipe.where(status: 'published').where(per_page: 3).to_a
|
51
|
+
Recipe.where(status: 'published').to_a
|
52
|
+
assert_requested endpoint_1
|
53
|
+
assert_requested endpoint_2
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_create_scoped
|
57
|
+
endpoint = stub_request(:post, 'http://sushi.com/recipes').with(body: { recipe: { title: 'Sushi', status: 'published' } })
|
58
|
+
|
59
|
+
Recipe.published.create(title: 'Sushi')
|
60
|
+
|
61
|
+
assert_requested endpoint
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_cached_result
|
65
|
+
endpoint_1 = stub_request(:get, 'http://sushi.com/recipes?status=published&per_page=3')
|
66
|
+
endpoint_2 = stub_request(:get, 'http://sushi.com/recipes?status=published')
|
67
|
+
|
68
|
+
recipes = Recipe.published.where(per_page: 3)
|
69
|
+
recipes.any?
|
70
|
+
recipes.to_a
|
71
|
+
assert_requested endpoint_1, times: 1
|
72
|
+
Recipe.published.to_a
|
73
|
+
assert_requested endpoint_2, times: 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/test/support/api.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
class Recipe < Spyke::Base
|
2
|
+
has_many :groups
|
3
|
+
has_one :image
|
4
|
+
has_one :background_image, class_name: 'Image', uri: nil
|
5
|
+
has_one :alternate, class_name: 'Recipe', uri: '/recipes/:recipe_id/alternates/recipe'
|
6
|
+
belongs_to :user
|
7
|
+
|
8
|
+
scope :published, -> { where(status: 'published') }
|
9
|
+
attributes :title
|
10
|
+
|
11
|
+
before_save :before_save_callback
|
12
|
+
before_create :before_create_callback
|
13
|
+
before_update :before_update_callback
|
14
|
+
|
15
|
+
accepts_nested_attributes_for :image, :user, :groups
|
16
|
+
|
17
|
+
def self.page(number)
|
18
|
+
if number.present?
|
19
|
+
where(page: number)
|
20
|
+
else
|
21
|
+
all
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def ingredients
|
26
|
+
groups.first.ingredients
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def before_create_callback; end
|
32
|
+
def before_update_callback; end
|
33
|
+
def before_save_callback; end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Image < Spyke::Base
|
37
|
+
method_for :create, :put
|
38
|
+
end
|
39
|
+
|
40
|
+
class StepImage < Image
|
41
|
+
end
|
42
|
+
|
43
|
+
class RecipeImage < Image
|
44
|
+
uri '/recipes/:recipe_id/image'
|
45
|
+
validates :url, presence: true
|
46
|
+
attributes :url
|
47
|
+
include_root_in_json false
|
48
|
+
end
|
49
|
+
|
50
|
+
class Group < Spyke::Base
|
51
|
+
has_many :ingredients, uri: nil
|
52
|
+
accepts_nested_attributes_for :ingredients
|
53
|
+
end
|
54
|
+
|
55
|
+
class Ingredient < Spyke::Base
|
56
|
+
uri '/recipes/:recipe_id/ingredients/:id'
|
57
|
+
end
|
58
|
+
|
59
|
+
class User < Spyke::Base
|
60
|
+
has_many :recipes
|
61
|
+
end
|
62
|
+
|
63
|
+
class Photo < Spyke::Base
|
64
|
+
uri '/images/photos/:id'
|
65
|
+
end
|