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,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
|