carwow-json_api_client 1.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +706 -0
  4. data/Rakefile +32 -0
  5. data/lib/json_api_client.rb +30 -0
  6. data/lib/json_api_client/associations.rb +8 -0
  7. data/lib/json_api_client/associations/base_association.rb +33 -0
  8. data/lib/json_api_client/associations/belongs_to.rb +31 -0
  9. data/lib/json_api_client/associations/has_many.rb +8 -0
  10. data/lib/json_api_client/associations/has_one.rb +16 -0
  11. data/lib/json_api_client/connection.rb +41 -0
  12. data/lib/json_api_client/error_collector.rb +91 -0
  13. data/lib/json_api_client/errors.rb +107 -0
  14. data/lib/json_api_client/formatter.rb +145 -0
  15. data/lib/json_api_client/helpers.rb +9 -0
  16. data/lib/json_api_client/helpers/associatable.rb +88 -0
  17. data/lib/json_api_client/helpers/callbacks.rb +27 -0
  18. data/lib/json_api_client/helpers/dirty.rb +75 -0
  19. data/lib/json_api_client/helpers/dynamic_attributes.rb +78 -0
  20. data/lib/json_api_client/helpers/uri.rb +9 -0
  21. data/lib/json_api_client/implementation.rb +12 -0
  22. data/lib/json_api_client/included_data.rb +58 -0
  23. data/lib/json_api_client/linking.rb +6 -0
  24. data/lib/json_api_client/linking/links.rb +22 -0
  25. data/lib/json_api_client/linking/top_level_links.rb +39 -0
  26. data/lib/json_api_client/meta_data.rb +19 -0
  27. data/lib/json_api_client/middleware.rb +7 -0
  28. data/lib/json_api_client/middleware/json_request.rb +26 -0
  29. data/lib/json_api_client/middleware/parse_json.rb +31 -0
  30. data/lib/json_api_client/middleware/status.rb +67 -0
  31. data/lib/json_api_client/paginating.rb +6 -0
  32. data/lib/json_api_client/paginating/nested_param_paginator.rb +140 -0
  33. data/lib/json_api_client/paginating/paginator.rb +89 -0
  34. data/lib/json_api_client/parsers.rb +5 -0
  35. data/lib/json_api_client/parsers/parser.rb +102 -0
  36. data/lib/json_api_client/query.rb +6 -0
  37. data/lib/json_api_client/query/builder.rb +239 -0
  38. data/lib/json_api_client/query/requestor.rb +73 -0
  39. data/lib/json_api_client/relationships.rb +6 -0
  40. data/lib/json_api_client/relationships/relations.rb +55 -0
  41. data/lib/json_api_client/relationships/top_level_relations.rb +30 -0
  42. data/lib/json_api_client/request_params.rb +57 -0
  43. data/lib/json_api_client/resource.rb +643 -0
  44. data/lib/json_api_client/result_set.rb +25 -0
  45. data/lib/json_api_client/schema.rb +154 -0
  46. data/lib/json_api_client/utils.rb +48 -0
  47. data/lib/json_api_client/version.rb +3 -0
  48. metadata +213 -0
