cuprum-rails 0.1.0

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