apiwork 0.0.0.pre → 0.1.1
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.
- checksums.yaml +4 -4
- data/LICENSE.txt +2 -2
- data/README.md +117 -1
- data/Rakefile +5 -3
- data/app/controllers/apiwork/errors_controller.rb +13 -0
- data/app/controllers/apiwork/exports_controller.rb +22 -0
- data/lib/apiwork/abstractable.rb +26 -0
- data/lib/apiwork/adapter/base.rb +369 -0
- data/lib/apiwork/adapter/builder/api/base.rb +66 -0
- data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
- data/lib/apiwork/adapter/capability/api/base.rb +51 -0
- data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
- data/lib/apiwork/adapter/capability/base.rb +291 -0
- data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
- data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
- data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
- data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
- data/lib/apiwork/adapter/capability/result.rb +21 -0
- data/lib/apiwork/adapter/capability/runner.rb +56 -0
- data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
- data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
- data/lib/apiwork/adapter/registry.rb +16 -0
- data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
- data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
- data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
- data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
- data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
- data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
- data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
- data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
- data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
- data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
- data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
- data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
- data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
- data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
- data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
- data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
- data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
- data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
- data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
- data/lib/apiwork/adapter/standard.rb +22 -0
- data/lib/apiwork/adapter/wrapper/base.rb +70 -0
- data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
- data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
- data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
- data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
- data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
- data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
- data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
- data/lib/apiwork/adapter.rb +50 -0
- data/lib/apiwork/api/base.rb +802 -0
- data/lib/apiwork/api/element.rb +110 -0
- data/lib/apiwork/api/enum_registry/definition.rb +51 -0
- data/lib/apiwork/api/enum_registry.rb +98 -0
- data/lib/apiwork/api/info/contact.rb +67 -0
- data/lib/apiwork/api/info/license.rb +50 -0
- data/lib/apiwork/api/info/server.rb +50 -0
- data/lib/apiwork/api/info.rb +221 -0
- data/lib/apiwork/api/object.rb +235 -0
- data/lib/apiwork/api/registry.rb +33 -0
- data/lib/apiwork/api/representation_registry.rb +76 -0
- data/lib/apiwork/api/resource/action.rb +41 -0
- data/lib/apiwork/api/resource.rb +648 -0
- data/lib/apiwork/api/router.rb +104 -0
- data/lib/apiwork/api/type_registry/definition.rb +117 -0
- data/lib/apiwork/api/type_registry.rb +99 -0
- data/lib/apiwork/api/union.rb +49 -0
- data/lib/apiwork/api.rb +85 -0
- data/lib/apiwork/configurable.rb +71 -0
- data/lib/apiwork/configuration/option.rb +125 -0
- data/lib/apiwork/configuration/validatable.rb +25 -0
- data/lib/apiwork/configuration.rb +95 -0
- data/lib/apiwork/configuration_error.rb +6 -0
- data/lib/apiwork/constraint_error.rb +20 -0
- data/lib/apiwork/contract/action/request.rb +79 -0
- data/lib/apiwork/contract/action/response.rb +87 -0
- data/lib/apiwork/contract/action.rb +258 -0
- data/lib/apiwork/contract/base.rb +714 -0
- data/lib/apiwork/contract/element.rb +130 -0
- data/lib/apiwork/contract/object/coercer.rb +194 -0
- data/lib/apiwork/contract/object/deserializer.rb +101 -0
- data/lib/apiwork/contract/object/transformer.rb +95 -0
- data/lib/apiwork/contract/object/validator/result.rb +27 -0
- data/lib/apiwork/contract/object/validator.rb +734 -0
- data/lib/apiwork/contract/object.rb +566 -0
- data/lib/apiwork/contract/request_parser/result.rb +25 -0
- data/lib/apiwork/contract/request_parser.rb +72 -0
- data/lib/apiwork/contract/response_parser/result.rb +25 -0
- data/lib/apiwork/contract/response_parser.rb +35 -0
- data/lib/apiwork/contract/union.rb +56 -0
- data/lib/apiwork/contract_error.rb +9 -0
- data/lib/apiwork/controller.rb +300 -0
- data/lib/apiwork/domain_error.rb +13 -0
- data/lib/apiwork/element.rb +386 -0
- data/lib/apiwork/engine.rb +20 -0
- data/lib/apiwork/error.rb +6 -0
- data/lib/apiwork/error_code/definition.rb +63 -0
- data/lib/apiwork/error_code/registry.rb +18 -0
- data/lib/apiwork/error_code.rb +132 -0
- data/lib/apiwork/export/base.rb +291 -0
- data/lib/apiwork/export/open_api.rb +600 -0
- data/lib/apiwork/export/pipeline/writer.rb +66 -0
- data/lib/apiwork/export/pipeline.rb +84 -0
- data/lib/apiwork/export/registry.rb +16 -0
- data/lib/apiwork/export/surface_resolver.rb +189 -0
- data/lib/apiwork/export/type_analysis.rb +170 -0
- data/lib/apiwork/export/type_script.rb +23 -0
- data/lib/apiwork/export/type_script_mapper.rb +349 -0
- data/lib/apiwork/export/zod.rb +39 -0
- data/lib/apiwork/export/zod_mapper.rb +421 -0
- data/lib/apiwork/export.rb +80 -0
- data/lib/apiwork/http_error.rb +16 -0
- data/lib/apiwork/introspection/action/request.rb +66 -0
- data/lib/apiwork/introspection/action/response.rb +57 -0
- data/lib/apiwork/introspection/action.rb +124 -0
- data/lib/apiwork/introspection/api/info/contact.rb +59 -0
- data/lib/apiwork/introspection/api/info/license.rb +49 -0
- data/lib/apiwork/introspection/api/info/server.rb +50 -0
- data/lib/apiwork/introspection/api/info.rb +107 -0
- data/lib/apiwork/introspection/api/resource.rb +83 -0
- data/lib/apiwork/introspection/api.rb +92 -0
- data/lib/apiwork/introspection/contract.rb +63 -0
- data/lib/apiwork/introspection/dump/action.rb +101 -0
- data/lib/apiwork/introspection/dump/api.rb +119 -0
- data/lib/apiwork/introspection/dump/contract.rb +129 -0
- data/lib/apiwork/introspection/dump/param.rb +486 -0
- data/lib/apiwork/introspection/dump/resource.rb +112 -0
- data/lib/apiwork/introspection/dump/type.rb +339 -0
- data/lib/apiwork/introspection/dump.rb +17 -0
- data/lib/apiwork/introspection/enum.rb +63 -0
- data/lib/apiwork/introspection/error_code.rb +44 -0
- data/lib/apiwork/introspection/param/array.rb +88 -0
- data/lib/apiwork/introspection/param/base.rb +285 -0
- data/lib/apiwork/introspection/param/binary.rb +73 -0
- data/lib/apiwork/introspection/param/boolean.rb +73 -0
- data/lib/apiwork/introspection/param/date.rb +73 -0
- data/lib/apiwork/introspection/param/date_time.rb +73 -0
- data/lib/apiwork/introspection/param/decimal.rb +121 -0
- data/lib/apiwork/introspection/param/integer.rb +131 -0
- data/lib/apiwork/introspection/param/literal.rb +45 -0
- data/lib/apiwork/introspection/param/number.rb +121 -0
- data/lib/apiwork/introspection/param/object.rb +59 -0
- data/lib/apiwork/introspection/param/reference.rb +45 -0
- data/lib/apiwork/introspection/param/string.rb +122 -0
- data/lib/apiwork/introspection/param/time.rb +73 -0
- data/lib/apiwork/introspection/param/union.rb +57 -0
- data/lib/apiwork/introspection/param/unknown.rb +26 -0
- data/lib/apiwork/introspection/param/uuid.rb +73 -0
- data/lib/apiwork/introspection/param.rb +31 -0
- data/lib/apiwork/introspection/type.rb +129 -0
- data/lib/apiwork/introspection.rb +28 -0
- data/lib/apiwork/issue.rb +80 -0
- data/lib/apiwork/json_pointer.rb +21 -0
- data/lib/apiwork/object.rb +1618 -0
- data/lib/apiwork/reference_generator.rb +622 -0
- data/lib/apiwork/registry.rb +56 -0
- data/lib/apiwork/representation/association.rb +391 -0
- data/lib/apiwork/representation/attribute.rb +335 -0
- data/lib/apiwork/representation/base.rb +819 -0
- data/lib/apiwork/representation/deserializer.rb +95 -0
- data/lib/apiwork/representation/element.rb +128 -0
- data/lib/apiwork/representation/inheritance.rb +78 -0
- data/lib/apiwork/representation/model_detector.rb +75 -0
- data/lib/apiwork/representation/root_key.rb +35 -0
- data/lib/apiwork/representation/serializer.rb +127 -0
- data/lib/apiwork/request.rb +79 -0
- data/lib/apiwork/response.rb +56 -0
- data/lib/apiwork/union.rb +102 -0
- data/lib/apiwork/version.rb +2 -2
- data/lib/apiwork.rb +61 -3
- data/lib/generators/apiwork/api_generator.rb +38 -0
- data/lib/generators/apiwork/contract_generator.rb +25 -0
- data/lib/generators/apiwork/install_generator.rb +27 -0
- data/lib/generators/apiwork/representation_generator.rb +25 -0
- data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
- data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
- data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
- data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
- data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
- data/lib/tasks/apiwork.rake +102 -0
- metadata +319 -19
- data/.rubocop.yml +0 -8
- data/sig/apiwork.rbs +0 -4
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module API
|
|
5
|
+
class Router
|
|
6
|
+
class << self
|
|
7
|
+
def draw
|
|
8
|
+
new.draw
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def draw
|
|
13
|
+
api_classes = Registry.values
|
|
14
|
+
router = self
|
|
15
|
+
|
|
16
|
+
set = ActionDispatch::Routing::RouteSet.new
|
|
17
|
+
|
|
18
|
+
set.draw do
|
|
19
|
+
api_classes.each do |api_class|
|
|
20
|
+
next if api_class.base_path.blank? || api_class.root_resource.blank?
|
|
21
|
+
|
|
22
|
+
if api_class.export_configs.any?
|
|
23
|
+
scope path: api_class.transform_path(api_class.base_path) do
|
|
24
|
+
api_class.export_configs.each do |export_name, export_config|
|
|
25
|
+
next unless case export_config.endpoint.mode
|
|
26
|
+
when :always then true
|
|
27
|
+
when :never then false
|
|
28
|
+
when :auto then Rails.env.development?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
get export_config.endpoint.path || "/.#{export_name}",
|
|
32
|
+
defaults: { export_name:, api_base_path: api_class.base_path },
|
|
33
|
+
to: 'apiwork/exports#show'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
scope module: api_class.namespaces.map(&:to_s).join('/').underscore,
|
|
39
|
+
path: api_class.transform_path(api_class.base_path) do
|
|
40
|
+
router.draw_resources(self, api_class.root_resource.resources, api_class)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
scope path: api_class.transform_path(api_class.base_path) do
|
|
44
|
+
match '*unmatched', to: 'apiwork/errors#not_found', via: :all
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
set
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def draw_resources(context, resources_hash, api_class)
|
|
53
|
+
resources_hash.each_value do |resource|
|
|
54
|
+
resource_method = resource.singular ? :resource : :resources
|
|
55
|
+
options = {
|
|
56
|
+
constraints: resource.constraints,
|
|
57
|
+
controller: resource.controller,
|
|
58
|
+
defaults: resource.defaults,
|
|
59
|
+
except: resource.except,
|
|
60
|
+
only: resource.only,
|
|
61
|
+
param: resource.param,
|
|
62
|
+
}.compact
|
|
63
|
+
|
|
64
|
+
path_option = api_class.transform_path(resource.path || resource.name)
|
|
65
|
+
options[:path] = path_option unless path_option == resource.name.to_s
|
|
66
|
+
|
|
67
|
+
router = self
|
|
68
|
+
|
|
69
|
+
context.instance_eval do
|
|
70
|
+
send(resource_method, resource.name, **options) do
|
|
71
|
+
if resource.member_actions.any?
|
|
72
|
+
member do
|
|
73
|
+
resource.member_actions.each_value do |action|
|
|
74
|
+
action_path = api_class.transform_path(action.name)
|
|
75
|
+
if action_path == action.name.to_s
|
|
76
|
+
send(action.method, action.name)
|
|
77
|
+
else
|
|
78
|
+
send(action.method, action.name, path: action_path)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if resource.collection_actions.any?
|
|
85
|
+
collection do
|
|
86
|
+
resource.collection_actions.each_value do |action|
|
|
87
|
+
action_path = api_class.transform_path(action.name)
|
|
88
|
+
if action_path == action.name.to_s
|
|
89
|
+
send(action.method, action.name)
|
|
90
|
+
else
|
|
91
|
+
send(action.method, action.name, path: action_path)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
router.draw_resources(self, resource.resources, api_class)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module API
|
|
5
|
+
class TypeRegistry
|
|
6
|
+
class Definition
|
|
7
|
+
attr_reader :deprecated,
|
|
8
|
+
:description,
|
|
9
|
+
:discriminator,
|
|
10
|
+
:example,
|
|
11
|
+
:kind,
|
|
12
|
+
:name,
|
|
13
|
+
:scope
|
|
14
|
+
|
|
15
|
+
def initialize(
|
|
16
|
+
name,
|
|
17
|
+
kind:,
|
|
18
|
+
scope: nil,
|
|
19
|
+
block: nil,
|
|
20
|
+
deprecated: false,
|
|
21
|
+
description: nil,
|
|
22
|
+
discriminator: nil,
|
|
23
|
+
example: nil,
|
|
24
|
+
fragment: false
|
|
25
|
+
)
|
|
26
|
+
@name = name
|
|
27
|
+
@kind = kind
|
|
28
|
+
@scope = scope
|
|
29
|
+
@block = block
|
|
30
|
+
@deprecated = deprecated
|
|
31
|
+
@description = description
|
|
32
|
+
@discriminator = discriminator
|
|
33
|
+
@example = example
|
|
34
|
+
@fragment = fragment
|
|
35
|
+
@shape = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def deprecated?
|
|
39
|
+
@deprecated == true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def fragment?
|
|
43
|
+
@fragment == true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def object?
|
|
47
|
+
@kind == :object
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def union?
|
|
51
|
+
@kind == :union
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def shape
|
|
55
|
+
ensure_shape_built!
|
|
56
|
+
@shape
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def params
|
|
60
|
+
shape.params if object?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def variants
|
|
64
|
+
shape.variants if union?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def merge(block:, deprecated:, description:, example:)
|
|
68
|
+
Definition.new(
|
|
69
|
+
@name,
|
|
70
|
+
block: merge_blocks(@block, block),
|
|
71
|
+
deprecated: deprecated || @deprecated,
|
|
72
|
+
description: description || @description,
|
|
73
|
+
discriminator: @discriminator,
|
|
74
|
+
example: example || @example,
|
|
75
|
+
fragment: @fragment,
|
|
76
|
+
kind: @kind,
|
|
77
|
+
scope: @scope,
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def merge_blocks(existing_block, new_block)
|
|
84
|
+
return existing_block unless new_block
|
|
85
|
+
return new_block unless existing_block
|
|
86
|
+
|
|
87
|
+
proc do |shape|
|
|
88
|
+
if existing_block.arity.positive?
|
|
89
|
+
existing_block.call(shape)
|
|
90
|
+
else
|
|
91
|
+
shape.instance_eval(&existing_block)
|
|
92
|
+
end
|
|
93
|
+
if new_block.arity.positive?
|
|
94
|
+
new_block.call(shape)
|
|
95
|
+
else
|
|
96
|
+
shape.instance_eval(&new_block)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def ensure_shape_built!
|
|
102
|
+
return if @shape
|
|
103
|
+
|
|
104
|
+
@shape = if object?
|
|
105
|
+
Object.new
|
|
106
|
+
else
|
|
107
|
+
Union.new(discriminator: @discriminator)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return unless @block
|
|
111
|
+
|
|
112
|
+
@block.arity.positive? ? @block.call(@shape) : @shape.instance_eval(&@block)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module API
|
|
5
|
+
class TypeRegistry
|
|
6
|
+
def initialize
|
|
7
|
+
@store = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def register(
|
|
11
|
+
name,
|
|
12
|
+
kind:,
|
|
13
|
+
scope: nil,
|
|
14
|
+
deprecated: false,
|
|
15
|
+
description: nil,
|
|
16
|
+
discriminator: nil,
|
|
17
|
+
example: nil,
|
|
18
|
+
fragment: false,
|
|
19
|
+
&block
|
|
20
|
+
)
|
|
21
|
+
key = scoped_name(scope, name)
|
|
22
|
+
|
|
23
|
+
if @store.key?(key)
|
|
24
|
+
validate_kind_consistency!(key, kind)
|
|
25
|
+
merge(key, block:, deprecated:, description:, example:)
|
|
26
|
+
else
|
|
27
|
+
@store[key] = Definition.new(
|
|
28
|
+
key,
|
|
29
|
+
block:,
|
|
30
|
+
deprecated:,
|
|
31
|
+
description:,
|
|
32
|
+
discriminator:,
|
|
33
|
+
example:,
|
|
34
|
+
fragment:,
|
|
35
|
+
kind:,
|
|
36
|
+
scope:,
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def [](name)
|
|
42
|
+
@store[name]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def key?(name)
|
|
46
|
+
@store.key?(name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def each_pair(&block)
|
|
50
|
+
@store.each_pair(&block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def exists?(name, scope: nil)
|
|
54
|
+
find(name, scope:).present?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def find(name, scope: nil)
|
|
58
|
+
definition = scope ? @store[scoped_name(scope, name)] : nil
|
|
59
|
+
definition || @store[name]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def scoped_name(scope, name)
|
|
63
|
+
return name unless scope
|
|
64
|
+
|
|
65
|
+
prefix = scope.respond_to?(:scope_prefix) ? scope.scope_prefix : nil
|
|
66
|
+
return name unless prefix
|
|
67
|
+
return prefix.to_sym if name.nil?
|
|
68
|
+
return prefix.to_sym if name.to_s.empty?
|
|
69
|
+
return name.to_sym if name.to_s == prefix
|
|
70
|
+
|
|
71
|
+
[prefix, name].join('_').to_sym
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def clear!
|
|
75
|
+
@store.clear
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def validate_kind_consistency!(key, new_kind)
|
|
81
|
+
definition = @store[key]
|
|
82
|
+
return if definition.kind == new_kind
|
|
83
|
+
|
|
84
|
+
raise ConfigurationError,
|
|
85
|
+
"Cannot redefine :#{key} as #{new_kind}, already defined as #{definition.kind}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def merge(key, block:, deprecated:, description:, example:)
|
|
89
|
+
definition = @store[key]
|
|
90
|
+
@store[key] = definition.merge(
|
|
91
|
+
block:,
|
|
92
|
+
deprecated:,
|
|
93
|
+
description:,
|
|
94
|
+
example:,
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module API
|
|
5
|
+
# @api public
|
|
6
|
+
# Block context for defining reusable union types.
|
|
7
|
+
#
|
|
8
|
+
# Accessed via `union :name do` in API or contract definitions.
|
|
9
|
+
# Use {#variant} to define possible types.
|
|
10
|
+
#
|
|
11
|
+
# @see API::Element Block context for variant types
|
|
12
|
+
# @see Contract::Union Block context for inline unions
|
|
13
|
+
#
|
|
14
|
+
# @example instance_eval style
|
|
15
|
+
# union :payment_method, discriminator: :type do
|
|
16
|
+
# variant tag: 'card' do
|
|
17
|
+
# object do
|
|
18
|
+
# string :last_four
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
# variant tag: 'bank' do
|
|
22
|
+
# object do
|
|
23
|
+
# string :account_number
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example yield style
|
|
29
|
+
# union :payment_method, discriminator: :type do |union|
|
|
30
|
+
# union.variant tag: 'card' do |variant|
|
|
31
|
+
# variant.object do |object|
|
|
32
|
+
# object.string :last_four
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
# union.variant tag: 'bank' do |variant|
|
|
36
|
+
# variant.object do |object|
|
|
37
|
+
# object.string :account_number
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
class Union < Apiwork::Union
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_element
|
|
45
|
+
Element.new
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/apiwork/api.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
# @api public
|
|
5
|
+
module API
|
|
6
|
+
class << self
|
|
7
|
+
# @!method find(base_path)
|
|
8
|
+
# @api public
|
|
9
|
+
# Finds an API by base path.
|
|
10
|
+
# @param base_path [String]
|
|
11
|
+
# The API base path.
|
|
12
|
+
# @return [Class<API::Base>, nil]
|
|
13
|
+
# @see .find!
|
|
14
|
+
# @example
|
|
15
|
+
# Apiwork::API.find('/api/v1')
|
|
16
|
+
#
|
|
17
|
+
# @!method find!(base_path)
|
|
18
|
+
# @api public
|
|
19
|
+
# Finds an API by base path.
|
|
20
|
+
# @param base_path [String]
|
|
21
|
+
# The API base path.
|
|
22
|
+
# @return [Class<API::Base>]
|
|
23
|
+
# @raise [KeyError] if the API is not found
|
|
24
|
+
# @see .find
|
|
25
|
+
# @example
|
|
26
|
+
# Apiwork::API.find!('/api/v1')
|
|
27
|
+
delegate :clear!,
|
|
28
|
+
:exists?,
|
|
29
|
+
:find,
|
|
30
|
+
:find!,
|
|
31
|
+
:keys,
|
|
32
|
+
:unregister,
|
|
33
|
+
:values,
|
|
34
|
+
to: Registry
|
|
35
|
+
|
|
36
|
+
# @api public
|
|
37
|
+
# Defines a new API at the given base path.
|
|
38
|
+
#
|
|
39
|
+
# This is the main entry point for creating an Apiwork API.
|
|
40
|
+
# The block receives an API recorder for defining resources,
|
|
41
|
+
# types, and configuration.
|
|
42
|
+
#
|
|
43
|
+
# @param base_path [String]
|
|
44
|
+
# The API base path.
|
|
45
|
+
# @yield block for API definition
|
|
46
|
+
# @return [Class<API::Base>]
|
|
47
|
+
#
|
|
48
|
+
# @example Basic API
|
|
49
|
+
# Apiwork::API.define '/api/v1' do
|
|
50
|
+
# resources :users
|
|
51
|
+
# resources :posts
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# @example With configuration
|
|
55
|
+
# Apiwork::API.define '/api/v1' do
|
|
56
|
+
# key_format :camel
|
|
57
|
+
#
|
|
58
|
+
# resources :invoices
|
|
59
|
+
# end
|
|
60
|
+
def define(base_path, &block)
|
|
61
|
+
return unless block
|
|
62
|
+
|
|
63
|
+
Class.new(Base).tap do |klass|
|
|
64
|
+
klass.mount(base_path)
|
|
65
|
+
klass.class_eval(&block)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @api public
|
|
70
|
+
# The introspection data for an API.
|
|
71
|
+
#
|
|
72
|
+
# @param base_path [String]
|
|
73
|
+
# The API base path.
|
|
74
|
+
# @param locale [Symbol, nil] (nil)
|
|
75
|
+
# The locale for descriptions.
|
|
76
|
+
# @return [Introspection::API]
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# Apiwork::API.introspect('/api/v1')
|
|
80
|
+
def introspect(base_path, locale: nil)
|
|
81
|
+
find!(base_path).introspect(locale:)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Configurable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def define(extends: nil, &block)
|
|
9
|
+
Class.new do
|
|
10
|
+
include Configurable
|
|
11
|
+
|
|
12
|
+
self.options = extends.options.dup if extends
|
|
13
|
+
|
|
14
|
+
class_eval(&block) if block
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
included do
|
|
20
|
+
class_attribute :options, default: {}, instance_predicate: false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @!method option(name, type:, default: nil, enum: nil, &block)
|
|
24
|
+
# @!scope class
|
|
25
|
+
# @api public
|
|
26
|
+
# Defines a configuration option.
|
|
27
|
+
#
|
|
28
|
+
# For nested options, use `type: :hash` with a block. Inside the block,
|
|
29
|
+
# call `option` to define child options.
|
|
30
|
+
#
|
|
31
|
+
# @param name [Symbol]
|
|
32
|
+
# The option name.
|
|
33
|
+
# @param type [Symbol] [:boolean, :hash, :integer, :string, :symbol]
|
|
34
|
+
# The option type.
|
|
35
|
+
# @param default [Object, nil] (nil)
|
|
36
|
+
# The default value.
|
|
37
|
+
# @param enum [Array, nil] (nil)
|
|
38
|
+
# The allowed values.
|
|
39
|
+
# @yield block evaluated in {Configuration::Option} context (for :hash type)
|
|
40
|
+
# @return [void]
|
|
41
|
+
# @see Configuration::Option
|
|
42
|
+
#
|
|
43
|
+
# @example Symbol option
|
|
44
|
+
# option :locale, type: :symbol, default: :en
|
|
45
|
+
#
|
|
46
|
+
# @example String option with enum
|
|
47
|
+
# option :version, type: :string, default: '5', enum: %w[4 5]
|
|
48
|
+
#
|
|
49
|
+
# @example Nested options
|
|
50
|
+
# option :pagination, type: :hash do
|
|
51
|
+
# option :strategy, type: :symbol, default: :offset, enum: %i[offset cursor]
|
|
52
|
+
# option :default_size, type: :integer, default: 20
|
|
53
|
+
# option :max_size, type: :integer, default: 100
|
|
54
|
+
# end
|
|
55
|
+
|
|
56
|
+
class_methods do
|
|
57
|
+
def inherited(subclass)
|
|
58
|
+
super
|
|
59
|
+
subclass.options = options.dup
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def option(name, default: nil, enum: nil, type:, &block)
|
|
63
|
+
options[name] = Configuration::Option.new(name, type, default:, enum:, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def default_options
|
|
67
|
+
options.transform_values(&:effective_default).compact
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
class Configuration
|
|
5
|
+
# @api public
|
|
6
|
+
# Block context for nested configuration options.
|
|
7
|
+
#
|
|
8
|
+
# Used inside `option :name, type: :hash do ... end` blocks
|
|
9
|
+
# in {Adapter::Base} and {Export::Base} subclasses.
|
|
10
|
+
#
|
|
11
|
+
# @see Adapter::Base
|
|
12
|
+
# @see Export::Base
|
|
13
|
+
# @see Configuration
|
|
14
|
+
#
|
|
15
|
+
# @example instance_eval style
|
|
16
|
+
# option :pagination, type: :hash do
|
|
17
|
+
# option :strategy, type: :symbol, default: :offset
|
|
18
|
+
# option :default_size, type: :integer, default: 20
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example yield style
|
|
22
|
+
# option :pagination, type: :hash do |option|
|
|
23
|
+
# option.option :strategy, type: :symbol, default: :offset
|
|
24
|
+
# option.option :default_size, type: :integer, default: 20
|
|
25
|
+
# end
|
|
26
|
+
class Option
|
|
27
|
+
include Validatable
|
|
28
|
+
|
|
29
|
+
attr_reader :children,
|
|
30
|
+
:default,
|
|
31
|
+
:enum,
|
|
32
|
+
:name,
|
|
33
|
+
:type
|
|
34
|
+
|
|
35
|
+
def initialize(name, type, children: nil, default: nil, enum: nil, &block)
|
|
36
|
+
@name = name
|
|
37
|
+
@type = type
|
|
38
|
+
@default = default
|
|
39
|
+
@enum = enum
|
|
40
|
+
@children = children || {}
|
|
41
|
+
|
|
42
|
+
return unless block && type == :hash
|
|
43
|
+
|
|
44
|
+
block.arity.positive? ? yield(self) : instance_eval(&block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @api public
|
|
48
|
+
# Defines a nested option.
|
|
49
|
+
#
|
|
50
|
+
# @param name [Symbol]
|
|
51
|
+
# The option name.
|
|
52
|
+
# @param default [Object, nil] (nil)
|
|
53
|
+
# The default value.
|
|
54
|
+
# @param enum [Array, nil] (nil)
|
|
55
|
+
# The allowed values.
|
|
56
|
+
# @param type [Symbol] [:boolean, :hash, :integer, :string, :symbol]
|
|
57
|
+
# The option type.
|
|
58
|
+
# @yield block for nested options (type: :hash)
|
|
59
|
+
# @yieldparam option [Option]
|
|
60
|
+
# @return [void]
|
|
61
|
+
#
|
|
62
|
+
# @example instance_eval style
|
|
63
|
+
# option :pagination, type: :hash do
|
|
64
|
+
# option :strategy, type: :symbol, default: :offset
|
|
65
|
+
# option :default_size, type: :integer, default: 20
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# @example yield style
|
|
69
|
+
# option :pagination, type: :hash do |option|
|
|
70
|
+
# option.option :strategy, type: :symbol, default: :offset
|
|
71
|
+
# option.option :default_size, type: :integer, default: 20
|
|
72
|
+
# end
|
|
73
|
+
def option(name, default: nil, enum: nil, type:, &block)
|
|
74
|
+
@children[name] = Option.new(name, type, default:, enum:, &block)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def nested?
|
|
78
|
+
children.any?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def effective_default
|
|
82
|
+
return default unless nested?
|
|
83
|
+
|
|
84
|
+
children.transform_values(&:default)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate!(value)
|
|
88
|
+
return if value.nil?
|
|
89
|
+
|
|
90
|
+
if nested?
|
|
91
|
+
raise ConfigurationError, "#{name} must be a Hash" unless value.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
validate_children!(value)
|
|
94
|
+
else
|
|
95
|
+
validate_type!(value)
|
|
96
|
+
validate_enum!(value) if enum
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def cast(value)
|
|
101
|
+
return nil if value.nil?
|
|
102
|
+
return value unless value.is_a?(String)
|
|
103
|
+
|
|
104
|
+
case type
|
|
105
|
+
when :symbol then value.to_sym
|
|
106
|
+
when :string then value
|
|
107
|
+
when :integer then value.to_i
|
|
108
|
+
when :boolean then %w[true 1 yes].include?(value.downcase)
|
|
109
|
+
when :hash then value
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def validate_children!(hash)
|
|
116
|
+
hash.each do |key, value|
|
|
117
|
+
child = children[key]
|
|
118
|
+
raise ConfigurationError, "Unknown option: #{name}.#{key}" unless child
|
|
119
|
+
|
|
120
|
+
child.validate!(value)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
class Configuration
|
|
5
|
+
module Validatable
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def validate_type!(value)
|
|
9
|
+
valid = case type
|
|
10
|
+
when :symbol then value.is_a?(Symbol)
|
|
11
|
+
when :string then value.is_a?(String)
|
|
12
|
+
when :integer then value.is_a?(Integer)
|
|
13
|
+
when :hash then value.is_a?(Hash)
|
|
14
|
+
end
|
|
15
|
+
raise ConfigurationError, "#{name} must be #{type}, got #{value.class}" unless valid
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate_enum!(value)
|
|
19
|
+
return if enum.include?(value)
|
|
20
|
+
|
|
21
|
+
raise ConfigurationError, "#{name} must be one of #{enum.inspect}, got #{value.inspect}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|