easy-jsonapi 1.0.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 (91) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/publish-gem.yml +60 -0
  3. data/.github/workflows/rake.yml +35 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +5 -0
  9. data/Gemfile.lock +106 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +209 -0
  12. data/Rakefile +20 -0
  13. data/UsingTheRequestObject.md +74 -0
  14. data/UsingUserConfigurations.md +95 -0
  15. data/bin/bundle +114 -0
  16. data/bin/console +15 -0
  17. data/bin/htmldiff +29 -0
  18. data/bin/kramdown +29 -0
  19. data/bin/ldiff +29 -0
  20. data/bin/license_finder +29 -0
  21. data/bin/license_finder_pip.py +29 -0
  22. data/bin/maruku +29 -0
  23. data/bin/marutex +29 -0
  24. data/bin/nokogiri +29 -0
  25. data/bin/racc +29 -0
  26. data/bin/rackup +29 -0
  27. data/bin/rake +29 -0
  28. data/bin/redcarpet +29 -0
  29. data/bin/reverse_markdown +29 -0
  30. data/bin/rspec +29 -0
  31. data/bin/rubocop +29 -0
  32. data/bin/ruby-parse +29 -0
  33. data/bin/ruby-rewrite +29 -0
  34. data/bin/setup +8 -0
  35. data/bin/solargraph +29 -0
  36. data/bin/thor +29 -0
  37. data/bin/tilt +29 -0
  38. data/bin/yard +29 -0
  39. data/bin/yardoc +29 -0
  40. data/bin/yri +29 -0
  41. data/easy-jsonapi.gemspec +39 -0
  42. data/lib/easy/jsonapi.rb +12 -0
  43. data/lib/easy/jsonapi/collection.rb +144 -0
  44. data/lib/easy/jsonapi/config_manager.rb +144 -0
  45. data/lib/easy/jsonapi/config_manager/config.rb +49 -0
  46. data/lib/easy/jsonapi/document.rb +71 -0
  47. data/lib/easy/jsonapi/document/error.rb +48 -0
  48. data/lib/easy/jsonapi/document/error/error_member.rb +15 -0
  49. data/lib/easy/jsonapi/document/jsonapi.rb +26 -0
  50. data/lib/easy/jsonapi/document/jsonapi/jsonapi_member.rb +15 -0
  51. data/lib/easy/jsonapi/document/links.rb +36 -0
  52. data/lib/easy/jsonapi/document/links/link.rb +15 -0
  53. data/lib/easy/jsonapi/document/meta.rb +26 -0
  54. data/lib/easy/jsonapi/document/meta/meta_member.rb +14 -0
  55. data/lib/easy/jsonapi/document/resource.rb +56 -0
  56. data/lib/easy/jsonapi/document/resource/attributes.rb +37 -0
  57. data/lib/easy/jsonapi/document/resource/attributes/attribute.rb +29 -0
  58. data/lib/easy/jsonapi/document/resource/relationships.rb +40 -0
  59. data/lib/easy/jsonapi/document/resource/relationships/relationship.rb +50 -0
  60. data/lib/easy/jsonapi/document/resource_id.rb +28 -0
  61. data/lib/easy/jsonapi/exceptions.rb +27 -0
  62. data/lib/easy/jsonapi/exceptions/document_exceptions.rb +619 -0
  63. data/lib/easy/jsonapi/exceptions/headers_exceptions.rb +156 -0
  64. data/lib/easy/jsonapi/exceptions/naming_exceptions.rb +36 -0
  65. data/lib/easy/jsonapi/exceptions/query_params_exceptions.rb +67 -0
  66. data/lib/easy/jsonapi/exceptions/user_defined_exceptions.rb +253 -0
  67. data/lib/easy/jsonapi/field.rb +43 -0
  68. data/lib/easy/jsonapi/header_collection.rb +38 -0
  69. data/lib/easy/jsonapi/header_collection/header.rb +11 -0
  70. data/lib/easy/jsonapi/item.rb +88 -0
  71. data/lib/easy/jsonapi/middleware.rb +158 -0
  72. data/lib/easy/jsonapi/name_value_pair.rb +72 -0
  73. data/lib/easy/jsonapi/name_value_pair_collection.rb +78 -0
  74. data/lib/easy/jsonapi/parser.rb +38 -0
  75. data/lib/easy/jsonapi/parser/document_parser.rb +196 -0
  76. data/lib/easy/jsonapi/parser/headers_parser.rb +33 -0
  77. data/lib/easy/jsonapi/parser/rack_req_params_parser.rb +117 -0
  78. data/lib/easy/jsonapi/request.rb +40 -0
  79. data/lib/easy/jsonapi/request/query_param_collection.rb +56 -0
  80. data/lib/easy/jsonapi/request/query_param_collection/fields_param.rb +32 -0
  81. data/lib/easy/jsonapi/request/query_param_collection/fields_param/fieldset.rb +34 -0
  82. data/lib/easy/jsonapi/request/query_param_collection/filter_param.rb +28 -0
  83. data/lib/easy/jsonapi/request/query_param_collection/filter_param/filter.rb +34 -0
  84. data/lib/easy/jsonapi/request/query_param_collection/include_param.rb +119 -0
  85. data/lib/easy/jsonapi/request/query_param_collection/page_param.rb +55 -0
  86. data/lib/easy/jsonapi/request/query_param_collection/query_param.rb +47 -0
  87. data/lib/easy/jsonapi/request/query_param_collection/sort_param.rb +25 -0
  88. data/lib/easy/jsonapi/response.rb +22 -0
  89. data/lib/easy/jsonapi/utility.rb +158 -0
  90. data/lib/easy/jsonapi/version.rb +8 -0
  91. metadata +248 -0
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'easy/jsonapi/item'
4
+
5
+ module JSONAPI
6
+ # Field is the name of key value pair
7
+ class Field < JSONAPI::Item
8
+
9
+ # @param name [String] The name of the field
10
+ # @param type [String | nil] The type of the field
11
+ def initialize(name, type: String)
12
+ super({ name: name.to_s, type: type })
13
+ end
14
+
15
+ # @return [String] The Field's name
16
+ def name
17
+ @item[:name]
18
+ end
19
+
20
+ # @raise RunTimeError You shoulddn't be able to update the name of a
21
+ # Resource::Field
22
+ def name=(_)
23
+ raise 'Cannot change the name of a Resource::Field'
24
+ end
25
+
26
+ # @return [Object] The type of the field
27
+ def type
28
+ @item[:type]
29
+ end
30
+
31
+ # @param new_type [Object] The new type of field.
32
+ def type=(new_type)
33
+ @item[:type] = new_type
34
+ end
35
+
36
+ # @return [String] The name of the field.
37
+ def to_s
38
+ name
39
+ end
40
+
41
+ private :method_missing, :item, :item=
42
+ end
43
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'easy/jsonapi/name_value_pair_collection'
4
+
5
+ module JSONAPI
6
+ # header_collection # { include: Include, sort: Sort, filter: Filter }
7
+ class HeaderCollection < JSONAPI::NameValuePairCollection
8
+
9
+ # Initialize as empty if a array of Header objects not passed to it.
10
+ # @param header_arr [JSONAPI::HeaderCollection::Header] The array of Header objects that can be used to init
11
+ # a Header collection
12
+ # @return JSONAPI::HeaderCollection
13
+ def initialize(header_arr = [])
14
+ super(header_arr, item_type: JSONAPI::HeaderCollection::Header)
15
+ end
16
+
17
+ # Add a header to the collection. (CASE-INSENSITIVE).
18
+ # @param header [JSONAPI::HeaderCollection::Header] The header to add
19
+ def add(header)
20
+ super(header) { |hdr| hdr.name.downcase.gsub(/-/, '_') }
21
+ end
22
+
23
+ # Call super's get but make it case insensitive
24
+ # @param key [Symbol] The hash key associated with a header
25
+ def get(key)
26
+ super(key.to_s.downcase.gsub(/-/, '_'))
27
+ end
28
+
29
+ # #empyt? provided by super
30
+ # #include provided by super
31
+ # add provided by super
32
+ # #each provided from super
33
+ # #remove provided from super
34
+ # #keys provided by super
35
+ # #size provided by super
36
+
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'easy/jsonapi/header_collection'
4
+
5
+ module JSONAPI
6
+ class HeaderCollection
7
+ # A http request or response header
8
+ class Header < JSONAPI::NameValuePair
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+
5
+ # Models a Item's key -> value relationship
6
+ class Item
7
+
8
+ # @return the value of an Item
9
+ attr_accessor :item
10
+
11
+ # Able to take a hash and dynamically create instance variables using the hash keys
12
+ # Ex: obj == { :name => 'fields', :value => {'articles' => 'title,body,author', 'people' => 'name' }}
13
+ # @param obj [Object] Can be anything, but if a hash is provided, dynamic instance variable can be created
14
+ # upon trying to access them.
15
+ def initialize(obj)
16
+ if obj.is_a? Hash
17
+ ensure_keys_are_sym(obj)
18
+ end
19
+ @item = obj
20
+ end
21
+
22
+ # A special to_string method if @item is a hash.
23
+ def to_s
24
+ return @item.to_s unless @item.is_a? Hash
25
+ tr = '{ '
26
+ first = true
27
+ @item.each do |k, v|
28
+ if first
29
+ first = false
30
+ tr += "\"#{k}\": \"#{v}\", "
31
+ else
32
+ tr += "\"#{k}\": \"#{v}\""
33
+ end
34
+ end
35
+ tr += ' }'
36
+ end
37
+
38
+ # Represent item as a hash
39
+ def to_h
40
+ @item.to_h
41
+ end
42
+
43
+ private
44
+
45
+ # Only used if implementing Item directly.
46
+ # dynamically creates accessor methods for instance variables
47
+ # created in the initialize
48
+ def method_missing(method_name, *args, &block)
49
+ return super unless is_a? JSONAPI::Item
50
+ return super unless @item.is_a? Hash
51
+ if should_update_var?(method_name)
52
+ @item[method_name[..-2].to_sym] = args[0]
53
+ elsif should_get_var?(method_name)
54
+ @item[method_name]
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ # Needed when using #method_missing
61
+ def respond_to_missing?(method_name, *args)
62
+ instance_variables.include?("@#{method_name}".to_sym) || super
63
+ end
64
+
65
+ # Ensures that hash keys are symbol (and not String) when passing a hash to item.
66
+ # @param obj [Object] A hash that can represent an item.
67
+ def ensure_keys_are_sym(obj)
68
+ obj.each_key do |k|
69
+ raise "All keys must be Symbols. '#{k}' was #{k.class}" unless k.is_a? Symbol
70
+ end
71
+ end
72
+
73
+ # Checks to see if the method name has a '=' at the end and if the
74
+ # prefix before the '=' has the same name as an existing instance
75
+ # variable.
76
+ # @param (see #method_missing)
77
+ def should_update_var?(method_name)
78
+ method_name.to_s[-1] == '=' && @item[method_name[..-2].to_sym].nil? == false
79
+ end
80
+
81
+ # Checks to see if the method has the same name as an existing instance
82
+ # variable
83
+ # @param (see #method_missing)
84
+ def should_get_var?(method_name)
85
+ @item[method_name].nil? == false
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'easy/jsonapi/exceptions'
4
+ require 'easy/jsonapi/config_manager'
5
+ require 'oj'
6
+
7
+ module JSONAPI
8
+
9
+ # The middleware of the gem and also the contact point between the
10
+ # the gem and the rack application using it
11
+ class Middleware
12
+ # @param app The Rack Application
13
+ def initialize(app, &block)
14
+ @app = app
15
+ return unless block_given?
16
+
17
+ @config_manager = JSONAPI::ConfigManager.new
18
+ block.call(@config_manager)
19
+ end
20
+
21
+ # If not in maintenance_mode and the request is intended to be JSONAPI,
22
+ # it checks headers, params, and body it for compliance and raises
23
+ # and error if any section is found to be non-compliant.
24
+ # @param env The rack envirornment hash
25
+ def call(env)
26
+ if in_maintenance_mode?(env)
27
+ return maintenance_response(env)
28
+ end
29
+
30
+ if jsonapi_request?(env)
31
+ error_response = check_compliance(env, @config_manager)
32
+ return error_response unless error_response.nil?
33
+ end
34
+
35
+ @app.call(env)
36
+ end
37
+
38
+ private
39
+
40
+ # Checks the 'MAINTENANCE' environment variable
41
+ # @param (see #call)
42
+ # @return [TrueClass | FalseClass]
43
+ def in_maintenance_mode?(env)
44
+ !env['MAINTENANCE'].nil?
45
+ end
46
+
47
+ # Return 503 with or without msg depending on environment
48
+ # @param (see #call)
49
+ # @return [Array] Http Error Responses
50
+ def maintenance_response(env)
51
+ if environment_development?(env)
52
+ [503, {}, ['MAINTENANCE envirornment variable set']]
53
+ else
54
+ [503, {}, []]
55
+ end
56
+ end
57
+
58
+ # If the Content-type or Accept header values include the JSON:API media type without media
59
+ # parameters, then it is a jsonapi request.
60
+ # @param (see #call)
61
+ def jsonapi_request?(env)
62
+ accept_header_jsonapi?(env) || content_type_header_jsonapi?(env)
63
+ end
64
+
65
+ # Determines whether the request is JSONAPI or not by looking at
66
+ # the ACCEPT header.
67
+ # @env (see #call)
68
+ # @return [TrueClass | FalseClass] Whether or not the request is JSONAPI
69
+ def accept_header_jsonapi?(env)
70
+ return true if env['HTTP_ACCEPT'].nil? # no header means assume any
71
+
72
+ env['HTTP_ACCEPT'].split(',').any? do |hdr|
73
+ ['application/vnd.api+json', '*/*', 'application/*'].include?(hdr.split(';').first)
74
+ end
75
+ end
76
+
77
+ # Determines whether there is a request body, and whether the Content-Type is jsonapi compliant.
78
+ # @param (see #call)
79
+ # @return [TrueClass | FalseClass] Whether the document body is supposed to be jsonapi
80
+ def content_type_header_jsonapi?(env)
81
+ return false unless env['CONTENT_TYPE']
82
+
83
+ env['CONTENT_TYPE'].include? 'application/vnd.api+json'
84
+ end
85
+
86
+ # Checks whether the request is JSON:API compliant and raises an error if not.
87
+ # @param env (see #call)
88
+ # @param config_manager [JSONAPI::ConfigManager::Config] The config object to use modify compliance checking
89
+ # @return [NilClass | Array] Nil meaning no error or a 400 level http response
90
+ def check_compliance(env, config_manager)
91
+ opts = { http_method: env['REQUEST_METHOD'], path: env['PATH_INFO'] }
92
+
93
+ header_error = check_headers_compliance(env, config_manager, opts)
94
+ return header_error unless header_error.nil?
95
+
96
+ req = Rack::Request.new(env)
97
+ param_error = check_query_param_compliance(env, req.GET, config_manager, opts)
98
+ return param_error unless param_error.nil?
99
+
100
+ return unless env['CONTENT_TYPE']
101
+
102
+ body_error = check_req_body_compliance(env, config_manager, opts)
103
+ return body_error unless body_error.nil?
104
+ end
105
+
106
+ # Checks whether the http headers are jsonapi compliant
107
+ # @param (see #call)
108
+ # @return [NilClass | Array] Nil meaning no error or a 400 level http response
109
+ def check_headers_compliance(env, config_manager, opts)
110
+ JSONAPI::Exceptions::HeadersExceptions.check_request(env, config_manager, opts)
111
+ rescue JSONAPI::Exceptions::HeadersExceptions::InvalidHeader || JSONAPI::Exceptions::UserDefinedExceptions::InvalidHeader => e
112
+ raise if environment_development?(env)
113
+
114
+ [e.status_code, {}, []]
115
+ end
116
+
117
+ # @param query_params [Hash] The rack request query_param hash
118
+ # @raise If the query parameters are not JSONAPI compliant
119
+ # @return [NilClass | Array] Nil meaning no error or a 400 level http response
120
+ def check_query_param_compliance(env, query_params, config_manager, opts)
121
+ JSONAPI::Exceptions::QueryParamsExceptions.check_compliance(query_params, config_manager, opts)
122
+ rescue JSONAPI::Exceptions::QueryParamsExceptions::InvalidQueryParameter || JSONAPI::Exceptions::UserDefinedExceptions::InvalidQueryParam => e
123
+ raise if environment_development?(env)
124
+
125
+ [e.status_code, {}, []]
126
+ end
127
+
128
+ # @param env (see #call)
129
+ # @param req (see #check_query_param_compliance)
130
+ # @raise If the document body is not JSONAPI compliant
131
+ def check_req_body_compliance(env, config_manager, opts)
132
+ # Store separately so you can rewind for next middleware or app
133
+ body = env['rack.input'].read
134
+ env['rack.input'].rewind
135
+ JSONAPI::Exceptions::DocumentExceptions.check_compliance(body, config_manager, opts)
136
+ rescue JSONAPI::Exceptions::DocumentExceptions::InvalidDocument || JSONAPI::Exceptions::UserDefinedExceptions::InvalidDocument => e
137
+ raise if environment_development?(env)
138
+
139
+ [e.status_code, {}, []]
140
+ rescue Oj::ParseError
141
+ raise if environment_development?(env)
142
+
143
+ [400, {}, []]
144
+ end
145
+
146
+ # @param (see #call)
147
+ def post_put_or_patch?(env)
148
+ env['REQUEST_METHOD'] == 'POST' ||
149
+ env['REQUEST_METHOD'] == 'PATCH' ||
150
+ env['REQUEST_METHOD'] == 'PUT'
151
+ end
152
+
153
+ # @param (see #call)
154
+ def environment_development?(env)
155
+ env['RACK_ENV'].to_s.downcase == 'development' || env['RACK_ENV'].nil?
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'easy/jsonapi/item'
4
+ require 'easy/jsonapi/utility'
5
+
6
+ module JSONAPI
7
+ # A generic name->value query pair
8
+ class NameValuePair < JSONAPI::Item
9
+
10
+ # @param name The name of the pair
11
+ # @param value The value of the pair
12
+ def initialize(name, value)
13
+ name = name.to_s.gsub('-', '_')
14
+ super({ name: name.to_s, value: value })
15
+ end
16
+
17
+ # @return [String] The name of the name->val pair
18
+ def name
19
+ @item[:name]
20
+ end
21
+
22
+ # @raise RunTimeError You shouldn't be able to update the name of a
23
+ # NameValuePair
24
+ def name=(_)
25
+ raise 'Cannot change the name of NameValuePair Objects'
26
+ end
27
+
28
+ # @return [String] The value of the name->val pair
29
+ def value
30
+ @item[:value]
31
+ end
32
+
33
+ # @param new_value [String | Symbol] The name->val pair value
34
+ def value=(new_value)
35
+ @item[:value] = new_value
36
+ end
37
+
38
+ # Represents a pair as a string
39
+ def to_s
40
+ v = value
41
+ val_str = case v
42
+ when Array
43
+ val_str = '['
44
+ first = true
45
+ v.each do |val|
46
+ if first
47
+ val_str += "\"#{val}\""
48
+ first = false
49
+ else
50
+ val_str += ", \"#{val}\""
51
+ end
52
+ end
53
+ val_str += ']'
54
+ when String
55
+ "\"#{v}\""
56
+ when JSONAPI::NameValuePair
57
+ "{ #{v} }"
58
+ else
59
+ v
60
+ end
61
+ "\"#{name}\": #{val_str}"
62
+ end
63
+
64
+ # Represents a pair as a hash
65
+ def to_h
66
+ { name.to_sym => JSONAPI::Utility.to_h_value(value) }
67
+ end
68
+
69
+ # prevent users and sublcasses from accessing Parent's #method_missing
70
+ private :method_missing, :item, :item=
71
+ end
72
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'easy/jsonapi/collection'
4
+ require 'easy/jsonapi/name_value_pair'
5
+ require 'easy/jsonapi/utility'
6
+
7
+ module JSONAPI
8
+
9
+ # Collection of Items that all have names and values.
10
+ class NameValuePairCollection < JSONAPI::Collection
11
+
12
+ # Creates an empty collection by default
13
+ # @param pair_arr [Array<JSONAPI::NameValuePair>] The pairs to be initialized with.
14
+ def initialize(pair_arr = [], item_type: JSONAPI::NameValuePair, &block)
15
+ if block_given?
16
+ super(pair_arr, item_type: item_type, &block)
17
+ else
18
+ super(pair_arr, item_type: item_type, &:name)
19
+ end
20
+ end
21
+
22
+ # #empyt? provided by super
23
+ # #include provided by super
24
+
25
+ # Add a pair to the collection. (CASE-SENSITIVE)
26
+ # @param pair [JSONAPI::NameValuePair] The pair to add
27
+ def add(pair, &block)
28
+ if block_given?
29
+ super(pair, &block)
30
+ else
31
+ super(pair, &:name)
32
+ end
33
+ end
34
+
35
+ # Another way to add a query_param
36
+ # @oaram (see #add)
37
+ def <<(pair, &block)
38
+ add(pair, &block)
39
+ end
40
+
41
+ # #each provided from super
42
+ # #remove provided from super
43
+ # #get provided by super
44
+ # #keys provided by super
45
+ # #size provided by super
46
+
47
+ # Represent the collection as a string
48
+ # @return [String] The representation of the collection
49
+ def to_s
50
+ JSONAPI::Utility.to_string_collection(self, pre_string: '{ ', post_string: ' }')
51
+ end
52
+
53
+ # Represent the collection as a hash
54
+ # @return [Hash] The representation of the collection
55
+ def to_h
56
+ JSONAPI::Utility.to_h_collection(self)
57
+ end
58
+
59
+ protected :insert
60
+
61
+ private
62
+
63
+ # Gets the NameValuePair object value whose name matches the method_name called
64
+ # @param method_name [Symbol] The name of the method called
65
+ # @param args If any arguments were passed to the method called
66
+ # @param block If a block was passed to the method called
67
+ def method_missing(method_name, *args, &block)
68
+ super unless include?(method_name)
69
+ get(method_name).value
70
+ end
71
+
72
+ # Whether or not method missing should be called.
73
+ def respond_to_missing?(method_name, *)
74
+ include?(method_name) || super
75
+ end
76
+
77
+ end
78
+ end