@@ -0,0 +1,7 @@
1
+ module JsonApiClient
2
+ module Middleware
3
+ autoload :JsonRequest, 'json_api_client/middleware/json_request'
4
+ autoload :ParseJson, 'json_api_client/middleware/parse_json'
5
+ autoload :Status, 'json_api_client/middleware/status'
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ module JsonApiClient
2
+ module Middleware
3
+ class JsonRequest < Faraday::Middleware
4
+ def call(environment)
5
+ accept_header = update_accept_header(environment[:request_headers])
6
+
7
+ environment[:request_headers]["Content-Type"] = 'application/vnd.api+json'
8
+ environment[:request_headers]["Accept"] = accept_header
9
+ @app.call(environment)
10
+ end
11
+
12
+ private
13
+
14
+ def update_accept_header(headers)
15
+ return 'application/vnd.api+json' if headers["Accept"].nil?
16
+ accept_params = headers["Accept"].split(",")
17
+
18
+ unless accept_params.include?('application/vnd.api+json')
19
+ accept_params.unshift('application/vnd.api+json')
20
+ end
21
+
22
+ accept_params.join(",")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ module JsonApiClient
2
+ module Middleware
3
+ class ParseJson < Faraday::Middleware
4
+
5
+ def call(environment)
6
+ @app.call(environment).on_complete do |env|
7
+ if process_response_type?(response_type(env))
8
+ env[:raw_body] = env[:body]
9
+ env[:body] = parse(env[:body])
10
+ end
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def parse(body)
17
+ ::JSON.parse(body) unless body.strip.empty?
18
+ end
19
+
20
+ def response_type(env)
21
+ type = env[:response_headers]['Content-Type'].to_s
22
+ type = type.split(';', 2).first if type.index(';')
23
+ type
24
+ end
25
+
26
+ def process_response_type?(type)
27
+ !!type.match(/\bjson$/)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ module JsonApiClient
2
+ module Middleware
3
+ class Status < Faraday::Middleware
4
+ def initialize(app, options)
5
+ super(app)
6
+ @options = options
7
+ end
8
+
9
+ def call(environment)
10
+ @app.call(environment).on_complete do |env|
11
+ handle_status(env[:status], env)
12
+
13
+ # look for meta[:status]
14
+ if env[:body].is_a?(Hash)
15
+ code = env[:body].fetch("meta", {}).fetch("status", 200).to_i
16
+ handle_status(code, env)
17
+ end
18
+ end
19
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
20
+ raise Errors::ConnectionError.new environment, e.to_s
21
+ end
22
+
23
+ private
24
+
25
+ def custom_handler_for(code)
26
+ @options.fetch(:custom_handlers, {})[code]
27
+ end
28
+
29
+ def handle_status(code, env)
30
+ custom_handler = custom_handler_for(code)
31
+ return custom_handler.call(env) if custom_handler.present?
32
+
33
+ case code
34
+ when 200..399
35
+ when 401
36
+ raise Errors::NotAuthorized, env
37
+ when 403
38
+ raise Errors::AccessDenied, env
39
+ when 404
40
+ raise Errors::NotFound, env[:url]
41
+ when 408
42
+ raise Errors::RequestTimeout, env
43
+ when 409
44
+ raise Errors::Conflict, env
45
+ when 422
46
+ # Allow to proceed as resource errors will be populated
47
+ when 429
48
+ raise Errors::TooManyRequests, env
49
+ when 400..499
50
+ raise Errors::ClientError, env
51
+ when 500
52
+ raise Errors::InternalServerError, env
53
+ when 502
54
+ raise Errors::BadGateway, env
55
+ when 503
56
+ raise Errors::ServiceUnavailable, env
57
+ when 504
58
+ raise Errors::GatewayTimeout, env
59
+ when 501..599
60
+ raise Errors::ServerError, env
61
+ else
62
+ raise Errors::UnexpectedStatus.new(code, env[:url])
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,6 @@
1
+ module JsonApiClient
2
+ module Paginating
3
+ autoload :Paginator, 'json_api_client/paginating/paginator'
4
+ autoload :NestedParamPaginator, 'json_api_client/paginating/nested_param_paginator'
5
+ end
6
+ end
@@ -0,0 +1,140 @@
1
+ module JsonApiClient
2
+ module Paginating
3
+ # An alternate, more consistent Paginator that always wraps
4
+ # pagination query string params in a top-level wrapper_name,
5
+ # e.g. page[offset]=2, page[limit]=10.
6
+ class NestedParamPaginator
7
+ DEFAULT_WRAPPER_NAME = "page".freeze
8
+ DEFAULT_PAGE_PARAM = "page".freeze
9
+ DEFAULT_PER_PAGE_PARAM = "per_page".freeze
10
+
11
+ # Define class accessors as methods to enforce standard way
12
+ # of defining pagination related query string params.
13
+ class << self
14
+
15
+ def wrapper_name
16
+ @_wrapper_name ||= DEFAULT_WRAPPER_NAME
17
+ end
18
+
19
+ def wrapper_name=(param = DEFAULT_WRAPPER_NAME)
20
+ raise ArgumentError, "don't wrap wrapper_name" unless valid_param?(param)
21
+
22
+ @_wrapper_name = param.to_s
23
+ end
24
+
25
+ def page_param
26
+ @_page_param ||= DEFAULT_PAGE_PARAM
27
+ "#{wrapper_name}[#{@_page_param}]"
28
+ end
29
+
30
+ def page_param=(param = DEFAULT_PAGE_PARAM)
31
+ raise ArgumentError, "don't wrap page_param" unless valid_param?(param)
32
+
33
+ @_page_param = param.to_s
34
+ end
35
+
36
+ def per_page_param
37
+ @_per_page_param ||= DEFAULT_PER_PAGE_PARAM
38
+ "#{wrapper_name}[#{@_per_page_param}]"
39
+ end
40
+
41
+ def per_page_param=(param = DEFAULT_PER_PAGE_PARAM)
42
+ raise ArgumentError, "don't wrap per_page_param" unless valid_param?(param)
43
+
44
+ @_per_page_param = param
45
+ end
46
+
47
+ private
48
+
49
+ def valid_param?(param)
50
+ !(param.nil? || param.to_s.include?("[") || param.to_s.include?("]"))
51
+ end
52
+
53
+ end
54
+
55
+ attr_reader :params, :result_set, :links
56
+
57
+ def initialize(result_set, data)
58
+ @params = params_for_uri(result_set.uri)
59
+ @result_set = result_set
60
+ @links = data["links"]
61
+ end
62
+
63
+ def next
64
+ result_set.links.fetch_link("next")
65
+ end
66
+
67
+ def prev
68
+ result_set.links.fetch_link("prev")
69
+ end
70
+
71
+ def first
72
+ result_set.links.fetch_link("first")
73
+ end
74
+
75
+ def last
76
+ result_set.links.fetch_link("last")
77
+ end
78
+
79
+ def total_pages
80
+ if links["last"]
81
+ uri = result_set.links.link_url_for("last")
82
+ last_params = params_for_uri(uri)
83
+ last_params.fetch(page_param, &method(:current_page)).to_i
84
+ else
85
+ current_page
86
+ end
87
+ end
88
+
89
+ # this is an estimate, not necessarily an exact count
90
+ def total_entries
91
+ per_page * total_pages
92
+ end
93
+ def total_count; total_entries; end
94
+
95
+ def offset
96
+ per_page * (current_page - 1)
97
+ end
98
+
99
+ def per_page
100
+ params.fetch(per_page_param) do
101
+ result_set.length
102
+ end.to_i
103
+ end
104
+
105
+ def current_page
106
+ params.fetch(page_param, 1).to_i
107
+ end
108
+
109
+ def out_of_bounds?
110
+ current_page > total_pages
111
+ end
112
+
113
+ def previous_page
114
+ current_page > 1 ? (current_page - 1) : nil
115
+ end
116
+
117
+ def next_page
118
+ current_page < total_pages ? (current_page + 1) : nil
119
+ end
120
+
121
+ def page_param
122
+ self.class.page_param
123
+ end
124
+
125
+ def per_page_param
126
+ self.class.per_page_param
127
+ end
128
+
129
+ alias limit_value per_page
130
+
131
+ protected
132
+
133
+ def params_for_uri(uri)
134
+ return {} unless uri
135
+ uri = Addressable::URI.parse(uri)
136
+ ( uri.query_values || {} ).with_indifferent_access
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,89 @@
1
+ module JsonApiClient
2
+ module Paginating
3
+ class Paginator
4
+ class_attribute :page_param,
5
+ :per_page_param
6
+
7
+ self.page_param = "page"
8
+ self.per_page_param = "per_page"
9
+
10
+ attr_reader :params, :result_set, :links
11
+
12
+ def initialize(result_set, data)
13
+ @params = params_for_uri(result_set.uri)
14
+ @result_set = result_set
15
+ @links = data["links"]
16
+ end
17
+
18
+ def next
19
+ result_set.links.fetch_link("next")
20
+ end
21
+
22
+ def prev
23
+ result_set.links.fetch_link("prev")
24
+ end
25
+
26
+ def first
27
+ result_set.links.fetch_link("first")
28
+ end
29
+
30
+ def last
31
+ result_set.links.fetch_link("last")
32
+ end
33
+
34
+ def total_pages
35
+ if links["last"]
36
+ uri = result_set.links.link_url_for("last")
37
+ last_params = params_for_uri(uri)
38
+ last_params.fetch(page_param) do
39
+ current_page
40
+ end.to_i
41
+ else
42
+ current_page
43
+ end
44
+ end
45
+
46
+ # this number may be off
47
+ def total_entries
48
+ per_page * total_pages
49
+ end
50
+ def total_count; total_entries; end
51
+
52
+ def offset
53
+ per_page * (current_page - 1)
54
+ end
55
+
56
+ def per_page
57
+ params.fetch(per_page_param) do
58
+ result_set.length
59
+ end.to_i
60
+ end
61
+
62
+ def current_page
63
+ params.fetch(page_param, 1).to_i
64
+ end
65
+
66
+ def out_of_bounds?
67
+ current_page > total_pages
68
+ end
69
+
70
+ def previous_page
71
+ current_page > 1 ? (current_page - 1) : nil
72
+ end
73
+
74
+ def next_page
75
+ current_page < total_pages ? (current_page + 1) : nil
76
+ end
77
+
78
+ alias limit_value per_page
79
+
80
+ protected
81
+
82
+ def params_for_uri(uri)
83
+ return {} unless uri
84
+ uri = Addressable::URI.parse(uri)
85
+ ( uri.query_values || {} ).with_indifferent_access
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,5 @@
1
+ module JsonApiClient
2
+ module Parsers
3
+ autoload :Parser, 'json_api_client/parsers/parser'
4
+ end
5
+ end
@@ -0,0 +1,102 @@
1
+ module JsonApiClient
2
+ module Parsers
3
+ class Parser
4
+ class << self
5
+ def parse(klass, response)
6
+ data = response.body.present? ? response.body : {}
7
+
8
+ ResultSet.new.tap do |result_set|
9
+ result_set.record_class = klass
10
+ result_set.uri = response.env[:url]
11
+ handle_json_api(result_set, data)
12
+ handle_data(result_set, data)
13
+ handle_errors(result_set, data)
14
+ handle_meta(result_set, data)
15
+ handle_links(result_set, data)
16
+ handle_relationships(result_set, data)
17
+ handle_pagination(result_set, data)
18
+ handle_included(result_set, data)
19
+ end
20
+ end
21
+
22
+ #
23
+ # Given a resource hash, returns a Resource.new friendly hash
24
+ # which flattens the attributes in w/ id and type.
25
+ #
26
+ # Example:
27
+ #
28
+ # Given:
29
+ # {
30
+ # id: 1.
31
+ # type: 'person',
32
+ # attributes: {
33
+ # first_name: 'Jeff',
34
+ # last_name: 'Ching'
35
+ # },
36
+ # links: {...},
37
+ # relationships: {...}
38
+ # }
39
+ #
40
+ # Returns:
41
+ # {
42
+ # id: 1,
43
+ # type: 'person',
44
+ # first_name: 'Jeff',
45
+ # last_name: 'Ching'
46
+ # links: {...},
47
+ # relationships: {...}
48
+ # }
49
+ #
50
+ #
51
+ def parameters_from_resource(params)
52
+ attrs = params.slice('id', 'links', 'meta', 'type', 'relationships')
53
+ attrs.merge(params.fetch('attributes', {}))
54
+ end
55
+
56
+ private
57
+
58
+ def handle_json_api(result_set, data)
59
+ result_set.implementation = Implementation.new(data.fetch("jsonapi", {}))
60
+ end
61
+
62
+ def handle_data(result_set, data)
63
+ # all data lives under the "data" attribute
64
+ results = data.fetch("data", [])
65
+
66
+ # we will treat everything as an Array
67
+ results = [results] unless results.is_a?(Array)
68
+ resources = results.compact.map do |res|
69
+ resource = result_set.record_class.load(parameters_from_resource(res))
70
+ resource.last_result_set = result_set
71
+ resource
72
+ end
73
+ result_set.concat(resources)
74
+ end
75
+
76
+ def handle_errors(result_set, data)
77
+ result_set.errors = ErrorCollector.new(data.fetch("errors", []))
78
+ end
79
+
80
+ def handle_meta(result_set, data)
81
+ result_set.meta = MetaData.new(data.fetch("meta", {}), result_set.record_class)
82
+ end
83
+
84
+ def handle_links(result_set, data)
85
+ result_set.links = Linking::TopLevelLinks.new(result_set.record_class, data.fetch("links", {}))
86
+ end
87
+
88
+ def handle_relationships(result_set, data)
89
+ result_set.relationships = Relationships::TopLevelRelations.new(result_set.record_class, data.fetch("relationships", {}))
90
+ end
91
+
92
+ def handle_pagination(result_set, data)
93
+ result_set.pages = result_set.record_class.paginator.new(result_set, data)
94
+ end
95
+
96
+ def handle_included(result_set, data)
97
+ result_set.included = IncludedData.new(result_set, data.fetch("included", []))
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end