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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +48 -3
- data/TODO.md +112 -0
- data/UPGRADING.md +25 -0
- data/grape-swagger.gemspec +1 -1
- data/lib/grape-swagger/doc_methods/move_params.rb +3 -3
- data/lib/grape-swagger/doc_methods/operation_id.rb +6 -7
- data/lib/grape-swagger/doc_methods.rb +18 -4
- data/lib/grape-swagger/endpoint.rb +74 -24
- data/lib/grape-swagger/request_param_parsers/route.rb +81 -6
- data/lib/grape-swagger/swagger_documentation_adder.rb +72 -0
- data/lib/grape-swagger/swagger_routing.rb +79 -0
- data/lib/grape-swagger/version.rb +1 -1
- data/lib/grape-swagger.rb +8 -162
- metadata +9 -7
- data/CLAUDE.md +0 -88
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59942b4f1100e7e7f8e3bbfa1595c669eef093829848555e55928d17bc79792b
|
|
4
|
+
data.tar.gz: b9bc794755d12dec3f93981457f35c936b13f611ae833ce4c7efdc92fcc66671
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
|
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.
|
data/grape-swagger.gemspec
CHANGED
|
@@ -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
|
|
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.
|
|
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,
|
|
142
|
+
def build_body_parameter(name, body_name)
|
|
143
143
|
{}.tap do |x|
|
|
144
|
-
x[: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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
135
|
+
route.deprecated
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def security_object(route)
|
|
139
|
-
route.
|
|
139
|
+
route.security
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
def summary_object(route)
|
|
143
|
-
summary = route.
|
|
144
|
-
summary = route.description if route.description.present? && route.
|
|
145
|
-
summary = route.
|
|
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.
|
|
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
|
-
|
|
159
|
-
|
|
158
|
+
produces = route.produces
|
|
159
|
+
return ['application/octet-stream'] if file_response?(route.success) && !produces.present?
|
|
160
160
|
|
|
161
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|
|
20
|
+
version: '2.1'
|
|
21
21
|
- - "<"
|
|
22
22
|
- !ruby/object:Gem::Version
|
|
23
|
-
version: '
|
|
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
|
|
30
|
+
version: '2.1'
|
|
31
31
|
- - "<"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
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
|