roda-endpoints 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a5d5c0b50d6405cc257a76d098dd673351ade2d4
4
- data.tar.gz: 432c3139a367f1907a69c6310bb8188015bb247c
3
+ metadata.gz: aecfae0fdfb0156f3821d1273697d8ec24b698f6
4
+ data.tar.gz: 8218ec6e8c89ab3a3967b23f14148a14e3098fcc
5
5
  SHA512:
6
- metadata.gz: ed59766efaca29ac760dc05cab86b6e979878db4895a1142803fd00e6ef022e925b9b0190208a83d722d2159aa906ef0120b26a77711cd7314e5a9d2049369d2
7
- data.tar.gz: 29c953dea3afb2f38a06505bf3d7d54c616fb56387b8f821fcb97078e7f6f06e4641e8184062d41b8aed0acd934a799422ae3fe2a7bcdf4e4092d4e0d3f5a3e9
6
+ metadata.gz: 5d247edfad454ac8b11e196bbb1f88b91c663ea881a4241af55edb079c2f8cd9dfced72fea2f9a1cafd2ad8750b3e0d78a13b993aee7448ae29fbe7beea8e92c
7
+ data.tar.gz: a7055fce9bc21bf0bd3d5054cd6d218029c1cfba764b48ee06c0e80243f84603a256ae08202345115bcbd32ed9d2c044c930c5fae347cd38f56dfef565cad153
@@ -37,6 +37,15 @@ class Roda
37
37
  child.defaults = defaults.dup
38
38
  child.statuses = statuses.dup
39
39
  child.verbs = verbs.dup
40
+ # child.verbs.each do |verb|
41
+ # key = "operations.#{child.type}.#{verb}"
42
+ # implementation = "operations.#{type}.#{verb}"
43
+ # container.register key do
44
+ # Endpoints.container.resolve implementation
45
+ # end
46
+ # end
47
+ child.transactions = transactions.dup
48
+ child.route(&@route_block)
40
49
  super
41
50
  end
42
51
 
@@ -49,10 +58,12 @@ class Roda
49
58
  @type ||= Inflecto.underscore(Inflecto.demodulize(name)).to_sym
50
59
  end
51
60
 
52
- # @param [Symbol] name
61
+ # @param [Symbol] verb
53
62
  # @param [Proc] block
54
63
  # @return [Symbol] name of the defined method
55
- def verb(name, rescue_from: [], &block)
64
+ def verb(verb, rescue_from: [], &block)
65
+ self.verbs ||= superclass.verbs
66
+ (self.verbs += [verb]).freeze
56
67
  rescue_from = Array(rescue_from).flatten
57
68
  if rescue_from.any?
58
69
  block = proc do |*args|
@@ -63,10 +74,8 @@ class Roda
63
74
  end
64
75
  end
65
76
  end
66
- define_method(name, &block)
67
- container.register "operations.#{type}.#{name}", block
68
- self.verbs ||= superclass.verbs
69
- (self.verbs += [name]).freeze
77
+ define_method(verb, &block)
78
+ container.register "operations.#{type}.#{verb}", block
70
79
  end
71
80
 
72
81
  # @param [String, Symbol] key
@@ -117,6 +126,8 @@ class Roda
117
126
  def transactions
118
127
  @transactions ||= []
119
128
  end
129
+
130
+ attr_writer :transactions
120
131
  end
121
132
  end
122
133
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'roda/endpoints/endpoint'
4
3
  require 'roda/endpoints/endpoint/data'
5
4
  require 'roda/endpoints/endpoint/caching'
5
+ require 'roda/endpoints/endpoint'
6
6
  require 'inflecto'
7
7
  require 'rom/sql'
8
8
 
@@ -10,13 +10,12 @@ class Roda
10
10
  module Endpoints
11
11
  class Endpoint
12
12
  # HTTP endpoint representing a collection of items of the same type.
13
- class Collection < Endpoint
14
- include Data
13
+ class Collection < Roda::Endpoints::Endpoint
14
+ prepend Data
15
15
  prepend Caching
16
16
 
17
17
  self.attributes += %i(item)
18
18
  self.defaults = defaults.merge(
19
- type: :collection,
20
19
  last_modified: :last_modified
21
20
  )
22
21
 
@@ -28,28 +27,28 @@ class Roda
28
27
  @last_modified ? repository.public_send(@last_modified) : super
