Skip to content

Commit 02d5473

Browse files
committed
Add @ext_doc_id for externalDocs in OpenAPI documents (#3028)
1 parent 16028e6 commit 02d5473

File tree

18 files changed

+158
-587
lines changed

18 files changed

+158
-587
lines changed

compiler-rs/clients_schema/src/lib.rs

+66
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ pub trait Documented {
5555
fn description(&self) -> Option<&str>;
5656
}
5757

58+
pub trait ExternalDocument {
59+
fn ext_doc_id(&self) -> Option<&str>;
60+
fn ext_doc_url(&self) -> Option<&str>;
61+
}
62+
5863
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
5964
pub struct TypeName {
6065
// Order is important for Ord implementation
@@ -314,6 +319,12 @@ pub struct Property {
314319
#[serde(skip_serializing_if = "Option::is_none")]
315320
pub doc_id: Option<String>,
316321

322+
#[serde(skip_serializing_if = "Option::is_none")]
323+
pub ext_doc_url: Option<String>,
324+
325+
#[serde(skip_serializing_if = "Option::is_none")]
326+
pub ext_doc_id: Option<String>,
327+
317328
#[serde(skip_serializing_if = "Option::is_none")]
318329
pub server_default: Option<ServerDefault>,
319330

@@ -355,6 +366,16 @@ impl Documented for Property {
355366
}
356367
}
357368

369+
impl ExternalDocument for Property {
370+
fn ext_doc_url(&self) -> Option<&str> {
371+
self.ext_doc_url.as_deref()
372+
}
373+
374+
fn ext_doc_id(&self) -> Option<&str> {
375+
self.ext_doc_id.as_deref()
376+
}
377+
}
378+
358379
#[derive(Debug, Clone, Serialize, Deserialize)]
359380
#[serde(untagged)]
360381
pub enum ServerDefault {
@@ -477,6 +498,13 @@ pub struct BaseType {
477498
#[serde(skip_serializing_if = "Option::is_none")]
478499
pub doc_id: Option<String>,
479500

501+
/// Link to public documentation
502+
#[serde(skip_serializing_if = "Option::is_none")]
503+
pub ext_doc_url: Option<String>,
504+
505+
#[serde(skip_serializing_if = "Option::is_none")]
506+
pub ext_doc_id: Option<String>,
507+
480508
#[serde(skip_serializing_if = "Option::is_none")]
481509
pub deprecation: Option<Deprecation>,
482510

@@ -512,6 +540,8 @@ impl BaseType {
512540
description: None,
513541
variant_name: None,
514542
spec_location: None,
543+
ext_doc_id: None,
544+
ext_doc_url: None,
515545
}
516546
}
517547
}
@@ -530,6 +560,16 @@ impl Documented for BaseType {
530560
}
531561
}
532562

563+
impl ExternalDocument for BaseType {
564+
fn ext_doc_url(&self) -> Option<&str> {
565+
self.ext_doc_url.as_deref()
566+
}
567+
568+
fn ext_doc_id(&self) -> Option<&str> {
569+
self.ext_doc_id.as_deref()
570+
}
571+
}
572+
533573
trait WithBaseType {
534574
fn base(&self) -> &BaseType;
535575
}
@@ -548,6 +588,16 @@ impl<T: WithBaseType> Documented for T {
548588
}
549589
}
550590

591+
impl<T: WithBaseType> ExternalDocument for T {
592+
fn ext_doc_url(&self) -> Option<&str> {
593+
self.base().doc_url()
594+
}
595+
596+
fn ext_doc_id(&self) -> Option<&str> {
597+
self.base().doc_id()
598+
}
599+
}
600+
551601
/// An interface type
552602
#[derive(Debug, Clone, Serialize, Deserialize)]
553603
#[serde(rename_all = "camelCase")]
@@ -809,6 +859,12 @@ pub struct Endpoint {
809859
#[serde(skip_serializing_if = "Option::is_none")]
810860
pub doc_id: Option<String>,
811861

862+
#[serde(skip_serializing_if = "Option::is_none")]
863+
pub ext_doc_id: Option<String>,
864+
865+
#[serde(skip_serializing_if = "Option::is_none")]
866+
pub ext_doc_url: Option<String>,
867+
812868
#[serde(skip_serializing_if = "Option::is_none")]
813869
pub deprecation: Option<Deprecation>,
814870

@@ -854,6 +910,16 @@ impl Documented for Endpoint {
854910
}
855911
}
856912

