roda-endpoints 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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