grape-swagger 2.1.4 → 2.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d2c2cc9aaea81baa5c7796897dd104559e7e8aa57020097fe899e35a21287cd
4
- data.tar.gz: f8451dde88f7b7af02ff90f59cdd66ed7d5d8ff34b318be91f1e2ac6767c865f
3
+ metadata.gz: 59942b4f1100e7e7f8e3bbfa1595c669eef093829848555e55928d17bc79792b
4
+ data.tar.gz: b9bc794755d12dec3f93981457f35c936b13f611ae833ce4c7efdc92fcc66671
5
5
  SHA512:
6
- metadata.gz: a627ebc160edef37753bbfe3c049ece8426195d3a1d08a26a4fef7f6f2a0b8f0a49b250be74cca0d096e277c46a041c56a6f4e336ccf1aa0f09ec5a9037f6edb
7
- data.tar.gz: 2ebf30ff9123454a77151a48a3f55f4d301a15e984c0dea2fd8ee92867a254d28581546796e629cbd94bb362e0902e4c80574b88be01b75df625c95bd7b0324c
6
+ metadata.gz: 3cea699e55ab2b291446d84652a38bdd549824e2aa83012c1bd100334039c2e867b5729e160beac988186e13d9b9389159fd1845e2355e0ac84fa5adbe68c692
7
+ data.tar.gz: cd4cb51496aa6dafedc34dd12f1fc549c33f190e4128265792cecc41f2811870d9251834c24b43afabd4301da378c8ddacbf6cfe26c7f51833a1e5d7cf8eeddf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ### 2.2.0 (2026-08-06)
2
+
3
+ #### Features
4
+
5
+ * [#973](https://github.com/ruby-grape/grape-swagger/pull/973): Support custom model descriptions and examples - [@numbata](https://github.com/numbata).
6
+ * [#976](https://github.com/ruby-grape/grape-swagger/pull/976): Ruby 3.4 and refactor swagger documentation modules; deprecate top-level `SwaggerRouting` and `SwaggerDocumentationAdder` aliases in favor of `GrapeSwagger::...` - [@moskvin](https://github.com/moskvin).
7
+
8
+ #### Fixes
9
+
10
+ * [#978](https://github.com/ruby-grape/grape-swagger/pull/978): Fix Grape 3.2+ compatibility: desc kwargs, custom types, multi-type param recovery; bump Grape to `>= 2.1, < 5.0`. See [UPGRADING](UPGRADING.md) - [@numbata](https://github.com/numbata).
11
+ * [#982](https://github.com/ruby-grape/grape-swagger/pull/982): Fix test suite compatibility with Grape 4.0 (grape=HEAD CI) - [@numbata](https://github.com/numbata).
12
+ * [#981](https://github.com/ruby-grape/grape-swagger/pull/981): Use `endpoint.endpoints` instead of `endpoint.options[:app]` - [@ericproulx](https://github.com/ericproulx).
13
+ * [#985](https://github.com/ruby-grape/grape-swagger/pull/985): Fix the test suite against unreleased Grape 4.0: give collection custom types a `parse`, and stop building `Grape::Util::StackableValues` directly now that it is a read-only view. See [UPGRADING](UPGRADING.md) - [@ericproulx](https://github.com/ericproulx).
14
+ * [#983](https://github.com/ruby-grape/grape-swagger/pull/983): Read route metadata via reader methods instead of `route.options[...]` - [@ericproulx](https://github.com/ericproulx).
15
+ * [#984](https://github.com/ruby-grape/grape-swagger/pull/984): Drop the undocumented `formats:` / `content_types:` aliases for `produces:` - [@ericproulx](https://github.com/ericproulx).
16
+
1
17
  ### 2.1.4 (2026-02-02)
2
18
 
3
19
  #### Features
data/README.md CHANGED
@@ -76,6 +76,7 @@
76
76
  - [Multiple present Response ](#multiple-present-response-)
77
77
  - [Using Grape Entities ](#using-grape-entities-)
78
78
  - [Documented class/definition](#documented-classdefinition)
79
+ - [Custom Model Description and Example](#custom-model-description-and-example)
79
80
  - [Relationships](#relationships)
80
81
  - [1xN](#1xn)
81
82
  - [1x1](#1x1)
@@ -114,7 +115,7 @@ This screenshot is based on the [Hussars](https://github.com/LeFnord/hussars) sa
114
115
  The following versions of grape, grape-entity and grape-swagger can currently be used together.
115
116
 
116
117
  | grape-swagger | swagger spec | grape | grape-entity | representable |
117
- | --------------------- | ------------ | ----------------------- | ------------ | ------------- |
118
+ |-----------------------|--------------|-------------------------|--------------|---------------|
118
119
  | 0.10.5 | 1.2 | >= 0.10.0 ... <= 0.14.0 | < 0.5.0 | n/a |
119
120
  | 0.11.0 | 1.2 | >= 0.16.2 | < 0.5.0 | n/a |
120
121
  | 0.25.2 | 2.0 | >= 0.14.0 ... <= 0.18.0 | <= 0.6.0 | >= 2.4.1 |
@@ -123,9 +124,9 @@ The following versions of grape, grape-entity and grape-swagger can currently be
123
124
  | 0.32.0 | 2.0 | >= 0.16.2 | >= 0.5.0 | >= 2.4.1 |
124
125
  | 0.34.0 | 2.0 | >= 0.16.2 ... < 1.3.0 | >= 0.5.0 | >= 2.4.1 |
125
126
  | >= 1.0.0 | 2.0 | >= 1.3.0 | >= 0.5.0 | >= 2.4.1 |
126
- | >= 2.0.0 | 2.0 | >= 1.7.0 | >= 0.5.0 | >= 2.4.1 |
127
127
  | >= 2.0.0 ... <= 2.1.2 | 2.0 | >= 1.8.0 ... < 2.3.0 | >= 0.5.0 | >= 2.4.1 |
128
- | > 2.1.2 | 2.0 | >= 1.8.0 ... < 4.0 | >= 0.5.0 | >= 2.4.1 |
128
+ | >= 2.1.3 ... < 2.2.0 | 2.0 | >= 1.8.0 ... < 4.0 | >= 0.5.0 | >= 2.4.1 |
129
+ | >= 2.2.0 | 2.0 | >= 2.1 ... < 5.0 | >= 0.5.0 | >= 2.4.1 |
129
130
 
130
131
 
131
132
  ## Swagger-Spec <a name="swagger-spec"></a>
@@ -498,6 +499,8 @@ add_swagger_documentation \
498
499
  api_documentation: { desc: 'Reticulated splines API swagger-compatible documentation.' }
499
500
  ```
500
501
 
502
+ `:description` is accepted as an alias for `:desc` (when both are supplied, `:desc` wins; an explicit `desc: nil` is respected and does not fall through). String keys (e.g. when loading from YAML/JSON) are accepted too.
503
+
501
504
  #### specific_api_documentation
502
505
 
503
506
  Customize the Swagger API specific documentation route, typically contains a `desc` field. The default description is "Swagger compatible API description for specific API".
@@ -1611,6 +1614,48 @@ Should generate the following definitions in your swagger json:
1611
1614
  }
1612
1615
  ```
1613
1616
 
1617
+ ### Custom Model Description and Example
1618
+
1619
+ By default, a model's `description` in the generated swagger definition falls back to `"#{ModelName} model"`, and no `example` is set. You can override both by defining a `self.documentation` class method on the entity that returns a hash with `:desc` and/or `:example` keys. This is separate from field-level `documentation:` options passed to `expose`, and does not affect them:
1620
+
1621
+ ```ruby
1622
+ module API
1623
+ module Entities
1624
+ class User < Grape::Entity
1625
+ def self.documentation
1626
+ {
1627
+ desc: 'Represents a user account',
1628
+ example: { id: 1, name: 'John Doe', email: 'john@example.com' }
1629
+ }
1630
+ end
1631
+
1632
+ expose :id, documentation: { type: 'integer' }
1633
+ expose :name, documentation: { type: 'string' }
1634
+ expose :email, documentation: { type: 'string' }
1635
+ end
1636
+ end
1637
+ end
1638
+ ```
1639
+
1640
+ Should generate the following definition in your swagger json:
1641
+
1642
+ ```json
1643
+ {
1644
+ "definitions": {
1645
+ "API_Entities_User": {
1646
+ "type": "object",
1647
+ "description": "Represents a user account",
1648
+ "example": { "id": 1, "name": "John Doe", "email": "john@example.com" },
1649
+ "properties": {
1650
+ "id": { "type": "integer" },
1651
+ "name": { "type": "string" },
1652
+ "email": { "type": "string" }
1653
+ }
1654
+ }
1655
+ }
1656
+ }
1657
+ ```
1658
+
1614
1659
  ### Relationships
1615
1660
 
1616
1661
  You may safely omit `type` from relationships, as it can be inferred. However, if you need to specify or override it, use the full name of the class leaving out any modules named `Entities` or `Entity`.
data/TODO.md ADDED
@@ -0,0 +1,112 @@
1
+ # grape-swagger Task Board (updated 2025-11-22)
2
+
3
+ ## Priority Cards (Top 30) — ordered by quickest/highest impact first
4
+ - **[#957] array_use_braces drops params** (PR)
5
+ Impact: missing body params; fix ready.
6
+ Next: replace suffix-strip hack with safer builder; add body/query array regressions.
7
+ - **[#952] array_use_braces omission** (Issue)
8
+ Impact: same as #957.
9
+ Next: close via #957; ensure shared params still rendered.
10
+ - **[#950] Array default on items** (Issue)
11
+ Impact: produces nested arrays; small fix.
12
+ Next: place default at array level; add fixture.
13
+ - **[#964] Arrays of primitives in responses** (PR)
14
+ Impact: response schemas wrong; PR ready.
15
+ Next: add response schema tests (OAS2/3); merge.
16
+ - **[#905] Float ranges** (PR)
17
+ Impact: rejects float ranges; PR small.
18
+ Next: review, add float range specs (inclusive/exclusive), merge if green.
19
+ - **[#885] Skip examples for non-schema objects** (PR)
20
+ Impact: invalid examples; easy merge.
21
+ Next: add coverage; merge, then close #884.
22
+ - **[#884] Don’t document examples for non-schema params** (Issue)
23
+ Impact: invalid spec.
24
+ Next: close via #885 after verify.
25
+ - **[#741] `[]=` nil crash** (Issue)
26
+ Impact: doc generation crashes; small guard.
27
+ Next: nil-guard map creation; add failing fixture.
28
+ - **[#400] Params lost between desc + params** (Issue)
29
+ Impact: silent param loss; narrow scope.
30
+ Next: merge params with precedence; coverage.
31
+ - **[#552] base_path duplicated in paths** (Issue)
32
+ Impact: invalid paths; localized change.
33
+ Next: normalize path building; regression test.
34
+ - **[#921] Route ordering wrong** (Issue)
35
+ Impact: collisions/incorrect ops; contained.
36
+ Next: deterministic sort (path, verb) respecting mounts; snapshot test.
37
+ - **[#959] Proc/lambda defaults** (PR)
38
+ Impact: shows proc string; PR ready.
39
+ Next: evaluate callable during doc build; add per-request test to avoid caching.
40
+ - **[#666] Array of entities in body** (Issue)
41
+ Impact: incorrect schema; medium effort.
42
+ Next: render collection as array `$ref`; add request-body test.
43
+ - **[#657] Entity as POST body breaks docs** (Issue)
44
+ Impact: common pattern fails; medium.
45
+ Next: respect entity exposure for body; add spec.
46
+ - **[#725] Singular Array[String] body wrong** (Issue)
47
+ Impact: wrong client contract; similar area.
48
+ Next: fix param parser for singular names; regression spec.
49
+ - **[#617] Array of Hash fails** (Issue)
50
+ Impact: common shape; medium.
51
+ Next: ensure nested hash arrays map to schema; JSON schema test.
52
+ - **[#795] Handle DELETE endpoints** (PR)
53
+ Impact: DELETE docs incorrect; PR exists.
54
+ Next: rebase; add DELETE request-body tests; merge.
55
+ - **[#735] Spec for #730** (PR)
56
+ Impact: needed failing test; quick rebase.
57
+ Next: keep red until #730 fixed, then merge.
58
+ - **[#730] Mounted rack app not detected** (Issue)
59
+ Impact: missing routes; linked to #735.
60
+ Next: traverse mounted rack apps in route collection; add fixture app.
61
+ - **[#573] accept_version_header?** (Issue)
62
+ Impact: versioned APIs under-documented; scoped.
63
+ Next: detect header-based versioning, include in info/paths; rack test.
64
+ - **[#690] `additionalProperties` support** (Issue)
65
+ Impact: JSON schema parity; targeted change.
66
+ Next: add `additional_properties` option; schema test.
67
+ - **[#875] Override entity description** (Issue)
68
+ Impact: docs mismatch language; small API addition.
69
+ Next: allow `model_name/description` override; unit spec.
70
+ - **[#877] Search routes containing swagger_doc** (PR)
71
+ Impact: missing mounts/perf; PR present.
72
+ Next: rebase, perf check, merge if behavior same.
73
+ - **[#767] target_class option broken** (Issue)
74
+ Impact: entity resolution fails on Grape >=1.2.5.
75
+ Next: reproduce on latest; restore lookup; regression test.
76
+ - **[#603] Bundle Swagger UI 3** (Issue)
77
+ Impact: shipped UI outdated; medium-sized.
78
+ Next: upgrade assets to UI 3.x, verify with OAS2/3 outputs, rack smoke test.
79
+ - **[#928] OpenAPI 3 support** (PR, WIP) ✅ **IMPLEMENTED**
80
+ Impact: modern tooling gap; large.
81
+ Status: **Complete** - OpenAPI 3.0 and 3.1 support implemented via `openapi_version` option.
82
+ - **[#919] "Support OAS3?"** (Issue) ✅ **IMPLEMENTED**
83
+ Impact: user pressure; closes after #928/#603.
84
+ Status: **Complete** - Use `add_swagger_documentation(openapi_version: '3.0')` or `'3.1'`.
85
+ - **[#967] Support Grape ~> 3.0** (Issue)
86
+ Impact: blocks upgrades; larger effort.
87
+ Next: loosen gemspec (<4.0), appraisals on Grape 3, fix breaks, ship 2.1.x/3.0.0.
88
+ - **[#452] Hook to intercept generated doc** (Issue)
89
+ Impact: customization pain; design work.
90
+ Next: expose pre-render hook; document.
91
+ - **[#414] Nested namespace not exposed** (Issue)
92
+ Impact: missing routes; may piggyback on namespace sprint.
93
+ Next: flatten nested mounts; add spec.
94
+
95
+ ## Duplicates / Closures
96
+ - #957 (PR) fixes #952 (Issue).
97
+ - #885 (PR) fixes #884 (Issue).
98
+ - #928 (PR) + #919 (Issue) – close #919 when OAS3 ships.
99
+ - #879 (PR) vs #878 (Issue) – pick path parsing solution; close other.
100
+ - #936 (PR) vs #935 (Issue) – choose approach for base_path; merge or supersede.
101
+
102
+ ## Opportunistic Batches (work together)
103
+ - **Path parsing sprint** – #879 #874 #873 #878 #831 #830. Unify route parsing for optional/wildcard/implicit segments; verify swagger params.
104
+ - **Base path & formatting sprint** – #936 #935 #870 #400. Normalize base_path/filenames; make path_string request-aware; ensure desc+params merge.
105
+ - **Presentation/visibility sprint** – #882 #881 #838 #867 #866 #828 #812. Fix examples, consumes for file params, global headers, filters, response-only visibility.
106
+ - **Metadata & tags sprint** – #700 #677 #668 #689 #820 #623. Wire mime types, consumes/produces, header params, custom tags, pattern support, default consumes.
107
+ - **Versioning & namespace sprint** – #694 #682 #552 #414 #644. Consistent version handling (header/path); avoid base_path duplication; flatten namespaces.
108
+ - **Arrays/body edge sprint** – #792 #628 #626 #612 #611 #699 #640. Correct array typing across verbs/namespaces; required/optional propagation; nested resource rendering.
109
+ - **Hidden/visibility/cache sprint** – #766 #755 #781 #857. Stronger visibility toggles; doc cache invalidation after entity changes.
110
+ - **Customization hooks & naming sprint** – #549 #517 #465 #452 #519 #652. Pre-render hook, i18n, naming conventions, helper access.
111
+ - **Schema fidelity sprint** – #690 #581 #615 #480 #614. additionalProperties, nested hash/array correctness, JSON types, nil-guard crashers.
112
+ - **UX/doc cleanup sprint** – #754 #865 #897 + other “how-to” tickets. Resolve via documentation or close as answered.
data/UPGRADING.md CHANGED
@@ -1,5 +1,30 @@
1
1
  ## Upgrading Grape-swagger
2
2
 
3
+ ### Upgrading to >= 2.2.0
4
+
5
+ - **Minimum Grape version is now `>= 2.1`** (was `>= 1.7`). Grape 1.8.0 and 2.0.0 cannot be used on Ruby 3.3+ because of an upstream Mustermann/forwardable incompatibility; the CI rows for those combinations were already failing on `master` and have been removed.
6
+ - **`SwaggerRouting` and `SwaggerDocumentationAdder` are now also namespaced under `GrapeSwagger::`**. The top-level constants remain as deprecated compatibility aliases for now and are planned for removal in grape-swagger 3.0; prefer `GrapeSwagger::SwaggerRouting` and `GrapeSwagger::SwaggerDocumentationAdder` in downstream code.
7
+ - **`type: 'Object'` (and other string type names) in `params` blocks**: Grape 3.2+ rejects string type names. If you previously declared a swagger-only documentation hint via `params { optional :foo, type: 'Object' }`, move the type under `documentation:`:
8
+
9
+ ```ruby
10
+ optional :foo, documentation: { type: 'Object' }
11
+ ```
12
+
13
+ grape-swagger picks the type up from the merged settings unchanged, so the swagger output is identical.
14
+ - **Custom type classes** used via `type: MyClass` must implement `MyClass.parse(value)` (arity 1) on Grape 3.2+; otherwise Grape's dry-types lookup raises `ArgumentError`. `Grape::Entity` already provides `parse`; `Representable::Decorator` and plain Ruby classes need to define it explicitly:
15
+
16
+ ```ruby
17
+ class MyType
18
+ def self.parse(value) = new(value)
19
+ # ...
20
+ end
21
+ ```
22
+
23
+ On **Grape 4.0** the same requirement extends to collections: `type: Array[MyClass]` and `type: Set[MyClass]` now raise the same `ArgumentError` while the `params` block is evaluated, i.e. as the API class loads. Grape used to build the element coercer lazily, on the first request that supplied the parameter, so a documentation-only collection type was never coerced and never raised — until it was, as a confusing per-request `400 "is invalid"`. See [ruby-grape/grape#2817](https://github.com/ruby-grape/grape/pull/2817) and Grape's UPGRADING. The fix is the same `parse` — note that `parse(val) = val` performs no validation and is only appropriate when the type is documentation-only and the route never actually coerces client input; otherwise implement real coercion or supply `coerce_with:`, or drop `type:` and declare the type under `documentation:` if it only describes the parameter.
24
+
25
+ - **Multi-type params (`type: [A, B]`) on Grape 3.2+**: swagger output now reflects the first declared type (e.g. `type: [Integer, Float]` produces `"integer"`). Previously, Grape 3.2+ serialized the `VariantCollectionCoercer` wrapper via `#to_s`, leaking `"#<Grape::Validations::Types::VariantCollectionCoercer:0x...>"` into the documentation. No action required, but if you were programmatically post-processing that string, the fix will change the output.
26
+ - **`desc(..., formats:)` and `desc(..., content_types:)` no longer set the swagger `produces` field.** These were undocumented aliases for `produces:`. Use `produces:` instead (e.g. `desc 'x', produces: ['application/xml']`), which is unchanged.
27
+
3
28
  ### Upgrading to >= x.y.z
4
29
 
5
30
  - Grape-swagger now documents array parameters within an object schema in Swagger. This aligns with grape's JSON structure requirements and ensures the documentation is correct.
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.metadata['rubygems_mfa_required'] = 'true'
16
16
 
17
17
  s.required_ruby_version = '>= 3.1'
18
- s.add_dependency 'grape', '>= 1.7', '< 4.0'
18
+ s.add_dependency 'grape', '>= 2.1', '< 5.0'
19
19
 
20
20
  s.files = Dir['lib/**/*', '*.md', 'LICENSE.txt', 'grape-swagger.gemspec']
21
21
  s.require_paths = ['lib']
@@ -34,7 +34,7 @@ module GrapeSwagger
34
34
 
35
35
  definition[:description] = route.description if route.try(:description)
36
36
 
37
- build_body_parameter(definition_name, route.options)
37
+ build_body_parameter(definition_name, route.body_name)
38
38
  end
39
39
 
40
40
  def move_params_to_new(definition, params)
@@ -139,9 +139,9 @@ module GrapeSwagger
139
139
  definition[:required].push(*value)
140
140
  end
141
141
 
142
- def build_body_parameter(name, options)
142
+ def build_body_parameter(name, body_name)
143
143
  {}.tap do |x|
144
- x[:name] = options[:body_name] || name
144
+ x[:name] = body_name || name
145
145
  x[:in] = 'body'
146
146
  x[:required] = true
147
147
  x[:schema] = { '$ref' => "#/definitions/#{name}" }
@@ -5,13 +5,12 @@ module GrapeSwagger
5
5
  class OperationId
6
6
  class << self
7
7
  def build(route, path = nil)
8
- if route.options[:nickname]
9
- route.options[:nickname]
10
- else
11
- verb = route.request_method.to_s.downcase
12
- operation = manipulate(path) unless path.nil?
13
- "#{verb}#{operation}"
14
- end
8
+ nickname = route.nickname
9
+ return nickname if nickname
10
+
11
+ verb = route.request_method.to_s.downcase
12
+ operation = manipulate(path) unless path.nil?
13
+ "#{verb}#{operation}"
15
14
  end
16
15
 
17
16
  def manipulate(path)
@@ -86,14 +86,16 @@ module GrapeSwagger
86
86
  # for available options see #defaults
87
87
  target_class = options[:target_class]
88
88
  guard = options[:swagger_endpoint_guard]
89
- api_doc = options[:api_documentation].dup
90
- specific_api_doc = options[:specific_api_documentation].dup
89
+ # transform_keys normalizes string-keyed input, e.g. loaded from YAML/JSON.
90
+ api_doc = (options[:api_documentation] || {}).transform_keys(&:to_sym)
91
+ specific_api_doc = (options[:specific_api_documentation] || {}).transform_keys(&:to_sym)
91
92
 
92
93
  class_variables_from(options)
93
94
 
94
95
  setup_formatter(options[:format])
95
96
 
96
- desc api_doc.delete(:desc), api_doc
97
+ # Only the named-resource endpoint extracts :params for its required route param below.
98
+ desc(pop_desc(api_doc), **api_doc)
97
99
 
98
100
  instance_eval(guard) unless guard.nil?
99
101
 
@@ -105,7 +107,9 @@ module GrapeSwagger
105
107
  .output_path_definitions(target_class.combined_namespace_routes, self, target_class, options)
106
108
  end
107
109
 
108
- desc specific_api_doc.delete(:desc), { params: specific_api_doc.delete(:params) || {}, **specific_api_doc }
110
+ specific_desc = pop_desc(specific_api_doc)
111
+ specific_params = specific_api_doc.delete(:params) || {}
112
+ desc(specific_desc, params: specific_params, **specific_api_doc)
109
113
 
110
114
  params do
111
115
  requires :name, type: String, desc: 'Resource name of mounted API'
@@ -136,5 +140,15 @@ module GrapeSwagger
136
140
 
137
141
  FORMATTER_METHOD.each { |method| send(method, formatter) }
138
142
  end
143
+
144
+ private
145
+
146
+ # explicit nil under :desc wins — don't fall through to :description
147
+ def pop_desc(doc)
148
+ result = doc.key?(:desc) ? doc.delete(:desc) : doc.delete(:description)
149
+ # Also remove the alias so it does not leak into **doc kwargs.
150
+ doc.delete(:description)
151
+ result
152
+ end
139
153
  end
140
154
  end
@@ -100,7 +100,7 @@ module Grape
100
100
  next if hidden?(route, options)
101
101
 
102
102
  @item, path = GrapeSwagger::DocMethods::PathString.build(route, options)
103
- @entity = route.entity || route.options[:success]
103
+ @entity = route.entity || route.success
104
104
 
105
105
  verb, method_object = method_object(route, options, path)
106
106
 
@@ -123,7 +123,7 @@ module Grape
123
123
  method[:parameters] = params_object(route, options, path, method[:consumes])
124
124
  method[:security] = security_object(route)
125
125
  method[:responses] = response_object(route, options)
126
- method[:tags] = route.options.fetch(:tags, tag_object(route, path))
126
+ method[:tags] = route.options.key?(:tags) ? route.tags : tag_object(route, path)
127
127
  method[:operationId] = GrapeSwagger::DocMethods::OperationId.build(route, path)
128
128
  method[:deprecated] = deprecated_object(route)
129
129
  method.delete_if { |_, value| value.nil? }
@@ -132,40 +132,34 @@ module Grape
132
132
  end
133
133
 
134
134
  def deprecated_object(route)
135
- route.options[:deprecated] if route.options.key?(:deprecated)
135
+ route.deprecated
136
136
  end
137
137
 
138
138
  def security_object(route)
139
- route.options[:security] if route.options.key?(:security)
139
+ route.security
140
140
  end
141
141
 
142
142
  def summary_object(route)
143
- summary = route.options[:desc] if route.options.key?(:desc)
144
- summary = route.description if route.description.present? && route.options.key?(:detail)
145
- summary = route.options[:summary] if route.options.key?(:summary)
143
+ summary = route.desc if route.desc
144
+ summary = route.description if route.description.present? && route.detail
145
+ summary = route.summary if route.summary
146
146
 
147
147
  summary
148
148
  end
149
149
 
150
150
  def description_object(route)
151
151
  description = route.description if route.description.present?
152
- description = route.options[:detail] if route.options.key?(:detail)
152
+ description = route.detail if route.detail
153
153
 
154
154
  description
155
155
  end
156
156
 
157
157
  def produces_object(route, format)
158
- return ['application/octet-stream'] if file_response?(route.options[:success]) &&
159
- !route.options[:produces].present?
158
+ produces = route.produces
159
+ return ['application/octet-stream'] if file_response?(route.success) && !produces.present?
160
160
 
161
- mime_types = GrapeSwagger::DocMethods::ProducesConsumes.call(format)
162
-
163
- route_mime_types = %i[formats content_types produces].map do |producer|
164
- possible = route.options[producer]
165
- GrapeSwagger::DocMethods::ProducesConsumes.call(possible) if possible.present?
166
- end.flatten.compact.uniq
167
-
168
- route_mime_types.present? ? route_mime_types : mime_types
161
+ route_mime_types = GrapeSwagger::DocMethods::ProducesConsumes.call(produces) if produces.present?
162
+ route_mime_types.presence || GrapeSwagger::DocMethods::ProducesConsumes.call(format)
169
163
  end
170
164
 
171
165
  SUPPORTS_CONSUMES = %i[post put patch].freeze
@@ -218,7 +212,8 @@ module Grape
218
212
  next unless @definitions[response_model]
219
213
  next if response_model.start_with?('Swagger_doc')
220
214
 
221
- @definitions[response_model][:description] ||= "#{response_model} model"
215
+ model_for_documentation = value[:model].is_a?(String) ? value[:model].constantize : value[:model]
216
+ apply_model_documentation(response_model, model_for_documentation)
222
217
  build_memo_schema(memo, route, value, response_model, options)
223
218
  memo[value[:code]][:examples] = value[:examples] if value[:examples]
224
219
  end
@@ -240,7 +235,7 @@ module Grape
240
235
  route.http_codes.clone
241
236
  else
242
237
  success_codes_from_route(route) + default_code_from_route(route) +
243
- (route.http_codes || route.options[:failure] || [])
238
+ (route.http_codes || route.failure || [])
244
239
  end
245
240
  end
246
241
 
@@ -268,7 +263,7 @@ module Grape
268
263
  private
269
264
 
270
265
  def default_code_from_route(route)
271
- entity = route.options[:default_response]
266
+ entity = route.default_response
272
267
  return [] if entity.nil?
273
268
 
274
269
  default_code = { code: 'default', message: 'Default Response' }
@@ -345,7 +340,7 @@ module Grape
345
340
 
346
341
  if value.key?(:as) && value.key?(:is_array)
347
342
  reference[value[:as]] = build_reference_array(reference[value[:as]])
348
- elsif route.options[:is_array]
343
+ elsif route.is_array
349
344
  reference = build_reference_array(reference)
350
345
  end
351
346
 
@@ -362,7 +357,7 @@ module Grape
362
357
 
363
358
  def build_root(route, reference, response_model, settings)
364
359
  default_root = response_model.underscore
365
- default_root = default_root.pluralize if route.options[:is_array]
360
+ default_root = default_root.pluralize if route.is_array
366
361
  case route.settings.dig(:swagger, :root)
367
362
  when true
368
363
  { type: 'object', properties: { default_root => reference } }
@@ -437,7 +432,8 @@ module Grape
437
432
 
438
433
  def hidden?(route, options)
439
434
  route_hidden = route.settings.try(:[], :swagger).try(:[], :hidden)
440
- route_hidden = route.options[:hidden] if route.options.key?(:hidden)
435
+ hidden_val = route.hidden
436
+ route_hidden = hidden_val unless hidden_val.nil?
441
437
  return route_hidden unless route_hidden.is_a?(Proc)
442
438
 
443
439
  return route_hidden.call unless options[:token_owner]
@@ -456,6 +452,60 @@ module Grape
456
452
  end
457
453
  end
458
454
 
455
+ def apply_model_documentation(response_model, model)
456
+ doc = model_documentation(model)
457
+ @definitions[response_model][:description] ||= model_description(doc) || "#{response_model} model"
458
+ example = model_example(doc)
459
+ @definitions[response_model][:example] ||= example unless example.nil?
460
+ end
461
+
462
+ def model_description(doc)
463
+ desc = doc[:desc] if doc
464
+ desc if desc.is_a?(String)
465
+ end
466
+
467
+ def model_example(doc)
468
+ doc[:example] if doc
469
+ end
470
+
471
+ def model_documentation(model)
472
+ return unless model.respond_to?(:documentation)
473
+ return if defined?(Grape::Entity) && model.method(:documentation).owner == Grape::Entity.singleton_class
474
+
475
+ doc = begin
476
+ model.documentation
477
+ rescue StandardError
478
+ nil
479
+ end
480
+ return unless doc.is_a?(Hash)
481
+ return unless (doc.keys - %i[desc example]).empty?
482
+ return if field_documentation?(doc[:example])
483
+
484
+ doc
485
+ end
486
+
487
+ def field_documentation?(value)
488
+ return false unless value.is_a?(Hash)
489
+ return false unless (value.keys - documentation_keys).empty?
490
+ return true unless value[:type]
491
+
492
+ documentation_type?(value[:type])
493
+ end
494
+
495
+ def documentation_keys
496
+ %i[
497
+ collectionFormat default desc description documentation format hidden
498
+ in is_array param_type required type values
499
+ ]
500
+ end
501
+
502
+ def documentation_type?(value)
503
+ return true if value.is_a?(Class) || value.is_a?(Module) || value.is_a?(Symbol)
504
+ return false unless value.is_a?(String)
505
+
506
+ %w[array boolean file integer link number object string text].include?(value.downcase)
507
+ end
508
+
459
509
  def success_code_from_entity(route, entity)
460
510
  default_code = GrapeSwagger::DocMethods::StatusCodes.get[route.request_method.downcase.to_sym]
461
511
  if entity.is_a?(Hash)
@@ -19,8 +19,9 @@ module GrapeSwagger
19
19
  stackable_values = route.app&.inheritable_setting&.namespace_stackable
20
20
 
21
21
  path_params = build_path_params(stackable_values)
22
+ variant_types = collect_variant_types(stackable_values)
22
23
 
23
- fulfill_params(path_params)
24
+ fulfill_params(path_params, variant_types)
24
25
  end
25
26
 
26
27
  private
@@ -29,25 +30,90 @@ module GrapeSwagger
29
30
  params = {}
30
31
 
31
32
  while stackable_values.is_a?(Grape::Util::StackableValues)
32
- params.merge!(fetch_inherited_params(stackable_values))
33
+ params = merge_path_params(fetch_inherited_params(stackable_values), params)
33
34
  stackable_values = stackable_values.inherited_values
34
35
  end
35
36
 
36
37
  params
37
38
  end
38
39
 
40
+ def merge_path_params(outer_params, inner_params)
41
+ outer_params.merge(inner_params) do |_key, outer_options, inner_options|
42
+ merge_path_param_options(outer_options, inner_options)
43
+ end
44
+ end
45
+
46
+ def merge_path_param_options(outer_options, inner_options)
47
+ return inner_options unless outer_options.is_a?(Hash) && inner_options.is_a?(Hash)
48
+
49
+ outer_options.merge(inner_options) do |_key, outer_value, inner_value|
50
+ merge_path_param_options(outer_value, inner_value)
51
+ end
52
+ end
53
+
39
54
  def fetch_inherited_params(stackable_values)
40
55
  return {} unless stackable_values.new_values
41
56
 
42
57
  namespaces = stackable_values.new_values[:namespace] || []
43
58
 
44
59
  namespaces.each_with_object({}) do |namespace, params|
45
- space = namespace.space.to_s.gsub(':', '')
46
- params[space] = namespace.options || {}
60
+ space = namespace.space.to_s.delete_prefix(':')
61
+ params[space.to_sym] = namespace.options || {}
62
+ end
63
+ end
64
+
65
+ # Grape 3.2+ serializes `type: [A, B]` via VariantCollectionCoercer#to_s, losing the type list.
66
+ # Grape 3.2+ stores validator metadata as Hash entries; older supported versions use
67
+ # CoerceValidator object instances and require private-ivar reads below.
68
+ # If the internal structure changes in a future Grape version this silently returns {}.
69
+ def collect_variant_types(stackable_values)
70
+ variant_types = {}
71
+ return variant_types unless defined?(Grape::Validations::Types::VariantCollectionCoercer) &&
72
+ defined?(Grape::Validations::Validators::CoerceValidator) &&
73
+ stackable_values.respond_to?(:[])
74
+
75
+ # StackableValues#[] concatenates this level and all inherited levels;
76
+ # no explicit chain walk is needed here.
77
+ (stackable_values[:validations] || []).each do |validator|
78
+ attrs, scope, converter = extract_variant_validator_parts(validator)
79
+ next unless attrs
80
+ next unless converter.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
81
+
82
+ # TODO: use a public API once Grape exposes VariantCollectionCoercer#types.
83
+ types = converter.instance_variable_get(:@types).to_a
84
+ next if types.empty?
85
+
86
+ next unless scope.respond_to?(:full_name)
87
+
88
+ attrs.each do |attr|
89
+ # Key format must match param.to_s in restore_variant_type.
90
+ variant_types[scope.full_name(attr)] = types
91
+ end
47
92
  end
93
+
94
+ variant_types
95
+ end
96
+
97
+ def extract_variant_validator_parts(validator)
98
+ if validator.is_a?(Hash)
99
+ return unless validator[:validator_class] == Grape::Validations::Validators::CoerceValidator
100
+
101
+ attrs = Array(validator[:attributes])
102
+ scope = validator[:params_scope]
103
+ converter = validator[:options].is_a?(Hash) ? validator[:options][:type] : nil
104
+ return [attrs, scope, converter]
105
+ end
106
+
107
+ return unless validator.is_a?(Grape::Validations::Validators::CoerceValidator)
108
+ return unless validator.respond_to?(:attrs)
109
+
110
+ attrs = Array(validator.attrs)
111
+ scope = validator.instance_variable_get(:@scope)
112
+ converter = validator.instance_variable_get(:@converter)
113
+ [attrs, scope, converter]
48
114
  end
49
115
 
50
- def fulfill_params(path_params)
116
+ def fulfill_params(path_params, variant_types)
51
117
  # Merge path params options into route params
52
118
  route.params.each_with_object({}) do |(param, definition), accum|
53
119
  # The route.params hash includes both parametrized params (with a string as a key)
@@ -57,10 +123,19 @@ module GrapeSwagger
57
123
  next if param.is_a?(String) && accum.key?(key)
58
124
 
59
125
  defined_options = definition.is_a?(Hash) ? definition : {}
60
- value = (path_params[param] || {}).merge(defined_options)
126
+ defined_options = restore_variant_type(defined_options, param, variant_types)
127
+ path_options = path_params[key] || {}
128
+ value = path_options.merge(defined_options)
61
129
  accum[key] = value.empty? ? DEFAULT_PARAM_TYPE : value
62
130
  end
63
131
  end
132
+
133
+ def restore_variant_type(defined_options, param, variant_types)
134
+ types = variant_types[param.to_s]
135
+ return defined_options unless types
136
+
137
+ defined_options.merge(type: types)
138
+ end
64
139
  end
65
140
  end
66
141
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'swagger_routing'
4
+
5
+ module GrapeSwagger
6
+ module SwaggerDocumentationAdder
7
+ attr_accessor :combined_namespaces, :combined_routes, :combined_namespace_routes
8
+
9
+ include GrapeSwagger::SwaggerRouting
10
+
11
+ def add_swagger_documentation(options = {})
12
+ documentation_class = create_documentation_class
13
+
14
+ version_for(options)
15
+ options = { target_class: self }.merge(options)
16
+ @target_class = options[:target_class]
17
+ auth_wrapper = options[:endpoint_auth_wrapper] || Class.new
18
+
19
+ use auth_wrapper if auth_wrapper.method_defined?(:before) && !middleware.flatten.include?(auth_wrapper)
20
+
21
+ documentation_class.setup(options)
22
+ mount(documentation_class)
23
+
24
+ combined_routes = combine_routes(@target_class, documentation_class)
25
+ combined_namespaces = combine_namespaces(@target_class)
26
+ combined_namespace_routes = combine_namespace_routes(combined_namespaces, combined_routes)
27
+ exclusive_route_keys = combined_routes.keys - combined_namespaces.keys
28
+ @target_class.combined_namespace_routes = combined_namespace_routes.merge(
29
+ combined_routes.slice(*exclusive_route_keys)
30
+ )
31
+ @target_class.combined_routes = combined_routes
32
+ @target_class.combined_namespaces = combined_namespaces
33
+
34
+ documentation_class
35
+ end
36
+
37
+ private
38
+
39
+ def version_for(options)
40
+ options[:version] = version if version
41
+ end
42
+
43
+ def combine_namespaces(app)
44
+ combined_namespaces = {}
45
+ endpoints = app.endpoints.dup
46
+
47
+ while endpoints.any?
48
+ endpoint = endpoints.shift
49
+
50
+ nested_endpoints = endpoint.endpoints
51
+ endpoints.push(*nested_endpoints) if nested_endpoints
52
+ namespace_stackable = endpoint.inheritable_setting.namespace_stackable
53
+ ns = (namespace_stackable[:namespace] || []).last
54
+ next unless ns
55
+
56
+ # use the full namespace here (not the latest level only)
57
+ # and strip leading slash
58
+ mount_path = (namespace_stackable[:mount_path] || []).join('/')
59
+ full_namespace = (mount_path + endpoint.namespace).gsub(/\/{2,}/, '/').sub(/^\//, '')
60
+ combined_namespaces[full_namespace] = ns
61
+ end
62
+
63
+ combined_namespaces
64
+ end
65
+
66
+ def create_documentation_class
67
+ Class.new(GrapeInstance) do
68
+ extend GrapeSwagger::DocMethods
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrapeSwagger
4
+ module SwaggerRouting
5
+ private
6
+
7
+ def combine_routes(app, doc_klass)
8
+ app.routes.each_with_object({}) do |route, combined_routes|
9
+ route_path = route.path
10
+ route_match = route_path.split(/^.*?#{Regexp.escape(route.prefix.to_s)}/).last
11
+ next unless route_match
12
+
13
+ route_match = route_match.match('\/([\p{Alnum}\-\_]*?)[\.\/\(]') || route_match.match('\/([\p{Alpha}\-\_]*)$')
14
+ next unless route_match
15
+
16
+ resource = route_match.captures.first
17
+ resource = '/' if resource.empty?
18
+ combined_routes[resource] ||= []
19
+ if doc_klass.hide_documentation_path && route.path.match(/#{Regexp.escape(doc_klass.mount_path)}($|\/|\(\.)/)
20
+ next
21
+ end
22
+
23
+ combined_routes[resource] << route
24
+ end
25
+ end
26
+
27
+ def determine_namespaced_routes(name, parent_route, routes)
28
+ return routes.values.flatten if parent_route.nil?
29
+
30
+ parent_route.select do |route|
31
+ route_path_start_with?(route, name) || route_namespace_equals?(route, name)
32
+ end
33
+ end
34
+
35
+ def combine_namespace_routes(namespaces, routes)
36
+ combined_namespace_routes = {}
37
+ standalone_namespaces = namespaces.select { |_, ns| ns.options.dig(:swagger, :nested) == false }
38
+
39
+ namespaces.each_key do |name|
40
+ parent_route_name = extract_parent_route(name)
41
+ parent_route = routes[parent_route_name]
42
+ namespace_routes = determine_namespaced_routes(name, parent_route, routes)
43
+
44
+ parent_standalone_namespaces = standalone_namespaces.select do |ns_name, _|
45
+ name == ns_name || name.start_with?("#{ns_name}/")
46
+ end
47
+ if parent_standalone_namespaces.empty?
48
+ combined_namespace_routes[parent_route_name] ||= []
49
+ combined_namespace_routes[parent_route_name].push(*namespace_routes)
50
+ end
51
+ end
52
+
53
+ combined_namespace_routes
54
+ end
55
+
56
+ def extract_parent_route(name)
57
+ route_name = name.match(%r{^/?([^/]*).*$})[1]
58
+ return route_name unless route_name.include? ':'
59
+
60
+ matches = name.match(/\/\p{Alpha}+/)
61
+ matches.nil? ? route_name : matches[0].delete('/')
62
+ end
63
+
64
+ def route_namespace_equals?(route, name)
65
+ ["/#{name}", "/:version/#{name}"].any? { |p| route.namespace == p }
66
+ end
67
+
68
+ def route_path_start_with?(route, name)
69
+ # String#start_with? is a literal prefix check, so Regexp.escape is not needed here.
70
+ patterns = if route.prefix.to_s.empty?
71
+ ["/#{name}", "/:version/#{name}"]
72
+ else
73
+ ["/#{route.prefix}/#{name}", "/#{route.prefix}/:version/#{name}"]
74
+ end
75
+
76
+ patterns.any? { |p| route.path.start_with?(p) }
77
+ end
78
+ end
79
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GrapeSwagger
4
- VERSION = '2.1.4'
4
+ VERSION = '2.2.0'
5
5
  end
data/lib/grape-swagger.rb CHANGED
@@ -11,6 +11,8 @@ require 'grape-swagger/errors'
11
11
  require 'grape-swagger/doc_methods'
12
12
  require 'grape-swagger/model_parsers'
13
13
  require 'grape-swagger/request_param_parser_registry'
14
+ require 'grape-swagger/swagger_routing'
15
+ require 'grape-swagger/swagger_documentation_adder'
14
16
  require 'grape-swagger/token_owner_resolver'
15
17
 
16
18
  module GrapeSwagger
@@ -44,166 +46,10 @@ module GrapeSwagger
44
46
  }.freeze
45
47
  end
46
48
 
47
- module SwaggerRouting
48
- private
49
+ # Temporary compatibility aliases for downstream code that still references
50
+ # the pre-namespace constants directly.
51
+ SwaggerRouting = GrapeSwagger::SwaggerRouting
52
+ SwaggerDocumentationAdder = GrapeSwagger::SwaggerDocumentationAdder
53
+ Object.send(:deprecate_constant, :SwaggerRouting, :SwaggerDocumentationAdder)
49
54
 
50
- def combine_routes(app, doc_klass)
51
- app.routes.each_with_object({}) do |route, combined_routes|
52
- route_path = route.path
53
- route_match = route_path.split(/^.*?#{route.prefix}/).last
54
- next unless route_match
55
-
56
- # want to match emojis … ;)
57
- # route_match = route_match
58
- # .match('\/([\p{Alnum}p{Emoji}\-\_]*?)[\.\/\(]') || route_match.match('\/([\p{Alpha}\p{Emoji}\-\_]*)$')
59
- route_match = route_match.match('\/([\p{Alnum}\-\_]*?)[\.\/\(]') || route_match.match('\/([\p{Alpha}\-\_]*)$')
60
- next unless route_match
61
-
62
- resource = route_match.captures.first
63
- resource = '/' if resource.empty?
64
- combined_routes[resource] ||= []
65
- next if doc_klass.hide_documentation_path && route.path.match(/#{doc_klass.mount_path}($|\/|\(\.)/)
66
-
67
- combined_routes[resource] << route
68
- end
69
- end
70
-
71
- def determine_namespaced_routes(name, parent_route, routes)
72
- return routes.values.flatten if parent_route.nil?
73
-
74
- parent_route.select do |route|
75
- route_path_start_with?(route, name) || route_namespace_equals?(route, name)
76
- end
77
- end
78
-
79
- def combine_namespace_routes(namespaces, routes)
80
- combined_namespace_routes = {}
81
- # iterate over each single namespace
82
- namespaces.each_key do |name, _|
83
- # get the parent route for the namespace
84
- parent_route_name = extract_parent_route(name)
85
- parent_route = routes[parent_route_name]
86
- # fetch all routes that are within the current namespace
87
- namespace_routes = determine_namespaced_routes(name, parent_route, routes)
88
-
89
- # default case when not explicitly specified or nested == true
90
- standalone_namespaces = namespaces.reject do |_, ns|
91
- !ns.options.key?(:swagger) ||
92
- !ns.options[:swagger].key?(:nested) ||
93
- ns.options[:swagger][:nested] != false
94
- end
95
-
96
- parent_standalone_namespaces = standalone_namespaces.select { |ns_name, _| name.start_with?(ns_name) }
97
- # add only to the main route
98
- # if the namespace is not within any other namespace appearing as standalone resource
99
- # rubocop:disable Style/Next
100
- if parent_standalone_namespaces.empty?
101
- # default option, append namespace methods to parent route
102
- combined_namespace_routes[parent_route_name] ||= []
103
- combined_namespace_routes[parent_route_name].push(*namespace_routes)
104
- end
105
- # rubocop:enable Style/Next
106
- end
107
-
108
- combined_namespace_routes
109
- end
110
-
111
- def extract_parent_route(name)
112
- route_name = name.match(%r{^/?([^/]*).*$})[1]
113
- return route_name unless route_name.include? ':'
114
-
115
- matches = name.match(/\/\p{Alpha}+/)
116
- matches.nil? ? route_name : matches[0].delete('/')
117
- end
118
-
119
- def route_namespace_equals?(route, name)
120
- patterns = Enumerator.new do |yielder|
121
- yielder << "/#{name}"
122
- yielder << "/:version/#{name}"
123
- end
124
-
125
- patterns.any? { |p| route.namespace == p }
126
- end
127
-
128
- def route_path_start_with?(route, name)
129
- patterns = Enumerator.new do |yielder|
130
- if route.prefix
131
- yielder << "/#{route.prefix}/#{name}"
132
- yielder << "/#{route.prefix}/:version/#{name}"
133
- else
134
- yielder << "/#{name}"
135
- yielder << "/:version/#{name}"
136
- end
137
- end
138
-
139
- patterns.any? { |p| route.path.start_with?(p) }
140
- end
141
- end
142
-
143
- module SwaggerDocumentationAdder
144
- attr_accessor :combined_namespaces, :combined_routes, :combined_namespace_routes
145
-
146
- include SwaggerRouting
147
-
148
- def add_swagger_documentation(options = {})
149
- documentation_class = create_documentation_class
150
-
151
- version_for(options)
152
- options = { target_class: self }.merge(options)
153
- @target_class = options[:target_class]
154
- auth_wrapper = options[:endpoint_auth_wrapper] || Class.new
155
-
156
- use auth_wrapper if auth_wrapper.method_defined?(:before) && !middleware.flatten.include?(auth_wrapper)
157
-
158
- documentation_class.setup(options)
159
- mount(documentation_class)
160
-
161
- combined_routes = combine_routes(@target_class, documentation_class)
162
- combined_namespaces = combine_namespaces(@target_class)
163
- combined_namespace_routes = combine_namespace_routes(combined_namespaces, combined_routes)
164
- exclusive_route_keys = combined_routes.keys - combined_namespaces.keys
165
- @target_class.combined_namespace_routes = combined_namespace_routes.merge(
166
- combined_routes.slice(*exclusive_route_keys)
167
- )
168
- @target_class.combined_routes = combined_routes
169
- @target_class.combined_namespaces = combined_namespaces
170
-
171
- documentation_class
172
- end
173
-
174
- private
175
-
176
- def version_for(options)
177
- options[:version] = version if version
178
- end
179
-
180
- def combine_namespaces(app)
181
- combined_namespaces = {}
182
- endpoints = app.endpoints.clone
183
-
184
- while endpoints.any?
185
- endpoint = endpoints.shift
186
-
187
- endpoints.push(*endpoint.options[:app].endpoints) if endpoint.options[:app]
188
- namespace_stackable = endpoint.inheritable_setting.namespace_stackable
189
- ns = (namespace_stackable[:namespace] || []).last
190
- next unless ns
191
-
192
- # use the full namespace here (not the latest level only)
193
- # and strip leading slash
194
- mount_path = (namespace_stackable[:mount_path] || []).join('/')
195
- full_namespace = (mount_path + endpoint.namespace).sub(/\/{2,}/, '/').sub(/^\//, '')
196
- combined_namespaces[full_namespace] = ns
197
- end
198
-
199
- combined_namespaces
200
- end
201
-
202
- def create_documentation_class
203
- Class.new(GrapeInstance) do
204
- extend GrapeSwagger::DocMethods
205
- end
206
- end
207
- end
208
-
209
- GrapeInstance.extend(SwaggerDocumentationAdder)
55
+ GrapeInstance.extend(GrapeSwagger::SwaggerDocumentationAdder)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape-swagger
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.4
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LeFnord
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-02-03 00:00:00.000000000 Z
12
+ date: 2026-08-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: grape
@@ -17,20 +17,20 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '1.7'
20
+ version: '2.1'
21
21
  - - "<"
22
22
  - !ruby/object:Gem::Version
23
- version: '4.0'
23
+ version: '5.0'
24
24
  type: :runtime
25
25
  prerelease: false
26
26
  version_requirements: !ruby/object:Gem::Requirement
27
27
  requirements:
28
28
  - - ">="
29
29
  - !ruby/object:Gem::Version
30
- version: '1.7'
30
+ version: '2.1'
31
31
  - - "<"
32
32
  - !ruby/object:Gem::Version
33
- version: '4.0'
33
+ version: '5.0'
34
34
  description:
35
35
  email:
36
36
  - pscholz.le@gmail.com
@@ -40,11 +40,11 @@ extensions: []
40
40
  extra_rdoc_files: []
41
41
  files:
42
42
  - CHANGELOG.md
43
- - CLAUDE.md
44
43
  - CONTRIBUTING.md
45
44
  - LICENSE.txt
46
45
  - README.md
47
46
  - RELEASING.md
47
+ - TODO.md
48
48
  - UPGRADING.md
49
49
  - grape-swagger.gemspec
50
50
  - lib/grape-swagger.rb
@@ -71,6 +71,8 @@ files:
71
71
  - lib/grape-swagger/request_param_parsers/body.rb
72
72
  - lib/grape-swagger/request_param_parsers/headers.rb
73
73
  - lib/grape-swagger/request_param_parsers/route.rb
74
+ - lib/grape-swagger/swagger_documentation_adder.rb
75
+ - lib/grape-swagger/swagger_routing.rb
74
76
  - lib/grape-swagger/token_owner_resolver.rb
75
77
  - lib/grape-swagger/version.rb
76
78
  homepage: https://github.com/ruby-grape/grape-swagger
data/CLAUDE.md DELETED
@@ -1,88 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- grape-swagger is a Ruby gem that auto-generates Swagger 2.0 documentation for Grape APIs. It extends Grape APIs with `add_swagger_documentation` to register documentation endpoints that output OpenAPI/Swagger-compliant JSON.
8
-
9
- **Key dependencies:** Grape >= 1.7 (supports up to 3.x), Ruby >= 3.1
10
-
11
- ## Common Commands
12
-
13
- ```bash
14
- # Install dependencies
15
- bundle install
16
-
17
- # Run all tests + RuboCop
18
- bundle exec rake
19
-
20
- # Run tests only
21
- bundle exec rspec
22
-
23
- # Run a single test file
24
- bundle exec rspec spec/swagger_v2/api_swagger_v2_spec.rb
25
-
26
- # Run a specific test by line number
27
- bundle exec rspec spec/swagger_v2/api_swagger_v2_spec.rb:42
28
-
29
- # Run RuboCop linting
30
- bundle exec rubocop
31
-
32
- # Test with different model parsers
33
- MODEL_PARSER=grape-swagger-entity bundle exec rspec
34
- MODEL_PARSER=grape-swagger-representable bundle exec rspec
35
-
36
- # Test with a specific Grape version
37
- GRAPE_VERSION=2.2.0 bundle update && bundle exec rspec
38
- ```
39
-
40
- ## Architecture
41
-
42
- ### Core Extension Flow
43
-
44
- 1. **Entry point:** `lib/grape-swagger.rb` - Extends `GrapeInstance` with `SwaggerDocumentationAdder` mixin
45
- 2. **Endpoint generation:** `lib/grape-swagger/endpoint.rb` - Extends `Grape::Endpoint` to build Swagger objects from route definitions
46
- 3. **Documentation helpers:** `lib/grape-swagger/doc_methods.rb` - Central module for generating paths, definitions, tags
47
-
48
- ### Pluggable Systems
49
-
50
- **Model Parsers** (`lib/grape-swagger/model_parsers.rb`):
51
- - Registry for handling different entity types (Grape::Entity, representable, custom)
52
- - Access via `GrapeSwagger.model_parsers`
53
- - Supports `insert_before` and `insert_after` for ordering
54
-
55
- **Request Param Parsers** (`lib/grape-swagger/request_param_parser_registry.rb`):
56
- - Three default parsers: Headers, Route, Body (in `request_param_parsers/`)
57
- - Access via `GrapeSwagger.request_param_parsers`
58
-
59
- ### Key Modules
60
-
61
- - `SwaggerDocumentationAdder` - Adds `add_swagger_documentation` method to Grape APIs
62
- - `SwaggerRouting` - Combines routes by resource path, handles namespace routing
63
- - `GrapeSwagger::DocMethods` - Helpers in `doc_methods/` subdirectory for specific documentation tasks
64
-
65
- ## Testing Patterns
66
-
67
- - Tests use `Rack::Test::Methods` for HTTP testing
68
- - Define an `app` method returning a `Grape::API` subclass in specs
69
- - Use shared contexts like `include_context "#{MODEL_PARSER} swagger example"` for model parser testing
70
- - Tests run with random order (seed: 40834)
71
- - `MODEL_PARSER` env var controls which parser to test (mock, entity, representable)
72
-
73
- ## Code Style
74
-
75
- - Always include `# frozen_string_literal: true` at file start
76
- - Max line length: 120 characters
77
- - RuboCop enforced with some rules relaxed in `.rubocop_todo.yml`
78
- - Naming cops disabled; Style cops mostly disabled
79
- - Spec files excluded from most length/complexity checks
80
-
81
- ## Contributing Workflow
82
-
83
- 1. Create feature branch from master
84
- 2. Write tests first (add to `spec/`)
85
- 3. Implement feature
86
- 4. Run `bundle exec rake` (must pass)
87
- 5. Add entry to CHANGELOG.md under *Next Release*
88
- 6. Submit PR