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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +435 -32
- data/lib/permittable/audit.rb +241 -24
- data/lib/permittable/authored_values.rb +70 -10
- data/lib/permittable/column_guard.rb +31 -11
- data/lib/permittable/contract.rb +50 -1
- data/lib/permittable/error_envelope.rb +60 -6
- data/lib/permittable/generator.rb +335 -66
- data/lib/permittable/json_schema.rb +102 -15
- data/lib/permittable/open_api.rb +470 -81
- data/lib/permittable/railtie.rb +87 -0
- data/lib/permittable/rspec.rb +49 -7
- data/lib/permittable/tasks/generate.rake +11 -6
- data/lib/permittable/version.rb +1 -1
- data/lib/permittable.rb +367 -69
- metadata +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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).
|