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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +98 -0
  3. data/.gitignore +9 -0
  4. data/.gitlab-ci.yml +85 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +51 -0
  7. data/.ruby-version +1 -0
  8. data/.simplecov +7 -0
  9. data/.travis.yml +5 -0
  10. data/CODE_OF_CONDUCT.md +74 -0
  11. data/Dockerfile +11 -0
  12. data/Gemfile +5 -0
  13. data/LICENSE.txt +21 -0
  14. data/README.md +42 -0
  15. data/Rakefile +4 -0
  16. data/bin/bundle-audit +17 -0
  17. data/bin/console +15 -0
  18. data/bin/rspec +17 -0
  19. data/bin/rubocop +17 -0
  20. data/bin/setup +8 -0
  21. data/bin/yard +17 -0
  22. data/bin/yardoc +17 -0
  23. data/bin/yri +17 -0
  24. data/lib/roda/endpoints/endpoint/caching.rb +28 -0
  25. data/lib/roda/endpoints/endpoint/class_interface.rb +123 -0
  26. data/lib/roda/endpoints/endpoint/collection.rb +77 -0
  27. data/lib/roda/endpoints/endpoint/data.rb +29 -0
  28. data/lib/roda/endpoints/endpoint/item.rb +117 -0
  29. data/lib/roda/endpoints/endpoint/namespace.rb +48 -0
  30. data/lib/roda/endpoints/endpoint/operations.rb +28 -0
  31. data/lib/roda/endpoints/endpoint/transactions.rb +67 -0
  32. data/lib/roda/endpoints/endpoint/validations.rb +98 -0
  33. data/lib/roda/endpoints/endpoint/verbs.rb +45 -0
  34. data/lib/roda/endpoints/endpoint.rb +83 -0
  35. data/lib/roda/endpoints/functions.rb +26 -0
  36. data/lib/roda/endpoints/repository.rb +26 -0
  37. data/lib/roda/endpoints/transactions.rb +75 -0
  38. data/lib/roda/endpoints/types.rb +13 -0
  39. data/lib/roda/endpoints/version.rb +7 -0
  40. data/lib/roda/endpoints.rb +21 -0
  41. data/lib/roda/plugins/endpoints.rb +161 -0
  42. data/lib/rom/struct/to_json.rb +18 -0
  43. data/rakelib/bundle_audit.rake +4 -0
  44. data/rakelib/bundler.rake +2 -0
  45. data/rakelib/rspec.rake +6 -0
  46. data/rakelib/rubocop.rake +5 -0
  47. data/rakelib/yard.rake +5 -0
  48. data/roda-endpoints.gemspec +49 -0
  49. 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'roda/endpoints'
4
+ require 'dry-types'
5
+
6
+ class Roda
7
+ module Endpoints
8
+ # Generic HTTP endpoint abstraction.
9
+ module Types
10
+ include Dry::Types.module
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Roda
4
+ module Endpoints
5
+ VERSION = '0.1.0'
6
+ end
7
+ 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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/audit/task'
3
+
4
+ Bundler::Audit::Task.new
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new :spec do |t|
5
+ t.rspec_path = 'bin/rspec'
6
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ require 'rubocop/rake_task'
3
+ require 'rubocop'
4
+
5
+ RuboCop::RakeTask.new(:cop)
data/rakelib/yard.rake ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ require 'yard'
3
+ require 'yard/rake/yardoc_task'
4
+
5
+ YARD::Rake::YardocTask.new :doc
@@ -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