Skip to content

Commit d155441

Browse files
lornajanetatomyradamaltman
authoredOct 23, 2024
Add sorting decorators (like old Redoc had) (#38)
* adopt the tag sorting, add method sorting (no docs yet) * Add sorting decorators for properties, operatiosns and enums * Remove the operation sort, cannot separate operations within a pathItem * Add information about the available sorting options * Better formatting * Update custom-plugin-decorators/sorting/sort-methods.js Co-authored-by: Andrew Tatomyr <[email protected]> * fix a syntax problem * Adopt updated plugin format * Add configuration to the method sorting decorator * Add a rule for checking method sort order * Make the method sorting rule configurable * Add alphabetical property sorting, update README and config files * Sort out formatting for code and words * set a more expected default order for methods * Apply suggestions from code review Co-authored-by: Adam Altman <[email protected]> * Move sorting to be a top-level plugin, add index page listing --------- Co-authored-by: Andrew Tatomyr <[email protected]> Co-authored-by: Adam Altman <[email protected]>
1 parent 09608b7 commit d155441

11 files changed

+409
-0
lines changed
 

‎README.md

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ There are some fantastic examples of [configurable rules](https://redocly.com/do
5151

5252
The [custom plugin](https://redocly.com/docs/cli/custom-plugins/) is the ultimate in extensibility, but it's an advanced feature. Try these plugins for inspiration and to get you started. Rather than including the whole plugin, there are also sections for individual rules and decorators further down.
5353

54+
- [Sorting plugin](./custom-plugins/sorting) - rules to check sort order and decorators to transform an API description into the correct order. Includes sorting for tags, methods, properties and enum values.
55+
5456
#### Decorators (for custom plugins)
5557

5658
- [Tag sorting](./custom-plugin-decorators/tag-sorting) - put your tags list in alphabetical order.

‎custom-plugins/sorting/README.md

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# A suite of sorting decorators
2+
3+
Authors:
4+
5+
- [`@lornajane`](https://github.com/lornajane), Lorna Mitchell (Redocly)
6+
7+
## What this does and why
8+
9+
There are lots of reasons that you'd want to alter the order of the items in your API description, such as putting required fields first, or just ordering things alphabetically (or logically!) to make things consistent and easier to find.
10+
When Redocly only made API documentation tools, the sorting changes were made as part of the docs build.
11+
But now we make more complex tools, and many API pipelines have more than just docs in them too - so these operations are more commonly done with a decorator to transform the API description; then it can be used with any tools.
12+
13+
Redocly CLI has a [`tags-alphabetical`](https://redocly.com/docs/cli/rules/tags-alphabetical) rule to error if the `tags` section isn't in alphabetical order by `name`.
14+
This plugin adds some additional *rules* for checking sort orders.
15+
16+
- `method-sort` rule to put your methods in the desired order. The default is `["post", "patch", "put", "get", "delete"]`, but you can supply your own with an `order` parameter.
17+
- `property-sort` rule to sort properties either alphabetically with `order: alpha` (the default sort order for this rule) or with `order: required` to put the required properties first.
18+
19+
The plugin also includes *decorators* to sort your OpenAPI descriptions, perhaps to allow an existing OpenAPI description to be easily updated to meet the expectations of the sorting rules.
20+
Here's a full list of the sorting features:
21+
22+
- `methods`: sorts methods consistently in the order you supply (or `GET`, `POST`, `PUT`, `PATCH` and `DELETE` by default), with any unsorted methods appended afterwards
23+
- `enums-alphabetical`: sorts the options for an enum field alphabetically
24+
- `properties-alphabetical`: sorts object properties in schemas alphabetically
25+
- `properties-required-first`: puts the required properties at the top of the list (run this *after* any other property sorting decorators)
26+
- `tags-alphabetical`: sorts tags alphabetically
27+
28+
## Code
29+
30+
Here's the main plugin entrypoint, it's in `sorting.js`:
31+
32+
```javascript
33+
const SortTagsAlphabetically = require("./sort-tags");
34+
const SortEnumsAlphabetically = require("./sort-enums");
35+
const SortMethods = require("./sort-methods");
36+
const SortPropertiesAlphabetically = require("./sort-props-alpha");
37+
const SortPropertiesRequiredFirst = require("./sort-props-required");
38+
const RuleSortMethods = require("./rule-sort-methods");
39+
const RuleSortProps = require("./rule-sort-props");
40+
41+
module.exports = function Sorting() {
42+
return {
43+
id: "sorting",
44+
rules: {
45+
oas3: {
46+
"method-sort": RuleSortMethods,
47+
"property-sort": RuleSortProps,
48+
},
49+
},
50+
decorators: {
51+
oas3: {
52+
"tags-alphabetical": SortTagsAlphabetically,
53+
"enums-alphabetical": SortEnumsAlphabetically,
54+
methods: SortMethods,
55+
"properties-alphabetical": SortPropertiesAlphabetically,
56+
"properties-required-first": SortPropertiesRequiredFirst,
57+
},
58+
},
59+
};
60+
};
61+
```
62+
63+
Each of the available rules/decorators is in its own file, rather than copying them here, you can view them in the same directory as this `README`:
64+
65+
- [rule-sort-methods.js](./rule-sort-methods.js)
66+
- [rule-sort-props.js](./rule-sort-props.js)
67+
- [sort-tags.js](./sort-tags.js)
68+
- [sort-enums.js](./sort-enums.js)
69+
- [sort-methods.js](./sort-methods.js)
70+
- [sort-props-alpha.js](./sort-props-alpha.js)
71+
- [sort-props-required.js](./sort-props-required.js)
72+
73+
You can copy or adapt the plugins here to meet your own needs, changing the sorting algorithms or sorting different fields.
74+
One thing to look out for is that if you need to re-order the properties of an object, then you should visit the parent of the object, and assign the new object to the key that should be updated.
75+
76+
## Examples
77+
78+
Add the plugin to `redocly.yaml` and enable the decorators and/or rules:
79+
80+
```yaml
81+
plugins:
82+
- sorting.js
83+
84+
decorators:
85+
sorting/methods: on
86+
order: [delete, get]
87+
sorting/tags-alphabetical: on
88+
sorting/enums-alphabetical: on
89+
sorting/properties-alphabetical: on
90+
sorting/properties-required-first: on
91+
92+
rules:
93+
sorting/method-sort:
94+
severity: error
95+
order: [get, post, delete]
96+
sorting/property-sort:
97+
severity: warn
98+
type: required # default is alpha
99+
100+
```
101+
102+
### Lint with rules
103+
104+
Run the [lint command](https://redocly.com/docs/cli/commands/lint) and the rules are used during linting.
105+
Your command will look something like the following example:
106+
107+
```bash
108+
redocly lint openapi.yaml
109+
```
110+
111+
If your OpenAPI doesn't fulfil the criteria in the configured rules, the details of the warnings/errors are shown in the output.
112+
113+
Adjust the rule configuration or severity levels to meet your needs, and let us know if there's some other rules you'd like to see included.
114+
115+
### Bundle with decorators
116+
117+
Run the [bundle command](https://redocly.com/docs/cli/commands/bundle) and the decorators are applied during bundling.
118+
Your command will look something like the following example:
119+
120+
```bash
121+
redocly bundle openapi.yaml -o updated-openapi.yaml
122+
```
123+
124+
Use your favorite "diff" tool to look at the changes made between your existing API description and the updated version.
125+
126+
Remove or turn off any of the decorators that don't fit your use case, and let us know if there are any other sorting features you need by opening an issue on this repository.
127+
128+
## References
129+
130+
- [`tags-alphabetical' rule](https://redocly.com/docs/cli/rules/tags-alphabetical)

‎custom-plugins/sorting/redocly.yaml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
plugins:
2+
- sorting.js
3+
4+
rules:
5+
sorting/method-sort:
6+
severity: error
7+
order: [get, post, delete]
8+
sorting/property-sort:
9+
severity: warn
10+
type: required
11+
12+
decorators:
13+
sorting/properties-alphabetical: on
14+
sorting/tags-alphabetical: on
15+
sorting/methods:
16+
order: [get, post, delete]
17+
sorting/enums-alphabetical: on
18+
sorting/properties-required-first: on
19+
20+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
function RuleSortMethods({ order }) {
2+
console.log("check method order");
3+
return {
4+
PathItem: {
5+
enter(pathItem, ctx) {
6+
// default method sort order, can be changed with an "order" param in config
7+
let methodOrder = ["post", "patch", "put", "get", "delete"];
8+
if (order) {
9+
methodOrder = order;
10+
}
11+
12+
// Identify the methods that are present and put them in order
13+
const methods = Object.getOwnPropertyNames(pathItem);
14+
const expectedOrder = methodOrder.filter((item) =>
15+
methods.includes(item)
16+
);
17+
18+
i = 0;
19+
while (i < expectedOrder.length) {
20+
// if this method is in the array, it must be in the expected order
21+
if (
22+
expectedOrder.includes(methods[i]) &&
23+
methods[i] !== expectedOrder[i]
24+
) {
25+
ctx.report({
26+
message: `Unexpected method order, expected ${expectedOrder[i]} but found ${methods[i]}`,
27+
});
28+
}
29+
i++;
30+
}
31+
},
32+
},
33+
};
34+
}
35+
36+
module.exports = RuleSortMethods;
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
function RuleSortProps({ type }) {
2+
let sortType = "alpha"; // default
3+
const supportedSortTypes = ["alpha", "required"];
4+
if (type && supportedSortTypes.includes(type)) {
5+
sortType = type;
6+
}
7+
console.log(`check properties order (${sortType})`);
8+
return {
9+
Schema: {
10+
enter(schema, ctx) {
11+
if (schema.type == "object") {
12+
const propList = Object.getOwnPropertyNames(schema.properties);
13+
14+
if (sortType == "required") {
15+
// exit early if there are no required properties
16+
let requiredList = [];
17+
if (!schema.required) {
18+
return;
19+
} else {
20+
requiredList = schema.required;
21+
}
22+
23+
const notRequiredList = propList.filter(
24+
(item) => !requiredList.includes(item)
25+
);
26+
// if the notRequiredList is empty, everything was required, we can exit
27+
if (notRequiredList.length == 0) {
28+
return;
29+
}
30+
31+
// loop through, if we find an optional field before a required one then report
32+
let required = true;
33+
let prevProp = "";
34+
let prevReq = true;
35+
propList.forEach((prop) => {
36+
const isReq = requiredList.includes(prop);
37+
// did we go from an optional field to a required one?
38+
if (isReq && !prevReq) {
39+
ctx.report({
40+
message: `Unexpected property order, found required \`${prop}\` after optional \`${prevProp}\``,
41+
});
42+
}
43+
44+
// need these to refer to on the next iteration
45+
prevReq = isReq;
46+
prevProp = prop;
47+
});
48+
} else {
49+
// alpha sort is default
50+
const sortedList = [...propList].sort();
51+
52+
// use a loop so we can show exactly where the order failed for large objects
53+
let i = 0;
54+
55+
while (i < propList.length) {
56+
if (sortedList[i] !== propList[i]) {
57+
ctx.report({
58+
message: `Unexpected property order, found \`${propList[i]}\` but expected \`${sortedList[i]}\``,
59+
});
60+
return; // if one property is out of order, there might be many others, return to avoid noise
61+
}
62+
63+
i++;
64+
}
65+
}
66+
}
67+
},
68+
},
69+
};
70+
}
71+
72+
module.exports = RuleSortProps;

