roda-endpoints 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.dockerignore +98 -0
- data/.gitignore +9 -0
- data/.gitlab-ci.yml +85 -0
- data/.rspec +3 -0
- data/.rubocop.yml +51 -0
- data/.ruby-version +1 -0
- data/.simplecov +7 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +11 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +42 -0
- data/Rakefile +4 -0
- data/bin/bundle-audit +17 -0
- data/bin/console +15 -0
- data/bin/rspec +17 -0
- data/bin/rubocop +17 -0
- data/bin/setup +8 -0
- data/bin/yard +17 -0
- data/bin/yardoc +17 -0
- data/bin/yri +17 -0
- data/lib/roda/endpoints/endpoint/caching.rb +28 -0
- data/lib/roda/endpoints/endpoint/class_interface.rb +123 -0
- data/lib/roda/endpoints/endpoint/collection.rb +77 -0
- data/lib/roda/endpoints/endpoint/data.rb +29 -0
- data/lib/roda/endpoints/endpoint/item.rb +117 -0
- data/lib/roda/endpoints/endpoint/namespace.rb +48 -0
- data/lib/roda/endpoints/endpoint/operations.rb +28 -0
- data/lib/roda/endpoints/endpoint/transactions.rb +67 -0
- data/lib/roda/endpoints/endpoint/validations.rb +98 -0
- data/lib/roda/endpoints/endpoint/verbs.rb +45 -0
- data/lib/roda/endpoints/endpoint.rb +83 -0
- data/lib/roda/endpoints/functions.rb +26 -0
- data/lib/roda/endpoints/repository.rb +26 -0
- data/lib/roda/endpoints/transactions.rb +75 -0
- data/lib/roda/endpoints/types.rb +13 -0
- data/lib/roda/endpoints/version.rb +7 -0
- data/lib/roda/endpoints.rb +21 -0
- data/lib/roda/plugins/endpoints.rb +161 -0
- data/lib/rom/struct/to_json.rb +18 -0
- data/rakelib/bundle_audit.rake +4 -0
- data/rakelib/bundler.rake +2 -0
- data/rakelib/rspec.rake +6 -0
- data/rakelib/rubocop.rake +5 -0
- data/rakelib/yard.rake +5 -0
- data/roda-endpoints.gemspec +49 -0
- metadata +385 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roda/endpoints'
|
4
|
+
require 'roda/endpoints/transactions'
|
5
|
+
require 'inflecto'
|
6
|
+
|
7
|
+
class Roda
|
8
|
+
module Endpoints
|
9
|
+
class Endpoint
|
10
|
+
module ClassInterface
|
11
|
+
# @return [<Symbol>]
|
12
|
+
attr_accessor :attributes
|
13
|
+
|
14
|
+
# @return [{Symbol=>Object}]
|
15
|
+
attr_accessor :defaults
|
16
|
+
|
17
|
+
# @return [{Symbol=>{Symbol=>Symbol}}]
|
18
|
+
attr_accessor :statuses
|
19
|
+
|
20
|
+
# @return [<Symbol>]
|
21
|
+
attr_accessor :verbs
|
22
|
+
|
23
|
+
# @param [Symbol] key
|
24
|
+
def define_attribute(key)
|
25
|
+
self.attributes += [key]
|
26
|
+
attr_reader key
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Dry::Container]
|
30
|
+
def container
|
31
|
+
@container ||= Roda::Endpoints.container
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param [Class] child
|
35
|
+
def inherited(child)
|
36
|
+
child.attributes = attributes.dup
|
37
|
+
child.defaults = defaults.dup
|
38
|
+
child.statuses = statuses.dup
|
39
|
+
child.verbs = verbs.dup
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def ns
|
44
|
+
@ns ||= name.gsub(/^Roda::Endpoints::/, '').underscore.tr('/', '.')
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Symbol]
|
48
|
+
def type
|
49
|
+
@type ||= Inflecto.underscore(Inflecto.demodulize(name)).to_sym
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param [Symbol] name
|
53
|
+
# @param [Proc] block
|
54
|
+
# @return [Symbol] name of the defined method
|
55
|
+
def verb(name, rescue_from: [], &block)
|
56
|
+
rescue_from = Array(rescue_from).flatten
|
57
|
+
if rescue_from.any?
|
58
|
+
block = proc do |*args|
|
59
|
+
begin
|
60
|
+
instance_exec(*args, &block)
|
61
|
+
rescue *rescue_from
|
62
|
+
Left($ERROR_INFO)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
define_method(name, &block)
|
67
|
+
container.register "operations.#{type}.#{name}", block
|
68
|
+
self.verbs ||= superclass.verbs
|
69
|
+
(self.verbs += [name]).freeze
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param [String, Symbol] key
|
73
|
+
# @param [Proc] block
|
74
|
+
#
|
75
|
+
# @example
|
76
|
+
# r.collection :articles do |articles|
|
77
|
+
# # register validation at 'validations.endpoints.articles.default'
|
78
|
+
# articles.validate do
|
79
|
+
# required(:title).filled?
|
80
|
+
# required(:contents).filled?
|
81
|
+
# end
|
82
|
+
# # redefine validation for patch method at
|
83
|
+
# # 'validations.endpoints.articles.patch'
|
84
|
+
# articles.validate(:patch) do
|
85
|
+
# required(:title).filled?
|
86
|
+
# required(:contents).filled?
|
87
|
+
# require(:confirm_changes).
|
88
|
+
# end
|
89
|
+
# end
|
90
|
+
def validate(key = :default, &block)
|
91
|
+
key = "validations.#{ns}.#{key}" if key.is_a?(Symbol)
|
92
|
+
schema = Dry::Validation.Form(&block)
|
93
|
+
container.register(key) do |params|
|
94
|
+
validation = schema.call(params)
|
95
|
+
if validation.success?
|
96
|
+
Right(validation.output)
|
97
|
+
else
|
98
|
+
Left([:unprocessable_entity, {}, validation])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
schema
|
102
|
+
end
|
103
|
+
# rubocop:enable Metrics/MethodLength
|
104
|
+
|
105
|
+
# @param [Proc] block
|
106
|
+
# @return [Proc]
|
107
|
+
def route(&block)
|
108
|
+
@route_block = block if block_given?
|
109
|
+
@route_block
|
110
|
+
end
|
111
|
+
|
112
|
+
def transaction(name, &block)
|
113
|
+
transactions << [name, block]
|
114
|
+
end
|
115
|
+
|
116
|
+
# @return [(Symbol, Hash, Proc)]
|
117
|
+
def transactions
|
118
|
+
@transactions ||= []
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roda/endpoints/endpoint'
|
4
|
+
require 'roda/endpoints/endpoint/data'
|
5
|
+
require 'roda/endpoints/endpoint/caching'
|
6
|
+
require 'inflecto'
|
7
|
+
require 'rom/sql'
|
8
|
+
|
9
|
+
class Roda
|
10
|
+
module Endpoints
|
11
|
+
class Endpoint
|
12
|
+
# HTTP endpoint representing a collection of items of the same type.
|
13
|
+
class Collection < Endpoint
|
14
|
+
include Data
|
15
|
+
prepend Caching
|
16
|
+
|
17
|
+
self.attributes += %i(item)
|
18
|
+
self.defaults = defaults.merge(
|
19
|
+
type: :collection,
|
20
|
+
last_modified: :last_modified
|
21
|
+
)
|
22
|
+
|
23
|
+
# @return [{Symbol=>Object}]
|
24
|
+
attr_reader :item
|
25
|
+
|
26
|
+
# @return [Time]
|
27
|
+
def last_modified
|
28
|
+
@last_modified ? repository.public_send(@last_modified) : super
|
29
|
+
end
|
30
|
+
|
31
|
+
def child(**params)
|
32
|
+
with(
|
33
|
+
type: Item,
|
34
|
+
name: resource_name,
|
35
|
+
**params
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Symbol]
|
40
|
+
def resource_name
|
41
|
+
Inflecto.singularize(name).to_sym
|
42
|
+
end
|
43
|
+
|
44
|
+
route do |r, endpoint| # r.collection :articles do |articles|
|
45
|
+
r.last_modified endpoint.last_modified if endpoint.last_modified
|
46
|
+
|
47
|
+
endpoint.verbs.each do |verb|
|
48
|
+
# @route #{verb} /:name
|
49
|
+
r.public_send(verb, transaction: verb)
|
50
|
+
|
51
|
+
r.child **endpoint.item if endpoint.item # child by: :id
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
verb :get do |params|
|
56
|
+
Right(repository.list(**params))
|
57
|
+
end
|
58
|
+
|
59
|
+
verb :post do |params|
|
60
|
+
params = params[resource_name] || {}
|
61
|
+
Right(repository.create(**params))
|
62
|
+
end
|
63
|
+
|
64
|
+
# @route GET /{collection.name}
|
65
|
+
transaction :get do |endpoint|
|
66
|
+
step :retrieve, with: endpoint.operation_for(:get)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @route POST /{collection.name}
|
70
|
+
transaction :post do |endpoint|
|
71
|
+
step :validate, with: endpoint.validation_for(:post)
|
72
|
+
step :persist, with: endpoint.operation_for(:post)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module Endpoints
|
5
|
+
# Generic HTTP endpoint abstraction.
|
6
|
+
class Endpoint
|
7
|
+
# Accessing data inside of endpoint.
|
8
|
+
module Data
|
9
|
+
# @param name [String]
|
10
|
+
# @param repository [String]
|
11
|
+
# @param attributes [{Symbol=>Object}]
|
12
|
+
def initialize(name:, repository: "repositories.#{name}", **attributes)
|
13
|
+
@repository_key = repository
|
14
|
+
super(name: name, **attributes)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [ROM::Repository]
|
18
|
+
def repository
|
19
|
+
container[@repository_key] if @repository_key
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [{Symbol=>Object}]
|
23
|
+
def to_hash
|
24
|
+
super.merge(repository: @repository_key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roda/endpoints/endpoint'
|
4
|
+
require 'roda/endpoints/endpoint/data'
|
5
|
+
require 'roda/endpoints/endpoint/caching'
|
6
|
+
|
7
|
+
class Roda
|
8
|
+
module Endpoints
|
9
|
+
class Endpoint
|
10
|
+
# HTTP endpoint representing a specific item of collection uniquely
|
11
|
+
# identified by some parameter.
|
12
|
+
class Item < Endpoint
|
13
|
+
include Data
|
14
|
+
include Caching
|
15
|
+
|
16
|
+
self.attributes += %i(id by finder last_modified)
|
17
|
+
self.defaults = defaults.merge(
|
18
|
+
by: :fetch,
|
19
|
+
finder: lambda do
|
20
|
+
repository.public_send(by, id)
|
21
|
+
end,
|
22
|
+
last_modified: :updated_at
|
23
|
+
)
|
24
|
+
|
25
|
+
# @return [Symbol]
|
26
|
+
attr_reader :by
|
27
|
+
|
28
|
+
# @return [Symbol]
|
29
|
+
attr_reader :id
|
30
|
+
|
31
|
+
# @return [Symbol]
|
32
|
+
attr_reader :finder
|
33
|
+
|
34
|
+
# @return [Endpoint::Collection]
|
35
|
+
def collection
|
36
|
+
parent
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [ROM::Struct]
|
40
|
+
def entity
|
41
|
+
@entity ||= fetch_entity
|
42
|
+
end
|
43
|
+
|
44
|
+
def fetch_entity
|
45
|
+
instance_exec(&finder)
|
46
|
+
end
|
47
|
+
|
48
|
+
def last_modified
|
49
|
+
@last_modified ? entity.public_send(@last_modified) : super
|
50
|
+
end
|
51
|
+
|
52
|
+
route do |r, endpoint|
|
53
|
+
endpoint.verbs.each do |verb|
|
54
|
+
# @route #{verb} /{collection.name}/{id}
|
55
|
+
# STDOUT.puts [verb].pretty_inspect
|
56
|
+
r.public_send(verb, transaction: verb)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
transaction :get do |endpoint|
|
61
|
+
step :retrieve, with: endpoint.operation_for(:get)
|
62
|
+
end
|
63
|
+
|
64
|
+
transaction :patch do |endpoint|
|
65
|
+
step :validate, with: endpoint.validation_for(:patch)
|
66
|
+
step :persist, with: endpoint.operation_for(:patch)
|
67
|
+
end
|
68
|
+
|
69
|
+
transaction :put do |endpoint|
|
70
|
+
step :validate, with: endpoint.validation_for(:put)
|
71
|
+
# step :reset, with: 'endpoints.operations.reset'
|
72
|
+
step :persist, with: endpoint.operation_for(:put)
|
73
|
+
end
|
74
|
+
|
75
|
+
transaction :delete do |endpoint|
|
76
|
+
step :validate, with: endpoint.validation_for(:delete)
|
77
|
+
step :persist, with: endpoint.operation_for(:delete)
|
78
|
+
end
|
79
|
+
|
80
|
+
# @route GET /{collection.name}/{id}
|
81
|
+
# @param [Hash] params
|
82
|
+
# @return [Dry::Monads::Either]
|
83
|
+
verb :get do |_params|
|
84
|
+
Right(entity)
|
85
|
+
end
|
86
|
+
|
87
|
+
# @route PATCH /{collection.name}/{id}
|
88
|
+
# @return [Dry::Monads::Either]
|
89
|
+
verb :patch do |params|
|
90
|
+
changeset = params[name]
|
91
|
+
Right(repository.update(id, changeset))
|
92
|
+
end
|
93
|
+
|
94
|
+
# @route PUT /{collection.name}/{id}
|
95
|
+
# @return [Dry::Monads::Either]
|
96
|
+
verb :put do |params|
|
97
|
+
changeset = entity.to_hash.keys.each_with_object(
|
98
|
+
{}
|
99
|
+
) do |key, changeset|
|
100
|
+
changeset[key] = nil
|
101
|
+
end.merge(params[name] || {})
|
102
|
+
Right(repository.update(id, changeset))
|
103
|
+
end
|
104
|
+
|
105
|
+
# @route DELETE /{collection.name}/{id}
|
106
|
+
# @return [Dry::Monads::Either]
|
107
|
+
verb :delete do |_params|
|
108
|
+
if (result = repository.delete(id))
|
109
|
+
Right(nil)
|
110
|
+
else
|
111
|
+
Left(result)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module Endpoints
|
5
|
+
# Generic HTTP endpoint abstraction.
|
6
|
+
class Endpoint
|
7
|
+
# Namespacing operations, validations, etc.
|
8
|
+
module Namespace
|
9
|
+
def self.included(child)
|
10
|
+
child.attributes += %i(name ns parent)
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param name [Symbol]
|
14
|
+
# @param ns [String]
|
15
|
+
# @param attributes [{Symbol=>Object}]
|
16
|
+
# @param parent [Endpoint?]
|
17
|
+
def initialize(name:, ns: name.to_s, parent: Undefined, **attributes)
|
18
|
+
@name = name
|
19
|
+
@ns = ns
|
20
|
+
unless parent == Undefined
|
21
|
+
@parent = parent
|
22
|
+
@ns = "#{parent.ns}.#{ns}"
|
23
|
+
end
|
24
|
+
super(name: name, **attributes)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Symbol]
|
28
|
+
attr_reader :name
|
29
|
+
|
30
|
+
# @return [String]
|
31
|
+
attr_reader :ns
|
32
|
+
|
33
|
+
# @return [Endpoint]
|
34
|
+
attr_reader :parent
|
35
|
+
|
36
|
+
# @return [ROM::Repository]
|
37
|
+
def repository
|
38
|
+
container[@repository_key] if @repository_key
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [{Symbol=>Object}]
|
42
|
+
def to_hash
|
43
|
+
super.merge(repository: @repository_key)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module Endpoints
|
5
|
+
# Generic HTTP endpoint abstraction.
|
6
|
+
class Endpoint
|
7
|
+
# Parameters validations for {Endpoints} based on `Dry::Validation` gem.
|
8
|
+
module Operations
|
9
|
+
def initialize(**kwargs)
|
10
|
+
super(**kwargs)
|
11
|
+
container.merge(Endpoints.container)
|
12
|
+
self.class.verbs.each do |verb|
|
13
|
+
container.register "operations.#{ns}.#{verb}", &method(verb)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [Symbol] verb
|
18
|
+
# @return [String]
|
19
|
+
def operation_for(verb)
|
20
|
+
%W(
|
21
|
+
operations.#{ns}.#{verb}
|
22
|
+
operations.#{type}.#{verb}
|
23
|
+
).detect { |key| container.key?(key) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roda/endpoints/transactions'
|
4
|
+
|
5
|
+
class Roda
|
6
|
+
module Endpoints
|
7
|
+
# Generic HTTP endpoint abstraction.
|
8
|
+
class Endpoint
|
9
|
+
# Namespacing operations, validations, etc.
|
10
|
+
module Transactions
|
11
|
+
# @return [Endpoints::Transactions]
|
12
|
+
def transactions
|
13
|
+
endpoint = self
|
14
|
+
@transactions ||=
|
15
|
+
begin
|
16
|
+
transactions = Endpoints::Transactions.new(endpoint: self)
|
17
|
+
self.class.transactions.each do |(name, block)|
|
18
|
+
transactions.define(name) { instance_exec(endpoint, &block) }
|
19
|
+
end
|
20
|
+
transactions
|
21
|
+
end
|
22
|
+
yield @transaction if block_given?
|
23
|
+
@transactions
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param [Symbol] verb
|
27
|
+
# @return [String]
|
28
|
+
def transaction_for(verb)
|
29
|
+
%W(
|
30
|
+
transactions.#{ns}.#{verb}
|
31
|
+
transactions.#{type}.#{verb}
|
32
|
+
).detect { |key| container.key?(key) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [Symbol, String] name
|
36
|
+
# @param [Proc] block
|
37
|
+
def step(name, only: [], **kwargs, &block)
|
38
|
+
name = "operations.#{ns}.#{name}"
|
39
|
+
container.register(name, &block) if block_given?
|
40
|
+
verbs = Array(only).flatten
|
41
|
+
verbs.each do |verb|
|
42
|
+
result = transactions[verb].insert(container: container, **kwargs) do
|
43
|
+
step name
|
44
|
+
end
|
45
|
+
@transaction = result
|
46
|
+
end
|
47
|
+
transactions
|
48
|
+
end
|
49
|
+
|
50
|
+
def before(name, **kwargs, &block)
|
51
|
+
step "before_#{name}", before: name, **kwargs, &block
|
52
|
+
end
|
53
|
+
|
54
|
+
def after(name, **kwargs, &block)
|
55
|
+
step "after_#{name}", after: name, **kwargs, &block
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param [Symbol] operation
|
59
|
+
# @param [Array] args
|
60
|
+
# @return [Dry::Monads::Either]
|
61
|
+
def perform(operation, *args, **options)
|
62
|
+
transactions[operation].call(*args, **options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roda/endpoints'
|
4
|
+
require 'roda/endpoints/functions'
|
5
|
+
require 'dry-validation'
|
6
|
+
|
7
|
+
class Roda
|
8
|
+
module Endpoints
|
9
|
+
# Generic HTTP endpoint abstraction.
|
10
|
+
class Endpoint
|
11
|
+
# Parameters validations for {Endpoints} based on `Dry::Validation` gem.
|
12
|
+
module Validations
|
13
|
+
include Functions::Shortcut
|
14
|
+
|
15
|
+
# @param [Class] child
|
16
|
+
def self.included(child)
|
17
|
+
child.defaults = child.defaults.merge(validation: 'validation').freeze
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param [String, Symbol] key
|
22
|
+
# @param [Proc] block
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# r.collection :articles do |articles|
|
26
|
+
# # register validation at 'validations.endpoints.articles.default'
|
27
|
+
# articles.validate do
|
28
|
+
# required(:title).filled?
|
29
|
+
# required(:contents).filled?
|
30
|
+
# end
|
31
|
+
# # redefine validation for patch method at
|
32
|
+
# # 'validations.endpoints.articles.patch'
|
33
|
+
# articles.validate(:patch) do
|
34
|
+
# required(:title).filled?
|
35
|
+
# required(:contents).filled?
|
36
|
+
# require(:confirm_changes).
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
def validate(key = :default, &block)
|
40
|
+
defaults = "validations.defaults.#{ns}.#{key}"
|
41
|
+
key = "validations.#{ns}.#{key}"
|
42
|
+
schema = Dry::Validation.Form(&block)
|
43
|
+
container.register(key) do |params|
|
44
|
+
if container.key?(defaults)
|
45
|
+
params = f(:deep_merge).call(params, container[defaults])
|
46
|
+
end
|
47
|
+
validation = schema.call(params)
|
48
|
+
if validation.success?
|
49
|
+
Right(validation.output)
|
50
|
+
else
|
51
|
+
Left([:unprocessable_entity, {}, validation])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
schema
|
55
|
+
end
|
56
|
+
|
57
|
+
# rubocop:enable Metrics/MethodLength
|
58
|
+
|
59
|
+
def defaults(verb = :default, **attributes)
|
60
|
+
key = "validations.defaults.#{ns}.#{verb}" if key.is_a?(Symbol)
|
61
|
+
container.register key, attributes
|
62
|
+
end
|
63
|
+
|
64
|
+
# @param [Symbol] verb
|
65
|
+
# @return [Dry::Validation::Schema::Form]
|
66
|
+
def validation(verb)
|
67
|
+
if (validation = validation_for(verb))
|
68
|
+
container[validation]
|
69
|
+
else
|
70
|
+
# default validation requires no params and provide no results
|
71
|
+
provide_default_validation!
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def provide_default_validation!
|
76
|
+
validate {} unless container.key?("validations.#{ns}.default")
|
77
|
+
end
|
78
|
+
|
79
|
+
# @param [Symbol] verb
|
80
|
+
# @return [String]
|
81
|
+
def validation_for(verb)
|
82
|
+
validation = %W(
|
83
|
+
validations.#{ns}.#{verb}
|
84
|
+
validations.#{ns}.default
|
85
|
+
).detect do |key|
|
86
|
+
container.key?(key)
|
87
|
+
end
|
88
|
+
validation = provide_default_validation! unless validation
|
89
|
+
validation
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate!(verb, params)
|
93
|
+
validation(verb).call(params)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
class Roda
|
6
|
+
module Endpoints
|
7
|
+
# Generic HTTP endpoint abstraction.
|
8
|
+
class Endpoint
|
9
|
+
# Accessing data inside of endpoint.
|
10
|
+
module Verbs
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
# @param [<Symbol>] only
|
14
|
+
# @param [<Symbol>] except
|
15
|
+
# @param attributes [{Symbol=>Object}]
|
16
|
+
def initialize(only: implemented_verbs, except: [], **attributes)
|
17
|
+
only = Array(only).flatten
|
18
|
+
except = Array(except).flatten
|
19
|
+
if ((unknown_only = only - implemented_verbs) +
|
20
|
+
(unknown_except = except - implemented_verbs)).any?
|
21
|
+
params = { only: unknown_only, except: unknown_except }
|
22
|
+
raise ArgumentError, "unknown verbs in params: #{params}"
|
23
|
+
end
|
24
|
+
@verbs = only - except
|
25
|
+
super(**attributes)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [<Symbol>]
|
29
|
+
def implemented_verbs
|
30
|
+
self.class.verbs.to_a
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [<Symbol>]
|
34
|
+
attr_reader :verbs
|
35
|
+
|
36
|
+
def verb(name, **kwargs, &block)
|
37
|
+
key = "operations.#{ns}.#{name}"
|
38
|
+
container.register key, &block
|
39
|
+
singleton_class.verb(name, **kwargs, &container[key])
|
40
|
+
end
|
41
|
+
def_delegator :singleton_class, :verb
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|