simple_jsonapi_client 0.1.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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +8 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Dockerfile +9 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +264 -0
  10. data/Rakefile +8 -0
  11. data/bin/console +3 -0
  12. data/bin/development_start +12 -0
  13. data/bin/rails +3 -0
  14. data/bin/setup +7 -0
  15. data/bin/wait_for_it +178 -0
  16. data/docker-compose.yml +50 -0
  17. data/lib/simple_jsonapi_client.rb +5 -0
  18. data/lib/simple_jsonapi_client/base.rb +325 -0
  19. data/lib/simple_jsonapi_client/error.rb +6 -0
  20. data/lib/simple_jsonapi_client/errors/api_error.rb +84 -0
  21. data/lib/simple_jsonapi_client/redirection/fetch_all.rb +43 -0
  22. data/lib/simple_jsonapi_client/redirection/proxy.rb +54 -0
  23. data/lib/simple_jsonapi_client/relationships/array_data_relationship.rb +19 -0
  24. data/lib/simple_jsonapi_client/relationships/array_link_relationship.rb +15 -0
  25. data/lib/simple_jsonapi_client/relationships/data_relationship_proxy.rb +30 -0
  26. data/lib/simple_jsonapi_client/relationships/has_many_relationship.rb +16 -0
  27. data/lib/simple_jsonapi_client/relationships/has_one_relationship.rb +16 -0
  28. data/lib/simple_jsonapi_client/relationships/link_relationship_proxy.rb +19 -0
  29. data/lib/simple_jsonapi_client/relationships/relationship.rb +34 -0
  30. data/lib/simple_jsonapi_client/relationships/singular_data_relationship.rb +17 -0
  31. data/lib/simple_jsonapi_client/relationships/singular_link_relationship.rb +13 -0
  32. data/lib/simple_jsonapi_client/version.rb +3 -0
  33. data/simple_jsonapi_client.gemspec +31 -0
  34. metadata +180 -0
