apia 3.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 (120) hide show
  1. checksums.yaml +7 -0
  2. data/VERSION +1 -0
  3. data/lib/apia.rb +21 -0
  4. data/lib/apia/api.rb +100 -0
  5. data/lib/apia/argument_set.rb +221 -0
  6. data/lib/apia/authenticator.rb +57 -0
  7. data/lib/apia/callable_with_environment.rb +43 -0
  8. data/lib/apia/controller.rb +32 -0
  9. data/lib/apia/defineable.rb +60 -0
  10. data/lib/apia/definition.rb +27 -0
  11. data/lib/apia/definitions/api.rb +51 -0
  12. data/lib/apia/definitions/argument.rb +77 -0
  13. data/lib/apia/definitions/argument_set.rb +33 -0
  14. data/lib/apia/definitions/authenticator.rb +46 -0
  15. data/lib/apia/definitions/controller.rb +41 -0
  16. data/lib/apia/definitions/endpoint.rb +74 -0
  17. data/lib/apia/definitions/enum.rb +31 -0
  18. data/lib/apia/definitions/error.rb +59 -0
  19. data/lib/apia/definitions/field.rb +117 -0
  20. data/lib/apia/definitions/lookup_argument_set.rb +27 -0
  21. data/lib/apia/definitions/object.rb +29 -0
  22. data/lib/apia/definitions/polymorph.rb +29 -0
  23. data/lib/apia/definitions/polymorph_option.rb +53 -0
  24. data/lib/apia/definitions/scalar.rb +23 -0
  25. data/lib/apia/definitions/type.rb +109 -0
  26. data/lib/apia/dsl.rb +23 -0
  27. data/lib/apia/dsls/api.rb +37 -0
  28. data/lib/apia/dsls/argument.rb +27 -0
  29. data/lib/apia/dsls/argument_set.rb +35 -0
  30. data/lib/apia/dsls/authenticator.rb +38 -0
  31. data/lib/apia/dsls/concerns/has_fields.rb +38 -0
  32. data/lib/apia/dsls/controller.rb +34 -0
  33. data/lib/apia/dsls/endpoint.rb +79 -0
  34. data/lib/apia/dsls/enum.rb +19 -0
  35. data/lib/apia/dsls/error.rb +26 -0
  36. data/lib/apia/dsls/field.rb +27 -0
  37. data/lib/apia/dsls/lookup_argument_set.rb +24 -0
  38. data/lib/apia/dsls/object.rb +19 -0
  39. data/lib/apia/dsls/polymorph.rb +19 -0
  40. data/lib/apia/dsls/route_group.rb +43 -0
  41. data/lib/apia/dsls/route_set.rb +40 -0
  42. data/lib/apia/dsls/scalar.rb +23 -0
  43. data/lib/apia/dsls/scope_descriptions.rb +17 -0
  44. data/lib/apia/endpoint.rb +110 -0
  45. data/lib/apia/enum.rb +43 -0
  46. data/lib/apia/environment_error_handling.rb +74 -0
  47. data/lib/apia/error.rb +61 -0
  48. data/lib/apia/error_set.rb +15 -0
  49. data/lib/apia/errors/error_exception_error.rb +32 -0
  50. data/lib/apia/errors/field_spec_parse_error.rb +23 -0
  51. data/lib/apia/errors/invalid_argument_error.rb +68 -0
  52. data/lib/apia/errors/invalid_enum_option_error.rb +21 -0
  53. data/lib/apia/errors/invalid_helper_error.rb +6 -0
  54. data/lib/apia/errors/invalid_json_error.rb +23 -0
  55. data/lib/apia/errors/invalid_polymorph_value_error.rb +21 -0
  56. data/lib/apia/errors/invalid_scalar_value_error.rb +21 -0
  57. data/lib/apia/errors/manifest_error.rb +43 -0
  58. data/lib/apia/errors/missing_argument_error.rb +40 -0
  59. data/lib/apia/errors/null_field_value_error.rb +37 -0
  60. data/lib/apia/errors/parse_error.rb +10 -0
  61. data/lib/apia/errors/runtime_error.rb +30 -0
  62. data/lib/apia/errors/scope_not_granted_error.rb +15 -0
  63. data/lib/apia/errors/standard_error.rb +6 -0
  64. data/lib/apia/field_set.rb +76 -0
  65. data/lib/apia/field_spec.rb +155 -0
  66. data/lib/apia/helpers.rb +34 -0
  67. data/lib/apia/hook_set.rb +30 -0
  68. data/lib/apia/lookup_argument_set.rb +57 -0
  69. data/lib/apia/lookup_environment.rb +27 -0
  70. data/lib/apia/manifest_errors.rb +62 -0
  71. data/lib/apia/mock_request.rb +18 -0
  72. data/lib/apia/object.rb +68 -0
  73. data/lib/apia/object_set.rb +21 -0
  74. data/lib/apia/pagination_object.rb +34 -0
  75. data/lib/apia/polymorph.rb +50 -0
  76. data/lib/apia/rack.rb +184 -0
  77. data/lib/apia/rack_error.rb +17 -0
  78. data/lib/apia/request.rb +67 -0
  79. data/lib/apia/request_environment.rb +84 -0
  80. data/lib/apia/request_headers.rb +42 -0
  81. data/lib/apia/response.rb +64 -0
  82. data/lib/apia/route.rb +61 -0
  83. data/lib/apia/route_group.rb +20 -0
  84. data/lib/apia/route_set.rb +89 -0
  85. data/lib/apia/scalar.rb +52 -0
  86. data/lib/apia/scalars.rb +25 -0
  87. data/lib/apia/scalars/base64.rb +31 -0
  88. data/lib/apia/scalars/boolean.rb +37 -0
  89. data/lib/apia/scalars/date.rb +45 -0
  90. data/lib/apia/scalars/decimal.rb +36 -0
  91. data/lib/apia/scalars/integer.rb +34 -0
  92. data/lib/apia/scalars/string.rb +24 -0
  93. data/lib/apia/scalars/unix_time.rb +40 -0
  94. data/lib/apia/schema/api_controller_schema_type.rb +17 -0
  95. data/lib/apia/schema/api_schema_type.rb +43 -0
  96. data/lib/apia/schema/argument_schema_type.rb +28 -0
  97. data/lib/apia/schema/argument_set_schema_type.rb +21 -0
  98. data/lib/apia/schema/authenticator_schema_type.rb +22 -0
  99. data/lib/apia/schema/controller.rb +39 -0
  100. data/lib/apia/schema/controller_endpoint_schema_type.rb +17 -0
  101. data/lib/apia/schema/controller_schema_type.rb +32 -0
  102. data/lib/apia/schema/endpoint_schema_type.rb +35 -0
  103. data/lib/apia/schema/enum_schema_type.rb +20 -0
  104. data/lib/apia/schema/enum_value_schema_type.rb +14 -0
  105. data/lib/apia/schema/error_schema_type.rb +23 -0
  106. data/lib/apia/schema/field_schema_type.rb +38 -0
  107. data/lib/apia/schema/field_spec_options_schema_type.rb +16 -0
  108. data/lib/apia/schema/lookup_argument_set_schema_type.rb +25 -0
  109. data/lib/apia/schema/object_schema_polymorph.rb +31 -0
  110. data/lib/apia/schema/object_schema_type.rb +21 -0
  111. data/lib/apia/schema/polymorph_option_schema_type.rb +16 -0
  112. data/lib/apia/schema/polymorph_schema_type.rb +20 -0
  113. data/lib/apia/schema/request_method_enum.rb +21 -0
  114. data/lib/apia/schema/route_group_schema_type.rb +19 -0
  115. data/lib/apia/schema/route_schema_type.rb +31 -0
  116. data/lib/apia/schema/route_set_schema_type.rb +20 -0
  117. data/lib/apia/schema/scalar_schema_type.rb +15 -0
  118. data/lib/apia/schema/scope_type.rb +14 -0
  119. data/lib/apia/version.rb +12 -0
  120. metadata +188 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Apia
