apia 3.0.0

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