json_api_client 0.0.3
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/LICENSE +20 -0
- data/README.md +118 -0
- data/Rakefile +32 -0
- data/lib/json_api_client/associations/base_association.rb +18 -0
- data/lib/json_api_client/associations/belongs_to.rb +28 -0
- data/lib/json_api_client/associations/has_many.rb +19 -0
- data/lib/json_api_client/associations/has_one.rb +20 -0
- data/lib/json_api_client/associations.rb +63 -0
- data/lib/json_api_client/attributes.rb +59 -0
- data/lib/json_api_client/connection.rb +32 -0
- data/lib/json_api_client/errors.rb +41 -0
- data/lib/json_api_client/links.rb +14 -0
- data/lib/json_api_client/middleware/json_request.rb +12 -0
- data/lib/json_api_client/middleware/status.rb +28 -0
- data/lib/json_api_client/middleware.rb +6 -0
- data/lib/json_api_client/parser.rb +42 -0
- data/lib/json_api_client/query/base.rb +27 -0
- data/lib/json_api_client/query/create.rb +12 -0
- data/lib/json_api_client/query/destroy.rb +12 -0
- data/lib/json_api_client/query/find.rb +19 -0
- data/lib/json_api_client/query/update.rb +12 -0
- data/lib/json_api_client/query.rb +9 -0
- data/lib/json_api_client/resource.rb +119 -0
- data/lib/json_api_client/result_set.rb +13 -0
- data/lib/json_api_client/scope.rb +48 -0
- data/lib/json_api_client/utils.rb +28 -0
- data/lib/json_api_client/version.rb +3 -0
- data/lib/json_api_client.rb +18 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f5db14498ef10dc6d6aaa4bcb65b0a6c5bdcb300
|
4
|
+
data.tar.gz: 75c000bfceb5104a78ab2929f0c160254ce129e5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f5eae53897bbd0f37a524e979b3c81374ba74607ee4f8e3594d6c581230ad4cca41fb9b355d306ca7da6ef222e9af37613c19e9b4856545908f66a28a16dde2c
|
7
|
+
data.tar.gz: de3bcf1db477b01d2f89bd9744b5c94eeb7bdd1644752335f44ea667e9b6c0c0ccca3e042a54e6a3e540cb4c1ed63db41c1caa5f2d1ab4232c84eff8350be4fa
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) {{year}} {{fullname}}
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# JsonApiClient [](https://travis-ci.org/chingor13/json_api_client)
|
2
|
+
|
3
|
+
This gem is meant to help you build an API client for interacting with REST APIs as laid out by [http://jsonapi.org](http://jsonapi.org). It attempts to give you a query building framework that is easy to understand (it is similar to ActiveRecord scopes).
|
4
|
+
|
5
|
+
*Note: This is still a work in progress.*
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
```
|
10
|
+
module MyApi
|
11
|
+
class User < JsonApiClient::Resource
|
12
|
+
has_many :accounts
|
13
|
+
end
|
14
|
+
|
15
|
+
class Account < JsonApiClient::Resource
|
16
|
+
belongs_to :user
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
MyApi::User.all
|
21
|
+
MyApi::User.where(account_id: 1).find(1)
|
22
|
+
MyApi::User.where(account_id: 1).all
|
23
|
+
|
24
|
+
MyApi::User.where(name: "foo").order("created_at desc").includes(:preferences, :cars).all
|
25
|
+
|
26
|
+
u = MyApi::User.new(foo: "bar", bar: "foo")
|
27
|
+
u.save
|
28
|
+
|
29
|
+
u = MyApi::User.find(1).first
|
30
|
+
u.update_attributes(
|
31
|
+
a: "b",
|
32
|
+
c: "d"
|
33
|
+
)
|
34
|
+
|
35
|
+
u = MyApi::User.create(
|
36
|
+
a: "b",
|
37
|
+
c: "d"
|
38
|
+
)
|
39
|
+
|
40
|
+
u = MyApi::User.find(1).first
|
41
|
+
u.accounts
|
42
|
+
=> MyApi::Account.where(user_id: u.id).all
|
43
|
+
```
|
44
|
+
|
45
|
+
## Connection options
|
46
|
+
|
47
|
+
You can configure your connection using Faraday middleware. In general, you'll want
|
48
|
+
to do this in a base model that all your resources inherit from:
|
49
|
+
|
50
|
+
```
|
51
|
+
MyApi::Base.connection do |connection|
|
52
|
+
# set OAuth2 headers
|
53
|
+
connection.use Faraday::Request::Oauth2, 'MYTOKEN'
|
54
|
+
|
55
|
+
# log responses
|
56
|
+
connection.use Faraday::Response::Logger
|
57
|
+
|
58
|
+
connection.use MyCustomMiddleware
|
59
|
+
end
|
60
|
+
|
61
|
+
module MyApi
|
62
|
+
class User < Base
|
63
|
+
# will use the customized connection
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
## Custom Parser
|
69
|
+
|
70
|
+
You can configure your API client to use a custom parser that implements the `parse` class method. It should return a `JsonApiClient::ResultSet` instance. You can use it by setting the parser attribute on your model:
|
71
|
+
|
72
|
+
```
|
73
|
+
class MyCustomParser
|
74
|
+
def self.parse(klass, response)
|
75
|
+
…
|
76
|
+
# returns some ResultSet object
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class MyApi::Base < JsonApiClient::Resource
|
81
|
+
self.parser = MyCustomParser
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
## Handling Validation Errors
|
86
|
+
|
87
|
+
```
|
88
|
+
User.create(name: "Bob", email_address: "invalid email")
|
89
|
+
=> false
|
90
|
+
|
91
|
+
user = User.new(name: "Bob", email_address: "invalid email")
|
92
|
+
user.save
|
93
|
+
=> false
|
94
|
+
user.errors
|
95
|
+
=> ["Email address is invalid"]
|
96
|
+
|
97
|
+
user = User.find(1)
|
98
|
+
user.update_attributes(email_address: "invalid email")
|
99
|
+
=> false
|
100
|
+
user.errors
|
101
|
+
=> ["Email address is invalid"]
|
102
|
+
user.email_address
|
103
|
+
=> "invalid email"
|
104
|
+
```
|
105
|
+
|
106
|
+
## Nested Resources
|
107
|
+
|
108
|
+
You can force nested resource paths for your models by using a `belongs_to` association.
|
109
|
+
|
110
|
+
```
|
111
|
+
module MyApi
|
112
|
+
class Account < JsonApiClient::Resource
|
113
|
+
belongs_to :user
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'JsonApiClient'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << 'lib'
|
26
|
+
t.libs << 'test'
|
27
|
+
t.pattern = 'test/**/*_test.rb'
|
28
|
+
t.verbose = false
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Associations
|
3
|
+
class BaseAssociation
|
4
|
+
attr_reader :attr_name, :klass, :options
|
5
|
+
def initialize(attr_name, klass, options = {})
|
6
|
+
@attr_name = attr_name
|
7
|
+
@klass = klass
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def association_class
|
12
|
+
@association_class ||= Utils.compute_type(klass, options.fetch(:class_name) do
|
13
|
+
attr_name.to_s.classify
|
14
|
+
end)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Associations
|
3
|
+
module BelongsTo
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def belongs_to(attr_name, options = {})
|
8
|
+
# self.associations = self.associations + [HasOne::Association.new(attr_name, self, options)]
|
9
|
+
self.associations += [BelongsTo::Association.new(attr_name, self, options)]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Association < BaseAssociation
|
14
|
+
def parse(params)
|
15
|
+
params ? association_class.new(params) : nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def param
|
19
|
+
:"#{attr_name}_id"
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_prefix_path
|
23
|
+
"#{attr_name.to_s.pluralize}/%{#{param}}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Associations
|
3
|
+
module HasMany
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def has_many(attr_name, options = {})
|
8
|
+
self.associations = self.associations + [HasMany::Association.new(attr_name, self, options)]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Association < BaseAssociation
|
13
|
+
def parse(param)
|
14
|
+
[param].flatten.map{|data| association_class.new(data) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Associations
|
3
|
+
module HasOne
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def has_one(attr_name, options = {})
|
8
|
+
# self.associations = self.associations + [HasOne::Association.new(attr_name, self, options)]
|
9
|
+
self.associations += [HasOne::Association.new(attr_name, self, options)]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Association < BaseAssociation
|
14
|
+
def parse(params)
|
15
|
+
params ? association_class.new(params) : nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Associations
|
3
|
+
autoload :BaseAssociation, 'json_api_client/associations/base_association'
|
4
|
+
autoload :BelongsTo, 'json_api_client/associations/belongs_to'
|
5
|
+
autoload :HasMany, 'json_api_client/associations/has_many'
|
6
|
+
autoload :HasOne, 'json_api_client/associations/has_one'
|
7
|
+
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :associations
|
12
|
+
self.associations = []
|
13
|
+
|
14
|
+
include BelongsTo
|
15
|
+
include HasMany
|
16
|
+
include HasOne
|
17
|
+
|
18
|
+
initialize :load_associations
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def belongs_to_associations
|
23
|
+
associations.select{|association| association.is_a?(BelongsTo::Association) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def prefix_params
|
27
|
+
belongs_to_associations.map(&:param)
|
28
|
+
end
|
29
|
+
|
30
|
+
def prefix_path
|
31
|
+
belongs_to_associations.map(&:to_prefix_path).join("/")
|
32
|
+
end
|
33
|
+
|
34
|
+
def path(params = nil)
|
35
|
+
parts = [table_name]
|
36
|
+
if params
|
37
|
+
slurp = params.slice(*prefix_params)
|
38
|
+
prefix_params.each do |param|
|
39
|
+
params.delete(param)
|
40
|
+
end
|
41
|
+
parts.unshift(prefix_path % slurp)
|
42
|
+
else
|
43
|
+
parts.unshift(prefix_path)
|
44
|
+
end
|
45
|
+
parts.reject!{|part| part == "" }
|
46
|
+
File.join(*parts)
|
47
|
+
rescue KeyError
|
48
|
+
raise ArgumentError, "Not all prefix parameters specified"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def load_associations(params)
|
55
|
+
associations.each do |association|
|
56
|
+
if params.has_key?(association.attr_name.to_s)
|
57
|
+
set_attribute(association.attr_name, association.parse(params[association.attr_name.to_s]))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Attributes
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
attr_reader :attributes
|
7
|
+
initialize do |obj, params|
|
8
|
+
obj.attributes = params
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def attributes=(attrs = {})
|
13
|
+
@attributes ||= {}.with_indifferent_access
|
14
|
+
@attributes.merge!(attrs)
|
15
|
+
end
|
16
|
+
|
17
|
+
def update_attributes(attrs = {})
|
18
|
+
self.attributes = attrs
|
19
|
+
save
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing(method, *args, &block)
|
23
|
+
if match = method.to_s.match(/^(.*)=$/)
|
24
|
+
set_attribute(match[1], args.first)
|
25
|
+
elsif has_attribute?(method)
|
26
|
+
attributes[method]
|
27
|
+
else
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def persisted?
|
33
|
+
attributes.has_key?(primary_key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def query_params
|
37
|
+
attributes.except(primary_key)
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_param
|
41
|
+
attributes.fetch(primary_key, "").to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def read_attribute(name)
|
47
|
+
attributes.fetch(name, nil)
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_attribute(name, value)
|
51
|
+
attributes[name] = value
|
52
|
+
end
|
53
|
+
|
54
|
+
def has_attribute?(attr_name)
|
55
|
+
attributes.has_key?(attr_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
class Connection
|
3
|
+
|
4
|
+
attr_reader :faraday
|
5
|
+
|
6
|
+
def initialize(site)
|
7
|
+
@faraday = Faraday.new(site) do |builder|
|
8
|
+
builder.request :url_encoded
|
9
|
+
builder.use Middleware::JsonRequest
|
10
|
+
builder.use Middleware::Status
|
11
|
+
builder.use FaradayMiddleware::ParseJson, content_type: /\bjson$/
|
12
|
+
builder.adapter Faraday.default_adapter
|
13
|
+
end
|
14
|
+
yield(self) if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
# insert middleware before ParseJson - middleware executed in reverse order -
|
18
|
+
# inserted middleware will run after json parsed
|
19
|
+
def use(middleware, *args, &block)
|
20
|
+
faraday.builder.insert_before(FaradayMiddleware::ParseJson, middleware, *args, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(middleware)
|
24
|
+
faraday.builder.delete(middleware)
|
25
|
+
end
|
26
|
+
|
27
|
+
def execute(query)
|
28
|
+
faraday.send(query.request_method, query.path, query.params, query.headers)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Errors
|
3
|
+
class ApiError < Exception
|
4
|
+
attr_reader :env
|
5
|
+
def initialize(env)
|
6
|
+
@env = env
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class ClientError < ApiError
|
11
|
+
end
|
12
|
+
|
13
|
+
class ServerError < ApiError
|
14
|
+
def message
|
15
|
+
"Internal server error"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class NotFound < ServerError
|
20
|
+
attr_reader :uri
|
21
|
+
def initialize(uri)
|
22
|
+
@uri = uri
|
23
|
+
end
|
24
|
+
def message
|
25
|
+
"Couldn't find resource at: #{uri.to_s}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class UnexpectedStatus < ServerError
|
30
|
+
attr_reader :code, :uri
|
31
|
+
def initialize(code, uri)
|
32
|
+
@code = code
|
33
|
+
@uri = uri
|
34
|
+
end
|
35
|
+
def message
|
36
|
+
"Unexpected response status: #{code} from: #{uri.to_s}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Middleware
|
3
|
+
class JsonRequest < Faraday::Middleware
|
4
|
+
def call(environment)
|
5
|
+
environment[:request_headers]["Accept"] = "application/json,*/*"
|
6
|
+
uri = environment[:url]
|
7
|
+
uri.path = uri.path + ".json" unless uri.path.match(/\.json$/)
|
8
|
+
@app.call(environment)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Middleware
|
3
|
+
class Status < Faraday::Middleware
|
4
|
+
def call(environment)
|
5
|
+
@app.call(environment).on_complete do |env|
|
6
|
+
handle_status(env[:status], env)
|
7
|
+
|
8
|
+
# look for meta[:status]
|
9
|
+
if env[:body].is_a?(Hash)
|
10
|
+
code = env[:body].fetch("meta", {}).fetch("status", 200).to_i
|
11
|
+
handle_status(code, env)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def handle_status(code, env)
|
19
|
+
case code
|
20
|
+
when 404
|
21
|
+
raise Errors::NotFound, env[:uri]
|
22
|
+
when 500..599
|
23
|
+
raise Errors::ServerError, env
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
class Parser
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def parse(klass, response)
|
6
|
+
data = response.body
|
7
|
+
ResultSet.build(klass, data) do |result_set|
|
8
|
+
handle_pagination(result_set, data)
|
9
|
+
handle_errors(result_set, data)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def handle_pagination(result_set, data)
|
16
|
+
meta = data.fetch("meta", {})
|
17
|
+
result_set.per_page = meta.fetch("per_page") do
|
18
|
+
result_set.length
|
19
|
+
end
|
20
|
+
result_set.total_entries = meta.fetch("total_entries") do
|
21
|
+
result_set.length
|
22
|
+
end
|
23
|
+
result_set.current_page = meta.fetch("current_page", 1)
|
24
|
+
|
25
|
+
# can fall back to calculating via total entries and per_page
|
26
|
+
result_set.total_pages = meta.fetch("total_pages") do
|
27
|
+
(1.0 * result_set.total_entries / result_set.per_page).ceil rescue 1
|
28
|
+
end
|
29
|
+
|
30
|
+
# can fall back to calculating via per_page and current_page
|
31
|
+
result_set.offset = meta.fetch("offset") do
|
32
|
+
result_set.per_page * (result_set.current_page - 1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def handle_errors(result_set, data)
|
37
|
+
result_set.errors = data.fetch("meta", {}).fetch("errors", [])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Query
|
3
|
+
class Base
|
4
|
+
class_attribute :request_method
|
5
|
+
attr_reader :klass, :headers, :path, :params
|
6
|
+
|
7
|
+
def initialize(klass, args)
|
8
|
+
@klass = klass
|
9
|
+
build_params(args)
|
10
|
+
@headers = klass.default_headers.dup
|
11
|
+
|
12
|
+
@path = begin
|
13
|
+
p = klass.path(@params)
|
14
|
+
if @params.has_key?(klass.primary_key) && !@params[klass.primary_key].is_a?(Array)
|
15
|
+
p = File.join(p, @params.delete(klass.primary_key).to_s)
|
16
|
+
end
|
17
|
+
p
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_params(args)
|
22
|
+
@params = args
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Query
|
3
|
+
class Find < Base
|
4
|
+
self.request_method = :get
|
5
|
+
|
6
|
+
def build_params(args)
|
7
|
+
@params = case args
|
8
|
+
when Hash
|
9
|
+
args
|
10
|
+
when Array
|
11
|
+
{klass.primary_key.to_s.pluralize.to_sym => args.join(",")}
|
12
|
+
else
|
13
|
+
{klass.primary_key => args}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Query
|
3
|
+
autoload :Base, 'json_api_client/query/base'
|
4
|
+
autoload :Create, 'json_api_client/query/create'
|
5
|
+
autoload :Destroy, 'json_api_client/query/destroy'
|
6
|
+
autoload :Find, 'json_api_client/query/find'
|
7
|
+
autoload :Update, 'json_api_client/query/update'
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'active_support/inflector'
|
4
|
+
require 'active_support/core_ext/hash'
|
5
|
+
require 'active_support/core_ext/class/attribute'
|
6
|
+
|
7
|
+
module JsonApiClient
|
8
|
+
class Resource
|
9
|
+
class_attribute :site, :primary_key, :link_style, :default_headers, :initializers, :parser
|
10
|
+
|
11
|
+
self.primary_key = :id
|
12
|
+
self.link_style = :id # or :url
|
13
|
+
self.default_headers = {}
|
14
|
+
self.initializers = []
|
15
|
+
self.parser = Parser
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# first 'scope' should build a new scope object
|
19
|
+
extend Forwardable
|
20
|
+
def_delegators :new_scope, :where, :order, :includes, :all, :paginate, :page, :first
|
21
|
+
|
22
|
+
# base URL for this resource
|
23
|
+
def resource
|
24
|
+
File.join(site, path)
|
25
|
+
end
|
26
|
+
|
27
|
+
def table_name
|
28
|
+
resource_name.pluralize
|
29
|
+
end
|
30
|
+
|
31
|
+
def resource_name
|
32
|
+
name.demodulize.underscore
|
33
|
+
end
|
34
|
+
|
35
|
+
def find(conditions)
|
36
|
+
run_request(Query::Find.new(self, conditions))
|
37
|
+
end
|
38
|
+
|
39
|
+
def create(conditions = {})
|
40
|
+
result = run_request(Query::Create.new(self, conditions))
|
41
|
+
return nil if result.errors.length > 0
|
42
|
+
result.first
|
43
|
+
end
|
44
|
+
|
45
|
+
def connection
|
46
|
+
@connection ||= begin
|
47
|
+
super
|
48
|
+
rescue
|
49
|
+
build_connection
|
50
|
+
end
|
51
|
+
yield(@connection) if block_given?
|
52
|
+
@connection
|
53
|
+
end
|
54
|
+
|
55
|
+
def run_request(query)
|
56
|
+
parse(connection.execute(query))
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def new_scope
|
62
|
+
Scope.new(self)
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse(data)
|
66
|
+
parser.parse(self, data)
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_connection
|
70
|
+
Connection.new(site)
|
71
|
+
end
|
72
|
+
|
73
|
+
def initialize(method = nil, &block)
|
74
|
+
self.initializers.push(method || block)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
include Attributes
|
79
|
+
include Associations
|
80
|
+
include Links
|
81
|
+
|
82
|
+
attr_accessor :errors
|
83
|
+
def initialize(params = {})
|
84
|
+
initializers.each do |initializer|
|
85
|
+
if initializer.respond_to?(:call)
|
86
|
+
initializer.call(self, params)
|
87
|
+
else
|
88
|
+
self.send(initializer, params)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def save
|
94
|
+
query = persisted? ?
|
95
|
+
Query::Update.new(self.class, attributes) :
|
96
|
+
Query::Create.new(self.class, attributes)
|
97
|
+
|
98
|
+
run_request(query)
|
99
|
+
end
|
100
|
+
|
101
|
+
def destroy
|
102
|
+
run_request(Query::Destroy.new(self.class, attributes))
|
103
|
+
end
|
104
|
+
|
105
|
+
protected
|
106
|
+
|
107
|
+
def run_request(query)
|
108
|
+
response = self.class.run_request(query)
|
109
|
+
self.errors = response.errors
|
110
|
+
if updated = response.first
|
111
|
+
self.attributes = updated.attributes
|
112
|
+
else
|
113
|
+
self.attributes = {}
|
114
|
+
end
|
115
|
+
return errors.length == 0
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
class ResultSet < Array
|
3
|
+
|
4
|
+
attr_accessor :total_pages, :total_entries, :offset, :per_page, :current_page, :errors
|
5
|
+
|
6
|
+
def self.build(klass, data)
|
7
|
+
result_data = data.fetch(klass.table_name, [])
|
8
|
+
new(result_data.map {|attributes| klass.new(attributes) }).tap do |result_set|
|
9
|
+
yield(result_set) if block_given?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
class Scope
|
3
|
+
attr_reader :klass, :params
|
4
|
+
|
5
|
+
def initialize(klass)
|
6
|
+
@klass = klass
|
7
|
+
@params = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def where(conditions = {})
|
11
|
+
@params.merge!(conditions)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
alias paginate where
|
15
|
+
|
16
|
+
def order(conditions)
|
17
|
+
where(order: conditions)
|
18
|
+
end
|
19
|
+
|
20
|
+
def includes(*tables)
|
21
|
+
@params[:includes] ||= []
|
22
|
+
@params[:includes] += tables.flatten
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def page(number)
|
27
|
+
where(page: number)
|
28
|
+
end
|
29
|
+
|
30
|
+
def first
|
31
|
+
paginate(page: 1, per_page: 1).to_a.first
|
32
|
+
end
|
33
|
+
|
34
|
+
def build
|
35
|
+
klass.new(params)
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_a
|
39
|
+
@to_a ||= klass.find(params)
|
40
|
+
end
|
41
|
+
alias all to_a
|
42
|
+
|
43
|
+
def method_missing(method_name, *args, &block)
|
44
|
+
to_a.send(method_name, *args, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
def self.compute_type(klass, type_name)
|
5
|
+
# If the type is prefixed with a scope operator then we assume that
|
6
|
+
# the type_name is an absolute reference.
|
7
|
+
return type_name.constantize if type_name.match(/^::/)
|
8
|
+
|
9
|
+
# Build a list of candidates to search for
|
10
|
+
candidates = []
|
11
|
+
klass.name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
|
12
|
+
candidates << type_name
|
13
|
+
|
14
|
+
candidates.each do |candidate|
|
15
|
+
begin
|
16
|
+
constant = candidate.constantize
|
17
|
+
return constant if candidate == constant.to_s
|
18
|
+
rescue NameError => e
|
19
|
+
# We don't want to swallow NoMethodError < NameError errors
|
20
|
+
raise e unless e.instance_of?(NameError)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
raise NameError, "uninitialized constant #{candidates.first}"
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module JsonApiClient
|
6
|
+
autoload :Associations, 'json_api_client/associations'
|
7
|
+
autoload :Attributes, 'json_api_client/attributes'
|
8
|
+
autoload :Connection, 'json_api_client/connection'
|
9
|
+
autoload :Errors, 'json_api_client/errors'
|
10
|
+
autoload :Links, 'json_api_client/links'
|
11
|
+
autoload :Middleware, 'json_api_client/middleware'
|
12
|
+
autoload :Parser, 'json_api_client/parser'
|
13
|
+
autoload :Query, 'json_api_client/query'
|
14
|
+
autoload :Resource, 'json_api_client/resource'
|
15
|
+
autoload :ResultSet, 'json_api_client/result_set'
|
16
|
+
autoload :Scope, 'json_api_client/scope'
|
17
|
+
autoload :Utils, 'json_api_client/utils'
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: json_api_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeff Ching
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-10-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.8.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.8.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faraday_middleware
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.9.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.9.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: webmock
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Build client libraries compliant with specification defined by jsonapi.org
|
70
|
+
email: ching.jeff@gmail.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- lib/json_api_client/associations/base_association.rb
|
76
|
+
- lib/json_api_client/associations/belongs_to.rb
|
77
|
+
- lib/json_api_client/associations/has_many.rb
|
78
|
+
- lib/json_api_client/associations/has_one.rb
|
79
|
+
- lib/json_api_client/associations.rb
|
80
|
+
- lib/json_api_client/attributes.rb
|
81
|
+
- lib/json_api_client/connection.rb
|
82
|
+
- lib/json_api_client/errors.rb
|
83
|
+
- lib/json_api_client/links.rb
|
84
|
+
- lib/json_api_client/middleware/json_request.rb
|
85
|
+
- lib/json_api_client/middleware/status.rb
|
86
|
+
- lib/json_api_client/middleware.rb
|
87
|
+
- lib/json_api_client/parser.rb
|
88
|
+
- lib/json_api_client/query/base.rb
|
89
|
+
- lib/json_api_client/query/create.rb
|
90
|
+
- lib/json_api_client/query/destroy.rb
|
91
|
+
- lib/json_api_client/query/find.rb
|
92
|
+
- lib/json_api_client/query/update.rb
|
93
|
+
- lib/json_api_client/query.rb
|
94
|
+
- lib/json_api_client/resource.rb
|
95
|
+
- lib/json_api_client/result_set.rb
|
96
|
+
- lib/json_api_client/scope.rb
|
97
|
+
- lib/json_api_client/utils.rb
|
98
|
+
- lib/json_api_client/version.rb
|
99
|
+
- lib/json_api_client.rb
|
100
|
+
- LICENSE
|
101
|
+
- Rakefile
|
102
|
+
- README.md
|
103
|
+
homepage: http://github.com/chingor13/json_api_client
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.0.3
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: Build client libraries compliant with specification defined by jsonapi.org
|
127
|
+
test_files: []
|