roda-endpoints 0.1.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.
- 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,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roda/endpoints'
|
4
|
+
require 'roda/endpoints/endpoint/class_interface'
|
5
|
+
require 'roda/endpoints/endpoint/namespace'
|
6
|
+
require 'roda/endpoints/endpoint/operations'
|
7
|
+
require 'roda/endpoints/endpoint/transactions'
|
8
|
+
require 'roda/endpoints/endpoint/validations'
|
9
|
+
require 'roda/endpoints/endpoint/verbs'
|
10
|
+
require 'dry/monads'
|
11
|
+
|
12
|
+
class Roda
|
13
|
+
module Endpoints
|
14
|
+
# Generic HTTP endpoint abstraction.
|
15
|
+
class Endpoint
|
16
|
+
extend ClassInterface
|
17
|
+
include Dry::Monads::Either::Mixin
|
18
|
+
|
19
|
+
autoload :Collection, 'roda/endpoints/endpoint/collection'
|
20
|
+
autoload :Item, 'roda/endpoints/endpoint/item'
|
21
|
+
|
22
|
+
self.attributes = %i(container type).freeze
|
23
|
+
self.defaults = EMPTY_HASH
|
24
|
+
self.statuses = {
|
25
|
+
get: { success: :ok, failure: :not_found },
|
26
|
+
put: { success: :accepted, failure: :unprocessable_entity },
|
27
|
+
post: { success: :created, failure: :unprocessable_entity },
|
28
|
+
patch: { success: :accepted, failure: :unprocessable_entity },
|
29
|
+
delete: { success: :no_content, failure: :unprocessable_entity }
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
self.verbs = EMPTY_SET
|
33
|
+
|
34
|
+
# @param attributes [{Symbol=>Object}]
|
35
|
+
def initialize(**attributes)
|
36
|
+
self.class.defaults.merge(attributes).each do |key, value|
|
37
|
+
# singleton_class.define_attribute(key) unless known_attribute?(key)
|
38
|
+
instance_variable_set(:"@#{key}", value)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
prepend Namespace
|
43
|
+
prepend Verbs
|
44
|
+
prepend Validations
|
45
|
+
prepend Operations
|
46
|
+
include Transactions
|
47
|
+
|
48
|
+
# @return [Dry::Container::Mixin]
|
49
|
+
def container
|
50
|
+
@container || parent&.container
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Symbol]
|
54
|
+
attr_reader :type
|
55
|
+
|
56
|
+
# @param [{Symbol=>Object}] attributes
|
57
|
+
# @param [:collection, :item] type
|
58
|
+
# @param [{Symbol=>Object}] attributes
|
59
|
+
def with(type: self.class, **attributes)
|
60
|
+
type.new to_hash.merge(attributes).merge(inheritable_attributes)
|
61
|
+
end
|
62
|
+
|
63
|
+
def inheritable_attributes
|
64
|
+
{
|
65
|
+
parent: self,
|
66
|
+
container: container
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Proc]
|
71
|
+
def route
|
72
|
+
self.class.route
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [{Symbol=>Object}]
|
76
|
+
def to_hash
|
77
|
+
self.class.attributes.each_with_object({}) do |name, hash|
|
78
|
+
hash[name] = public_send(name)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'transproc'
|
4
|
+
require 'roda/endpoints'
|
5
|
+
|
6
|
+
class Roda
|
7
|
+
# Useful transformations.
|
8
|
+
module Functions
|
9
|
+
extend Transproc::Registry
|
10
|
+
|
11
|
+
import Transproc::HashTransformations
|
12
|
+
|
13
|
+
# Shortcut f
|
14
|
+
module Shortcut
|
15
|
+
# @param [Symbol] fn
|
16
|
+
# @param [Array] args
|
17
|
+
def f(fn, *args)
|
18
|
+
if args.any?
|
19
|
+
Functions[fn].call(*args)
|
20
|
+
else
|
21
|
+
Functions[fn]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roda/endpoints'
|
4
|
+
|
5
|
+
class Roda
|
6
|
+
module Endpoints
|
7
|
+
# Generic HTTP endpoint abstraction.
|
8
|
+
module Repository
|
9
|
+
# @param [<ROM::Struct>] _kwargs
|
10
|
+
def list(**_kwargs)
|
11
|
+
root.to_a
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Time]
|
15
|
+
def last_modified
|
16
|
+
order(Sequel.desc(:updated_at)).first.updated_at
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Integer] id
|
20
|
+
# @return [ROM::Struct]
|
21
|
+
def fetch(id)
|
22
|
+
root.fetch(id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roda/endpoints'
|
4
|
+
require 'dry-configurable'
|
5
|
+
require 'dry-transaction'
|
6
|
+
require 'forwardable'
|
7
|
+
|
8
|
+
class Roda
|
9
|
+
module Endpoints
|
10
|
+
# The DSL for defining {Transactions transactions} used inside of
|
11
|
+
# The {Platform}.
|
12
|
+
class Transactions
|
13
|
+
extend Dry::Configurable
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
setting :container
|
17
|
+
setting :options, {}
|
18
|
+
|
19
|
+
def self.define
|
20
|
+
yield new
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [{Symbol=>Object}]
|
24
|
+
def self.options
|
25
|
+
{ container: config.container }.merge(config.options)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param [Endpoint] endpoint
|
29
|
+
# @param [Hash] options
|
30
|
+
def initialize(endpoint:, **options)
|
31
|
+
@endpoint = endpoint
|
32
|
+
@container = endpoint.container
|
33
|
+
@options = self.class.options
|
34
|
+
.merge(container: endpoint.container)
|
35
|
+
.merge(options)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [{Symbol=>Object}]
|
39
|
+
attr_reader :options
|
40
|
+
|
41
|
+
# @return [Endpoint]
|
42
|
+
attr_reader :endpoint
|
43
|
+
|
44
|
+
# @return [Dry::Container::Mixin]
|
45
|
+
attr_reader :container
|
46
|
+
|
47
|
+
def_delegators :endpoint, :operation_for, :validation_for
|
48
|
+
|
49
|
+
# @param [Symbol] shortcut
|
50
|
+
# @param [Proc] block
|
51
|
+
def define(shortcut, &block)
|
52
|
+
container.register(
|
53
|
+
key_for(shortcut),
|
54
|
+
Dry.Transaction(
|
55
|
+
container: container,
|
56
|
+
endpoint: endpoint,
|
57
|
+
&block
|
58
|
+
)
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param [Symbol] verb
|
63
|
+
# @return [String]
|
64
|
+
def key_for(verb)
|
65
|
+
"transactions.#{endpoint.ns}.#{verb}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param [Symbol] shortcut
|
69
|
+
def [](shortcut)
|
70
|
+
key = key_for(shortcut)
|
71
|
+
container[key]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'roda/endpoints/version'
|
3
|
+
require 'dry/core/constants'
|
4
|
+
|
5
|
+
class Roda
|
6
|
+
# Endpoints abstractions
|
7
|
+
module Endpoints
|
8
|
+
include Dry::Core::Constants
|
9
|
+
|
10
|
+
autoload :Endpoint, 'roda/endpoints/endpoint'
|
11
|
+
|
12
|
+
VERBS = %i(get post delete patch put head).freeze
|
13
|
+
|
14
|
+
# @return [Dry::Container]
|
15
|
+
def self.container
|
16
|
+
@container ||= Dry::Container.new
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'roda/plugins/endpoints'
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roda'
|
4
|
+
require 'roda/endpoints/endpoint/collection'
|
5
|
+
require 'roda/endpoints/endpoint/item'
|
6
|
+
require 'rom/struct/to_json'
|
7
|
+
|
8
|
+
class Roda
|
9
|
+
# Module containing {Roda} plugins.
|
10
|
+
module RodaPlugins
|
11
|
+
# Endpoints plugin for {Roda}
|
12
|
+
module Endpoints
|
13
|
+
# @param [Class(Roda)] app
|
14
|
+
def self.load_dependencies(app)
|
15
|
+
app.plugin :all_verbs
|
16
|
+
app.plugin :head
|
17
|
+
app.plugin :caching
|
18
|
+
app.plugin :monads
|
19
|
+
app.plugin :symbol_status
|
20
|
+
app.plugin :symbol_matchers
|
21
|
+
app.plugin :slash_path_empty
|
22
|
+
app.plugin :json_parser
|
23
|
+
app.plugin :indifferent_params
|
24
|
+
app.plugin :json, classes: [Array, Hash, ROM::Struct]
|
25
|
+
return if app.respond_to?(:[])
|
26
|
+
require 'dry-container'
|
27
|
+
app.extend Dry::Container::Mixin
|
28
|
+
app.plugin :flow
|
29
|
+
end
|
30
|
+
|
31
|
+
# `Roda::RodaRequest` instant extensions.
|
32
|
+
module RequestMethods
|
33
|
+
# Implements collection endpoint using given args
|
34
|
+
#
|
35
|
+
# @param name [Symbol]
|
36
|
+
# @param item [{Symbol=>Object}]
|
37
|
+
# @param kwargs [{Symbol=>Object}]
|
38
|
+
# @param (see Endpoint::Collection#initialize)
|
39
|
+
# @see Endpoint::Collection.defaults
|
40
|
+
# @yieldparam endpoint [Collection]
|
41
|
+
# @yieldreturn [#to_json]
|
42
|
+
# @return [Endpoint]
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# r.collection :articles, item: { only: %i(get delete) }
|
46
|
+
#
|
47
|
+
# # is equivalent to
|
48
|
+
#
|
49
|
+
# r.on 'articles', item: { only: %i(get delete) } do
|
50
|
+
# articles = Endpoint.new(
|
51
|
+
# name: :articles,
|
52
|
+
# container: container,
|
53
|
+
# )
|
54
|
+
#
|
55
|
+
# r.last_modified articles.last_modified
|
56
|
+
#
|
57
|
+
# r.get do
|
58
|
+
# articles.call(:get, r.params)
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# r.post do
|
62
|
+
# articles.call(:post, r.params)
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# r.child :id, only: %i(get delete)
|
66
|
+
# end
|
67
|
+
def collection(name, item: { by: :id }, path: name, **kwargs)
|
68
|
+
endpoint = Roda::Endpoints::Endpoint::Collection.new(
|
69
|
+
name: name,
|
70
|
+
container: roda_class,
|
71
|
+
item: item,
|
72
|
+
**kwargs
|
73
|
+
)
|
74
|
+
endpoints.push endpoint
|
75
|
+
on path.to_s do
|
76
|
+
# @route /:name
|
77
|
+
yield endpoint if block_given?
|
78
|
+
instance_exec(self, endpoint, &endpoint.route)
|
79
|
+
end
|
80
|
+
endpoints.pop #=> endpoint
|
81
|
+
end
|
82
|
+
|
83
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
84
|
+
|
85
|
+
# @param [Symbol] by
|
86
|
+
# @param [Endpoint::Collection] collection
|
87
|
+
# @param [Hash] kwargs
|
88
|
+
#
|
89
|
+
# @example
|
90
|
+
# r.collection :articles do |articles|
|
91
|
+
# r.child :id
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# # is equivalent to
|
95
|
+
#
|
96
|
+
# r.collection :articles do |articles|
|
97
|
+
# r.on :id do |id|
|
98
|
+
#
|
99
|
+
# def article
|
100
|
+
# @article ||= articles.repository.fetch(id)
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# r.get do
|
104
|
+
# article
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
def child(by: :id, collection: endpoint, **kwargs)
|
109
|
+
# @route /{collection.name}/{id}
|
110
|
+
on by do |identifier|
|
111
|
+
item = collection.child(id: identifier, identifier: by, **kwargs)
|
112
|
+
endpoints.push item
|
113
|
+
yield item if block_given?
|
114
|
+
instance_exec(self, item, &item.route)
|
115
|
+
endpoints.pop
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# @param [Symbol] verb
|
122
|
+
def match_transaction(verb)
|
123
|
+
resolve endpoint.transactions.key_for(verb) do |transaction|
|
124
|
+
transaction.call(params) do |m|
|
125
|
+
statuses = endpoint.class.statuses[verb]
|
126
|
+
|
127
|
+
m.success do |result|
|
128
|
+
response.status = statuses[:success]
|
129
|
+
result
|
130
|
+
end
|
131
|
+
|
132
|
+
m.failure do |result|
|
133
|
+
if result.is_a?(Array) && result.size == 2
|
134
|
+
response.status, value = result
|
135
|
+
value
|
136
|
+
else
|
137
|
+
response.status = statuses[:failure]
|
138
|
+
result
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# @return [Endpoint]
|
146
|
+
def endpoint
|
147
|
+
endpoints.last
|
148
|
+
end
|
149
|
+
|
150
|
+
# @return [<Endpoint>]
|
151
|
+
def endpoints
|
152
|
+
@endpoints ||= [Roda::Endpoints::Endpoint.new(
|
153
|
+
name: :root, container: roda_class
|
154
|
+
)]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
register_plugin :endpoints, Endpoints
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'rom'
|
3
|
+
require 'rom/struct'
|
4
|
+
|
5
|
+
module ROM
|
6
|
+
# Simple data-struct
|
7
|
+
#
|
8
|
+
# By default mappers use this as the model
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Struct
|
12
|
+
alias as_json to_hash
|
13
|
+
|
14
|
+
def to_json(*args)
|
15
|
+
as_json.to_json(*args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/rakelib/rspec.rake
ADDED
data/rakelib/yard.rake
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'roda/endpoints/version'
|
6
|
+
|
7
|
+
# rubocop:disable Metrics/BlockLength
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'roda-endpoints'
|
10
|
+
spec.version = Roda::Endpoints::VERSION
|
11
|
+
spec.authors = ['Alex Semyonov']
|
12
|
+
spec.email = ['alex@semyonov.us']
|
13
|
+
|
14
|
+
spec.summary = 'RESTful endpoints for Roda tree'
|
15
|
+
spec.description = 'Generic endpoints and specific implementations.'
|
16
|
+
spec.homepage = 'http://alsemyonov.gitlab.com/roda-endpoints/'
|
17
|
+
spec.license = 'MIT'
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
20
|
+
f.match(%r{^(test|spec|features)/})
|
21
|
+
end
|
22
|
+
spec.bindir = 'exe'
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ['lib']
|
25
|
+
|
26
|
+
spec.add_runtime_dependency 'dry-container'
|
27
|
+
spec.add_runtime_dependency 'dry-matcher'
|
28
|
+
spec.add_runtime_dependency 'dry-transaction'
|
29
|
+
spec.add_runtime_dependency 'dry-types'
|
30
|
+
spec.add_runtime_dependency 'dry-validation'
|
31
|
+
spec.add_runtime_dependency 'roda', '~> 2.21.0'
|
32
|
+
spec.add_runtime_dependency 'roda-flow', '~> 0.3.0'
|
33
|
+
spec.add_runtime_dependency 'roda-monads', '~> 0.1.0'
|
34
|
+
spec.add_runtime_dependency 'rom-repository'
|
35
|
+
|
36
|
+
spec.add_development_dependency 'bundler', '~> 1.13'
|
37
|
+
spec.add_development_dependency 'bundler-audit', '~> 0.5.0'
|
38
|
+
spec.add_development_dependency 'rack-test', '~> 0.6.3'
|
39
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
40
|
+
spec.add_development_dependency 'rspec-roda', '~> 0.1.0'
|
41
|
+
spec.add_development_dependency 'rubocop', '~> 0.47.0'
|
42
|
+
spec.add_development_dependency 'simplecov', '~> 0.12.0'
|
43
|
+
spec.add_development_dependency 'yard', '~> 0.9.5'
|
44
|
+
spec.add_development_dependency 'rom-repository', '>= 0.3.1'
|
45
|
+
spec.add_development_dependency 'rom-sql', '>= 0.9'
|
46
|
+
spec.add_development_dependency 'sqlite3'
|
47
|
+
spec.add_development_dependency 'pry-byebug'
|
48
|
+
end
|
49
|
+
# rubocop:enable Metrics/BlockLength
|