29
28
  end
30
29
 
31
- def child(**params)
32
- with(
33
- type: Item,
34
- name: resource_name,
35
- **params
36
- )
30
+ # @param [Symbol] name
31
+ # @param [Hash] params
32
+ def child(name: item_name, type: Item, **params)
33
+ super(name: name, type: type, **params)
37
34
  end
38
35
 
39
36
  # @return [Symbol]
40
- def resource_name
41
- Inflecto.singularize(name).to_sym
37
+ def item_name
38
+ @item_name ||= Inflecto.singularize(name).to_sym
42
39
  end
43
40
 
41
+ # @route /{collection.name}
44
42
  route do |r, endpoint| # r.collection :articles do |articles|
45
43
  r.last_modified endpoint.last_modified if endpoint.last_modified
46
44
 
47
45
  endpoint.verbs.each do |verb|
48
- # @route #{verb} /:name
46
+ # @route #{verb} /{collection.name}
49
47
  r.public_send(verb, transaction: verb)
50
-
51
- r.child **endpoint.item if endpoint.item # child by: :id
52
48
  end
49
+
50
+ # @route #{verb} /{collection.name}/{id}
51
+ r.child **endpoint.item if endpoint.item # child by: :id
53
52
  end
54
53
 
55
54
  verb :get do |params|
@@ -57,7 +56,7 @@ class Roda
57
56
  end
58
57
 
59
58
  verb :post do |params|
60
- params = params[resource_name] || {}
59
+ params = params[item_name] || {}
61
60
  Right(repository.create(**params))
62
61
  end
63
62
 
@@ -14,6 +14,9 @@ class Roda
14
14
  super(name: name, **attributes)
15
15
  end
16
16
 
17
+ # @return [String]
18
+ attr_reader :repository_key
19
+
17
20
  # @return [ROM::Repository]
18
21
  def repository
19
22
  container[@repository_key] if @repository_key
@@ -1,116 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'roda/endpoints/endpoint'
3
+ require 'roda/endpoints/endpoint/singleton'
4
4
  require 'roda/endpoints/endpoint/data'
5
- require 'roda/endpoints/endpoint/caching'
6
5
 
7
6
  class Roda
8
7
  module Endpoints
9
8
  class Endpoint
10
9
  # HTTP endpoint representing a specific item of collection uniquely
11
10
  # identified by some parameter.
12
- class Item < Endpoint
13
- include Data
14
- include Caching
11
+ class Item < Singleton
12
+ prepend Data
15
13
 
16
- self.attributes += %i(id by finder last_modified)
14
+ self.attributes += %i(id by on)
17
15
  self.defaults = defaults.merge(
18
16
  by: :fetch,
19
- finder: lambda do
20
- repository.public_send(by, id)
21
- end,
22
- last_modified: :updated_at
17
+ on: :id,
18
+ finder: -> { repository.public_send(by, id) }
23
19
  )
24
20
 
25
21
  # @return [Symbol]
26
22
  attr_reader :by
27
23
 
28
- # @return [Symbol]
29
- attr_reader :id
30
-
31
- # @return [Symbol]
32
- attr_reader :finder
24
+ # @return [Integer]
25
+ def id
26
+ captures.first
27
+ end
33
28
 
34
29
  # @return [Endpoint::Collection]
35
30
  def collection
36
31
  parent
37
32
  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
33
  end
115
34
  end
