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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 834322a8dd87426fd34415e096964a331ca3fefd5101e05e23a216da197e1820
4
+ data.tar.gz: d422b240f990920a61e520e360d15414aef1b8c0f39021684fa32d31d0aaf1a9
5
+ SHA512:
6
+ metadata.gz: 6a42e4aaf195d8bd6dc7053a473bbc107ff4303d184bfae3c5bf1e7bb25ef618f57dbc86436c697b06786847f67077d9a06ae9451c6211fbf6072a5cfd6b755e
7
+ data.tar.gz: 73762ac070550ec052ffa79f2a5e8fdbb0317b8cdb62020b74d494706a1f2e63a65d52691fed82da4f2ba60ed2d66a2572686d3e0c9fef237ec2632a67bc339b
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 3.0.0
data/lib/apia.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/api'
4
+ require 'apia/argument_set'
5
+ require 'apia/authenticator'
6
+ require 'apia/controller'
7
+ require 'apia/endpoint'
8
+ require 'apia/enum'
9
+ require 'apia/error'
10
+ require 'apia/lookup_argument_set'
11
+ require 'apia/scalar'
12
+ require 'apia/object'
13
+ require 'apia/polymorph'
14
+
15
+ require 'apia/scalars/string'
16
+ require 'apia/scalars/integer'
17
+ require 'apia/scalars/boolean'
18
+ require 'apia/scalars/date'
19
+ require 'apia/scalars/unix_time'
20
+ require 'apia/scalars/decimal'
21
+ require 'apia/scalars/base64'
data/lib/apia/api.rb ADDED
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/defineable'
4
+ require 'apia/definitions/api'
5
+ require 'apia/helpers'
6
+ require 'apia/manifest_errors'
7
+ require 'apia/object_set'
8
+ require 'apia/errors/standard_error'
9
+ require 'apia/mock_request'
10
+
11
+ module Apia
12
+ class API
13
+
14
+ extend Defineable
15
+
16
+ class << self
17
+
18
+ # Return the definition for this API
19
+ #
20
+ # @return [Apia::Definitions::API]
21
+ def definition
22
+ @definition ||= Definitions::API.new(Helpers.class_name_to_id(name))
23
+ end
24
+
25
+ # Return all objects which are referenced by the API. This list is used for the purposes
26
+ # of validating all objects and generating schemas.
27
+ #
28
+ # @param include_apia_controller [Boolean] whether the schema/internal API should be included
29
+ # @return [Apia::ObjectSet]
30
+ def objects
31
+ set = ObjectSet.new([self])
32
+ if definition.authenticator
33
+ set.add_object(definition.authenticator)
34
+ end
35
+
36
+ definition.route_set.controllers.each do |con|
37
+ set.add_object(con)
38
+ end
39
+
40
+ definition.route_set.endpoints.each do |endpoint|
41
+ set.add_object(endpoint)
42
+ end
43
+
44
+ set
45
+ end
46
+
47
+ # Validate all objects in the API and return details of any issues encountered
48
+ #
49
+ # @return [Apia::ManifestErrors]
50
+ def validate_all
51
+ errors = ManifestErrors.new
52
+ objects.each do |object|
53
+ next unless object.respond_to?(:definition)
54
+
55
+ object.definition.validate(errors)
56
+ end
57
+ errors
58
+ end
59
+
60
+ # Return the schema hash for this API
61
+ #
62
+ # @param host [String]
63
+ # @param namespace [String]
64
+ # @return [Hash]
65
+ def schema(host:, namespace:)
66
+ require 'apia/schema/controller'
67
+ Schema::Controller.definition.endpoints[:schema].definition.fields.generate_hash({
68
+ schema_version: 1,
69
+ host: host,
70
+ namespace: namespace,
71
+ api: definition.id,
72
+ objects: objects.map(&:definition).select(&:schema?)
73
+ })
74
+ end
75
+
76
+ # Execute a request for a given controller & endpoint
77
+ #
78
+ # @param controller [Apia::Controller]
79
+ # @param endpoint_name [Symbol]
80
+ # @return [Apia::Response]
81
+ def test_endpoint(endpoint, controller: nil)
82
+ if controller && endpoint.is_a?(Symbol) || endpoint.is_a?(String)
83
+ endpoint_name = endpoint
84
+ endpoint = controller.definition.endpoints[endpoint.to_sym]
85
+ if endpoint.nil?
86
+ raise Apia::StandardError, "Invalid endpoint name '#{endpoint_name}' for '#{controller.name}'"
87
+ end
88
+ end
89
+
90
+ endpoint.test do |r|
91
+ r.api = self
92
+ r.controller = controller
93
+ yield r if block_given?
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/defineable'
4
+ require 'apia/definitions/argument_set'
5
+ require 'apia/errors/invalid_argument_error'
6
+ require 'apia/errors/missing_argument_error'
7
+ require 'apia/helpers'
8
+
9
+ module Apia
10
+ class ArgumentSet
11
+
12
+ # This is a constant that represents a missing value where `nil` means
13
+ # the user actually wanted to send null/nil.
14
+ class MissingValue
15
+
16
+ def self.singleton
17
+ @singleton ||= new
18
+ end
19
+
20
+ end
21
+
22
+ extend Defineable
23
+
24
+ class << self
25
+
26
+ # Return the definition for this argument set
27
+ #
28
+ # @return [Apia::Definitions::ArgumentSet]
29
+ def definition
30
+ @definition ||= Definitions::ArgumentSet.new(Helpers.class_name_to_id(name))
31
+ end
32
+
33
+ # Finds all objects referenced by this argument set and add them
34
+ # to the provided set.
35
+ #
36
+ # @param set [Apia::ObjectSet]
37
+ # @return [void]
38
+ def collate_objects(set)
39
+ definition.arguments.each_value do |argument|
40
+ set.add_object(argument.type.klass) if argument.type.usable_for_argument?
41
+ end
42
+ end
43
+
44
+ # Create a new argument set from a request object
45
+ #
46
+ # @param request [Apia::Request]
47
+ # @return [Apia::ArgumentSet]
48
+ def create_from_request(request)
49
+ new(request.json_body || request.params || {}, request: request)
50
+ end
51
+
52
+ end
53
+
54
+ # Create a new argument set by providing a hash containing the raw
55
+ # arguments
56
+ #
57
+ # @param hash [Hash]
58
+ # @param path [Array]
59
+ # @return [Apia::ArgumentSet]
60
+ def initialize(hash, path: [], request: nil)
61
+ unless hash.is_a?(Hash)
62
+ raise Apia::RuntimeError, 'Hash was expected for argument'
63
+ end
64
+
65
+ @path = path
66
+ @request = request
67
+ @source = self.class.definition.arguments.each_with_object({}) do |(arg_key, argument), source|
68
+ given_value = lookup_value(hash, arg_key, argument, request)
69
+
70
+ if argument.required? && (given_value.nil? || given_value.is_a?(MissingValue))
71
+ raise MissingArgumentError.new(argument, path: @path + [argument])
72
+ end
73
+
74
+ # If the given value is missing, we'll just skip adding this to the hash
75
+ next if given_value.is_a?(MissingValue)
76
+
77
+ given_value = parse_value(argument, given_value)
78
+ validation_errors = argument.validate_value(given_value)
79
+ unless validation_errors.empty?
80
+ raise InvalidArgumentError.new(argument, issue: :validation_errors, errors: validation_errors, path: @path + [argument])
81
+ end
82
+
83
+ source[argument.name.to_sym] = given_value
84
+ end
85
+ end
86
+
87
+ # Return an item from the argument set
88
+ #
89
+ # @param value [String, Symbol]
90
+ # @return [Object, nil]
91
+ def [](value)
92
+ @source[value.to_sym]
93
+ end
94
+
95
+ # Return an item from this argument set
96
+ #
97
+ # @param values [Array<String, Symbol>]
98
+ # @return [Object, nil]
99
+ def dig(*values)
100
+ @source.dig(*values)
101
+ end
102
+
103
+ # Return the source object
104
+ #
105
+ # @return [Hash]
106
+ def to_hash
107
+ @source.transform_values do |value|
108
+ value.is_a?(ArgumentSet) ? value.to_hash : value
109
+ end
110
+ end
111
+
112
+ # Return whether an argument has been provided or not?
113
+ #
114
+ # @param name [Symbol]
115
+ # @return [Boolean]
116
+ def has?(key)
117
+ @source.key?(key.to_sym)
118
+ end
119
+
120
+ # Validate an argument set and return any errors as appropriate
121
+ #
122
+ # @param argument [Apia::Argument]
123
+ def validate(argument, index: nil)
124
+ end
125
+
126
+ private
127
+
128
+ def lookup_value(hash, key, argument, request)
129
+ if hash.key?(key.to_s)
130
+ hash[key.to_s]
131
+ elsif hash.key?(key.to_sym)
132
+ hash[key.to_sym]
133
+ else
134
+ route_value = value_from_route(argument, request)
135
+ return route_value unless route_value.is_a?(MissingValue)
136
+ return argument.default unless argument.default.nil?
137
+
138
+ MissingValue.singleton
139
+ end
140
+ end
141
+
142
+ def parse_value(argument, value, index: nil, in_array: false)
143
+ if value.nil?
144
+ nil
145
+
146
+ elsif argument.array? && value.is_a?(Array)
147
+ value.each_with_index.map do |v, i|
148
+ parse_value(argument, v, index: i, in_array: true)
149
+ end
150
+
151
+ elsif argument.array? && !in_array
152
+ raise InvalidArgumentError.new(argument, issue: :array_expected, index: index, path: @path + [argument])
153
+
154
+ elsif argument.type.scalar?
155
+ begin
156
+ type = argument.type.klass.parse(value)
157
+ rescue Apia::ParseError => e
158
+ # If we cannot parse the given input, this is cause for a parse error to be raised.
159
+ raise InvalidArgumentError.new(argument, issue: :parse_error, errors: [e.message], index: index, path: @path + [argument])
160
+ end
161
+
162
+ unless argument.type.klass.valid?(type)
163
+ # If value we have parsed is not actually valid, we 'll raise an argument error.
164
+ # In most cases, it is likely that an integer has been provided to string etc...
165
+ raise InvalidArgumentError.new(argument, issue: :invalid_scalar, index: index, path: @path + [argument])
166
+ end
167
+
168
+ type
169
+
170
+ elsif argument.type.argument_set?
171
+ unless value.is_a?(Hash)
172
+ raise InvalidArgumentError.new(argument, issue: :object_expected, index: index, path: @path + [argument])
173
+ end
174
+
175
+ value = argument.type.klass.new(value, path: @path + [argument], request: @request)
176
+ value.validate(argument, index: index)
177
+ value
178
+
179
+ elsif argument.type.enum?
180
+ unless argument.type.klass.definition.values[value]
181
+ raise InvalidArgumentError.new(argument, issue: :invalid_enum_value, index: index, path: @path + [argument])
182
+ end
183
+
184
+ value
185
+ end
186
+ end
187
+
188
+ def check_for_missing_required_arguments
189
+ self.class.definition.arguments.each_value do |arg|
190
+ next unless arg.required?
191
+ next if self[arg.name]
192
+ end
193
+ end
194
+
195
+ def value_from_route(argument, request)
196
+ return MissingValue.singleton if request.nil?
197
+ return MissingValue.singleton if request.route.nil?
198
+
199
+ route_args = request.route.extract_arguments(request.api_path)
200
+ unless route_args.key?(argument.name.to_s)
201
+ return MissingValue.singleton
202
+ end
203
+
204
+ value_for_arg = route_args[argument.name.to_s]
205
+ return nil if value_for_arg.nil?
206
+
207
+ if argument.type.argument_set?
208
+ # If the argument is an argument set, we'll just want to try and
209
+ # populate the first argument.
210
+ if first_arg = argument.type.klass.definition.arguments.keys.first
211
+ { first_arg.to_s => value_for_arg }
212
+ else
213
+ {}
214
+ end
215
+ else
216
+ value_for_arg
217
+ end
218
+ end
219
+
220
+ end
221
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/defineable'
4
+ require 'apia/definitions/authenticator'
5
+ require 'apia/helpers'
6
+ require 'apia/callable_with_environment'
7
+
8
+ module Apia
9
+ class Authenticator
10
+
11
+ extend Defineable
12
+ include CallableWithEnvironment
13
+
14
+ class << self
15
+
16
+ # Return the definition for this authenticator
17
+ #
18
+ # @return [Apia::Definitions::Authenticator]
19
+ def definition
20
+ @definition ||= Definitions::Authenticator.new(Helpers.class_name_to_id(name))
21
+ end
22
+
23
+ # Finds all objects referenced by this authenticator and add them
24
+ # to the provided set.
25
+ #
26
+ # @param set [Apia::ObjectSet]
27
+ # @return [void]
28
+ def collate_objects(set)
29
+ definition.potential_errors.each do |error|
30
+ set.add_object(error)
31
+ end
32
+ end
33
+
34
+ # Execute this authenticator within the given environment
35
+ #
36
+ # @param environment [Apia::RequestEnvironment]
37
+ # @return [void]
38
+ def execute(environment)
39
+ new(environment).call
40
+ end
41
+
42
+ # If any of the given scopes are valid
43
+ #
44
+ # @param environment [Apia::RequestEnvironment]
45
+ # @param scope [String]
46
+ # @return [Boolean]
47
+ def authorized_scope?(environment, scopes)
48
+ return true if definition.scope_validator.nil?
49
+ return true if scopes.empty?
50
+
51
+ scopes.any? { |s| environment.call(s, &definition.scope_validator) }
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apia
4
+ module CallableWithEnvironment
5
+
6
+ def initialize(environment, action_name: :action)
7
+ @environment = environment
8
+ @action_name = action_name
9
+ end
10
+
11
+ def call
12
+ action = self.class.definition.send(@action_name)
13
+ return if action.nil?
14
+
15
+ instance_exec(@environment.request, @environment.response, &action)
16
+ end
17
+
18
+ # rubocop:disable Lint/RescueException
19
+ def call_with_error_handling
20
+ call
21
+ rescue Exception => e
22
+ raise_exception(e)
23
+ end
24
+ # rubocop:enable Lint/RescueException
25
+
26
+ def respond_to_missing?(name, _include_private = false)
27
+ @environment.respond_to?(name) || super
28
+ end
29
+
30
+ def method_missing(name, *args, **kwargs, &block)
31
+ if @environment.respond_to?(name)
32
+ if kwargs.empty?
33
+ return @environment.send(name, *args, &block)
34
+ end
35
+
36
+ return @environment.send(name, *args, **kwargs, &block)
37
+ end
38
+
39
+ super
40
+ end
41
+
42
+ end
43
+ end