permittable 0.10.0 → 0.11.1

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.
@@ -45,20 +45,74 @@ module Permittable
45
45
 
46
46
  def render_envelope(controller, message:, status:, code: nil, details: nil)
47
47
  if controller.respond_to?(:render_error)
48
- # errors: only when there are details — a host may document its
49
- # render_error contract as `(message:, status:, code:)`, and an
50
- # unconditional errors: kwarg would break those implementations.
48
+ host = host_method(controller)
51
49
  kwargs = { message: message, code: code, status: status }
52
- kwargs[:errors] = details if details
53
- controller.render_error(**kwargs)
50
+ kwargs[:errors] = details if details && (host.nil? || takes_errors?(host))
51
+ host ? host.call(**kwargs) : controller.render_error(**kwargs)
54
52
  else
55
53
  error = { message: message }
56
54
  error[:code] = code if code
57
55
  error[:details] = details if details
58
- controller.render(json: { success: false, error: error }, status: status)
56
+ # content_type: because `render json:` only supplies application/json
57
+ # when the response has none yet, and `respond_to` has already set the
58
+ # negotiated one (text/html for a browser form, or for `Accept: */*`
59
+ # with html listed first). Without it this JSON body goes out as HTML.
60
+ # A JSON type the response already carries (a registered
61
+ # application/vnd.api+json, say) is the host's choice and is kept.
62
+ options = { json: { success: false, error: error }, status: status }
63
+ options[:content_type] = "application/json" unless json_media_type?(response_media_type(controller))
64
+ controller.render(**options)
59
65
  end
60
66
  end
61
67
 
68
+ # The media type (no parameters) the response already carries, or nil when
69
+ # it has none yet or the host has no response to ask.
70
+ def response_media_type(controller)
71
+ return nil unless controller.respond_to?(:response)
72
+
73
+ response = controller.response
74
+ return nil unless response
75
+
76
+ type = response.respond_to?(:media_type) ? response.media_type : response.content_type
77
+ return nil unless type
78
+
79
+ type.to_s.split(";").first.to_s.strip.downcase
80
+ end
81
+
82
+ def json_media_type?(type)
83
+ !type.nil? && (type == "application/json" || type.end_with?("+json"))
84
+ end
85
+
86
+ # The Method for the host's render_error, or nil when the controller claims
87
+ # to respond (respond_to? is true) yet has no such method to inspect: one
88
+ # served by method_missing without respond_to_missing?. Master called it
89
+ # straight through with errors: included, and nil keeps doing exactly that
90
+ # rather than raising NameError out of the rescue_from handler.
91
+ def host_method(controller)
92
+ controller.method(:render_error)
93
+ rescue NameError
94
+ nil
95
+ end
96
+
97
+ # A rejection always carries details, so passing errors: whenever there
98
+ # are some passed it on every call — and a host documenting its contract
99
+ # as `(message:, status:, code:)`, which the README promises keeps
100
+ # working, raised ArgumentError out of the rescue_from handler: a 500 for
101
+ # every invalid request. errors: now goes only to a method that can take
102
+ # it: one naming it, taking **kwargs, or declaring no keywords at all
103
+ # (`*args`, which received it inside its trailing Hash before).
104
+ def takes_errors?(method)
105
+ parameters = method.parameters
106
+ return true if parameters.any? { |kind, _| kind == :keyrest }
107
+
108
+ names = keyword_names(parameters)
109
+ names.empty? || names.include?(:errors)
110
+ end
111
+
112
+ def keyword_names(parameters)
113
+ parameters.filter_map { |kind, name| name if %i[key keyreq].include?(kind) }
114
+ end
115
+
62
116
  # Members are emitted in the order RFC 9457 documents them, so the wire
63
117
  # format is stable and readable. `instance` is omitted rather than guessed
64
118
  # when the host cannot name the request path (a params duck, a job).