116
35
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'roda/endpoints/endpoint/namespace'
4
+
5
+ class Roda
6
+ module Endpoints
7
+ class Endpoint
8
+ # Lookup everything in the container.
9
+ module Lookup
10
+ prepend Namespace
11
+
12
+ # @return [Dry::Container::Mixin]
13
+ def container
14
+ @container || parent&.container || Roda::Endpoints.roda_class
15
+ end
16
+
17
+ # @param [<String>] paths
18
+ def lookup(*paths, key: nil, scope: nil, default: nil)
19
+ if key && scope
20
+ paths += lookup_keys(key: key, scope: scope, default: default)
21
+ end
22
+ paths.flatten.detect { |full_key| container.key?(full_key) }
23
+ end
24
+
25
+ # @param [#to_s] key
26
+ # @param [#to_s] scope
27
+ def lookup_keys(key:, scope: nil, default: nil, lookup: lookup_path)
28
+ scope = "#{scope}." unless scope.to_s.end_with?('.')
29
+ keys = []
30
+ Array(key).flatten.map do |sect|
31
+ lookup.each do |choice|
32
+ [sect, default].compact.each do |value|
33
+ full_key = "#{scope}#{choice}.#{value}"
34
+ yield full_key if block_given?
35
+ keys << full_key
36
+ end
37
+ end
38
+ end
39
+ keys
40
+ end
41
+
42
+ def lookup_path
43
+ path = [ns]
44
+ endpoint = self.class
45
+ while endpoint < Roda::Endpoints::Endpoint
46
+ path << endpoint.type
47
+ endpoint = endpoint.superclass
48
+ end
49
+ path
50
+ end
51
+
52
+ # @param [Symbol] verb
53
+ # @return [String]
54
+ def operation_for(verb)
55
+ lookup key: verb, scope: 'operations.'
56
+ end
57
+
58
+ # @param [Symbol] verb
59
+ # @return [String]
60
+ def validation_for(verb)
61
+ lookup(key: verb, default: 'any', scope: 'validations.') ||
62
+ provide_default_validation!
63
+ end
64
+
65
+ # @param [Symbol] verb
66
+ # @return [String]
67
+ def transaction_for(verb)
68
+ lookup key: verb, scope: 'transactions'
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -19,7 +19,7 @@ class Roda
19
19
  @ns = ns
20
20
  unless parent == Undefined
21
21
  @parent = parent
22
- @ns = "#{parent.ns}.#{ns}"
22
+ @ns = [parent.ns, ns].compact.join('.')
23
23
  end
24
24
  super(name: name, **attributes)
25
25
  end
@@ -32,16 +32,6 @@ class Roda
32
32
 
33
33
  # @return [Endpoint]
34
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
35
  end
46
36
  end
47
37
  end
@@ -0,0 +1,107 @@
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 Singleton < Roda::Endpoints::Endpoint
13
+ prepend Caching
14
+
15
+ self.attributes += %i(finder last_modified)
16
+ self.defaults = defaults.merge(last_modified: :updated_at)
17
+
18
+ # @return [Symbol]
19
+ attr_reader :finder
20
+
21
+ # @return [ROM::Struct]
22
+ def entity
23
+ @entity ||= fetch_entity
24
+ end
25
+
26
+ attr_writer :entity
27
+
28
+ # @return [ROM::Struct]
29
+ def fetch_entity
30
+ instance_exec(&finder)
31
+ end
32
+
33
+ # @return [Time]
34
+ def last_modified
35
+ @last_modified ? entity.public_send(@last_modified) : super
36
+ end
37
+
38
+ route do |r, endpoint|
39
+ endpoint.verbs.each do |verb|
40
+ # @route #{verb} /{collection.name}/{id}
41
+ r.public_send(verb, transaction: verb)
42
+ end
43
+ end
44
+
45
+ transaction :get do |endpoint|
46
+ step :retrieve, with: endpoint.operation_for(:get)
47
+ end
48
+
49
+ transaction :patch do |endpoint|
50
+ step :validate, with: endpoint.validation_for(:patch)
51
+ step :persist, with: endpoint.operation_for(:patch)
52
+ end
53
+
54
+ transaction :put do |endpoint|
55
+ step :validate, with: endpoint.validation_for(:put)
56
+ # step :reset, with: 'endpoints.operations.reset'
57
+ step :persist, with: endpoint.operation_for(:put)
58
+ end
59
+
60
+ transaction :delete do |endpoint|
61
+ step :validate, with: endpoint.validation_for(:delete)
62
+ step :persist, with: endpoint.operation_for(:delete)
63
+ end
64
+
65
+ # @route GET /{collection.name}/{id}
66
+ # @!method get(params)
67
+ # @param [Hash] params
68
+ # @return [Dry::Monads::Either]
69
+ verb :get do |_params|
70
+ Right(entity)
71
+ end
72
+
73
+ # @route PATCH /{collection.name}/{id}
74
+ # @!method patch(params)
75
+ # @param [Hash] params
76
+ # @return [Dry::Monads::Either]
77
+ verb :patch do |params|
78
+ changeset = params[name]
79
+ Right(repository.update(id, changeset))
80
+ end
81
+
82
+ # @route PUT /{collection.name}/{id}
83
+ # @!method put(params)
84
+ # @param [Hash] params
85
+ # @return [Dry::Monads::Either]
86
+ verb :put do |params|
87
+ changeset = entity.to_hash.keys.each_with_object({}) do |key, changes|
88
+ changes[key] = nil
89
+ end.merge(params[name] || {})
90
+ Right(repository.update(id, changeset))
91
+ end
92
+
93
+ # @route DELETE /{collection.name}/{id}
94
+ # @!method delete(params)
95
+ # @param [Hash] params
96
+ # @return [Dry::Monads::Either]
97
+ verb :delete do |_params|
98
+ if (result = repository.delete(id))
99
+ Right(nil)
100
+ else
101
+ Left(result)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -23,15 +23,6 @@ class Roda
23
23
  @transactions
