easy-jsonapi 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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