spyke 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in spyke.gemspec
4
+ gemspec
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
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ task default: :test
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'test'
8
+ t.pattern = 'test/*_test.rb'
9
+ end
data/lib/spyke.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'active_support/core_ext'
2
+ require 'spyke/base'
3
+ require 'spyke/version'
4
+
5
+ module Spyke
6
+ end
@@ -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