24
24
  end
25
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
26
  # @param [Symbol, String] name
36
27
  # @param [Proc] block
37
28
  def step(name, only: [], **kwargs, &block)
@@ -12,13 +12,12 @@ class Roda
12
12
  module Validations
13
13
  include Functions::Shortcut
14
14
 
15
- # @param [Class] child
16
- def self.included(child)
17
- child.defaults = child.defaults.merge(validation: 'validation').freeze
18
- super
15
+ def defaults(verb = :any, **attributes)
16
+ key = "validations.defaults.#{ns}.#{verb}" if key.is_a?(Symbol)
17
+ container.register key, attributes
19
18
  end
20
19
 
21
- # @param [String, Symbol] key
20
+ # @param [String, Symbol] verb
22
21
  # @param [Proc] block
23
22
  #
24
23
  # @example
@@ -36,11 +35,11 @@ class Roda
36
35
  # require(:confirm_changes).
37
36
  # end
38
37
  # end
39
- def validate(key = :default, &block)
40
- defaults = "validations.defaults.#{ns}.#{key}"
41
- key = "validations.#{ns}.#{key}"
38
+ def validate(verb = :any, &block)
39
+ defaults = "validations.defaults.#{ns}.#{verb}"
40
+ verb = "validations.#{ns}.#{verb}"
42
41
  schema = Dry::Validation.Form(&block)
43
- container.register(key) do |params|
42
+ container.register(verb) do |params|
44
43
  if container.key?(defaults)
45
44
  params = f(:deep_merge).call(params, container[defaults])
46
45
  end
@@ -56,11 +55,6 @@ class Roda
56
55
 
57
56
  # rubocop:enable Metrics/MethodLength
58
57
 
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
58
  # @param [Symbol] verb
65
59
  # @return [Dry::Validation::Schema::Form]
66
60
  def validation(verb)
@@ -73,24 +67,19 @@ class Roda
73
67
  end
74
68
 
75
69
  def provide_default_validation!
76
- validate {} unless container.key?("validations.#{ns}.default")
70
+ validate {} unless container.key?("validations.#{ns}.any")
77
71
  end
78
72
 
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)
73
+ def prepare_validations!
74
+ return if @validations_prepared
75
+ (verbs - %i(get head options)).each do |verb|
76
+ key = "validations.#{ns}.#{verb}"
77
+ default = "validations.#{ns}.any"
78
+ unless container.key?(key) || container.key?(default)
79
+ provide_default_validation!
80
+ end
87
81
  end
88
- validation = provide_default_validation! unless validation
89
- validation
90
- end
91
-
92
- def validate!(verb, params)
93
- validation(verb).call(params)
82
+ @validations_prepared = true
94
83
  end
95
84
  end
96
85
  end
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
4
-
5
3
  class Roda
6
4
  module Endpoints
7
5
  # Generic HTTP endpoint abstraction.
8
6
  class Endpoint
9
7
  # Accessing data inside of endpoint.
10
8
  module Verbs
11
- extend Forwardable
12
-
13
- # @param [<Symbol>] only
14
- # @param [<Symbol>] except
15
9
  # @param attributes [{Symbol=>Object}]
16
- def initialize(only: implemented_verbs, except: [], **attributes)
10
+ def initialize(**attributes)
11
+ @verbs = verbs_to_implement(**attributes)
12
+ super(**attributes)
13
+ end
14
+
15
+ # @param only [<Symbol>]
16
+ # @param except [<Symbol>]
17
+ def verbs_to_implement(only: implemented_verbs, except: [], **_kwargs)
17
18
  only = Array(only).flatten
18
19
  except = Array(except).flatten
