cuprum-rails 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 (81) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +98 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/DEVELOPMENT.md +28 -0
  5. data/LICENSE +22 -0
  6. data/README.md +1045 -0
  7. data/lib/cuprum/rails/action.rb +45 -0
  8. data/lib/cuprum/rails/actions/create.rb +49 -0
  9. data/lib/cuprum/rails/actions/destroy.rb +22 -0
  10. data/lib/cuprum/rails/actions/edit.rb +22 -0
  11. data/lib/cuprum/rails/actions/index.rb +55 -0
  12. data/lib/cuprum/rails/actions/new.rb +19 -0
  13. data/lib/cuprum/rails/actions/resource_action.rb +75 -0
  14. data/lib/cuprum/rails/actions/show.rb +22 -0
  15. data/lib/cuprum/rails/actions/update.rb +59 -0
  16. data/lib/cuprum/rails/actions.rb +16 -0
  17. data/lib/cuprum/rails/collection.rb +115 -0
  18. data/lib/cuprum/rails/command.rb +137 -0
  19. data/lib/cuprum/rails/commands/assign_one.rb +66 -0
  20. data/lib/cuprum/rails/commands/build_one.rb +55 -0
  21. data/lib/cuprum/rails/commands/destroy_one.rb +43 -0
  22. data/lib/cuprum/rails/commands/find_many.rb +60 -0
  23. data/lib/cuprum/rails/commands/find_matching.rb +121 -0
  24. data/lib/cuprum/rails/commands/find_one.rb +50 -0
  25. data/lib/cuprum/rails/commands/insert_one.rb +41 -0
  26. data/lib/cuprum/rails/commands/update_one.rb +49 -0
  27. data/lib/cuprum/rails/commands/validate_one.rb +68 -0
  28. data/lib/cuprum/rails/commands.rb +18 -0
  29. data/lib/cuprum/rails/controller.rb +50 -0
  30. data/lib/cuprum/rails/controller_action.rb +121 -0
  31. data/lib/cuprum/rails/controllers/class_methods/actions.rb +57 -0
  32. data/lib/cuprum/rails/controllers/class_methods/configuration.rb +64 -0
  33. data/lib/cuprum/rails/controllers/class_methods/validations.rb +30 -0
  34. data/lib/cuprum/rails/controllers/class_methods.rb +15 -0
  35. data/lib/cuprum/rails/controllers/configuration.rb +53 -0
  36. data/lib/cuprum/rails/controllers.rb +10 -0
  37. data/lib/cuprum/rails/errors/missing_parameters.rb +33 -0
  38. data/lib/cuprum/rails/errors/missing_primary_key.rb +46 -0
  39. data/lib/cuprum/rails/errors/undefined_permitted_attributes.rb +34 -0
  40. data/lib/cuprum/rails/errors.rb +8 -0
  41. data/lib/cuprum/rails/map_errors.rb +44 -0
  42. data/lib/cuprum/rails/query.rb +77 -0
  43. data/lib/cuprum/rails/query_builder.rb +78 -0
  44. data/lib/cuprum/rails/repository.rb +44 -0
  45. data/lib/cuprum/rails/request.rb +105 -0
  46. data/lib/cuprum/rails/resource.rb +145 -0
  47. data/lib/cuprum/rails/responders/actions.rb +73 -0
  48. data/lib/cuprum/rails/responders/html/plural_resource.rb +62 -0
  49. data/lib/cuprum/rails/responders/html/singular_resource.rb +59 -0
  50. data/lib/cuprum/rails/responders/html.rb +11 -0
  51. data/lib/cuprum/rails/responders/html_responder.rb +129 -0
  52. data/lib/cuprum/rails/responders/json/resource.rb +60 -0
  53. data/lib/cuprum/rails/responders/json.rb +10 -0
  54. data/lib/cuprum/rails/responders/json_responder.rb +122 -0
  55. data/lib/cuprum/rails/responders/matching.rb +145 -0
  56. data/lib/cuprum/rails/responders/serialization.rb +36 -0
  57. data/lib/cuprum/rails/responders.rb +15 -0
  58. data/lib/cuprum/rails/responses/html/redirect_response.rb +29 -0
  59. data/lib/cuprum/rails/responses/html/render_response.rb +52 -0
  60. data/lib/cuprum/rails/responses/html.rb +11 -0
  61. data/lib/cuprum/rails/responses/json_response.rb +29 -0
  62. data/lib/cuprum/rails/responses.rb +11 -0
  63. data/lib/cuprum/rails/routes.rb +166 -0
  64. data/lib/cuprum/rails/routing/plural_routes.rb +26 -0
  65. data/lib/cuprum/rails/routing/singular_routes.rb +24 -0
  66. data/lib/cuprum/rails/routing.rb +11 -0
  67. data/lib/cuprum/rails/rspec/command_contract.rb +460 -0
  68. data/lib/cuprum/rails/rspec/define_route_contract.rb +84 -0
  69. data/lib/cuprum/rails/rspec.rb +8 -0
  70. data/lib/cuprum/rails/serializers/json/active_record_serializer.rb +24 -0
  71. data/lib/cuprum/rails/serializers/json/array_serializer.rb +40 -0
  72. data/lib/cuprum/rails/serializers/json/attributes_serializer.rb +217 -0
  73. data/lib/cuprum/rails/serializers/json/error_serializer.rb +24 -0
  74. data/lib/cuprum/rails/serializers/json/hash_serializer.rb +44 -0
  75. data/lib/cuprum/rails/serializers/json/identity_serializer.rb +21 -0
  76. data/lib/cuprum/rails/serializers/json/serializer.rb +66 -0
  77. data/lib/cuprum/rails/serializers/json.rb +40 -0
  78. data/lib/cuprum/rails/serializers.rb +10 -0
  79. data/lib/cuprum/rails/version.rb +59 -0
  80. data/lib/cuprum/rails.rb +31 -0
  81. metadata +286 -0
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/responders'
4
+ require 'cuprum/rails/responders/actions'
5
+ require 'cuprum/rails/responders/matching'
6
+ require 'cuprum/rails/responders/serialization'
7
+ require 'cuprum/rails/responses/json_response'
8
+ require 'cuprum/rails/serializers/json'
9
+
10
+ module Cuprum::Rails::Responders
11
+ # Provides a DSL for defining responses to JSON requests.
12
+ #
13
+ # By default, responds to any successful result by serializing the result
14
+ # value and generating a JSON object of the form { 'ok' => true, 'data' =>
15
+ # serialized_value }.
16
+ #
17
+ # For a failing result, it generates and serializes a generic error and
18
+ # generates a JSON object of the form { 'ok' => false, 'data' =>
19
+ # serialized_error }. This is to prevent leaks of internal states that might
20
+ # help an adversary access your system. Use the .match class method to define
21
+ # more useful responses for whitelisted errors.
22
+ #
23
+ # @example Defining Error Responses
24
+ # class CustomResponder < Cuprum::Rails::Responders::HtmlResponder
25
+ # match :failure, error: Spec::NotFound do |result|
26
+ # render_failure(result.error, status: 404)
27
+ # end
28
+ #
29
+ # match :failure, error: Spec::AuthorizationFailure do
30
+ # error = Cuprum::Error.new(message: "I can't let you do that, Dave")
31
+ #
32
+ # render_failure(error, status: 403)
33
+ # end
34
+ # end
35
+ class JsonResponder
36
+ include Cuprum::Rails::Responders::Matching
37
+ include Cuprum::Rails::Responders::Actions
38
+ include Cuprum::Rails::Responders::Serialization
39
+
40
+ GENERIC_ERROR = Cuprum::Error.new(
41
+ message: 'Something went wrong when processing the request'
42
+ ).freeze
43
+ private_constant :GENERIC_ERROR
44
+
45
+ match :success do |result|
46
+ render_success(result.value)
47
+ end
48
+
49
+ match :failure do
50
+ render_failure(GENERIC_ERROR)
51
+ end
52
+
53
+ # @param action_name [String, Symbol] The name of the action to match.
54
+ # @param matcher [Cuprum::Matcher] An optional matcher specific to the
55
+ # action. This will be matched before any of the generic matchers.
56
+ # @param member_action [Boolean] True if the action acts on a collection
57
+ # item, not on the collection as a whole.
58
+ # @param resource [Cuprum::Rails::Resource] The resource for the controller.
59
+ def initialize( # rubocop:disable Metrics/ParameterLists
60
+ action_name:,
61
+ resource:,
62
+ serializers:,
63
+ matcher: nil,
64
+ member_action: false,
65
+ **_options
66
+ )
67
+ super(
68
+ action_name: action_name,
69
+ matcher: matcher,
70
+ member_action: member_action,
71
+ resource: resource,
72
+ root_serializer: Cuprum::Rails::Serializers::Json::Serializer.instance,
73
+ serializers: serializers
74
+ )
75
+ end
76
+
77
+ # @!method call(result)
78
+ # (see Cuprum::Rails::Responders::Actions#call)
79
+
80
+ # @return [Symbol] the format of the responder.
81
+ def format
82
+ :json
83
+ end
84
+
85
+ # Creates a JsonResponse based on the given data and options.
86
+ #
87
+ # @param json [Object] The data to serialize.
88
+ # @param status [Integer] The HTTP status of the response.
89
+ #
90
+ # @return [Cuprum::Rails::Responses::JsonResponse] the response.
91
+ def render(json, status: 200)
92
+ Cuprum::Rails::Responses::JsonResponse.new(
93
+ data: serialize(json),
94
+ status: status
95
+ )
96
+ end
97
+
98
+ # Creates a JsonResponse for a failed result.
99
+ #
100
+ # @param error [Cuprum::Error] The error from the failed result.
101
+ # @param status [Integer] The HTTP status of the response.
102
+ #
103
+ # @return [Cuprum::Rails::Responses::JsonResponse] the response.
104
+ def render_failure(error, status: 500)
105
+ json = { 'ok' => false, 'error' => error }
106
+
107
+ render(json, status: status)
108
+ end
109
+
110
+ # Creates a JsonResponse for a successful result.
111
+ #
112
+ # @param value [Object] The value of the successful result.
113
+ # @param status [Integer] The HTTP status of the response.
114
+ #
115
+ # @return [Cuprum::Rails::Responses::JsonResponse] the response.
116
+ def render_success(value, status: 200)
117
+ json = { 'ok' => true, 'data' => value }
118
+
119
+ render(json, status: status)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/matcher'
4
+ require 'cuprum/matcher_list'
5
+
6
+ require 'cuprum/rails/responders'
7
+
8
+ module Cuprum::Rails::Responders
9
+ # Implements matching an action result to a response clause.
10
+ module Matching
11
+ # Provides a DSL for generating response clauses for matching results.
12
+ module ClassMethods
13
+ # Creates a match clause that maps a result to a response.
14
+ #
15
+ # @param status [Symbol] The status of the result, either :success or
16
+ # :failure.
17
+ # @param error [Class] The class of the result error. If given, the clause
18
+ # will only match results with an error that is an instance of this
19
+ # class or a subclass.
20
+ # @param value [Class] The class of the result value. If given, the clause
21
+ # will only match results with a value that is an instance of this class
22
+ # or a subclass.
23
+ #
24
+ # @yield The clause implementation. This block will be called in the
25
+ # context of the matcher.
26
+ # @yieldreturn [#call, #renderer] the response for the action.
27
+ def match(status, error: nil, value: nil, &block)
28
+ matcher = @matcher || Cuprum::Matcher.new
29
+
30
+ matcher.singleton_class.match(
31
+ status, error: error, value: value, &block
32
+ )
33
+
34
+ @matcher = matcher
35
+ end
36
+
37
+ # @private
38
+ def matchers(**_keywords)
39
+ return [] unless @matcher
40
+
41
+ [@matcher]
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :matcher
47
+ end
48
+
49
+ # @private
50
+ def self.included(other)
51
+ super
52
+
53
+ other.extend(ClassMethods)
54
+ end
55
+
56
+ # @param action_name [String, Symbol] The name of the action to match.
57
+ # @param matcher [Cuprum::Matcher] An optional matcher specific to the
58
+ # action. This will be matched before any of the generic matchers.
59
+ # @param member_action [Boolean] True if the action acts on a collection
60
+ # item, not on the collection as a whole.
61
+ # @param resource [Cuprum::Rails::Resource] The resource for the controller.
62
+ def initialize(
63
+ action_name:,
64
+ resource:,
65
+ matcher: nil,
66
+ member_action: false,
67
+ **_options
68
+ )
69
+ @action_name = action_name
70
+ @matcher = matcher
71
+ @member_action = !!member_action # rubocop:disable Style/DoubleNegation
72
+ @resource = resource
73
+ end
74
+
75
+ # @return [String, Symbol] the name of the action to match.
76
+ attr_reader :action_name
77
+
78
+ # @return [Cuprum::Matcher] an optional matcher specific to the action.
79
+ attr_reader :matcher
80
+
81
+ # @return [Cuprum::Rails::Resource] the resource for the controller.
82
+ attr_reader :resource
83
+
84
+ # @return [Cuprum::Result] the result of calling the action.
85
+ attr_reader :result
86
+
87
+ # Finds and calls the response clause that matches the given result.
88
+ #
89
+ # 1. Checks for an exact match (the result status, value, and error all
90
+ # match) in the given matcher (if any), then the responder class, then
91
+ # each ancestor of the responder class in ascending order.
92
+ # 2. If a match is not found, checks for a partial match (the result
93
+ # status, and either the value or the error match) in the same order.
94
+ # 3. If there is still no match found, checks for a generic match (the
95
+ # result status matches, and the match clause does not specify either an
96
+ # error or a value.
97
+ # 4. If there is no matching response clause, raises an exception.
98
+ #
99
+ # @return [#call, #renderer] The response object from the matching response
100
+ # clause.
101
+ #
102
+ # @raise [Cuprum::Matching::NoMatchError] if none of the response clauses
103
+ # match the result.
104
+ def call(result)
105
+ @result = result
106
+
107
+ Cuprum::MatcherList
108
+ .new(matchers.map { |matcher| matcher.with_context(self) })
109
+ .call(result)
110
+ end
111
+
112
+ # @return [Symbol, nil] the format of the responder.
113
+ def format
114
+ nil
115
+ end
116
+
117
+ # @return [Boolean] true if the action acts on a collection item, not on the
118
+ # collection as a whole.
119
+ def member_action?
120
+ @member_action
121
+ end
122
+
123
+ private
124
+
125
+ def class_matchers
126
+ options = matcher_options
127
+
128
+ singleton_class
129
+ .ancestors
130
+ .select { |ancestor| ancestor < Cuprum::Rails::Responders::Matching }
131
+ .map { |ancestor| ancestor.matchers(**options) }
132
+ .flatten
133
+ end
134
+
135
+ def matcher_options
136
+ {}
137
+ end
138
+
139
+ def matchers
140
+ return class_matchers if matcher.nil?
141
+
142
+ [matcher, *class_matchers]
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/responders'
4
+
5
+ module Cuprum::Rails::Responders
6
+ # Implements serializing a result value into response data.
7
+ module Serialization
8
+ # @param root_serializer [Class] The root serializer for serializing the
9
+ # result value.
10
+ # @param serializers [Hash<Class, Object>] The serializers for converting
11
+ # result values into serialized data.
12
+ # @param options [Hash] Additional parameters for the responder.
13
+ def initialize(root_serializer:, serializers:, **options)
14
+ super(**options)
15
+
16
+ @root_serializer = root_serializer
17
+ @serializers = serializers
18
+ end
19
+
20
+ # @return [Object] the root serializer for serializing the result value.
21
+ attr_reader :root_serializer
22
+
23
+ # @return [Hash<Class, Object>] The serializers for converting result values
24
+ # into serialized data.
25
+ attr_reader :serializers
26
+
27
+ # Converts a result value into a serialized data structure.
28
+ #
29
+ # @param object [Object] The object to serialize.
30
+ #
31
+ # @return [Object] the serialized data.
32
+ def serialize(object)
33
+ root_serializer.call(object, serializers: serializers)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails'
4
+
5
+ module Cuprum::Rails
6
+ # Namespace for responders, which process action results into responses.
7
+ module Responders
8
+ autoload :Actions, 'cuprum/rails/responders/actions'
9
+ autoload :Html, 'cuprum/rails/responders/html'
10
+ autoload :HtmlResponder, 'cuprum/rails/responders/html_responder'
11
+ autoload :Json, 'cuprum/rails/responders/json'
12
+ autoload :JsonResponder, 'cuprum/rails/responders/json_responder'
13
+ autoload :Matching, 'cuprum/rails/responders/matching'
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/responses/html'
4
+
5
+ module Cuprum::Rails::Responses::Html
6
+ # Encapsulates an HTML response that redirects to a given path.
7
+ class RedirectResponse
8
+ # @param path [String] The path or url to redirect to.
9
+ # @param status [Integer] The HTTP status of the response.
10
+ def initialize(path, status: 302)
11
+ @path = path
12
+ @status = status
13
+ end
14
+
15
+ # @return [String] the path or url to redirect to.
16
+ attr_reader :path
17
+
18
+ # @return [Integer] the HTTP status of the response.
19
+ attr_reader :status
20
+
21
+ # Calls the renderer's #redirect_to method with the path and status.
22
+ #
23
+ # @param renderer [#redirect_to] The context for executing the response,
24
+ # such as a Rails controller.
25
+ def call(renderer)
26
+ renderer.redirect_to(path, status: status)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/responses/html'
4
+
5
+ module Cuprum::Rails::Responses::Html
6
+ # Encapsulates an HTML response that renders a given template.
7
+ class RenderResponse
8
+ # @param assigns [Hash] Variables to assign when rendering the template.
9
+ # @param layout [String] The layout to render.
10
+ # @param status [Integer] The HTTP status of the response.
11
+ # @param template [String, Symbol] The template to render.
12
+ def initialize(template, assigns: {}, layout: nil, status: 200)
13
+ @assigns = assigns
14
+ @layout = layout
15
+ @status = status
16
+ @template = template
17
+ end
18
+
19
+ # @return [Hash] variables to assign when rendering the template.
20
+ attr_reader :assigns
21
+
22
+ # @return [String] the layout to render.
23
+ attr_reader :layout
24
+
25
+ # @return [Integer] the HTTP status of the response.
26
+ attr_reader :status
27
+
28
+ # @return [String, Symbol] the template to render.
29
+ attr_reader :template
30
+
31
+ # Calls the renderer's #render method with the template and parameters.
32
+ #
33
+ # @param renderer [#render] The context for executing the response, such as
34
+ # a Rails controller.
35
+ def call(renderer)
36
+ assign_variables(renderer)
37
+
38
+ options = { status: status }
39
+ options[:layout] = layout if layout
40
+
41
+ renderer.render(template, **options)
42
+ end
43
+
44
+ private
45
+
46
+ def assign_variables(renderer)
47
+ assigns.each do |key, value|
48
+ renderer.instance_variable_set("@#{key}", value)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/responses'
4
+
5
+ module Cuprum::Rails::Responses
6
+ # Namespace for response objects, which encapsulate Html responses.
7
+ module Html
8
+ autoload :RedirectResponse, 'cuprum/rails/responses/html/redirect_response'
9
+ autoload :RenderResponse, 'cuprum/rails/responses/html/render_response'
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/responses'
4
+
5
+ module Cuprum::Rails::Responses
6
+ # Encapsulates a JSON response that returns the given serialized data.
7
+ class JsonResponse
8
+ # @param data [Object] The JSON data to return.
9
+ # @param status [Integer] The HTTP status of the response.
10
+ def initialize(data:, status: 200)
11
+ @data = data
12
+ @status = status
13
+ end
14
+
15
+ # @return [Object] the JSON data to return.
16
+ attr_reader :data
17
+
18
+ # @return [Integer] the HTTP status of the response.
19
+ attr_reader :status
20
+
21
+ # Calls the renderer's #render method with the serialized data and status.
22
+ #
23
+ # @param renderer [#render] The context for executing the response, such as
24
+ # a Rails controller.
25
+ def call(renderer)
26
+ renderer.render(json: data, status: status)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails'
4
+
5
+ module Cuprum::Rails
6
+ # Namespace for response objects, which encapsulate server responses.
7
+ module Responses
8
+ autoload :Html, 'cuprum/rails/responses/html'
9
+ autoload :JsonResponse, 'cuprum/rails/responses/json_response'
10
+ end
11
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails'
4
+
5
+ module Cuprum::Rails
6
+ # Represent the routes available for a given resource.
7
+ class Routes
8
+ # Error class when a wildcard value is missing for a route.
9
+ class MissingWildcardError < StandardError; end
10
+
11
+ class << self
12
+ # Defines a route for the resource routes.
13
+ #
14
+ # Each defined route creates a helper method on the routes, which returns
15
+ # the full path of the resource route. A member action helper (if the
16
+ # path ends in an :id wildcard) will require passing in either the primary
17
+ # key of the member or the member entity.
18
+ #
19
+ # If the base path includes wildcards, then the wildcards must be set
20
+ # (using #with_wildcards) before calling a route helper.
21
+ #
22
+ # @param action_name [String, Symbol] The name of the action.
23
+ # @param path [String] The path of the action relative to the resource
24
+ # root. If the path has a leading slash, the path is treated as an
25
+ # absolute path and does not include the routes base path.
26
+ def route(action_name, path)
27
+ validate_action_name!(action_name)
28
+ validate_path!(path)
29
+
30
+ if member_route?(path)
31
+ define_member_route(action_name, path)
32
+ else
33
+ define_collection_route(action_name, path)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def define_collection_route(action_name, original_path)
40
+ define_method :"#{action_name}_path" do
41
+ path = resolve_base_path(original_path)
42
+
43
+ insert_wildcards(path)
44
+ end
45
+ end
46
+
47
+ def define_member_route(action_name, original_path)
48
+ define_method :"#{action_name}_path" do |entity|
49
+ path = resolve_base_path(original_path)
50
+
51
+ insert_wildcards(path, entity)
52
+ end
53
+ end
54
+
55
+ def member_route?(path)
56
+ path.split('/').include?(':id')
57
+ end
58
+
59
+ def validate_action_name!(action_name)
60
+ unless action_name.is_a?(String) || action_name.is_a?(Symbol)
61
+ raise ArgumentError,
62
+ 'action_name must be a String or Symbol',
63
+ caller(1..-1)
64
+ end
65
+
66
+ return unless action_name.to_s.empty?
67
+
68
+ raise ArgumentError, "action_name can't be blank", caller(1..-1)
69
+ end
70
+
71
+ def validate_path!(path)
72
+ return if path.is_a?(String) || path.is_a?(Symbol)
73
+
74
+ raise ArgumentError, 'path must be a String or Symbol', caller(1..-1)
75
+ end
76
+ end
77
+
78
+ # @param base_path [String] The relative path of the resource.
79
+ def initialize(base_path:, &block)
80
+ @base_path = base_path
81
+ @wildcards = {}
82
+
83
+ singleton_class.instance_exec(&block) if block_given?
84
+ end
85
+
86
+ # @return [String] the relative path of the resource.
87
+ attr_reader :base_path
88
+
89
+ # @return [String] the url wildcards for the resource.
90
+ attr_reader :wildcards
91
+
92
+ # @return [String] the path to the parent resource index.
93
+ def parent_path
94
+ root_path
95
+ end
96
+
97
+ # @return [String] the root path for the application.
98
+ def root_path
99
+ '/'
100
+ end
101
+
102
+ # @param wildcards [Hash] The wildcards to use with the routes.
103
+ #
104
+ # @return [Cuprum::Rails::Routes] a copy of the routes with the wildcards.
105
+ def with_wildcards(wildcards)
106
+ unless wildcards.is_a?(Hash)
107
+ raise ArgumentError, 'wildcards must be a Hash'
108
+ end
109
+
110
+ clone.apply_wildcards(wildcards)
111
+ end
112
+
113
+ protected
114
+
115
+ def apply_wildcards(wildcards)
116
+ @wildcards = wildcards.stringify_keys
117
+
118
+ self
119
+ end
120
+
121
+ private
122
+
123
+ def insert_wildcards(path, entity = nil)
124
+ path
125
+ .split('/')
126
+ .map do |segment|
127
+ next segment unless segment.start_with?(':')
128
+
129
+ next resolve_primary_key(entity) if segment == ':id'
130
+
131
+ resolve_wildcard(segment)
132
+ end
133
+ .join('/')
134
+ end
135
+
136
+ def resolve_base_path(path)
137
+ return path if path.start_with?('/')
138
+
139
+ return base_path if path.empty?
140
+
141
+ "#{base_path}/#{path}"
142
+ end
143
+
144
+ def resolve_primary_key(entity)
145
+ raise MissingWildcardError, 'missing wildcard :id' if entity.nil?
146
+
147
+ primary_key = entity.class.primary_key
148
+
149
+ entity[primary_key]
150
+ end
151
+
152
+ def resolve_wildcard(segment)
153
+ wildcard = segment[1..] # :something_id to something_id
154
+
155
+ return wildcards[wildcard] if wildcards.include?(wildcard)
156
+
157
+ wildcard = segment[1...-3] # :something_id to something
158
+
159
+ if wildcards.include?(wildcard)
160
+ return resolve_primary_key(wildcards[wildcard])
161
+ end
162
+
163
+ raise MissingWildcardError, "missing wildcard #{segment}"
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/routing'
4
+
5
+ module Cuprum::Rails::Routing
6
+ # Routes object with predefined routes for a RESTful resource.
7
+ #
8
+ # The following route helpers are defined:
9
+ #
10
+ # - #create_path => '/' or '/path/to/resource/'
11
+ # - #destroy_path => '/:id' or '/path/to/resource/:id'
12
+ # - #edit_path => '/:id/edit' or '/path/to/resource/:id/edit'
13
+ # - #index_path => '/' or '/path/to/resource/'
14
+ # - #new_path => '/new' or '/path/to/resource/new'
15
+ # - #show_path => '/:id/edit' or '/path/to/resource/:id/edit'
16
+ # - #update_path => '/:id/edit' or '/path/to/resource/:id/edit'
17
+ class PluralRoutes < Cuprum::Rails::Routes
18
+ route :create, ''
19
+ route :destroy, ':id'
20
+ route :edit, ':id/edit'
21
+ route :index, ''
22
+ route :new, 'new'
23
+ route :show, ':id'
24
+ route :update, ':id'
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/routing'
4
+
5
+ module Cuprum::Rails::Routing
6
+ # Routes object with predefined routes for a singular RESTful resource.
7
+ #
8
+ # The following route helpers are defined:
9
+ #
10
+ # - #create_path => '/' or '/path/to/resource/'
11
+ # - #destroy_path => '/' or '/path/to/resource/'
12
+ # - #edit_path => '/edit' or '/path/to/resource/edit'
13
+ # - #new_path => '/new' or '/path/to/resource/new'
14
+ # - #show_path => '/' or '/path/to/resource/'
15
+ # - #update_path => '/' or '/path/to/resource/'
16
+ class SingularRoutes < Cuprum::Rails::Routes
17
+ route :create, ''
18
+ route :destroy, ''
19
+ route :edit, 'edit'
20
+ route :new, 'new'
21
+ route :show, ''
22
+ route :update, ''
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails'
4
+
5
+ module Cuprum::Rails
6
+ # Namespace for routing-specific functionality.
7
+ module Routing
8
+ autoload :PluralRoutes, 'cuprum/rails/routing/plural_routes'
9
+ autoload :SingularRoutes, 'cuprum/rails/routing/singular_routes'
10
+ end
11
+ end