json_api_client 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://travis-ci.org/chingor13/json_api_client.png)](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: []
|