19
20
  if ((unknown_only = only - implemented_verbs) +
@@ -21,8 +22,7 @@ class Roda
21
22
  params = { only: unknown_only, except: unknown_except }
22
23
  raise ArgumentError, "unknown verbs in params: #{params}"
23
24
  end
24
- @verbs = only - except
25
- super(**attributes)
25
+ only - except
26
26
  end
27
27
 
28
28
  # @return [<Symbol>]
@@ -33,12 +33,28 @@ class Roda
33
33
  # @return [<Symbol>]
34
34
  attr_reader :verbs
35
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])
36
+ # @param [#to_s] verb
37
+ # @param [Hash] kwargs
38
+ # @param [Proc] block
39
+ def verb(verb, **kwargs, &block)
40
+ key = "operations.#{ns}.#{verb}"
41
+ block ||= container[key]
42
+ singleton_class.verb(verb, **kwargs, &block)
43
+ end
44
+
45
+ def prepare_verbs!
46
+ return if @verbs_prepared
47
+ verbs.each do |verb|
48
+ key = "operations.#{ns}.#{verb}"
49
+ next if container.key?(key)
50
+ endpoint = self
51
+ operation = method(verb)
52
+ container.register key do |*args|
53
+ endpoint.instance_exec(*args, &operation)
54
+ end
55
+ end
56
+ @verbs_prepared = true
40
57
  end
41
- def_delegator :singleton_class, :verb
42
58
  end
43
59
  end
44
60
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  require 'roda/endpoints'
4
4
  require 'roda/endpoints/endpoint/class_interface'
5
+ require 'roda/endpoints/endpoint/lookup'
5
6
  require 'roda/endpoints/endpoint/namespace'
6
- require 'roda/endpoints/endpoint/operations'
7
7
  require 'roda/endpoints/endpoint/transactions'
8
8
  require 'roda/endpoints/endpoint/validations'
9
9
  require 'roda/endpoints/endpoint/verbs'
@@ -18,6 +18,7 @@ class Roda
18
18
 
19
19
  autoload :Collection, 'roda/endpoints/endpoint/collection'
20
20
  autoload :Item, 'roda/endpoints/endpoint/item'
21
+ autoload :Singleton, 'roda/endpoints/endpoint/singleton'
21
22
 
22
23
  self.attributes = %i(container type).freeze
23
24
  self.defaults = EMPTY_HASH
@@ -42,33 +43,37 @@ class Roda
42
43
  prepend Namespace
43
44
  prepend Verbs
44
45
  prepend Validations
45
- prepend Operations
46
46
  include Transactions
47
+ prepend Lookup
47
48
 
48
- # @return [Dry::Container::Mixin]
49
- def container
50
- @container || parent&.container
51
- end
49
+ attr_accessor :captures
52
50
 
53
51
  # @return [Symbol]
54
- attr_reader :type
52
+ def type
53
+ self.class.type
54
+ end
55
55
 
56
- # @param [{Symbol=>Object}] attributes
57
- # @param [:collection, :item] type
58
- # @param [{Symbol=>Object}] attributes
56
+ # @param type [:collection, :item]
57
+ # @param attributes [{Symbol=>Object}]
59
58
  def with(type: self.class, **attributes)
60
59
  type.new to_hash.merge(attributes).merge(inheritable_attributes)
61
60
  end
62
61
 
62
+ # @param [Class(Endpoint)] type
63
+ # @param [Hash] params
64
+ def child(type: Singleton, **params)
65
+ with(type: type, **params)
66
+ end
67
+
68
+ # @return [{Symbol=>Object}]
63
69
  def inheritable_attributes
64
- {
65
- parent: self,
66
- container: container
67
- }
70
+ { parent: self, container: container }
68
71
  end
69
72
 
70
73
  # @return [Proc]
71
74
  def route
75
+ prepare_validations!
76
+ prepare_verbs!
72
77
  self.class.route
73
78
  end
74
79
 
@@ -7,8 +7,7 @@ require 'forwardable'
7
7
 
8
8
  class Roda
9
9
  module Endpoints
10
- # The DSL for defining {Transactions transactions} used inside of
11
- # The {Platform}.
10
+ # The DSL for defining {Transactions transactions} for endpoints.
12
11
  class Transactions
13
12
  extend Dry::Configurable
14
13
  extend Forwardable
@@ -2,6 +2,6 @@
2
2
 
3
3
  class Roda
