Skip to content

Commit 53102be

Browse files
Add support for additional_properties
1 parent 2c56845 commit 53102be

6 files changed

Lines changed: 183 additions & 19 deletions

File tree

.rubocop_todo.yml

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2025-05-28 16:52:10 UTC using RuboCop version 1.75.8.
3+
# on 2026-06-10 02:07:47 UTC using RuboCop version 1.86.1.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
77
# versions of RuboCop, may require this file to be generated again.
88

9-
# Offense count: 8
9+
# Offense count: 10
1010
# Configuration parameters: AllowedMethods.
1111
# AllowedMethods: enums
1212
Lint/ConstantDefinitionInBlock:
@@ -24,42 +24,44 @@ Lint/EmptyBlock:
2424
- 'spec/grape-swagger/entity/attribute_parser_spec.rb'
2525

2626
# Offense count: 1
27+
# Configuration parameters: AllowComments.
2728
Lint/EmptyClass:
2829
Exclude:
2930
- 'spec/support/shared_contexts/custom_type_parser.rb'
3031

3132
# Offense count: 2
3233
# This cop supports unsafe autocorrection (--autocorrect-all).
33-
# Configuration parameters: AllowedMethods.
34+
# Configuration parameters: AllowedMethods, InferNonNilReceiver, AdditionalNilMethods.
3435
# AllowedMethods: instance_of?, kind_of?, is_a?, eql?, respond_to?, equal?
36+
# AdditionalNilMethods: present?, blank?, try, try!
3537
Lint/RedundantSafeNavigation:
3638
Exclude:
3739
- 'lib/grape-swagger/entity/attribute_parser.rb'
3840

3941
# Offense count: 4
4042
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
4143
Metrics/AbcSize:
42-
Max: 34
44+
Max: 25
4345

4446
# Offense count: 2
4547
# Configuration parameters: CountComments, CountAsOne.
4648
Metrics/ClassLength:
47-
Max: 145
49+
Max: 142
4850

49-
# Offense count: 2
51+
# Offense count: 1
5052
# Configuration parameters: AllowedMethods, AllowedPatterns.
5153
Metrics/CyclomaticComplexity:
52-
Max: 11
54+
Max: 9
5355

5456
# Offense count: 7
5557
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
5658
Metrics/MethodLength:
5759
Max: 28
5860

59-
# Offense count: 2
61+
# Offense count: 1
6062
# Configuration parameters: AllowedMethods, AllowedPatterns.
6163
Metrics/PerceivedComplexity:
62-
Max: 13
64+
Max: 9
6365

6466
# Offense count: 5
6567
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
@@ -86,28 +88,31 @@ RSpec/ContextWording:
8688
- 'spec/support/shared_contexts/inheritance_api.rb'
8789
- 'spec/support/shared_contexts/this_api.rb'
8890

89-
# Offense count: 3
91+
# Offense count: 5
9092
# Configuration parameters: IgnoredMetadata.
9193
RSpec/DescribeClass:
9294
Exclude:
9395
- '**/spec/features/**/*'
94-
- '**/spec/issues/**/*'
9596
- '**/spec/requests/**/*'
9697
- '**/spec/routing/**/*'
9798
- '**/spec/system/**/*'
9899
- '**/spec/views/**/*'
100+
- 'spec/grape-swagger/entities/additional_properties_spec.rb'
99101
- 'spec/grape-swagger/entities/response_model_spec.rb'
102+
- 'spec/issues/44_desc_in_entity_type_spec.rb'
103+
- 'spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb'
100104

101-
# Offense count: 4
105+
# Offense count: 6
102106
# Configuration parameters: CountAsOne.
103107
RSpec/ExampleLength:
104-
Max: 225
108+
Max: 221
105109

106-
# Offense count: 30
110+
# Offense count: 33
107111
RSpec/LeakyConstantDeclaration:
108112
Exclude:
109113
- 'spec/grape-swagger/entities/response_model_spec.rb'
110-
- '**/spec/issues/**/*'
114+
- 'spec/issues/44_desc_in_entity_type_spec.rb'
115+
- 'spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb'
111116
- 'spec/support/shared_contexts/inheritance_api.rb'
112117
- 'spec/support/shared_contexts/this_api.rb'
113118

@@ -116,7 +121,7 @@ RSpec/MultipleDescribes:
116121
Exclude:
117122
- 'spec/grape-swagger/entities/response_model_spec.rb'
118123

