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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 22e34d4a8e27855f8532e9f079990487350e0ecf
|
4
|
+
data.tar.gz: 93c623d72dd7699be86e9bfa8f3f0c09911a748f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a294370b263e09eb49836d5c338835388e9a8c5471633b4565afdf1c3c3c902b9f3a867d05fc356814e488638aee7b94b73eeee2767d9e8b8a4cbc06d78d757d
|
7
|
+
data.tar.gz: 9e8aa1d7d557d57877e0e87177ef75069e87988a059097015f73d97e631fa484e981112d5dcd4e9a00b6198374cd9dfbd90274042b9c61955cf633fae64ce8fc
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jens Balvig
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Spyke
|
2
|
+
|
3
|
+
Spyke allows you to interact with remote REST services in an ActiveRecord-like manner.
|
4
|
+
|
5
|
+
It basically ~~rips off~~ takes inspiration from :innocent: [Her](https://github.com/remiprev/her), a gem which we sadly had to abandon as it showed significant performance problems and maintenance seemed to had gone stale.
|
6
|
+
|
7
|
+
We therefore made Spyke which adds a few fixes/features that we needed for our projects:
|
8
|
+
|
9
|
+
- Fast handling of even large amounts of JSON
|
10
|
+
- Proper support for scopes
|
11
|
+
- Ability to define custom URIs for associations
|
12
|
+
- Googlable name! :)
|
13
|
+
|
14
|
+
## Configuration
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
gem 'spyke'
|
19
|
+
|
20
|
+
Like Her, Spyke uses Faraday to handle requests and expects a hash in the following format:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
{ result: { id: 1, name: 'Bob' }, metadata: {} }
|
24
|
+
```
|
25
|
+
|
26
|
+
The simplest possible configuration that can work is something like this:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
# config/initializers/spyke.rb
|
30
|
+
|
31
|
+
class JSONParser < Faraday::Response::Middleware
|
32
|
+
def parse(body)
|
33
|
+
json = MultiJson.load(body, symbolize_keys: true)
|
34
|
+
{
|
35
|
+
data: json[:result],
|
36
|
+
metadata: json[:metadata]
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
Spyke::Config.connection = Faraday.new(url: 'http://api.com') do |c|
|
42
|
+
c.use JSONParser
|
43
|
+
c.use Faraday::Adapter::NetHttp
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
## Usage
|
48
|
+
|
49
|
+
Adding a class and inheriting from `Spyke::Base` will allow you to interact with the remote service:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
class User < Spyke::Base
|
53
|
+
has_many :posts
|
54
|
+
end
|
55
|
+
|
56
|
+
user = User.find(3) # => GET http://api.com/users/3
|
57
|
+
user.posts # => find embedded in user or GET http://api.com/users/3/posts
|
58
|
+
```
|
59
|
+
|
60
|
+
### Custom URIs
|
61
|
+
|
62
|
+
You can specify custom URIs on both the class and association level:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
class User < Spyke::Base
|
66
|
+
uri '/v1/users/:id'
|
67
|
+
|
68
|
+
has_one :image, uri: nil
|
69
|
+
has_many :posts, uri: '/posts/for_user/:user_id'
|
70
|
+
end
|
71
|
+
|
72
|
+
class Post < Spyke::Base
|
73
|
+
end
|
74
|
+
|
75
|
+
user = User.find(3) # => GET http://api.com/v1/users/3
|
76
|
+
user.image # Will only use embedded JSON and never call out to api
|
77
|
+
user.posts # => GET http://api.com/posts/for_user/3
|
78
|
+
Post.find(4) # => GET http://api.com/posts/4
|
79
|
+
```
|
80
|
+
|
81
|
+
## Contributing
|
82
|
+
|
83
|
+
If possible please take a look at the tests marked "wishlisted"!
|
84
|
+
These are features/fixes we want to implement but haven't gotten around to doing yet :)
|
data/Rakefile
ADDED
data/lib/spyke.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spyke/associations/association'
|
2
|
+
require 'spyke/associations/has_many'
|
3
|
+
require 'spyke/associations/has_one'
|
4
|
+
require 'spyke/associations/belongs_to'
|
5
|
+
|
6
|
+
module Spyke
|
7
|
+
module Associations
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :associations
|
12
|
+
self.associations = {}.freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def has_many(name, options = {})
|
17
|
+
self.associations = associations.merge(name => options.merge(type: HasMany))
|
18
|
+
|
19
|
+
define_method "#{name.to_s.singularize}_ids=" do |ids|
|
20
|
+
attributes[name] = []
|
21
|
+
ids.reject(&:blank?).each { |id| association(name).build(id: id) }
|
22
|
+
end
|
23
|
+
|
24
|
+
define_method "#{name.to_s.singularize}_ids" do
|
25
|
+
association(name).map(&:id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_one(name, options = {})
|
30
|
+
self.associations = associations.merge(name => options.merge(type: HasOne))
|
31
|
+
define_method "build_#{name}" do |attributes = nil|
|
32
|
+
association(name).build(attributes)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def belongs_to(name, options = {})
|
37
|
+
self.associations = associations.merge(name => options.merge(type: BelongsTo))
|
38
|
+
end
|
39
|
+
|
40
|
+
def accepts_nested_attributes_for(*names)
|
41
|
+
names.each do |association_name|
|
42
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
43
|
+
def #{association_name}_attributes=(association_attributes)
|
44
|
+
association(:#{association_name}).assign_nested_attributes(association_attributes)
|
45
|
+
end
|
46
|
+
RUBY
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def reflect_on_association(name)
|
51
|
+
Relation.new(name.to_s.classify.constantize) # Just enough to support nested_form gem
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spyke/relation'
|
2
|
+
require 'spyke/result'
|
3
|
+
|
4
|
+
module Spyke
|
5
|
+
module Associations
|
6
|
+
class Association < Relation
|
7
|
+
attr_reader :parent, :name
|
8
|
+
|
9
|
+
def initialize(parent, name, options = {})
|
10
|
+
super (options[:class_name] || name.to_s).classify.constantize, options
|
11
|
+
@parent, @name = parent, name
|
12
|
+
end
|
13
|
+
|
14
|
+
def load
|
15
|
+
find_one # Override for plural associations that return an association object
|
16
|
+
end
|
17
|
+
|
18
|
+
def assign_nested_attributes(attributes)
|
19
|
+
parent.attributes[name] = new(attributes).attributes
|
20
|
+
end
|
21
|
+
|
22
|
+
def create(attributes = {})
|
23
|
+
add_to_parent super
|
24
|
+
end
|
25
|
+
|
26
|
+
def new(*args)
|
27
|
+
add_to_parent super
|
28
|
+
end
|
29
|
+
|
30
|
+
def build(*args)
|
31
|
+
new(*args)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def add_to_parent(record)
|
37
|
+
parent.attributes[name] = record.attributes
|
38
|
+
record
|
39
|
+
end
|
40
|
+
|
41
|
+
def foreign_key
|
42
|
+
(@options[:foreign_key] || "#{parent.class.model_name.param_key}_id").to_sym
|
43
|
+
end
|
44
|
+
|
45
|
+
def fetch
|
46
|
+
fetch_embedded || super
|
47
|
+
end
|
48
|
+
|
49
|
+
def fetch_embedded
|
50
|
+
if embedded_attributes
|
51
|
+
Result.new(data: embedded_attributes)
|
52
|
+
elsif !uri
|
53
|
+
Result.new(data: nil)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def embedded_attributes
|
58
|
+
parent.attributes[name]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Spyke
|
2
|
+
module Associations
|
3
|
+
class BelongsTo < Association
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@options.reverse_merge!(uri: "/#{klass.model_name.plural}/:id", foreign_key: "#{klass.model_name.param_key}_id")
|
7
|
+
@params[:id] = parent.try(foreign_key)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Spyke
|
2
|
+
module Associations
|
3
|
+
class HasMany < Association
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@options.reverse_merge!(uri: "/#{parent.class.model_name.plural}/:#{foreign_key}/#{klass.model_name.plural}/:id")
|
7
|
+
@params[foreign_key] = parent.id
|
8
|
+
end
|
9
|
+
|
10
|
+
def load
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def assign_nested_attributes(collection)
|
15
|
+
collection = collection.values if collection.is_a?(Hash)
|
16
|
+
|
17
|
+
collection.each do |attributes|
|
18
|
+
if existing = find_existing_attributes(attributes.with_indifferent_access[:id])
|
19
|
+
existing.merge!(attributes)
|
20
|
+
else
|
21
|
+
build(attributes)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def find_existing_attributes(id)
|
29
|
+
embedded_attributes.to_a.find { |attr| attr[:id] && attr[:id].to_s == id.to_s }
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_to_parent(record)
|
33
|
+
parent.attributes[name] ||= []
|
34
|
+
parent.attributes[name] << record.attributes
|
35
|
+
record
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Spyke
|
2
|
+
module Associations
|
3
|
+
class HasOne < Association
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@options.reverse_merge!(uri: "/#{parent.class.model_name.plural}/:#{foreign_key}/#{klass.model_name.singular}")
|
7
|
+
@params[foreign_key] = parent.id
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'spyke/collection'
|
2
|
+
|
3
|
+
module Spyke
|
4
|
+
module Attributes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attr_reader :attributes
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def attributes(*args)
|
13
|
+
args.each do |attr|
|
14
|
+
define_method attr do
|
15
|
+
attribute(attr)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(attributes = {})
|
22
|
+
self.attributes = attributes
|
23
|
+
@uri_template = current_scope.uri
|
24
|
+
end
|
25
|
+
|
26
|
+
def attributes=(new_attributes)
|
27
|
+
@attributes ||= current_scope.params.with_indifferent_access
|
28
|
+
use_setters parse(new_attributes) if new_attributes
|
29
|
+
end
|
30
|
+
|
31
|
+
def id
|
32
|
+
attributes[:id]
|
33
|
+
end
|
34
|
+
|
35
|
+
def id=(value)
|
36
|
+
attributes[:id] = value if value.present?
|
37
|
+
end
|
38
|
+
|
39
|
+
def ==(other)
|
40
|
+
other.is_a?(Spyke::Base) && id == other.id
|
41
|
+
end
|
42
|
+
|
43
|
+
def inspect
|
44
|
+
"#<#{self.class}(#{uri}) id: #{id.inspect} #{inspect_attributes}>"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def default_attributes
|
50
|
+
self.class.default_attributes
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse(attributes)
|
54
|
+
attributes.each_with_object({}) do |(key, value), parameters|
|
55
|
+
parameters[key] = parse_value(value)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_value(value)
|
60
|
+
case
|
61
|
+
when value.is_a?(Spyke::Base) then parse(value.attributes)
|
62
|
+
when value.is_a?(Hash) then parse(value)
|
63
|
+
when value.is_a?(Array) then value.map { |v| parse_value(v) }
|
64
|
+
when value.respond_to?(:content_type) then Faraday::UploadIO.new(value.path, value.content_type)
|
65
|
+
else value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def use_setters(attributes)
|
70
|
+
attributes.each do |key, value|
|
71
|
+
send "#{key}=", value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def method_missing(name, *args, &block)
|
76
|
+
case
|
77
|
+
when association?(name) then association(name).load
|
78
|
+
when attribute?(name) then attribute(name)
|
79
|
+
when predicate?(name) then predicate(name)
|
80
|
+
when setter?(name) then set_attribute(name, args.first)
|
81
|
+
else super
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def respond_to_missing?(name, include_private = false)
|
86
|
+
association?(name) || attribute?(name) || predicate?(name) || super
|
87
|
+
end
|
88
|
+
|
89
|
+
def association?(name)
|
90
|
+
associations.has_key?(name)
|
91
|
+
end
|
92
|
+
|
93
|
+
def association(name)
|
94
|
+
options = associations[name]
|
95
|
+
options[:type].new(self, name, options)
|
96
|
+
end
|
97
|
+
|
98
|
+
def attribute?(name)
|
99
|
+
attributes.has_key?(name)
|
100
|
+
end
|
101
|
+
|
102
|
+
def attribute(name)
|
103
|
+
attributes[name]
|
104
|
+
end
|
105
|
+
|
106
|
+
def predicate?(name)
|
107
|
+
name.to_s.end_with?('?')
|
108
|
+
end
|
109
|
+
|
110
|
+
def predicate(name)
|
111
|
+
!!attribute(depredicate(name))
|
112
|
+
end
|
113
|
+
|
114
|
+
def depredicate(name)
|
115
|
+
name.to_s.chomp('?').to_sym
|
116
|
+
end
|
117
|
+
|
118
|
+
def setter?(name)
|
119
|
+
name.to_s.end_with?('=')
|
120
|
+
end
|
121
|
+
|
122
|
+
def set_attribute(name, value)
|
123
|
+
attributes[name.to_s.chomp('=')] = value
|
124
|
+
end
|
125
|
+
|
126
|
+
def inspect_attributes
|
127
|
+
attributes.except(:id).map { |k, v| "#{k}: #{v.inspect}" }.join(' ')
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|