4
4
  module Endpoints
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require 'roda/endpoints/version'
3
3
  require 'dry/core/constants'
4
+ require 'dry/container'
4
5
 
5
6
  class Roda
6
7
  # Endpoints abstractions
@@ -15,6 +16,10 @@ class Roda
15
16
  def self.container
16
17
  @container ||= Dry::Container.new
17
18
  end
19
+
20
+ class << self
21
+ attr_accessor :roda_class
22
+ end
18
23
  end
19
24
  end
20
25
 
@@ -11,7 +11,7 @@ class Roda
11
11
  # Endpoints plugin for {Roda}
12
12
  module Endpoints
13
13
  # @param [Class(Roda)] app
14
- def self.load_dependencies(app)
14
+ def self.load_dependencies(app, **_opts)
15
15
  app.plugin :all_verbs
16
16
  app.plugin :head
17
17
  app.plugin :caching
@@ -22,19 +22,49 @@ class Roda
22
22
  app.plugin :json_parser
23
23
  app.plugin :indifferent_params
24
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
25
  app.plugin :flow
29
26
  end
30
27
 
28
+ # @param [Class(Roda)] app
29
+ # @param [Hash] opts
30
+ def self.configure(app, container: app, **opts)
31
+ opts = (app.opts[:endpoints] || {}).merge(opts)
32
+ unless container.respond_to? :resolve
33
+ require 'dry-container'
34
+ container.extend Dry::Container::Mixin
35
+ end
36
+ app.opts[:endpoints] = opts.merge(container: container)
37
+ Roda::Endpoints.roda_class ||= app
38
+ end
39
+
40
+ # {ClassMethods#register `Roda.register`} &&
41
+ # {ClassMethods#merge `Roda.merge`}
42
+ module ClassMethods
43
+ # @param [String, Symbol] name
44
+ # @param [Array] args
45
+ # @param [Proc] block
46
+ def register(name, *args, &block)
47
+ opts[:container].register(name, *args, &block)
48
+ end
49
+
50
+ # @param [String, Symbol] name
51
+ # @param [Array] args
52
+ # @param [Proc] block
53
+ def merge(name, *args, &block)
54
+ opts[:container].merge(name, *args, &block)
55
+ end
56
+ end
57
+
31
58
  # `Roda::RodaRequest` instant extensions.
59
+ # rubocop:disable Metrics/ModuleLength
32
60
  module RequestMethods
33
61
  # Implements collection endpoint using given args
34
62
  #
35
63
  # @param name [Symbol]
36
64
  # @param item [{Symbol=>Object}]
37
65
  # @param kwargs [{Symbol=>Object}]
66
+ # @param type [Class(Roda::Endpoints::Endpoint::Collection)]
67
+ # @param on [String, Symbol]
38
68
  # @param (see Endpoint::Collection#initialize)
39
69
  # @see Endpoint::Collection.defaults
40
70
  # @yieldparam endpoint [Collection]
@@ -64,31 +94,28 @@ class Roda
64
94
  #
65
95
  # r.child :id, only: %i(get delete)
66
96
  # 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
97
+ def collection(name,
98
+ item: { on: :id },
99
+ type: Roda::Endpoints::Endpoint::Collection,
100
+ on: name.to_s,
101
+ **kwargs)
102
+ endpoint name: name,
103
+ item: item,
104
+ type: type,
105
+ on: on,
106
+ **kwargs do |endpoint|
77
107
  yield endpoint if block_given?
78
- instance_exec(self, endpoint, &endpoint.route)
79
108
  end
80
- endpoints.pop #=> endpoint
81
109
  end
82
110
 
83
111
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
84
112
 
85
- # @param [Symbol] by
86
- # @param [Endpoint::Collection] collection
87
- # @param [Hash] kwargs
113
+ # @param on [Symbol]
114
+ # @param kwargs [Hash]
88
115
  #
89
116
  # @example
90
117
  # r.collection :articles do |articles|
91
- # r.child :id
118
+ # r.item :id
92
119
  # end
93
120
  #
94
121
  # # is equivalent to
@@ -105,24 +132,75 @@ class Roda
105
132
  # end
106
133
  # end
107
134
  # end
