Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

* Your contribution here
- [#89](https://github.com/numbata/grape-oas/pull/89): Add `GrapeOAS.entity_exposure_required_default` to make entity exposures without explicit required metadata optional in generated schemas - [@abeljim8am](https://github.com/abeljim8am).

### Fixed

Expand Down
16 changes: 16 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This document covers all configuration options for Grape::OAS.

- [Global Options](#global-options)
- [Schema Ref Names](#schema-ref-names)
- [Entity Exposure Requiredness](#entity-exposure-requiredness)
- [Info Object](#info-object)
- [Nullable Strategy](#nullable-strategy)
- [OAS2 Composition Extensions](#oas2-composition-extensions)
Expand Down Expand Up @@ -65,6 +66,21 @@ GrapeOAS.schema_ref_name = nil
The callable receives the canonical class name as a string and should return a
valid OAS component key. Configure it once during application boot.

## Entity Exposure Requiredness

Unconditional `Grape::Entity` exposures are required in generated schemas by
default. Set the global default to `false` during application boot when clients
should tolerate those fields being absent:

```ruby
GrapeOAS.entity_exposure_required_default = false
```

An explicit `documentation: { required: true }` or `required: false` always
wins, and conditional exposures remain optional. This setting affects only the
generated OpenAPI schema; it does not change Grape's runtime serialization.
Set it to `nil` to restore the default of `true`.

## Info Object

```ruby
Expand Down
19 changes: 19 additions & 0 deletions lib/grape_oas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,25 @@ def logger=(value)

module_function :logger, :logger=

# @return [Boolean] whether entity exposures without required metadata
# default to required
def entity_exposure_required_default
return true if @entity_exposure_required_default.nil?

@entity_exposure_required_default
end

# @param value [true, false, nil] `nil` resets to the default (`true`)
def entity_exposure_required_default=(value)
unless value.nil? || value == true || value == false
raise ArgumentError, "entity_exposure_required_default must be true, false, or nil (got #{value.class})"
end

@entity_exposure_required_default = value
end

module_function :entity_exposure_required_default, :entity_exposure_required_default=

# Returns the global introspector registry.
#
# The registry manages introspectors that build schemas from various sources
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ def exposure_options(exposure)

# Determines whether a property should be marked required.
# Explicit doc[:required] takes precedence; conditional exposures
# default to false; unconditional exposures default to true.
# default to false; unconditional exposures fall back to
# GrapeOAS.entity_exposure_required_default (default `true`).
#
# @param doc [Hash] normalized documentation hash
# @param exposure the entity exposure
Expand All @@ -127,7 +128,7 @@ def determine_required(doc, exposure)
return doc[:required] unless doc[:required].nil?
return false if conditional?(exposure)

true
GrapeOAS.entity_exposure_required_default
end

private
Expand Down
16 changes: 16 additions & 0 deletions test/grape_oas/introspectors/entity_introspector_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,22 @@ def test_explicit_required_true_on_conditional_is_respected
assert_includes schema.required, "forced"
end

def test_configured_required_default_preserves_explicit_and_conditional_rules
entity_class = Class.new(Grape::Entity) do
expose :implicit, documentation: { type: String }
expose :required, documentation: { type: String, required: true }
expose :optional, documentation: { type: String, required: false }
expose :conditional, documentation: { type: String }, if: ->(_object, _options) { true }
end
GrapeOAS.entity_exposure_required_default = false

schema = Introspectors::EntityIntrospector.new(entity_class).build_schema

assert_equal ["required"], schema.required
ensure
GrapeOAS.entity_exposure_required_default = nil
end

def test_x_nullable_documentation_sets_nullable_on_entity_exposure
entity_class = Class.new(Grape::Entity) do
expose :note, documentation: { type: String, x: { nullable: true } }
Expand Down
25 changes: 25 additions & 0 deletions test/grape_oas/oas_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,29 @@ def test_schema_ref_name_receives_canonical_name_as_argument
ensure
GrapeOAS.schema_ref_name = nil
end

def test_entity_exposure_required_default_can_be_configured_and_reset
GrapeOAS.entity_exposure_required_default = nil

assert GrapeOAS.entity_exposure_required_default
GrapeOAS.entity_exposure_required_default = false

refute GrapeOAS.entity_exposure_required_default
GrapeOAS.entity_exposure_required_default = true

assert GrapeOAS.entity_exposure_required_default
GrapeOAS.entity_exposure_required_default = nil

assert GrapeOAS.entity_exposure_required_default
ensure
GrapeOAS.entity_exposure_required_default = nil
end

def test_entity_exposure_required_default_setter_raises_for_non_boolean
error = assert_raises(ArgumentError) { GrapeOAS.entity_exposure_required_default = "true" }
assert_match(/must be true, false, or nil/, error.message)
assert_match(/String/, error.message)
ensure
GrapeOAS.entity_exposure_required_default = nil
end
end
Loading