6
+ class ObjectSet < ::Set
7
+
8
+ def add_object(object)
9
+ return self if include?(object)
10
+
11
+ self << object
12
+ if object.respond_to?(:collate_objects)
13
+ # Attempt to add any other objects if the object responds to
14
+ # collate_objects.
15
+ object.collate_objects(self)
16
+ end
17
+ self
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/object'
4
+ require 'apia/scalars/integer'
5
+ require 'apia/scalars/boolean'
6
+
7
+ module Apia
8
+ class PaginationObject < Apia::Object
9
+
10
+ name 'Pagination Details'
11
+ description 'Provides information about how data has been paginated'
12
+
13
+ field :current_page, type: Scalars::Integer do
14
+ description 'The current page'
15
+ end
16
+
17
+ field :total_pages, type: Scalars::Integer, null: true do
18
+ description 'The total number of pages'
19
+ end
20
+
21
+ field :total, type: Scalars::Integer, null: true do
22
+ description 'The total number of items across all pages'
23
+ end
24
+
25
+ field :per_page, type: Scalars::Integer do
26
+ description 'The number of items per page'
27
+ end
28
+
29
+ field :large_set, type: Scalars::Boolean do
30
+ description 'Is this a large set and therefore the total number of records cannot be returned?'
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/helpers'
4
+ require 'apia/defineable'
5
+ require 'apia/definitions/polymorph'
6
+ require 'apia/errors/invalid_polymorph_value_error'
7
+
8
+ module Apia
9
+ class Polymorph
10
+
11
+ extend Defineable
12
+
13
+ class << self
14
+
15
+ # Return the definition for this polymorph
16
+ #
17
+ # @return [Apia::Definitions::Polymorph]
18
+ def definition
19
+ @definition ||= Definitions::Polymorph.new(Helpers.class_name_to_id(name))
20
+ end
21
+
22
+ # Collate all objects that this polymorph references and add them to the
23
+ # given object set
24
+ #
25
+ # @param set [Apia::ObjectSet]
26
+ # @return [void]
27
+ def collate_objects(set)
28
+ definition.options.each_value do |opt|
29
+ set.add_object(opt.type.klass) if opt.type.usable_for_field?
30
+ end
31
+ end
32
+
33
+ # Return the type which should be returned for the given value by running
34
+ # through each of the matchers to find the appropriate type.
35
+ def option_for_value(value)
36
+ option = definition.options.values.find do |opt|
37
+ opt.matches?(value)
38
+ end
39
+
40
+ if option.nil?
41
+ raise InvalidPolymorphValueError.new(self, value)
42
+ end
43
+
44
+ option
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
data/lib/apia/rack.rb ADDED
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'apia/rack_error'
5
+ require 'apia/request'
6
+ require 'apia/response'
7
+
8
+ module Apia
9
+ class Rack
10
+
11
+ def initialize(app, api, namespace, **options)
12
+ @app = app
13
+ @api = api
14
+ @namespace = '/' + namespace.sub(/\A\/+/, '').sub(/\/+\z/, '')
15
+ @options = options
16
+ end
17
+
18
+ # Is this supposed to be running in development? This will validate the whole
19
+ # API on each request as well as being more verbose about internal server
20
+ # errors that are encountered.
21
+ #
22
+ # @return [Boolean]
23
+ def development?
24
+ env_is_dev = ENV['RACK_ENV'] == 'development'
25
+ return true if env_is_dev && @options[:development].nil?
26
+
27
+ @options[:development] == true
28
+ end
29
+
30
+ # Parse a given full path and return nil if it doesn't match our
31
+ # namespace or return a hash with the controller and endpoint
32
+ # named as available.
33
+ #
34
+ # @param path [String] /core/v1/controller/endpoint
35
+ # @return [nil, Hash]
36
+ def find_route(method, path)
37
+ return if api.nil?
38
+
39
+ api.definition.route_set.find(method.to_s.downcase.to_sym, path).first
40
+ end
41
+
42
+ # Return the API object
43
+ #
44
+ # @return [Apia::API]
45
+ def api
46
+ return Object.const_get(@api) if @api.is_a?(String) && development?
47
+ return @cached_api ||= Object.const_get(@api) if @api.is_a?(String)
48
+
49
+ @api
50
+ end
51
+
52
+ # Actually make the request
53
+ #
54
+ # @param env [Hash]
55
+ # @return [Array] a rack triplet
56
+ def call(env)
57
+ if @options[:hosts]&.none? { |host| host == env['HTTP_HOST'] }
58
+ return @app.call(env)
59
+ end
60
+
61
+ unless env['PATH_INFO'] =~ /\A#{Regexp.escape(@namespace)}\/([a-z].*)\z/i
62
+ return @app.call(env)
63
+ end
64
+
65
+ api_path = Regexp.last_match(1)
66
+
67
+ triplet = handle_request(env, api_path)
68
+ add_cors_headers(env, triplet)
69
+ triplet
70
+ end
71
+
72
+ private
73
+
74
+ def handle_request(env, api_path)
75
+ if env['REQUEST_METHOD'].upcase == 'OPTIONS'
76
+ return [204, {}, ['']]
77
+ end
78
+
79
+ validate_api if development?
80
+
81
+ route = find_route(env['REQUEST_METHOD'], api_path)
82
+ if route.nil?
83
+ raise RackError.new(404, 'route_not_found', "No route matches '#{api_path}' for #{env['REQUEST_METHOD']}")
84
+ end
85
+
86
+ request = Apia::Request.new(env)
87
+ request.api_path = api_path
88
+ request.namespace = @namespace
89
+ request.api = api
90
+ request.controller = route.controller
91
+ request.endpoint = route.endpoint
92
+ request.route = route
93
+
94
+ response = request.endpoint.execute(request)
95
+ response.rack_triplet
96
+ rescue ::StandardError => e
97
+ if e.is_a?(RackError) || e.is_a?(Apia::ManifestError)
98
+ return e.triplet
99
+ end
100
+
101
+ api.definition.exception_handlers.call(e, {
102
+ env: env,
103
+ api: api,
104
+ request: defined?(request) ? request : nil
105
+ })
106
+
107
+ if development?
108
+ return triplet_for_exception(e)
109
+ end
110
+
111
+ self.class.error_triplet('unhandled_exception', status: 500)
112
+ end
113
+
114
+ def validate_api
115
+ api.validate_all.raise_if_needed
116
+ end
117
+
118
+ def triplet_for_exception(exception)
119
+ self.class.error_triplet(
120
+ 'unhandled_exception',
121
+ description: 'This is an exception that has occurred and not been handled.',
122
+ detail: {
123
+ class: exception.class.name,
124
+ message: exception.message,
125
+ backtrace: exception.backtrace
126
+ },
127
+ status: 500
128
+ )
129
+ end
130
+
131
+ # Add cross origin headers to the response triplet
132
+ #
133
+ # @param env [Hash]
134
+ # @param triplet [Array]
135
+ # @return [void]
136
+ def add_cors_headers(env, triplet)
137
+ triplet[1]['Access-Control-Allow-Origin'] = '*'
138
+ triplet[1]['Access-Control-Allow-Methods'] = '*'
139
+ if env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
140
+ triplet[1]['Access-Control-Allow-Headers'] = env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
141
+ end
142
+
143
+ true
144
+ end
145
+
146
+ class << self
147
+
148
+ # Return a JSON-ready triplet for the given body.
149
+ #
150
+ # @param body [Hash, Array]
151
+ # @param status [Integer]
152
+ # @param headers [Hash]
153
+ # @return [Array]
154
+ def json_triplet(body, status: 200, headers: {})
155
+ body_as_json = body.to_json
156
+ [
157
+ status,
158
+ headers.merge('content-type' => 'application/json', 'content-length' => body_as_json.bytesize.to_s),
159
+ [body_as_json]
160
+ ]
161
+ end
162
+
163
+ # Return a triplet for a given error using the standard error schema
164
+ #
165
+ # @param code [String]
166
+ # @param description [String]
167
+ # @param detail [Hash]
168
+ # @param status [Integer]
169
+ # @param headers [Hash]
170
+ # @return [Array]
171
+ def error_triplet(code, description: nil, detail: {}, status: 500, headers: {})
172
+ json_triplet({
173
+ error: {
174
+ code: code,
175
+ description: description,
176
+ detail: detail
177
+ }
178
+ }, status: status, headers: headers.merge('x-api-schema' => 'json-error'))
179
+ end
180
+
181
+ end
182
+
183
+ end
184
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apia
4
+ class RackError < StandardError
5
+
6
+ def initialize(http_status, code, message)
7
+ @http_status = http_status
8
+ @code = code
9
+ @message = message
10
+ end
11
+
12
+ def triplet
13
+ Rack.error_triplet(@code, description: @message, status: @http_status)
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/request'
4
+ require 'apia/request_headers'
5
+ require 'apia/errors/invalid_json_error'
6
+
7
+ module Apia
8
+ class Request < Rack::Request
9
+
10
+ attr_accessor :api
11
+ attr_accessor :controller
12
+ attr_accessor :endpoint
13
+ attr_accessor :identity
14
+ attr_writer :arguments
15
+ attr_accessor :authenticator
16
+ attr_accessor :namespace
17
+ attr_accessor :route
18
+ attr_accessor :api_path
19
+
20
+ def self.empty(options: {})
21
+ new(options)
22
+ end
23
+
24
+ def arguments
25
+ @arguments ||= {}
26
+ end
27
+
28
+ def headers
29
+ @headers ||= RequestHeaders.create_from_request(self)
30
+ end
31
+
32
+ def json_body
33
+ return @json_body if instance_variable_defined?('@json_body')
34
+
35
+ @json_body = get_json_body_from_body || get_json_body_from_params
36
+ end
37
+
38
+ def body?
39
+ has_header?('rack.input')
40
+ end
41
+
42
+ private
43
+
44
+ def parse_json_from_string(body)
45
+ return {} if body.empty?
46
+
47
+ JSON.parse(body)
48
+ rescue JSON::ParserError => e
49
+ raise InvalidJSONError, e.message
50
+ end
51
+
52
+ def get_json_body_from_body
53
+ return unless content_type =~ /\Aapplication\/json/
54
+ return unless body?
55
+
56
+ parse_json_from_string(body.read)
57
+ end
58
+
59
+ def get_json_body_from_params
60
+ return unless body?
61
+ return unless params['_arguments'].is_a?(String)
62
+
63
+ parse_json_from_string(params['_arguments'])
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/environment_error_handling'
4
+ require 'apia/errors/invalid_helper_error'
5
+
6
+ module Apia
7
+ class RequestEnvironment
8
+
9
+ attr_reader :request
10
+ attr_reader :response
11
+
12
+ include EnvironmentErrorHandling
13
+
14
+ def initialize(request, response)
15
+ @request = request
16
+ @response = response
17
+ end
18
+
19
+ def call(*args, &block)
20
+ return unless block_given?
21
+
22
+ instance_exec(@request, @response, *args, &block)
23
+ rescue ::StandardError => e
24
+ raise_exception(e)
25
+ end
26
+
27
+ # Call a helper
28
+ #
29
+ # @param name [Symbol]
30
+ # @return [Object, nil]
31
+ def helper(name, *args)
32
+ helper = @request.controller.definition.helpers[name.to_sym]
33
+ if helper.nil?
34
+ raise InvalidHelperError, "No helper found with name #{name}"
35
+ end
36
+
37
+ instance_exec(*args, &helper)
38
+ end
39
+
40
+ # Set appropriate pagination for the given set based on the configuration
41
+ # specified for the endpoint
42
+ #
43
+ # @param set [#limit, #count, #page, #per, #to_a, #total_pages, #current_page, #without_count]
44
+ # @param large_set [Boolean] whether or not this is expected to be a large set
45
+ # @return [void]
46
+ def paginate(set, potentially_large_set: false)
47
+ paginated_field = @request.endpoint.definition.paginated_field
48
+ if paginated_field.nil?
49
+ raise Apia::RuntimeError, 'Could not paginate response because no pagination has been configured for the endpoint'
50
+ end
51
+
52
+ paginated = set.page(@request.arguments[:page] || 1)
53
+ paginated = paginated.per(@request.arguments[:per_page] || 30)
54
+
55
+ large_set = false
56
+ if potentially_large_set
57
+ total_count = set.limit(1001).count
58
+ if total_count > 1000
59
+ large_set = true
60
+ paginated = paginated.without_count
61
+ end
62
+ end
63
+
64
+ @response.add_field paginated_field, paginated.to_a
65
+
66
+ pagination_info = {}
67
+ pagination_info[:current_page] = paginated.current_page
68
+ pagination_info[:per_page] = paginated.limit_value
69
+ pagination_info[:large_set] = large_set
70
+ unless large_set
71
+ pagination_info[:total] = paginated.total_count
72
+ pagination_info[:total_pages] = paginated.total_pages
73
+ end
74
+ @response.add_field :pagination, pagination_info
75
+ end
76
+
77
+ private
78
+
79
+ def potential_error_sources
80
+ [@request.endpoint, @request.authenticator].compact
81
+ end
82
+
83
+ end
84
+ end