@@ -0,0 +1,6 @@
1
+ module SimpleJSONAPIClient
2
+ class Error < StandardError
3
+ end
4
+
5
+ require 'simple_jsonapi_client/errors/api_error'
6
+ end
@@ -0,0 +1,84 @@
1
+ require 'delegate'
2
+
3
+ module SimpleJSONAPIClient
4
+ module Errors
5
+ class APIError < ::SimpleJSONAPIClient::Error
6
+ extend Forwardable
7
+
8
+ KNOWN_ERRORS = {
9
+ 400 => 'BadRequestError',
10
+ 404 => 'NotFoundError',
11
+ 422 => 'UnprocessableEntityError'
12
+ }
13
+ KNOWN_ERRORS.default = 'APIError'
14
+
15
+ def self.generate(response)
16
+ error = KNOWN_ERRORS[response.status]
17
+ SimpleJSONAPIClient::Errors.const_get(error).new(response)
18
+ end
19
+
20
+ attr_reader :response
21
+ def_delegators :response, :status, :body
22
+
23
+ def initialize(response)
24
+ @response = response
25
+ super(full_message)
26
+ end
27
+
28
+ def errors
29
+ Array(body['errors'])
30
+ end
31
+
32
+ def message
33
+ if !codes.empty?
34
+ codes_message
35
+ elsif !details.empty?
36
+ details_message
37
+ else
38
+ default_message
39
+ end
40
+ end
41
+
42
+ def full_message
43
+ "The API returned a #{status} error status and this content:\n" +
44
+ pretty_printed_response.each_line.map { |line| " #{line}" }.join
45
+ end
46
+
47
+ def codes
48
+ @codes ||= errors.map { |error| error['code'] }.compact
49
+ end
50
+
51
+ def details
52
+ @details ||= errors.map { |error| error['detail'] }.compact
53
+ end
54
+
55
+ private
56
+
57
+ def pretty_printed_response
58
+ JSON.pretty_generate(body)
59
+ end
60
+
61
+ def codes_message
62
+ codes_word = codes.size == 1 ? 'code' : 'codes'
63
+ "The API returned a #{status} error status with the following error #{codes_word}: #{
64
+ codes.map(&:inspect).join(', ')
65
+ }"
66
+ end
67
+
68
+ def details_message
69
+ details_word = details.size == 1 ? 'detail' : 'details'
70
+ "The API returned a #{status} error status with the following error #{details_word}: #{
71
+ details.map(&:inspect).join(', ')
72
+ }"
73
+ end
74
+
75
+ def default_message
76
+ "The API responded with a #{status} error status."
77
+ end
78
+ end
79
+
80
+ BadRequestError = Class.new(APIError)
81
+ NotFoundError = Class.new(APIError)
82
+ UnprocessableEntityError = Class.new(APIError)
83
+ end
84
+ end
@@ -0,0 +1,43 @@
1
+ require 'simple_jsonapi_client/redirection/proxy'
2
+
3
+ module SimpleJSONAPIClient
4
+ module Redirection
5
+ class FetchAll < ::SimpleJSONAPIClient::Redirection::Proxy
6
+ def_delegators :internal_object, *(Array.instance_methods(false) - instance_methods)
7
+ def_delegators :internal_enumerator, *(Enumerator.instance_methods(false) - instance_methods)
8
+
9
+ def initialize(base_opts, &operation)
10
+ @base_opts = base_opts
11
+ @operation = operation
12
+ end
13
+
14
+ private
15
+ attr_reader :base_opts, :operation
16
+
17
+ def pseudo_inspect
18
+ internal_enumerator.inspect
19
+ end
20
+
21
+ def fetch_internal_object
22
+ internal_enumerator.to_a
23
+ end
24
+
25
+ def internal_enumerator
26
+ @internal_enumerator ||= Enumerator.new do |yielder|
27
+ loop do
28
+ current_response = operation.call(current_opts)
29
+ current_response['data'].each do |record|
30
+ yielder << record
31
+ end
32
+ break unless (next_link = current_response.dig('links', 'next'))
33
+ current_opts.merge!(url_opts: {}, url: next_link)
34
+ end
35
+ end
36
+ end
37
+
38
+ def current_opts
39
+ @current_opts ||= base_opts.dup
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,54 @@
1
+ require 'delegate'
2
+
3
+ module SimpleJSONAPIClient
4
+ module Redirection
5
+ class Proxy
6
+ extend Forwardable
7
+
8
+ def_delegator :internal_object, :nil?
9
+
10
+ def inspect
11
+ if @internal_object
12
+ @internal_object.inspect
13
+ else
14
+ pseudo_inspect
15
+ end
16
+ end
17
+
18
+ def method_missing(meth, *args, &block)
19
+ self.class.def_delegator :internal_object, meth
20
+ internal_object.__send__(meth, *args, &block)
21
+ end
22
+
23
+ def respond_to_missing?(*args)
24
+ internal_object.__send__(:respond_to?, *args)
25
+ end
26
+
27
+ def as_json
28
+ if internal_object.respond_to?(:as_json)
29
+ internal_object.as_json
30
+ elsif internal_object.is_a?(Array)
31
+ internal_object.map(&:as_json)
32
+ elsif nil?
33
+ nil
34
+ else
35
+ raise "Cannot convert #{inspect} to JSON!"
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def internal_object
42
+ @internal_object ||= fetch_internal_object
43
+ end
44
+
45
+ def pseudo_inspect
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def fetch_internal_object
50
+ raise NotImplementedError
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,19 @@
1
+ require 'simple_jsonapi_client/relationships/data_relationship_proxy'
2
+
3
+ module SimpleJSONAPIClient
4
+ module Relationships
5
+ class ArrayDataRelationship < DataRelationshipProxy
6
+ private
7
+
8
+ def pseudo_inspect
9
+ "#<#{self.class.name} model_class=#{@klass} ids=#{@record_or_records.map { |record| record['id'] }}>"
10
+ end
11
+
12
+ def fetch_internal_object
13
+ @record_or_records.map { |record|
14
+ instantiated_relationship_record(record)
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ require 'simple_jsonapi_client/relationships/link_relationship_proxy'
2
+
3
+ module SimpleJSONAPIClient
4
+ module Relationships
5
+ class ArrayLinkRelationship < LinkRelationshipProxy
6
+ def_delegators :internal_object, *(Array.instance_methods(false) - instance_methods)
7
+
8
+ private
9
+
10
+ def fetch_internal_object
11
+ @klass.fetch_all(connection: @connection, url: @url)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ require 'simple_jsonapi_client/redirection/proxy'
2
+
3
+ module SimpleJSONAPIClient
4
+ module Relationships
5
+ class DataRelationshipProxy < ::SimpleJSONAPIClient::Redirection::Proxy
6
+ def initialize(klass, records, included, connection)
7
+ @klass = klass
8
+ @record_or_records = records
9
+ @included = included
10
+ @connection = connection
11
+ end
12
+
13
+ private
14
+
15
+ def instantiated_relationship_record(record)
16
+ @klass.model_from(
17
+ initialization_data(record),
18
+ @included,
19
+ @connection
20
+ )
21
+ end
22
+
23
+ def initialization_data(record)
24
+ @included.fetch(record) do
25
+ { 'attributes' => nil, 'relationships' => nil }.merge!(record)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ require 'simple_jsonapi_client/relationships/array_data_relationship'
2
+ require 'simple_jsonapi_client/relationships/array_link_relationship'
3
+
4
+ module SimpleJSONAPIClient
5
+ module Relationships
6
+ class HasManyRelationship < Relationship
7
+ def call(info, included, connection)
8
+ if (records = info['data'])
9
+ ArrayDataRelationship.new(model_class, records, included, connection)
10
+ elsif (link = link_from(info))
11
+ ArrayLinkRelationship.new(model_class, connection, link)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require 'simple_jsonapi_client/relationships/singular_data_relationship'
2
+ require 'simple_jsonapi_client/relationships/singular_link_relationship'
3
+
4
+ module SimpleJSONAPIClient
5
+ module Relationships
6
+ class HasOneRelationship < Relationship
7
+ def call(info, included, connection)
8
+ if (record = info['data'])
9
+ SingularDataRelationship.new(model_class, record, included, connection)
10
+ elsif (link = link_from(info))
11
+ SingularLinkRelationship.new(model_class, connection, link)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ require 'simple_jsonapi_client/redirection/proxy'
2
+
3
+ module SimpleJSONAPIClient
4
+ module Relationships
5
+ class LinkRelationshipProxy < ::SimpleJSONAPIClient::Redirection::Proxy
6
+ def initialize(klass, connection, url)
7
+ @klass = klass
8
+ @connection = connection
9
+ @url = url
10
+ end
11
+
12
+ private
13
+
14
+ def pseudo_inspect
15
+ "#<#{self.class.name} model_class=#{@klass} url=#{@url}>"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ module SimpleJSONAPIClient
2
+ module Relationships
3
+ class Relationship
4
+ def initialize(model_class, url_opts = {})
5
+ @model_class = model_class
6
+ @url_opts = url_opts
7
+ end
8
+
9
+ def call(*args)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ private
14
+ attr_reader :url_opts
15
+
16
+ def model_class
17
+ @evaluated_model_class ||=
18
+ case @model_class
19
+ when String
20
+ Kernel::const_get(@model_class)
21
+ else
22
+ @model_class
23
+ end
24
+ end
25
+
26
+ def link_from(info)
27
+ info['links'].to_h.values_at('related', 'self').compact.first
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ require 'simple_jsonapi_client/relationships/has_many_relationship'
34
+ require 'simple_jsonapi_client/relationships/has_one_relationship'
@@ -0,0 +1,17 @@
1
+ require 'simple_jsonapi_client/relationships/data_relationship_proxy'
2
+
3
+ module SimpleJSONAPIClient
4
+ module Relationships
5
+ class SingularDataRelationship < DataRelationshipProxy
6
+ private
7
+
8
+ def pseudo_inspect
9
+ "#<#{self.class.name} model_class=#{@klass} id=#{@record_or_records['id']}}>"
10
+ end
11
+
12
+ def fetch_internal_object
13
+ instantiated_relationship_record(@record_or_records)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ require 'simple_jsonapi_client/relationships/link_relationship_proxy'
2
+
3
+ module SimpleJSONAPIClient
4
+ module Relationships
5
+ class SingularLinkRelationship < LinkRelationshipProxy
6
+ private
7
+
8
+ def fetch_internal_object
9
+ @klass.fetch(connection: @connection, url: @url)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleJSONAPIClient
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "simple_jsonapi_client/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "simple_jsonapi_client"
8
+ spec.version = SimpleJSONAPIClient::VERSION
9
+ spec.authors = ["Ariel Caplan"]
10
+ spec.email = ["arielmcaplan@gmail.com"]
11
+
12
+ spec.summary = %q{Framework for writing clients for JSONAPI APIs in Ruby.}
13
+ spec.homepage = "https://github.com/amcaplan/simple_jsonapi_client"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "faraday", "~> 0.12"
24
+ spec.add_dependency "faraday_middleware", "~> 0.11"
25
+ spec.add_dependency "activesupport", ">= 3.1.12", "< 6"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.15"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ spec.add_development_dependency "rspec", "~> 3.0"
30
+ spec.add_development_dependency "pry", "~> 0"
31
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_jsonapi_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ariel Caplan
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-11-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.12'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday_middleware
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.11'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.1.12
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '6'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 3.1.12
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '6'
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.15'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.15'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '10.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '10.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rspec
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: pry
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ description:
118
+ email:
119
+ - arielmcaplan@gmail.com
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".gitignore"
125
+ - ".rspec"
126
+ - ".travis.yml"
127
+ - CODE_OF_CONDUCT.md
128
+ - Dockerfile
129
+ - Gemfile
130
+ - LICENSE.txt
131
+ - README.md
132
+ - Rakefile
133
+ - bin/console
134
+ - bin/development_start
135
+ - bin/rails
136
+ - bin/setup
137
+ - bin/wait_for_it
138
+ - docker-compose.yml
139
+ - lib/simple_jsonapi_client.rb
140
+ - lib/simple_jsonapi_client/base.rb
141
+ - lib/simple_jsonapi_client/error.rb
142
+ - lib/simple_jsonapi_client/errors/api_error.rb
143
+ - lib/simple_jsonapi_client/redirection/fetch_all.rb
144
+ - lib/simple_jsonapi_client/redirection/proxy.rb
145
+ - lib/simple_jsonapi_client/relationships/array_data_relationship.rb
146
+ - lib/simple_jsonapi_client/relationships/array_link_relationship.rb
147
+ - lib/simple_jsonapi_client/relationships/data_relationship_proxy.rb
148
+ - lib/simple_jsonapi_client/relationships/has_many_relationship.rb
149
+ - lib/simple_jsonapi_client/relationships/has_one_relationship.rb
150
+ - lib/simple_jsonapi_client/relationships/link_relationship_proxy.rb
151
+ - lib/simple_jsonapi_client/relationships/relationship.rb
152
+ - lib/simple_jsonapi_client/relationships/singular_data_relationship.rb
153
+ - lib/simple_jsonapi_client/relationships/singular_link_relationship.rb
154
+ - lib/simple_jsonapi_client/version.rb
155
+ - simple_jsonapi_client.gemspec
156
+ homepage: https://github.com/amcaplan/simple_jsonapi_client
157
+ licenses:
158
+ - MIT
159
+ metadata: {}
160
+ post_install_message:
161
+ rdoc_options: []
162
+ require_paths:
163
+ - lib
164
+ required_ruby_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ required_rubygems_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ requirements: []
175
+ rubyforge_project:
176
+ rubygems_version: 2.7.1
177
+ signing_key:
178
+ specification_version: 4
179
+ summary: Framework for writing clients for JSONAPI APIs in Ruby.
180
+ test_files: []