913+
impl ExternalDocument for Endpoint {
914+
fn ext_doc_url(&self) -> Option<&str> {
915+
self.ext_doc_url.as_deref()
916+
}
917+
918+
fn ext_doc_id(&self) -> Option<&str> {
919+
self.ext_doc_id.as_deref()
920+
}
921+
}
922+
857923
#[derive(Debug, Clone, Serialize, Deserialize)]
858924
#[serde(rename_all = "camelCase")]
859925
pub struct Privileges {

compiler-rs/clients_schema_to_openapi/src/paths.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,8 @@ pub fn add_endpoint(
203203
},
204204
summary: sum_desc.summary,
205205
description: sum_desc.description,
206-
// external_docs: tac.convert_external_docs(endpoint),
207-
external_docs: None, // Need values that differ from client purposes
206+
external_docs: tac.convert_external_docs(endpoint),
207+
// external_docs: None, // Need values that differ from client purposes
208208
operation_id: None, // set in clone_operation below with operation_counter
209209
parameters,
210210
request_body: request_body.clone(),

compiler-rs/clients_schema_to_openapi/src/schemas.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,9 @@ impl<'a> TypesAndComponents<'a> {
209209
self.for_body(&response.body)
210210
}
211211

212-
pub fn convert_external_docs(&self, obj: &impl clients_schema::Documented) -> Option<ExternalDocumentation> {
212+
pub fn convert_external_docs(&self, obj: &impl clients_schema::ExternalDocument) -> Option<ExternalDocumentation> {
213213
// FIXME: does the model contain resolved doc_id?
214-
obj.doc_url().map(|url| {
214+
obj.ext_doc_url().map(|url| {
215215
let branch: &str = self
216216
.model
217217
.info
Binary file not shown.

compiler-rs/openapi_to_clients_schema/src/types.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ fn generate_type_for_schema(
121121
})
122122
}
123123
if let Some(ref docs) = data.external_docs {
124-
base.doc_url = Some(docs.url.clone())
124+
base.ext_doc_url = Some(docs.ext_docs_url.clone())
125125
}
126126

127127
// TODO: data.readonly/writeonly -> OverloadOf?

compiler/src/model/build-model.ts

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export function compileEndpoints (): Record<string, model.Endpoint> {
6868
description: spec.documentation.description,
6969
docUrl: spec.documentation.url,
7070
docTag: spec.docTag,
71+
extDocUrl: spec.externalDocs?.url,
7172
// Setting these values by default should be removed
7273
// when we no longer use rest-api-spec stubs as the
7374
// source of truth for stability/visibility.

compiler/src/model/json-spec.ts

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ export interface JsonSpec {
6262
required?: boolean
6363
}
6464
docTag?: string
65+
externalDocs?: {
66+
url: string
67+
description?: string
68+
}
6569
}
6670

6771
export default function buildJsonSpec (): Map<string, JsonSpec> {

compiler/src/model/metamodel.ts

+6
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ export class Property {
126126
description?: string
127127
docUrl?: string
128128
docId?: string
129+
extDocId?: string
130+
extDocUrl?: string
129131
serverDefault?: boolean | string | number | string[] | number[]
130132
deprecation?: Deprecation
131133
availability?: Availabilities
@@ -158,6 +160,8 @@ export abstract class BaseType {
158160
/** Link to public documentation */
159161
docUrl?: string
160162
docId?: string
163+
extDocId?: string
164+
extDocUrl?: string
161165
deprecation?: Deprecation
162166
/** If this endpoint has a quirk that needs special attention, give a short explanation about it */
163167
esQuirk?: string
@@ -406,6 +410,8 @@ export class Endpoint {
406410
description: string
407411
docUrl: string
408412
docId?: string
413+
extDocId?: string
414+
extDocUrl?: string
409415
deprecation?: Deprecation
410416
availability: Availabilities
411417
docTag?: string

compiler/src/model/utils.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ export function hoistRequestAnnotations (
625625
request: model.Request, jsDocs: JSDoc[], mappings: Record<string, model.Endpoint>, response: model.TypeName | null
626626
): void {
627627
const knownRequestAnnotations = [
628-
'rest_spec_name', 'behavior', 'class_serializer', 'index_privileges', 'cluster_privileges', 'doc_id', 'availability', 'doc_tag'
628+
'rest_spec_name', 'behavior', 'class_serializer', 'index_privileges', 'cluster_privileges', 'doc_id', 'availability', 'doc_tag', 'ext_doc_id'
629629
]
630630
// in most of the cases the jsDocs comes in a single block,
631631
// but it can happen that the user defines multiple single line jsDoc.
@@ -685,6 +685,12 @@ export function hoistRequestAnnotations (
685685
const docUrl = docIds.find(entry => entry[0] === value.trim())
686686
assert(jsDocs, docUrl != null, `The @doc_id '${value.trim()}' is not present in _doc_ids/table.csv`)
687687
endpoint.docUrl = docUrl[1].replace(/\r/g, '')
688+
} else if (tag === 'ext_doc_id') {
689+
assert(jsDocs, value.trim() !== '', `Request ${request.name.name}'s @ext_doc_id cannot be empty`)
690+
endpoint.extDocId = value.trim()
691+
const docUrl = docIds.find(entry => entry[0] === value.trim())
692+
assert(jsDocs, docUrl != null, `The @ext_doc_id '${value.trim()}' is not present in _doc_ids/table.csv`)
693+
endpoint.extDocUrl = docUrl[1].replace(/\r/g, '')
688694
} else if (tag === 'availability') {
689695
// The @availability jsTag is different than most because it allows
690696
// multiple values within the same docstring, hence needing to parse
@@ -713,7 +719,7 @@ export function hoistTypeAnnotations (type: model.TypeDefinition, jsDocs: JSDoc[
713719
assert(jsDocs, jsDocs.length < 2, 'Use a single multiline jsDoc block instead of multiple single line blocks')
714720

715721
const validTags = ['class_serializer', 'doc_url', 'doc_id', 'behavior', 'variants', 'variant', 'shortcut_property',
716-
'codegen_names', 'non_exhaustive', 'es_quirk', 'behavior_meta']
722+
'codegen_names', 'non_exhaustive', 'es_quirk', 'behavior_meta', 'ext_doc_id']
717723
const tags = parseJsDocTags(jsDocs)
718724
if (jsDocs.length === 1) {
719725
const description = jsDocs[0].getDescription()
@@ -742,6 +748,12 @@ export function hoistTypeAnnotations (type: model.TypeDefinition, jsDocs: JSDoc[
742748
const docUrl = docIds.find(entry => entry[0] === value.trim())
743749
assert(jsDocs, docUrl != null, `The @doc_id '${value.trim()}' is not present in _doc_ids/table.csv`)
744750
type.docUrl = docUrl[1].replace(/\r/g, '')
751+
} else if (tag === 'ext_doc_id') {
752+
assert(jsDocs, value.trim() !== '', `Type ${type.name.namespace}.${type.name.name}'s @ext_doc_id cannot be empty`)
753+
type.extDocId = value.trim()
754+
const docUrl = docIds.find(entry => entry[0] === value.trim())
755+
assert(jsDocs, docUrl != null, `The @ext_doc_id '${value.trim()}' is not present in _doc_ids/table.csv`)
756+
type.extDocUrl = docUrl[1].replace(/\r/g, '')
745757
} else if (tag === 'codegen_names') {
746758
type.codegenNames = parseCommaSeparated(value)
747759
assert(jsDocs,
@@ -764,7 +776,7 @@ function hoistPropertyAnnotations (property: model.Property, jsDocs: JSDoc[]): v
764776
assert(jsDocs, jsDocs.length < 2, 'Use a single multiline jsDoc block instead of multiple single line blocks')
765777

766778
const validTags = ['prop_serializer', 'doc_url', 'aliases', 'codegen_name', 'server_default',
767-
'variant', 'doc_id', 'es_quirk', 'availability']
779+
'variant', 'doc_id', 'es_quirk', 'availability', 'ext_doc_id']
768780
const tags = parseJsDocTags(jsDocs)
769781
if (jsDocs.length === 1) {
770782
const description = jsDocs[0].getDescription()
@@ -808,6 +820,13 @@ function hoistPropertyAnnotations (property: model.Property, jsDocs: JSDoc[]): v
808820
if (docUrl != null) {
809821
property.docUrl = docUrl[1].replace(/\r/g, '')
810822
}
823+
} else if (tag === 'ext_doc_id') {
824+
assert(jsDocs, value.trim() !== '', `Property ${property.name}'s @ext_doc_id is cannot be empty`)
825+
property.extDocId = value
826+
const docUrl = docIds.find(entry => entry[0] === value)
827+
if (docUrl != null) {
828+
property.extDocUrl = docUrl[1].replace(/\r/g, '')
829+
}
811830
} else if (tag === 'server_default') {
812831
assert(jsDocs, property.type.kind === 'instance_of' || property.type.kind === 'union_of' || property.type.kind === 'array_of', `Default values can only be configured for instance_of or union_of types, you are using ${property.type.kind}`)
813832
assert(jsDocs, !property.required, 'Default values can only be specified on optional properties')

docs/doc-comments-guide.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export interface Request extends RequestBase {
5858
([original source code](https://github.com/elastic/elasticsearch-specification/blob/main/specification/_global/rank_eval/RankEvalRequest.ts))
5959

6060
For more information about the tags in this example (and other common tags such
61-
as `@deprecated` and `@doc_id`), refer to the [Modeling Guide](https://github.com/elastic/elasticsearch-specification/blob/main/docs/modeling-guide.md#additional-information).
61+
as `@deprecated` and `@ext_doc_id`), refer to the [Modeling Guide](https://github.com/elastic/elasticsearch-specification/blob/main/docs/modeling-guide.md#additional-information).
6262

6363
## Markup language
6464

@@ -76,9 +76,9 @@ GFM also has implementations in most languages, meaning that code generators wil
7676

7777
**Doc comments are reference material**: they should be as succinct as possible while capturing all the necessary information to use the elements they're documenting. Remember that they will often show up in small IDE autocompletion popups!
7878

79-
In particular, doc comments are not the right place for tutorials or examples, which should be in dedicated documentation pages. These pages can of course be linked from the doc comments.
79+
In particular, doc comments are not the right place for tutorials or extended examples, which should be in dedicated documentation pages. To reduce the risk of broken links, use `@ext_doc_id` to implement a link to additional documentation.
8080

81-
API endpoints will also have a `@doc_url` JSDoc tag that links to that API's detailed documentation page.
81+
API endpoints can also have `@doc_id` or `@doc_url` JSDoc tags that enable clients to link to the API docs, for example.
8282

8383
### Multi-paragraph doc comments
8484

docs/modeling-guide.md

+39-15
Original file line numberDiff line numberDiff line change
@@ -598,37 +598,61 @@ class Foo {
598598
}
599599
```
600600
601-
#### `@doc_url`
601+
#### `@doc_id`
602602
603-
The documentation url for the parameter or definition.
604-
If possible, use `@doc_id`.
603+
An identifier that can be used for generating the doc url in clients.
604+
The unique id/url pair must exist in `specification/_doc_ids/table.csv`.
605+
NOTE: This link is *not* included in the OpenAPI output.
605606
606607
```ts
607-
class Foo {
608-
bar: string
609-
/** @doc_url http://localhost:9200 */
610-
baz?: string
611-
faz: string
608+
/**
609+
* @rest_spec_name api
610+
* @doc_id foobar
611+
*/
612+
class Request {
613+
...
612614
}
613615
```
614616
615-
#### `@doc_id`
617+
```csv
618+
foobar,/guide/en/example
619+
```
620+
621+
#### `@ext_doc_id`
616622
617-
The documentation id that can be used for generating the doc url.
618-
You must add the id/url pair in `specification/_doc_ids/table.csv`.
623+
An identifier for a link.
624+
The unique id/url pair must exist in `specification/_doc_ids/table.csv`.
625+
NOTE: This link is included in the OpenAPI output.
619626
620627
```ts
621628
/**
622-
* @rest_spec_name api
623-
* @doc_id foobar
629+
* @variants container
630+
* @non_exhaustive
631+
* @ext_doc_id query-dsl
624632
*/
625-
class Request {
633+
export class QueryContainer {
626634
...
627635
}
628636
```
629637
630638
```csv
631-
foobar,/guide/en/example
639+
query-dsl,/guide/en/example
640+
```
641+
642+
643+
#### `@doc_url`
644+
645+
The documentation url for the parameter or definition.
646+
To reduce the risk of broken links, use `@doc_id` instead.
647+
NOTE: This link is *not* included in the OpenAPI output.
648+
649+
```ts
650+
class Foo {
651+
bar: string
652+
/** @doc_url http://localhost:9200 */
653+
baz?: string
654+
faz: string
655+
}
632656
```
633657
634658
#### `@doc_tag`

docs/overlays/elasticsearch-shared-overlays.yaml

-3
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,6 @@ actions:
240240
description: Add x-model and updated externalDocs for the QueryContainer object
241241
update:
242242
x-model: true
243-
externalDocs:
244-
url: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
245-
description: Query domain specific language (DSL) reference
246243
- target: "$.components['schemas']['_types.analysis:CharFilter'].oneOf"
247244
description: Remove existing oneOf definition for CharFilter
248245
remove: true

0 commit comments

Comments
 (0)