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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/dsl'
4
+ require 'apia/definitions/polymorph_option'
5
+ require 'apia/helpers'
6
+
7
+ module Apia
8
+ module DSLs
9
+ class Polymorph < DSL
10
+
11
+ def option(name, type: nil, matcher: nil)
12
+ id = "#{@definition.id}/#{Helpers.camelize(name)}Option"
13
+ option = Definitions::PolymorphOption.new(id, name, type: type, matcher: matcher)
14
+ @definition.options[name.to_sym] = option
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apia
4
+ module DSLs
5
+ class RouteGroup
6
+
7
+ def initialize(route_set, group)
8
+ @route_set = route_set
9
+ @group = group
10
+ end
11
+
12
+ def route(path, **options)
13
+ @route_set.dsl.route(path, controller: options[:controller] || @group.default_controller, group: @group, **options)
14
+ end
15
+
16
+ def group(id, &block)
17
+ group = Apia::RouteGroup.new("#{@group.id}.#{id}", @group)
18
+ dsl = Apia::DSLs::RouteGroup.new(@route_set, group)
19
+ dsl.instance_eval(&block)
20
+ @group.groups << group
21
+ end
22
+
23
+ Route::REQUEST_METHODS.each do |method_name|
24
+ define_method method_name do |path, **options|
25
+ route(path, request_method: method_name, **options)
26
+ end
27
+ end
28
+
29
+ def name(name)
30
+ @group.name = name
31
+ end
32
+
33
+ def description(description)
34
+ @group.description = description
35
+ end
36
+
37
+ def controller(controller)
38
+ @group.default_controller = controller
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/route'
4
+ require 'apia/route_group'
5
+ require 'apia/dsls/route_group'
6
+
7
+ module Apia
8
+ module DSLs
9
+ class RouteSet
10
+
11
+ def initialize(route_set)
12
+ @route_set = route_set
13
+ end
14
+
15
+ def schema(path: 'schema')
16
+ require 'apia/schema/controller'
17
+ get path, controller: Schema::Controller, endpoint: :schema
18
+ end
19
+
20
+ def route(path, request_method: nil, **options)
21
+ route = Route.new(path, request_method: request_method, **options)
22
+ @route_set.add(route)
23
+ end
24
+
25
+ Route::REQUEST_METHODS.each do |method_name|
26
+ define_method method_name do |path, **options|
27
+ route(path, request_method: method_name, **options)
28
+ end
29
+ end
30
+
31
+ def group(id, &block)
32
+ group = Apia::RouteGroup.new(id.to_s, nil)
33
+ dsl = Apia::DSLs::RouteGroup.new(@route_set, group)
34
+ dsl.instance_eval(&block)
35
+ @route_set.groups << group
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/dsl'
4
+
5
+ module Apia
6
+ module DSLs
7
+ class Scalar < DSL
8
+
9
+ def cast(&block)
10
+ @definition.cast = block
11
+ end
12
+
13
+ def parse(&block)
14
+ @definition.parse = block
15
+ end
16
+
17
+ def validator(&block)
18
+ @definition.validator = block
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apia
4
+ module DSLs
5
+ class ScopeDescriptions
6
+
7
+ def initialize(api)
8
+ @api = api
9
+ end
10
+
11
+ def add(name, description)
12
+ @api.scopes[name.to_s] = { description: description }
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/helpers'
4
+ require 'apia/defineable'
5
+ require 'apia/definitions/endpoint'
6
+ require 'apia/request_environment'
7
+ require 'apia/errors/scope_not_granted_error'
8
+ require 'apia/callable_with_environment'
9
+
10
+ module Apia
11
+ class Endpoint
12
+
13
+ extend Defineable
14
+ include CallableWithEnvironment
15
+
16
+ class << self
17
+
18
+ # Return the definition object for the endpoint
19
+ #
20
+ # @return [Apia::Definitions::Endpoint]
21
+ def definition
22
+ @definition ||= Definitions::Endpoint.new(Helpers.class_name_to_id(name))
23
+ end
24
+
25
+ # Collate all objects that this endpoint references and add them to the
26
+ # given object set
27
+ #
28
+ # @param set [Apia::ObjectSet]
29
+ # @return [void]
30
+ def collate_objects(set)
31
+ set.add_object(definition.argument_set)
32
+
33
+ definition.potential_errors.each do |error|
34
+ set.add_object(error)
35
+ end
36
+
37
+ definition.fields.each_value do |field|
38
+ set.add_object(field.type.klass) if field.type.usable_for_field?
39
+ end
40
+ end
41
+
42
+ # Run this request by providing a request to execute it with.
43
+ #
44
+ # @param request [Apia::Request]
45
+ # @return [Apia::Response]
46
+ def execute(request)
47
+ response = Response.new(request, self)
48
+ environment = RequestEnvironment.new(request, response)
49
+
50
+ catch_errors(response) do
51
+ # Determine an authenticator and execute it before the request happens
52
+ request.authenticator = definition.authenticator || request.controller&.definition&.authenticator || request.api&.definition&.authenticator
53
+ request.authenticator&.execute(environment)
54
+
55
+ # Determine if we're permitted to run the action based on the endpoint's scopes
56
+ if request.authenticator && !request.authenticator.authorized_scope?(environment, definition.scopes)
57
+ environment.raise_error Apia::ScopeNotGrantedError, scopes: definition.scopes
58
+ end
59
+
60
+ # Process arguments into the request. This happens after the authentication
61
+ # stage because a) authenticators shouldn't be using endpoint specific args
62
+ # and b) the argument conditions may need to know the identity.
63
+ request.arguments = definition.argument_set.create_from_request(request)
64
+
65
+ # Call the action for the endpoint
66
+ endpoint_instance = new(environment)
67
+ endpoint_instance.call_with_error_handling
68
+
69
+ # We're going to call this here because we want to cache the actual values of
70
+ # the output within the catch_errors block.
71
+ response.hash
72
+ end
73
+
74
+ response
75
+ end
76
+
77
+ # Catch any runtime errors and update the given response with the appropriate
78
+ # values.
79
+ #
80
+ # @param response [Apia::Response]
81
+ # @return [void]
82
+ def catch_errors(response)
83
+ yield
84
+ rescue Apia::RuntimeError => e
85
+ catch_errors(response) do
86
+ response.body = { error: e.hash }
87
+ response.status = e.http_status
88
+ response.headers['x-api-schema'] = 'json-error'
89
+ end
90
+ end
91
+
92
+ # Should a given field be included
93
+ #
94
+ def include_field?(*args)
95
+ definition.fields.spec.include_field?(*args)
96
+ end
97
+
98
+ # Allow an endpoint to be executed with a mocked request.
99
+ #
100
+ def test
101
+ request = Apia::MockRequest.empty
102
+ request.endpoint = self
103
+ yield request if block_given?
104
+ execute(request)
105
+ end
106
+
107
+ end
108
+
109
+ end
110
+ end
data/lib/apia/enum.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/helpers'
4
+ require 'apia/defineable'
5
+ require 'apia/definitions/enum'
6
+ require 'apia/errors/invalid_enum_option_error'
7
+
8
+ module Apia
9
+ class Enum
10
+
11
+ extend Defineable
12
+
13
+ class << self
14
+
15
+ # Return the definition object for the enum
16
+ #
17
+ # @return [Apia::Definitions::Enum]
18
+ def definition
19
+ @definition ||= Definitions::Enum.new(Helpers.class_name_to_id(name))
20
+ end
21
+
22
+ # Cast a value or define a new block for casting (for DSL purposes)
23
+ #
24
+ # @param value [Object?]
25
+ # @return [Object?]
26
+ def cast(value = nil, &block)
27
+ if block_given? && value.nil?
28
+ return definition.dsl.cast(&block)
29
+ end
30
+
31
+ value = definition.cast.call(value) if definition.cast
32
+
33
+ if definition.values[value].nil?
34
+ raise InvalidEnumOptionError.new(self, value)
35
+ end
36
+
37
+ value
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apia
4
+ module EnvironmentErrorHandling
5
+
6
+ # Raise an error
7
+ #
8
+ # @param error [String, Class] an error class or the name of a defined error
9
+ def raise_error(error, fields = {})
10
+ if error.respond_to?(:ancestors) && error.ancestors.include?(Apia::Error)
11
+ raise error.exception(fields)
12
+ end
13
+
14
+ if found_error = find_error_by_name(error)
15
+ raise found_error.exception(fields)
16
+ end
17
+
18
+ raise Apia::RuntimeError, "No error defined named #{error}"
19
+ end
20
+
21
+ # Return an error instance for a given exception class
22
+ #
23
+ # @param exception_class [Class] any error class
24
+ # @return [Class, nil] any class that inherits from Apia::Error or nil if no error is found
25
+ def error_for_exception(exception_class)
26
+ potential_error_sources.each do |source|
27
+ source.definition.potential_errors.each do |error|
28
+ if error.definition.catchable_exceptions.key?(exception_class)
29
+ return {
30
+ error: error,
31
+ block: error.definition.catchable_exceptions[exception_class]
32
+ }
33
+ end
34
+ end
35
+ end
36
+ nil
37
+ end
38
+
39
+ def raise_exception(exception)
40
+ error = error_for_exception(exception.class)
41
+ raise exception if error.nil?
42
+
43
+ fields = {}
44
+ error[:block]&.call(fields, exception)
45
+ raise error[:error].exception(fields)
46
+ end
47
+
48
+ private
49
+
50
+ def find_error_by_name(error_name)
51
+ return nil if potential_error_sources.nil?
52
+
53
+ potential_error_sources.each do |source|
54
+ error = find_potential_error(source, error_name)
55
+ return error if error
56
+ end
57
+
58
+ nil
59
+ end
60
+
61
+ def find_potential_error(source, name)
62
+ return nil if source.nil?
63
+
64
+ unless name =~ /\//
65
+ name = source.definition.id + '/' + name
66
+ end
67
+
68
+ source.definition.potential_errors.find do |error|
69
+ error.definition.id == name
70
+ end
71
+ end
72
+
73
+ end
74
+ end
data/lib/apia/error.rb ADDED
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apia/helpers'
4
+ require 'apia/defineable'
5
+ require 'apia/definitions/error'
6
+ require 'apia/object'
7
+ require 'apia/scalar'
8
+ require 'apia/errors/error_exception_error'
9
+
10
+ module Apia
11
+ # An Error represents a specific failure that can be raised by any
12
+ # action within the API.
13
+ #
14
+ # An error can specify a `code` which is textual description of the
15
+ # error which will be returned to the user.
16
+ #
17
+ # An HTTP status code can be provided which will be sent to the user
18
+ # if the error is incurred. If no HTTP status code is provided, the
19
+ # a 500 error code will be used.
20
+ #
21
+ # You can also define an array of additional fields that can
22
+ # included when the error is raised. This works in the same way as
23
+ # any type and an object implementing those methods should be provided
24
+ # when the error is raised.
25
+ class Error
26
+
27
+ extend Defineable
28
+
29
+ class << self
30
+
31
+ # Return the definition object for errors
32
+ #
33
+ # @return [Apia::Definitions::Error]
34
+ def definition
35
+ @definition ||= Definitions::Error.new(Helpers.class_name_to_id(name))
36
+ end
37
+
38
+ # Collate all objects that this error references and add them to the
39
+ # given object set
40
+ #
41
+ # @param set [Apia::ObjectSet]
42
+ # @return [void]
43
+ def collate_objects(set)
44
+ definition.fields.each_value do |field|
45
+ set.add_object(field.type.klass) if field.type.usable_for_field?
46
+ end
47
+ end
48
+
49
+ # Return an exception that should be raised to represent this error
50
+ # when it is actually invoked
51
+ #
52
+ # @param fields [Hash]
53
+ # @return [ErrorExceptionError]
54
+ def exception(fields = {})
55
+ ErrorExceptionError.new(self, fields)
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apia
4
+ class ErrorSet < Array
5
+
6
+ def validate(errors, object)
7
+ each_with_index do |error, index|
8
+ unless error.respond_to?(:ancestors) && error.ancestors.include?(Apia::Error)
9
+ errors.add object, 'InvalidPotentialError', "Potential error at index #{index} must be a class that inherits from Apia::Error"
10
+ end
11
+ end
12
+ end
13
+
14
+ end
15
+ end