roda-endpoints 0.1.0 → 0.2.0

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