108
- def child(by: :id, collection: endpoint, **kwargs)
135
+ def item(on: :id,
136
+ type: Roda::Endpoints::Endpoint::Item,
137
+ **kwargs)
138
+ unless current_endpoint.is_a?(Roda::Endpoints::Endpoint::Collection)
139
+ raise ArgumentError,
140
+ "#{self.class}#item called not within a collection endpoint"
141
+ end
109
142
  # @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
143
+ endpoint(
144
+ on: on,
145
+ name: current_endpoint.item_name,
146
+ type: type,
147
+ **kwargs
148
+ ) do |endpoint|
149
+ yield endpoint if block_given?
150
+ end
151
+ end
152
+
153
+ # @overload singleton(name, entity, type:, **kwargs)
154
+ # @overload singleton(name:, entity:, type:, **kwargs)
155
+ def singleton(*args,
156
+ name: args.first,
157
+ entity: args.last,
158
+ type: Roda::Endpoints::Endpoint::Singleton,
159
+ **kwargs)
160
+ endpoint(
161
+ name: name,
162
+ entity: entity,
163
+ type: type,
164
+ **kwargs
165
+ ) do |endpoint|
166
+ yield endpoint if block_given?
116
167
  end
117
168
  end
118
169
 
119
170
  private
120
171
 
172
+ # @param [Class(Roda::Endpoints::Endpoint::Singleton)] type
173
+ # @param [Dry::Container::Mixin, #register, #resolve, #merge] container
174
+ # @param [Roda::Endpoints::Endpoint] parent
175
+ # @param [Hash] kwargs
176
+ # rubocop:disable Metrics/ParameterLists
177
+ def endpoint(name:,
178
+ type:,
179
+ container: roda_class.opts[:endpoints][:container],
180
+ parent: current_endpoint,
181
+ on: name,
182
+ **kwargs)
183
+ on on do |*captures|
184
+ with_current_endpoint parent.child(
185
+ name: name,
186
+ type: type,
187
+ container: container,
188
+ parent: parent,
189
+ on: on,
190
+ captures: captures,
191
+ **kwargs
192
+ ) do |endpoint|
193
+ yield endpoint if block_given?
194
+ instance_exec(self, endpoint, &endpoint.route)
195
+ end
196
+ end
197
+ end
198
+
121
199
  # @param [Symbol] verb
122
200
  def match_transaction(verb)
123
- resolve endpoint.transactions.key_for(verb) do |transaction|
201
+ resolve current_endpoint.transactions.key_for(verb) do |transaction|
124
202
  transaction.call(params) do |m|
125
- statuses = endpoint.class.statuses[verb]
203
+ statuses = current_endpoint.class.statuses[verb]
126
204
 
127
205
  m.success do |result|
128
206
  response.status = statuses[:success]
@@ -143,17 +221,26 @@ class Roda
143
221
  end
144
222
 
145
223
  # @return [Endpoint]
146
- def endpoint
224
+ def current_endpoint
147
225
  endpoints.last
148
226
  end
149
227
 
150
228
  # @return [<Endpoint>]
151
229
  def endpoints
152
230
  @endpoints ||= [Roda::Endpoints::Endpoint.new(
153
- name: :root, container: roda_class
231
+ name: :root,
232
+ ns: nil,
233
+ container: roda_class.opts[:endpoints][:container]
154
234
  )]
155
235
  end
236
+
237
+ def with_current_endpoint(endpoint)
238
+ endpoints.push endpoint
239
+ yield endpoint
240
+ endpoints.pop
241
+ end
156
242
  end
243
+ # rubocop:enable Metrics/ModuleLength
157
244
  end
158
245
 
159
246
  register_plugin :endpoints, Endpoints
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda-endpoints
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Semyonov
@@ -340,8 +340,9 @@ files:
340
340
  - lib/roda/endpoints/endpoint/collection.rb
341
341
  - lib/roda/endpoints/endpoint/data.rb
342
342
  - lib/roda/endpoints/endpoint/item.rb
343
+ - lib/roda/endpoints/endpoint/lookup.rb
343
344
  - lib/roda/endpoints/endpoint/namespace.rb
344
- - lib/roda/endpoints/endpoint/operations.rb
345
+ - lib/roda/endpoints/endpoint/singleton.rb
345
346
  - lib/roda/endpoints/endpoint/transactions.rb
346
347
  - lib/roda/endpoints/endpoint/validations.rb
347
348
  - lib/roda/endpoints/endpoint/verbs.rb
@@ -1,28 +0,0 @@
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