‎custom-plugins/sorting/sort-enums.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = SortEnumsAlphabetically;
2+
3+
function SortEnumsAlphabetically() {
4+
console.log("re-ordering enums: alphabetical");
5+
return {
6+
Schema: {
7+
leave(target) {
8+
if (target.enum) {
9+
target.enum.sort();
10+
}
11+
},
12+
},
13+
};
14+
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module.exports = SortMethods;
2+
3+
function SortMethods({ order }) {
4+
console.log("re-ordering methods");
5+
return {
6+
PathItem: {
7+
leave(pathItem) {
8+
// start with the default ordering, override with config if we have it
9+
let methodList = ["get", "post", "patch", "put", "delete"];
10+
if (order) {
11+
methodList = order;
12+
}
13+
14+
let existingMethods = Object.getOwnPropertyNames(pathItem);
15+
16+
for (const method of methodList) {
17+
const operation = pathItem[method];
18+
// For each defined operation, delete it and re-add it to the path so they will be in the correct order:
19+
if (operation) {
20+
// remove it from the methods list so we know we processed it
21+
existingMethods = existingMethods.filter((x) => x != method);
22+
delete pathItem[method];
23+
pathItem[method] = operation;
24+
}
25+
}
26+
27+
// now re-add any methods that weren't in the list
28+
for (const method of existingMethods) {
29+
const operation = pathItem[method];
30+
// Delete and re-add unprocessed operations to the path so they will be in the correct order:
31+
if (operation) {
32+
delete pathItem[method];
33+
pathItem[method] = operation;
34+
}
35+
}
36+
},
37+
},
38+
};
39+
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module.exports = SortPropertiesAlphabetically;
2+
3+
function SortPropertiesAlphabetically() {
4+
console.log("re-ordering properties: alphabetical");
5+
return {
6+
Schema: {
7+
leave(schema) {
8+
if (schema.type == "object") {
9+
const propList = Object.getOwnPropertyNames(schema.properties).sort();
10+
let newProps = {};
11+
12+
propList.forEach((prop) => {
13+
newProps[prop] = schema.properties[prop];
14+
});
15+
16+
schema.properties = newProps;
17+
}
18+
},
19+
},
20+
};
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module.exports = SortPropertiesRequiredFirst;
2+
3+
function SortPropertiesRequiredFirst() {
4+
console.log("re-ordering properties: required first");
5+
return {
6+
Schema: {
7+
leave(schema) {
8+
if (schema.type == "object") {
9+
const propList = Object.getOwnPropertyNames(schema.properties);
10+
let newProps = {};
11+
12+
if (schema.required && schema.required.length > 0) {
13+
const requiredList = schema.required;
14+
// put the required items in first
15+
requiredList.forEach((prop) => {
16+
newProps[prop] = schema.properties[prop];
17+
});
18+
19+
// now add anything that wasn't already added
20+
propList.forEach((prop) => {
21+
if (!newProps[prop]) {
22+
newProps[prop] = schema.properties[prop];
23+
}
24+
});
25+
schema.properties = newProps;
26+
}
27+
}
28+
},
29+
},
30+
};
31+
}

‎custom-plugins/sorting/sort-tags.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module.exports = SortTagsAlphabetically;
2+
3+
function SortTagsAlphabetically() {
4+
console.log("re-ordering tags: alphabetical");
5+
return {
6+
TagList: {
7+
leave(target) {
8+
target.sort((a, b) => {
9+
if (a.name < b.name) {
10+
return -1;
11+
}
12+
});
13+
},
14+
},
15+
};
16+
}

0 commit comments

Comments
 (0)
Please sign in to comment.