119-
# Offense count: 5
124+
# Offense count: 6
120125
RSpec/MultipleExpectations:
121126
Max: 11
122127

@@ -127,14 +132,13 @@ RSpec/NamedSubject:
127132
Exclude:
128133
- 'spec/grape-swagger/entities/response_model_spec.rb'
129134

130-
# Offense count: 46
135+
# Offense count: 57
131136
# Configuration parameters: AllowedGroups.
132137
RSpec/NestedGroups:
133138
Max: 5
134139

135140
# Offense count: 3
136-
# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata.
137-
# Include: **/*_spec.rb
141+
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata.
138142
RSpec/SpecFilePathFormat:
139143
Exclude:
140144
- '**/spec/routing/**/*'
@@ -157,3 +161,9 @@ Style/OpenStructUse:
157161
Exclude:
158162
- 'spec/grape-swagger/entities/response_model_spec.rb'
159163
- 'spec/support/shared_contexts/this_api.rb'
164+
165+
# Offense count: 1
166+
# This cop supports unsafe autocorrection (--autocorrect-all).
167+
Style/ReduceToHash:
168+
Exclude:
169+
- 'lib/grape-swagger/entity/parser.rb'

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#### Features
44

55
* Your contribution here.
6+
* [#92](https://github.com/ruby-grape/grape-swagger-entity/pull/92) Add support for `additional_properties` in entity documentation, mirroring the [grape-swagger feature](https://github.com/ruby-grape/grape-swagger#additional-properties) for request parameters - [@olivier-thatch](https://github.com/olivier-thatch).
67

78
#### Fixes
89

docs/documentation-options.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The `documentation` hash in entity exposures supports the following options:
88
|--------|-------------|---------|
99
| `type` | OpenAPI data type | `String`, `Integer`, `Boolean`, `Float`, `Date`, `DateTime` |
1010
| `is_array` | Marks field as array type | `true` / `false` |
11+
| `additional_properties` | Schema for free-form object values (maps) | `true`, `String`, `'string'`, `SomeEntity` |
1112

1213
## Description Options
1314

@@ -87,3 +88,21 @@ expose :tags, documentation: {
8788
desc: 'Associated tags'
8889
}
8990
```
91+
92+
### Additional properties (maps)
93+
94+
For object-typed fields whose values follow a single schema (e.g. a string-to-string map),
95+
use `additional_properties` to set the OpenAPI [`additionalProperties`](https://swagger.io/specification/v2/#model-with-mapdictionary-properties)
96+
key on the resulting schema.
97+
98+
```ruby
99+
# Allow any additional properties
100+
expose :metadata, documentation: { type: Hash, additional_properties: true }
101+
102+
# Allow any additional properties of a particular type (Ruby class or type name)
103+
expose :counts, documentation: { type: Hash, additional_properties: Integer }
104+
expose :tags, documentation: { type: 'object', additional_properties: 'string' }
105+
106+
# Allow any additional properties matching a defined entity
107+
expose :widgets, documentation: { type: Hash, additional_properties: WidgetEntity }
108+
```

lib/grape-swagger/entity/attribute_parser.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,29 @@ def document_data_type(documentation, data_type)
110110
values = values.call if values.is_a?(Proc)
111111
type[:enum] = values if values.is_a?(Array)
112112

113+
if documentation.key?(:additional_properties)
114+
type[:additionalProperties] = parse_additional_properties(documentation[:additional_properties])
115+
end
116+
113117
type
114118
end
115119

120+
def parse_additional_properties(value)
121+
case value
122+
when String
123+
{ type: value.to_s }
124+
when Class
125+
if direct_model_type?(value) || ambiguous_model_type?(value)
126+
name = GrapeSwagger::Entity::Helper.model_name(value, endpoint)
127+
{ '$ref' => "#/definitions/#{name}" }
128+
else
129+
{ type: GrapeSwagger::DocMethods::DataType.call(value) }
130+
end
131+
else
132+
value
133+
end
134+
end
135+
116136
def entity_model_type(name, entity_options)
117137
documentation = entity_options[:documentation]
118138

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# frozen_string_literal: true
2+
3+
module AdditionalPropertiesApi
4+
class ValueEntity < Grape::Entity
5+
expose :name, documentation: { type: 'string' }
6+
end
7+
8+
class WithAdditionalProperties < Grape::Entity
9+
expose :open_map,
10+
documentation: { type: Hash, additional_properties: true }
11+
expose :string_map,
12+
documentation: { type: Hash, additional_properties: String }
13+
expose :string_map_via_type_name,
14+
documentation: { type: 'object', additional_properties: 'string' }
15+
expose :entity_map,
16+
documentation: { type: Hash, additional_properties: ValueEntity }
17+
end
18+
end
19+
20+
describe 'additional_properties' do
21+
subject(:swagger_doc) do
22+
get '/swagger_doc'
23+
JSON.parse(last_response.body)
24+
end
25+
26+
let(:app) do
27+
Class.new(Grape::API) do
28+
namespace :additional_properties do
29+
desc 'Get a thing', success: AdditionalPropertiesApi::WithAdditionalProperties
30+
get '/thing' do
31+
present({}, with: AdditionalPropertiesApi::WithAdditionalProperties)
32+
end
33+
end
34+
35+
add_swagger_documentation format: :json
36+
end
37+
end
38+
39+
let(:properties) { swagger_doc['definitions']['AdditionalPropertiesApi_WithAdditionalProperties']['properties'] }
40+
41+
it 'documents boolean additional_properties' do
42+
expect(properties['open_map']).to eq('type' => 'object', 'additionalProperties' => true)
43+
end
44+
45+
it 'documents a primitive Ruby class as a typed schema' do
46+
expect(properties['string_map']).to eq(
47+
'type' => 'object',
48+
'additionalProperties' => { 'type' => 'string' }
49+
)
50+
end
51+
52+
it 'documents a string type name as a typed schema' do
53+
expect(properties['string_map_via_type_name']).to eq(
54+
'type' => 'object',
55+
'additionalProperties' => { 'type' => 'string' }
56+
)
57+
end
58+
59+
it 'documents an entity class as a $ref' do
60+
expect(properties['entity_map']).to eq(
61+
'type' => 'object',
62+
'additionalProperties' => { '$ref' => '#/definitions/AdditionalPropertiesApi_ValueEntity' }
63+
)
64+
end
65+
66+
it 'registers the referenced entity in definitions' do
67+
expect(swagger_doc['definitions']).to include('AdditionalPropertiesApi_ValueEntity')
68+
expect(swagger_doc['definitions']['AdditionalPropertiesApi_ValueEntity']).to include(
69+
'type' => 'object',
70+
'properties' => { 'name' => { 'type' => 'string' } }
71+
)
72+
end
73+
end

spec/grape-swagger/entity/attribute_parser_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,47 @@ def self.to_s
341341
it { is_expected.to include(type: 'object', example: example_value, default: example_value) }
342342
end
343343
end
344+
345+
context 'when additional_properties is set' do
346+
context 'when the value is true' do
347+
let(:entity_options) { { documentation: { type: Hash, additional_properties: true } } }
348+
349+
it { is_expected.to include(type: 'object', additionalProperties: true) }
350+
end
351+
352+
context 'when the value is false' do
353+
let(:entity_options) { { documentation: { type: Hash, additional_properties: false } } }
354+
355+
it { is_expected.to include(type: 'object', additionalProperties: false) }
356+
end
357+
358+
context 'when the value is a primitive class' do
359+
let(:entity_options) { { documentation: { type: Hash, additional_properties: String } } }
360+
361+
it { is_expected.to include(type: 'object', additionalProperties: { type: 'string' }) }
362+
end
363+
364+
context 'when the value is a type name string' do
365+
let(:entity_options) { { documentation: { type: 'object', additional_properties: 'string' } } }
366+
367+
it { is_expected.to include(type: 'object', additionalProperties: { type: 'string' }) }
368+
end
369+
370+
context 'when the value is an entity class' do
371+
let(:entity_options) { { documentation: { type: Hash, additional_properties: ThisApi::Entities::Tag } } }
372+
373+
it { is_expected.to include(type: 'object', additionalProperties: { '$ref' => '#/definitions/Tag' }) }
374+
end
375+
376+
context 'when combined with is_array: true' do
377+
let(:entity_options) do
378+
{ documentation: { type: 'object', is_array: true, additional_properties: 'string' } }
379+
end
380+
381+
it { is_expected.to include(type: :array) }
382+
it { is_expected.to include(items: { type: 'object', additionalProperties: { type: 'string' } }) }
383+
end
384+
end
344385
end
345386
end
346387
end

0 commit comments

Comments
 (0)