Module: RESTFramework::Controller::ClassMethods
- Defined in:
- lib/rest_framework/controller.rb,
lib/rest_framework/controller/actions.rb,
lib/rest_framework/controller/openapi.rb
Constant Summary collapse
- IGNORE_VALIDATORS_WITH_KEYS =
[ :if, :unless ].freeze
- RRF_PROPAGATING_KEY =
Thread-local key toggled by
propagatewhile its block runs. :rrf_propagating
Instance Method Summary collapse
-
#_rrf_action_adds(type) ⇒ Object
Per-class action deltas (internal).
- #_rrf_action_chain ⇒ Object private
- #_rrf_action_removes(type) ⇒ Object
- #_rrf_add_action(type, name, methods, path: nil, metadata: nil, propagate: false, **kwargs) ⇒ Object private
- #_rrf_builtins(type) ⇒ Object private
- #_rrf_compose_actions(type) ⇒ Object private
-
#_rrf_normalize_propagate(value) ⇒ Object
private
Normalize
propagate:tofalse(local),true(self + descendants), or:exclude_self(descendants only). -
#_rrf_reaches?(propagate, is_self) ⇒ Boolean
private
Whether an entry with the given
propagate, declared on some class, reaches the controller we're composing for. - #_rrf_remove_action(type, name, propagate: false) ⇒ Object private
-
#_rrf_warn_action(name, methods, type, reason) ⇒ Object
private
Build the
type:warning foradd_action, echoing the call so its source is easy to find. -
#actions ⇒ Object
Source of truth: the effective collection / member actions (builtins + declared, composed across the inheritance chain), as an ordered
Hash{Symbol => ActionSpec}. -
#add_action(name, methods, type: nil, **opts) ⇒ Object
Route an action, choosing the collection/member scope.
-
#association_requestable_fields(ref) ⇒ Object
The fields a consumer may request for an association beyond its defaults, derived from the associated model's sibling controller: what that controller serializes, so the association can never expose more than its own endpoint would.
-
#field_configuration ⇒ Object
Get a full field configuration, including defaults and inferred values.
-
#get_fields(input_fields: nil) ⇒ Object
Get the available fields.
-
#get_title ⇒ Object
By default, this is the name of the controller class, titleized and with any custom inflection acronyms applied.
-
#label_for(s) ⇒ Object
Get a label from a field/column name, titleized and inflected.
- #member_actions ⇒ Object
- #openapi_document(request, route_group_name, routes) ⇒ Object
- #openapi_paths(routes, tag) ⇒ Object
- #openapi_request_content_types ⇒ Object
- #openapi_response_content_types ⇒ Object
-
#openapi_schema ⇒ Object
Only for model controllers.
-
#openapi_schema_name ⇒ Object
Only for model controllers.
-
#propagate ⇒ Object
Run a block in which configuration setters (
self.x = value) propagate to descendant controllers instead of applying locally. -
#remove_action(name, type: nil, propagate: false) ⇒ Object
Remove an action from routing (including builtins).
- #remove_actions(*names, type: nil, propagate: false) ⇒ Object
-
#rrf_class_attribute(*names, default: nil) ⇒ Object
Define one or more class-level configuration attributes.
-
#rrf_propagated_module ⇒ Object
The per-class module holding this class's propagated attribute getters (and the default baseline).
Instance Method Details
#_rrf_action_adds(type) ⇒ Object
Per-class action deltas (internal). Public because composition reads them off ancestors.
32 33 34 35 36 37 38 |
# File 'lib/rest_framework/controller/actions.rb', line 32 def _rrf_action_adds(type) if type == :member @_rrf_member_action_adds ||= {} else @_rrf_collection_action_adds ||= {} end end |
#_rrf_action_chain ⇒ Object (private)
184 185 186 |
# File 'lib/rest_framework/controller/actions.rb', line 184 def _rrf_action_chain self.ancestors.select { |a| a.is_a?(Class) && a.respond_to?(:_rrf_action_adds) }.reverse end |
#_rrf_action_removes(type) ⇒ Object
40 41 42 43 44 45 46 |
# File 'lib/rest_framework/controller/actions.rb', line 40 def _rrf_action_removes(type) if type == :member @_rrf_member_action_removes ||= {} else @_rrf_collection_action_removes ||= {} end end |
#_rrf_add_action(type, name, methods, path: nil, metadata: nil, propagate: false, **kwargs) ⇒ Object (private)
92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/rest_framework/controller/actions.rb', line 92 def _rrf_add_action(type, name, methods, path: nil, metadata: nil, propagate: false, **kwargs) name = name.to_sym spec = ActionSpec.new( name: name, type: type, methods: Array(methods).map(&:to_sym), path: (path || name).to_s, metadata: , builtin: false, kwargs: kwargs, ) _rrf_action_removes(type).delete(name) _rrf_action_adds(type)[name] = { spec: spec, propagate: _rrf_normalize_propagate(propagate) } end |
#_rrf_builtins(type) ⇒ Object (private)
144 145 146 |
# File 'lib/rest_framework/controller/actions.rb', line 144 def _rrf_builtins(type) type == :member ? RRF_BUILTIN_MEMBER_ACTIONS : RRF_BUILTIN_COLLECTION_ACTIONS end |
#_rrf_compose_actions(type) ⇒ Object (private)
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/rest_framework/controller/actions.rb', line 148 def _rrf_compose_actions(type) effective = {} # 1. Seed with the builtins whose condition holds for this controller. _rrf_builtins(type).each do |name, cfg| next unless cfg[:condition].call(self) effective[name] = ActionSpec.new( name: name, type: type, methods: cfg[:methods], path: "", metadata: nil, builtin: true, kwargs: cfg[:kwargs] || {}, ) end # 2. Walk RRF ancestors from farthest to nearest (ending at self), applying each class's # removes then adds that reach us. Removes-before-adds lets a subclass re-add something an # ancestor removed; later (nearer) classes win. _rrf_action_chain.each do |klass| is_self = klass.equal?(self) klass._rrf_action_removes(type).each do |name, remove| effective.delete(name) if _rrf_reaches?(remove[:propagate], is_self) end klass._rrf_action_adds(type).each do |name, add| effective[name] = add[:spec] if _rrf_reaches?(add[:propagate], is_self) end end effective end |
#_rrf_normalize_propagate(value) ⇒ Object (private)
Normalize propagate: to false (local), true (self + descendants), or :exclude_self
(descendants only). Non-standard values warn: nil becomes false, anything else truthy
becomes true.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/rest_framework/controller/actions.rb', line 116 def _rrf_normalize_propagate(value) case value when false false when true, :exclude_self value when nil Rails.logger.warn("RRF: `propagate: nil` is nonstandard; treating as `false`.") false else Rails.logger.warn("RRF: invalid `propagate:` value #{value.inspect}; treating as `true`.") true end end |
#_rrf_reaches?(propagate, is_self) ⇒ Boolean (private)
Whether an entry with the given propagate, declared on some class, reaches the controller
we're composing for. is_self is true when that class is the controller itself.
133 134 135 136 137 138 139 140 141 142 |
# File 'lib/rest_framework/controller/actions.rb', line 133 def _rrf_reaches?(propagate, is_self) case propagate when :exclude_self !is_self when true true else # false is_self end end |
#_rrf_remove_action(type, name, propagate: false) ⇒ Object (private)
107 108 109 110 111 |
# File 'lib/rest_framework/controller/actions.rb', line 107 def _rrf_remove_action(type, name, propagate: false) name = name.to_sym _rrf_action_adds(type).delete(name) _rrf_action_removes(type)[name] = { propagate: _rrf_normalize_propagate(propagate) } end |
#_rrf_warn_action(name, methods, type, reason) ⇒ Object (private)
Build the type: warning for add_action, echoing the call so its source is easy to find.
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/rest_framework/controller/actions.rb', line 189 def _rrf_warn_action(name, methods, type, reason) type_arg = type ? ", type: #{type.inspect}" : "" sig = "add_action(#{name.inspect}, #{methods.inspect}#{type_arg})" detail = if reason == :ambiguous "needs an explicit `type:` (`:collection` or `:member`); collection and member route " \ "differently on a plural model controller" else "has an unnecessary `type:`; member and collection route the same path on a singular " \ "model controller" end Rails.logger.warn("RRF: `#{sig}` #{detail}.") end |
#actions ⇒ Object
Source of truth: the effective collection / member actions (builtins + declared, composed
across the inheritance chain), as an ordered Hash{Symbol => ActionSpec}.
82 83 84 |
# File 'lib/rest_framework/controller/actions.rb', line 82 def actions _rrf_compose_actions(:collection) end |
#add_action(name, methods, type: nil, **opts) ⇒ Object
Route an action, choosing the collection/member scope. Pass type: on a plural model
controller, where the scopes differ; elsewhere the scope is implied — a singular controller's
sole resource is a member (so delegate targets the record), a modelless one has only a
collection — and type: warns as unnecessary (unless the action is delegated).
52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/rest_framework/controller/actions.rb', line 52 def add_action(name, methods, type: nil, **opts) singular_model = self.model && self.singular if self.model && !self.singular _rrf_warn_action(name, methods, type, :ambiguous) if type.nil? elsif singular_model && type && !opts[:metadata]&.[](:delegate) _rrf_warn_action(name, methods, type, :redundant) end _rrf_add_action(type || (singular_model ? :member : :collection), name, methods, **opts) end |
#association_requestable_fields(ref) ⇒ Object
The fields a consumer may request for an association beyond its defaults, derived from the
associated model's sibling controller: what that controller serializes, so the association can
never expose more than its own endpoint would. Empty unless the sibling is discoverable and
introspectable — a custom serializer makes its get_fields meaningless. Hidden fields are
included (retrievable via ?only= there); write-only fields and nested associations aren't.
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 |
# File 'lib/rest_framework/controller.rb', line 482 def association_requestable_fields(ref) return [] if ref.polymorphic? sibling = RESTFramework::Utils.controller_for_model(self, ref.klass) return [] unless sibling return [] if sibling.serializer_class || sibling.native_serializer_config || sibling.native_serializer_singular_config || sibling.native_serializer_plural_config cfg = sibling.field_configuration sibling.get_fields.reject { |sf| c = cfg[sf] c.nil? || c[:write_only] || c[:kind] == "association" } end |
#field_configuration ⇒ Object
Get a full field configuration, including defaults and inferred values.
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 |
# File 'lib/rest_framework/controller.rb', line 286 def field_configuration return @field_configuration if @field_configuration field_config = self.field_config&.with_indifferent_access || {} columns = self.model.columns_hash column_defaults = self.model.column_defaults reflections = self.model.reflections attributes = self.model._default_attributes readonly_attributes = self.model.readonly_attributes read_only_fields = self.read_only_fields&.map(&:to_s)&.to_set || Set[] write_only_fields = self.write_only_fields&.map(&:to_s)&.to_set || Set[] hidden_fields = self.hidden_fields&.map(&:to_s)&.to_set || Set[] rich_text_association_names = self.model.reflect_on_all_associations(:has_one) .collect(&:name) .select { |n| n.to_s.start_with?("rich_text_") } = self.model. @field_configuration = self.get_fields.map { |f| cfg = field_config[f]&.dup || {} cfg[:label] ||= self.label_for(f) # Annotate primary key. if self.model.primary_key == f cfg[:primary_key] = true unless cfg.key?(:read_only) cfg[:read_only] = true end end # Annotate field mutability and display properties. cfg[:read_only] = true if f.in?(readonly_attributes) || f.in?(read_only_fields) cfg[:write_only] = true if f.in?(write_only_fields) cfg[:hidden] = true if f.in?(hidden_fields) # Raise warnings on some bad combinations of properties. if cfg[:write_only] if cfg[:read_only] Rails.logger.warn("RRF: `#{f}` write_only conflicts with read_only.") end if cfg[:hidden] Rails.logger.warn("RRF: `#{f}` write_only implies hidden.") end if cfg[:hidden_from_index] Rails.logger.warn("RRF: `#{f}` write_only implies hidden_from_index.") end end # Annotate column data. if column = columns[f] cfg[:kind] = "column" cfg[:type] ||= column.type cfg[:required] = true unless column.null end # Add default values from the model's schema. if cfg[:default].nil? && (column_default = column_defaults[f]) cfg[:default] = column_default end # Add metadata from the model's attributes hash. if attributes.key?(f) && attribute = attributes[f] if cfg[:default].nil? && default = attribute.value_before_type_cast cfg[:default] = default end cfg[:kind] ||= "attribute" # Get any type information from the attribute. if type = attribute.type cfg[:type] ||= type.type if type.type # Get enum variants. if type.is_a?(ActiveRecord::Enum::EnumType) cfg[:enum_variants] = type.send(:mapping) # TranslateEnum Integration: translate_method = "translated_#{f.pluralize}" if self.model.respond_to?(translate_method) cfg[:enum_translations] = self.model.send(translate_method) end end end end # Get association metadata. if ref = reflections[f] cfg[:kind] = "association" # Determine the association's fields. if ref.polymorphic? ref_columns = {} else ref_columns = ref.klass.columns_hash end cfg[:fields] ||= RESTFramework::Utils.association_fields_for(ref) cfg[:fields] = cfg[:fields].map(&:to_s) # Strings, to match `:fields` when intersecting requested fields against the allowlist. if cfg[:requestable_fields] cfg[:requestable_fields] = cfg[:requestable_fields].map(&:to_s) end # Very basic metadata about the association's fields. cfg[:association_fields_metadata] = cfg[:fields].map { |sf| v = {} if ref_columns[sf] v[:kind] = "column" else v[:kind] = "method" end next [ sf, v ] }.to_h.compact.presence # Determine if we render id/ids fields. Unfortunately, `has_one` does not provide this # interface. if self.permit_id_assignment && id_field = RESTFramework::Utils.id_field_for(f, ref) cfg[:id_field] = id_field end # Determine if we render nested attributes options. if self.permit_nested_attributes_assignment && ( nested_opts = self.model.[f.to_sym].presence ) cfg[:nested_attributes_options] = { field: "#{f}_attributes", **nested_opts } end begin cfg[:association_pk] = ref.active_record_primary_key rescue ActiveRecord::UnknownPrimaryKey end cfg[:reflection] = ref end # Determine if this is an ActionText "rich text". if :"rich_text_#{f}".in?(rich_text_association_names) cfg[:kind] = "rich_text" end # Determine if this is an ActiveStorage attachment. if ref = [f] cfg[:kind] = "attachment" cfg[:attachment_type] = ref.macro end # Determine if this is just a method. if !cfg[:kind] && self.model.method_defined?(f) cfg[:kind] = "method" cfg[:read_only] = true if cfg[:read_only].nil? end # Collect validator options into a hash on their type, while also updating `required` based # on any presence validators. self.model.validators_on(f).each do |validator| kind = validator.kind = validator. # Reject validator if it includes keys like `:if` and `:unless` because those are # conditionally applied in a way that is not feasible to communicate via the API. next if IGNORE_VALIDATORS_WITH_KEYS.any? { |k| .key?(k) } # Update `required` if we find a presence validator. cfg[:required] = true if kind == :presence cfg[:validators] ||= {} cfg[:validators][kind] ||= [] cfg[:validators][kind] << end next [ f, cfg ] }.to_h.compact.with_indifferent_access # Compile each association's requestable-fields allowlist once (see # `enable_association_queries`). This runs as a second pass, after `@field_configuration` is # memoized, because resolving a sibling's fields reads its `field_configuration` — and a # self-referential or mutual association would otherwise recurse into this build. if self.enable_association_queries @field_configuration.each do |_f, cfg| next unless cfg[:kind] == "association" cfg[:requestable_fields] ||= self.association_requestable_fields(cfg[:reflection]) end end @field_configuration end |
#get_fields(input_fields: nil) ⇒ Object
Get the available fields. Fallback to this controller's model columns, or an empty array. This should always return an array of strings.
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
# File 'lib/rest_framework/controller.rb', line 258 def get_fields(input_fields: nil) input_fields ||= self.fields # If fields is a hash, then parse it. if input_fields.is_a?(Hash) return RESTFramework::Utils.parse_fields_hash( input_fields, self.model, exclude_associations: self.exclude_associations, action_text: self.enable_action_text, active_storage: self.enable_active_storage, ) elsif !input_fields # Otherwise, if fields is nil, then fallback to columns. return self.model ? RESTFramework::Utils.fields_for( self.model, exclude_associations: self.exclude_associations, action_text: self.enable_action_text, active_storage: self.enable_active_storage, ) : [] elsif input_fields input_fields = input_fields.map(&:to_s) end input_fields end |
#get_title ⇒ Object
By default, this is the name of the controller class, titleized and with any custom inflection acronyms applied.
241 242 243 244 245 246 |
# File 'lib/rest_framework/controller.rb', line 241 def get_title self.title || RESTFramework::Utils.inflect( self.name.demodulize.chomp("Controller").titleize(keep_id_suffix: true), self.inflect_acronyms, ) end |
#label_for(s) ⇒ Object
Get a label from a field/column name, titleized and inflected.
249 250 251 252 253 254 |
# File 'lib/rest_framework/controller.rb', line 249 def label_for(s) default_title = RESTFramework::Utils.inflect( s.to_s.titleize(keep_id_suffix: true), self.inflect_acronyms ) self.model&.human_attribute_name(s, default: default_title) || default_title end |
#member_actions ⇒ Object
86 87 88 |
# File 'lib/rest_framework/controller/actions.rb', line 86 def member_actions _rrf_compose_actions(:member) end |
#openapi_document(request, route_group_name, routes) ⇒ Object
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/rest_framework/controller/openapi.rb', line 102 def openapi_document(request, route_group_name, routes) server = request.base_url + request.original_fullpath.gsub(/\?.*/, "") { openapi: "3.1.1", info: { title: self.get_title, description: self.description, version: self.version.to_s, }.compact, servers: [ { url: server } ], paths: self.openapi_paths(routes, route_group_name), tags: [ { name: route_group_name, description: self.description }.compact ], components: { schemas: { "Error" => { type: "object", required: [ "message" ], properties: { message: { type: "string" }, errors: { type: "object" }, exception: { type: "string" }, }, }, }.merge(self.model ? { self.openapi_schema_name => self.openapi_schema } : {}), responses: { "BadRequest": { description: "Bad Request", content: self.openapi_response_content_types.map { |ct| [ ct, ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } }, ] }.to_h, }, "NotFound": { description: "Not Found", content: self.openapi_response_content_types.map { |ct| [ ct, ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } }, ] }.to_h, }, }, }, }.merge(self.model ? { "x-rrf-primary_key" => self.model.primary_key, "x-rrf-callbacks" => self._process_action_callbacks.as_json, # While bulk update/destroy are obvious because they create new router endpoints, bulk # create overloads the existing collection `POST` endpoint, so we add a special key to the # OpenAPI metadata to indicate bulk create is supported. "x-rrf-bulk-create": self.bulk, } : {}).compact end |
#openapi_paths(routes, tag) ⇒ Object
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/rest_framework/controller/openapi.rb', line 19 def openapi_paths(routes, tag) resp_cts = self.openapi_response_content_types req_cts = self.openapi_request_content_types schema_name = self.openapi_schema_name if self.model routes.group_by { |r| r[:concat_path] }.map { |concat_path, routes| [ concat_path.gsub(/:([0-9A-Za-z_-]+)/, "{\\1}"), routes.map { |route| = RESTFramework::ROUTE_METADATA[route[:path]] || {} summary = .delete(:label).presence || self.label_for(route[:action]) description = .delete(:description).presence extra_action = RESTFramework::EXTRA_ACTION_ROUTES.include?(route[:path]) error_response = { "$ref" => "#/components/responses/BadRequest" } not_found_response = { "$ref" => "#/components/responses/NotFound" } spec = { tags: [ tag ], summary: summary, description: description }.compact # All routes should have a successful response. success_code = if !extra_action if route[:action] == "create" 201 elsif route[:action] == "destroy" 204 else 200 end else 200 end spec[:responses] = { success_code => { content: resp_cts.map { |ct| [ ct, (self.model && !extra_action && route[:verb] != "OPTIONS") ? { schema: { "$ref" => "#/components/schemas/#{schema_name}" }, } : {}, ] }.to_h, description: "Success", }, } # Builtin POST, PUT, PATCH, and DELETE should have a 400 and 404 response. if route[:verb].in?([ "POST", "PUT", "PATCH", "DELETE" ]) && !extra_action spec[:responses][400] = error_response spec[:responses][404] = not_found_response end # All POST, PUT, PATCH should have a request body. if route[:verb].in?([ "POST", "PUT", "PATCH" ]) spec[:requestBody] ||= { content: req_cts.map { |ct| [ ct, (self.model && !extra_action) ? { schema: { "$ref" => "#/components/schemas/#{schema_name}" }, } : {}, ] }.to_h, } end # Add remaining metadata as an extension. spec["x-rrf-metadata"] = if .present? next route[:verb].downcase, spec }.to_h.merge( { parameters: routes.first[:route].required_parts.map { |p| { name: p, in: "path", required: true, schema: { type: "integer" }, } }, }, ), ] }.to_h end |
#openapi_request_content_types ⇒ Object
11 12 13 14 15 16 17 |
# File 'lib/rest_framework/controller/openapi.rb', line 11 def openapi_request_content_types @openapi_request_content_types ||= [ "application/json", "application/x-www-form-urlencoded", "multipart/form-data", ] end |
#openapi_response_content_types ⇒ Object
3 4 5 6 7 8 9 |
# File 'lib/rest_framework/controller/openapi.rb', line 3 def openapi_response_content_types @openapi_response_content_types ||= [ "text/html", self.serialize_to_json ? "application/json" : nil, self.serialize_to_xml ? "application/xml" : nil, ].compact end |
#openapi_schema ⇒ Object
Only for model controllers.
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/rest_framework/controller/openapi.rb', line 160 def openapi_schema return @openapi_schema if @openapi_schema field_configuration = self.field_configuration @openapi_schema = { required: field_configuration.select { |_, cfg| cfg[:required] }.keys, type: "object", properties: field_configuration.map { |f, cfg| v = { title: cfg[:label] } if cfg[:kind] == "association" v[:type] = cfg[:reflection].collection? ? "array" : "object" elsif cfg[:kind] == "rich_text" v[:type] = "string" v[:"x-rrf-rich_text"] = true elsif cfg[:kind] == "attachment" v[:type] = "string" v[:"x-rrf-attachment"] = cfg[:attachment_type] else v[:type] = cfg[:type] end v[:readOnly] = true if cfg[:read_only] v[:writeOnly] = true if cfg[:write_only] v[:default] = cfg[:default] if cfg.key?(:default) if enum_variants = cfg[:enum_variants] v[:enum] = enum_variants.keys v[:"x-rrf-enum_variants"] = enum_variants end if validators = cfg[:validators] v[:"x-rrf-validators"] = validators end v[:"x-rrf-kind"] = cfg[:kind] if cfg[:kind] if cfg[:reflection] ref = cfg[:reflection] v[:"x-rrf-reflection"] = { class_name: ref.respond_to?(:class_name) ? ref.class_name : nil, foreign_key: ref.respond_to?(:foreign_key) ? ref.foreign_key : nil, association_foreign_key: ref.respond_to?(:association_foreign_key) ? ref.association_foreign_key : nil, association_primary_key: ref.respond_to?(:association_primary_key) ? ref.association_primary_key : nil, inverse_of: ref.respond_to?(:inverse_of) ? ref.inverse_of&.name : nil, join_table: ref.respond_to?(:join_table) ? ref.join_table : nil, }.compact v[:"x-rrf-association_pk"] = cfg[:association_pk] v[:"x-rrf-association_fields"] = cfg[:fields] v[:"x-rrf-association_fields_metadata"] = cfg[:association_fields_metadata] v[:"x-rrf-id_field"] = cfg[:id_field] v[:"x-rrf-nested_attributes_options"] = cfg[:nested_attributes_options] end next [ f, v ] }.to_h, } @openapi_schema end |
#openapi_schema_name ⇒ Object
Only for model controllers.
224 225 226 |
# File 'lib/rest_framework/controller/openapi.rb', line 224 def openapi_schema_name @openapi_schema_name ||= self.name.chomp("Controller").gsub("::", ".") end |
#propagate ⇒ Object
Run a block in which configuration setters (self.x = value) propagate to descendant
controllers instead of applying locally. Use this on a shared base controller for settings you
want every subclass to inherit:
propagate do
self.paginator_class = RESTFramework::PageNumberPaginator
self.page_size = 30
end
231 232 233 234 235 236 237 |
# File 'lib/rest_framework/controller.rb', line 231 def propagate previous = Thread.current[RRF_PROPAGATING_KEY] Thread.current[RRF_PROPAGATING_KEY] = true yield ensure Thread.current[RRF_PROPAGATING_KEY] = previous end |
#remove_action(name, type: nil, propagate: false) ⇒ Object
Remove an action from routing (including builtins). With no type:, both scopes are removed —
keeping removal simple, and letting propagate: carry it to model descendants where member
scope matters. Pass type: to target a single scope.
67 68 69 70 71 72 73 74 |
# File 'lib/rest_framework/controller/actions.rb', line 67 def remove_action(name, type: nil, propagate: false) if type _rrf_remove_action(type, name, propagate: propagate) else _rrf_remove_action(:collection, name, propagate: propagate) _rrf_remove_action(:member, name, propagate: propagate) end end |
#remove_actions(*names, type: nil, propagate: false) ⇒ Object
76 77 78 |
# File 'lib/rest_framework/controller/actions.rb', line 76 def remove_actions(*names, type: nil, propagate: false) names.each { |name| remove_action(name, type: type, propagate: propagate) } end |
#rrf_class_attribute(*names, default: nil) ⇒ Object
Define one or more class-level configuration attributes. Assignments are local by default:
self.x = value # applies to this controller ONLY; descendants don't inherit it
propagate { self.x = value } # applies to this controller AND all descendants
This gives a single, uniform rule (an assignment is local unless wrapped in propagate), so
there's no per-attribute "does this inherit?" knowledge to carry around. Values are stored in
closures on redefined singleton methods (the same mechanism as class_attribute), never in
instance variables, so there is exactly one interface for configuration: the setter.
Only singleton (class-level) methods are defined, so config never leaks to controller instances (which would risk colliding with action methods).
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/rest_framework/controller.rb', line 179 def rrf_class_attribute(*names, default: nil) names.each do |name| # Propagating baseline: every controller sees the default until it's overridden. This lives # in the propagated module (see `rrf_propagated_module`) rather than directly on the # singleton class, so a local assignment can coexist with it via `super`. rrf_propagated_module.define_method(name) { default } # Parity with `class_attribute`, which also defines a predicate. singleton_class.define_method("#{name}?") { !!public_send(name) } singleton_class.define_method("#{name}=") do |value| if Thread.current[RRF_PROPAGATING_KEY] # Propagate: descendants inherit this getter via the module in the singleton chain. It's # kept separate from any local getter (defined directly on the singleton class) so a # subsequent local assignment doesn't clobber the value propagated to descendants. rrf_propagated_module.define_method(name) { value } else # Local: `value` for this class only; descendants fall back through `super` to the # nearest propagated ancestor value, or the default. klass = self singleton_class.define_method(name) do if equal?(klass) value elsif defined?(super) super() else default end end end end end end |
#rrf_propagated_module ⇒ Object
The per-class module holding this class's propagated attribute getters (and the default
baseline). It's included into the singleton class so descendants inherit propagated values
through the singleton-class chain, while local assignments—defined directly on the singleton
class—take precedence for the class itself and can super() back into this module. Created
lazily and memoized per class (instance variables aren't inherited, so each class gets its
own).
219 220 221 |
# File 'lib/rest_framework/controller.rb', line 219 def rrf_propagated_module @rrf_propagated_module ||= Module.new.tap { |mod| singleton_class.include(mod) } end |