Spring 数据剩余:更改 OpenAPI 规范中的操作 ID
Spring Data Rest: Change Operation ID in OpenAPI Specification
我正在尝试为我的 Spring Data Rest 服务生成一个 openapi.yaml,这样我们就可以使用 typescript-angular 生成器轻松生成客户端代码。不幸的是,生成的服务和方法的名称……不太理想。我们为实体、“搜索”和关系获得了不同的控制器。此外,生成的服务中的函数名称非常长,没有添加太多信息/好处。这是一个例子:
paths:
/pricingPlans:
get:
tags:
- pricing-plan-entity-controller
description: get-pricingplan
operationId: getCollectionResource-pricingplan-get_1
有了这个 openapi.yaml,我们得到一个 PricingPlanEntityControllerService
和一个函数 getCollectionResource-pricingplan-get_1
,这太荒谬了。我们想将其更改为 PricingPlanService
和 getAll
.
@Tag(name = "pricing-plan")
@CrossOrigin
public interface PricingPlanRepo extends CrudRepository<PricingPlan, UUID> {
@Override
Iterable<PricingPlan> findAll();
通过在 class 级别添加 @Tag(name = "pricing-plan")
,我们能够将生成的服务的名称更改为 PricingPlanService
,但无论我们尝试什么,operationId
一如既往
我希望 @Operation(operationId = "getAll")
做我们想做的事,但正如我所说:被忽略了。使用 Spring Data Rest 应用所有这些注释的正确方法是什么?
请注意,您使用 @Operation
注释自定义操作 ID 的方法不适用于大多数 spring-data-rest 存储库:原因是操作由框架内部生成,您无法添加注释。
适用于所有情况的简单方法是使用 OpenApiCustomiser
来更改生成的 OpenAPI 规范的任何部分的值,如 documentation 中所述。
@Bean
OpenApiCustomiser operationIdCustomiser() {
return openApi -> openApi.getPaths().values().stream().flatMap(pathItem -> pathItem.readOperations().stream())
.forEach(operation -> {
if ("id-to-change".equals(operation.getOperationId()))
operation.setOperationId("any id you want ...");
});
}
@mimi78 向我指出了如何自定义生成的 OpenAPI 规范。感谢那!我关心的是简单地添加 1:1 操作 ID 翻译的方法,因为 internal/original 名称可能会随着端点的添加或删除而改变。我想出了一个从路径模式(例如 /products/{id}/vendor
)和 HTTP 方法生成操作 ID 的解决方案。我认为这应该提供人类可读的稳定命名,并且更适合基于操作 ID 编写代码的客户端生成器。
我想分享这个解决方案,以防有一天其他人需要它:
@Configuration
public class OperationIdCustomizer {
@Bean
public OpenApiCustomiser operationIdCustomiser() {
// @formatter:off
return openApi -> openApi.getPaths().entrySet().stream()
.forEach(entry -> {
String path = entry.getKey();
PathItem pathItem = entry.getValue();
if (pathItem.getGet() != null)
pathItem.getGet().setOperationId(OperationIdGenerator.convert("get", path));
if (pathItem.getPost() != null)
pathItem.getPost().setOperationId(OperationIdGenerator.convert("post", path));
if (pathItem.getPut() != null)
pathItem.getPut().setOperationId(OperationIdGenerator.convert("put", path));
if (pathItem.getPatch() != null)
pathItem.getPatch().setOperationId(OperationIdGenerator.convert("patch", path));
if (pathItem.getDelete() != null)
pathItem.getDelete().setOperationId(OperationIdGenerator.convert("delete", path));
});
// @formatter:on
}
}
public class OperationIdGenerator {
private static String pattern1 = "^/([a-zA-Z]+)$"; // /products
private static String pattern2 = "^/([a-zA-Z]+)/(\{[a-zA-Z]+\})$"; // /products/{id}
private static String pattern3 = "^/([a-zA-Z]+)/(\{[a-zA-Z]+\})/([a-zA-Z]+)$"; // /products/{id}/vendor
private static String pattern4 = "^/([a-zA-Z]+)/(\{[a-zA-Z]+\})/([a-zA-Z]+)/(\{[a-zA-Z]+\})$"; // /products/{id}/vendor/{propertyId}
private static String pattern5 = "^/([a-zA-Z]+)/search/([a-zA-Z]+)$"; // /products/search/findByVendor
// @formatter:off
private static Map<String, String> httpMethodVerb = Map.of(
"get", "get",
"post", "create",
"put", "replace",
"patch", "update",
"delete", "delete");
// @formatter:on
private static String handlePattern1(String op, String path) {
Pattern r = Pattern.compile(pattern1);
Matcher m = r.matcher(path);
boolean found = m.find();
if (!found)
return null;
String noun = toCamelCase(m.group(1));
String verb = getVerb(op);
if (verb.equals("create"))
noun = singularize(noun);
return verb + noun;
}
private static String handlePattern2(String op, String path) {
Pattern r = Pattern.compile(pattern2);
Matcher m = r.matcher(path);
boolean found = m.find();
if (!found)
return null;
String noun = toCamelCase(singularize(m.group(1)));
return getVerb(op) + noun;
}
private static String handlePattern3(String op, String path) {
Pattern r = Pattern.compile(pattern3);
Matcher m = r.matcher(path);
boolean found = m.find();
if (!found)
return null;
String noun = toCamelCase(singularize(m.group(1)));
String relation = toCamelCase(m.group(3));
return op + noun + relation;
}
private static String handlePattern4(String op, String path) {
Pattern r = Pattern.compile(pattern4);
Matcher m = r.matcher(path);
boolean found = m.find();
if (!found)
return null;
String entity = toCamelCase(singularize(m.group(1)));
String relation = m.group(3);
return getVerb(op) + entity + toCamelCase(singularize(relation)) + "ById";
}
private static String handlePattern5(String op, String path) {
Pattern r = Pattern.compile(pattern5);
Matcher m = r.matcher(path);
boolean found = m.find();
if (!found)
return null;
String entity = toCamelCase(m.group(1));
String searchMethod = m.group(2);
r = Pattern.compile("findBy([a-zA-Z0-9]+)");
m = r.matcher(searchMethod);
if (m.find())
return "search" + entity + "By" + toCamelCase(m.group(1));
return "search" + entity + "By" + toCamelCase(searchMethod);
}
public static String singularize(String word) {
Inflector i = new Inflector();
return i.singularize(word);
}
public static boolean isSingular(String word) {
Inflector i = new Inflector();
return i.singularize(word).equals(word);
}
public static String getVerb(String op) {
return httpMethodVerb.get(op);
}
public static String convert(String op, String path) {
String result = handlePattern1(op, path);
if (result == null) {
result = handlePattern2(op, path);
if (result == null) {
result = handlePattern3(op, path);
if (result == null) {
result = handlePattern4(op, path);
if (result == null) {
result = handlePattern5(op, path);
}
}
}
}
return result;
}
private static String toCamelCase(String phrase) {
List<String> words = new ArrayList<>();
for (String word : phrase.split("_"))
words.add(word);
for (int i = 0; i < words.size(); i++) {
String word = words.get(i);
String firstLetter = word.substring(0, 1).toUpperCase();
word = firstLetter + word.substring(1);
words.set(i, word);
}
return String.join("", words.toArray(new String[words.size()]));
}
}
我正在尝试为我的 Spring Data Rest 服务生成一个 openapi.yaml,这样我们就可以使用 typescript-angular 生成器轻松生成客户端代码。不幸的是,生成的服务和方法的名称……不太理想。我们为实体、“搜索”和关系获得了不同的控制器。此外,生成的服务中的函数名称非常长,没有添加太多信息/好处。这是一个例子:
paths:
/pricingPlans:
get:
tags:
- pricing-plan-entity-controller
description: get-pricingplan
operationId: getCollectionResource-pricingplan-get_1
有了这个 openapi.yaml,我们得到一个 PricingPlanEntityControllerService
和一个函数 getCollectionResource-pricingplan-get_1
,这太荒谬了。我们想将其更改为 PricingPlanService
和 getAll
.
@Tag(name = "pricing-plan")
@CrossOrigin
public interface PricingPlanRepo extends CrudRepository<PricingPlan, UUID> {
@Override
Iterable<PricingPlan> findAll();
通过在 class 级别添加 @Tag(name = "pricing-plan")
,我们能够将生成的服务的名称更改为 PricingPlanService
,但无论我们尝试什么,operationId
一如既往
我希望 @Operation(operationId = "getAll")
做我们想做的事,但正如我所说:被忽略了。使用 Spring Data Rest 应用所有这些注释的正确方法是什么?
请注意,您使用 @Operation
注释自定义操作 ID 的方法不适用于大多数 spring-data-rest 存储库:原因是操作由框架内部生成,您无法添加注释。
适用于所有情况的简单方法是使用 OpenApiCustomiser
来更改生成的 OpenAPI 规范的任何部分的值,如 documentation 中所述。
@Bean
OpenApiCustomiser operationIdCustomiser() {
return openApi -> openApi.getPaths().values().stream().flatMap(pathItem -> pathItem.readOperations().stream())
.forEach(operation -> {
if ("id-to-change".equals(operation.getOperationId()))
operation.setOperationId("any id you want ...");
});
}
@mimi78 向我指出了如何自定义生成的 OpenAPI 规范。感谢那!我关心的是简单地添加 1:1 操作 ID 翻译的方法,因为 internal/original 名称可能会随着端点的添加或删除而改变。我想出了一个从路径模式(例如 /products/{id}/vendor
)和 HTTP 方法生成操作 ID 的解决方案。我认为这应该提供人类可读的稳定命名,并且更适合基于操作 ID 编写代码的客户端生成器。
我想分享这个解决方案,以防有一天其他人需要它:
@Configuration
public class OperationIdCustomizer {
@Bean
public OpenApiCustomiser operationIdCustomiser() {
// @formatter:off
return openApi -> openApi.getPaths().entrySet().stream()
.forEach(entry -> {
String path = entry.getKey();
PathItem pathItem = entry.getValue();
if (pathItem.getGet() != null)
pathItem.getGet().setOperationId(OperationIdGenerator.convert("get", path));
if (pathItem.getPost() != null)
pathItem.getPost().setOperationId(OperationIdGenerator.convert("post", path));
if (pathItem.getPut() != null)
pathItem.getPut().setOperationId(OperationIdGenerator.convert("put", path));
if (pathItem.getPatch() != null)
pathItem.getPatch().setOperationId(OperationIdGenerator.convert("patch", path));
if (pathItem.getDelete() != null)
pathItem.getDelete().setOperationId(OperationIdGenerator.convert("delete", path));
});
// @formatter:on
}
}
public class OperationIdGenerator {
private static String pattern1 = "^/([a-zA-Z]+)$"; // /products
private static String pattern2 = "^/([a-zA-Z]+)/(\{[a-zA-Z]+\})$"; // /products/{id}
private static String pattern3 = "^/([a-zA-Z]+)/(\{[a-zA-Z]+\})/([a-zA-Z]+)$"; // /products/{id}/vendor
private static String pattern4 = "^/([a-zA-Z]+)/(\{[a-zA-Z]+\})/([a-zA-Z]+)/(\{[a-zA-Z]+\})$"; // /products/{id}/vendor/{propertyId}
private static String pattern5 = "^/([a-zA-Z]+)/search/([a-zA-Z]+)$"; // /products/search/findByVendor
// @formatter:off
private static Map<String, String> httpMethodVerb = Map.of(
"get", "get",
"post", "create",
"put", "replace",
"patch", "update",
"delete", "delete");
// @formatter:on
private static String handlePattern1(String op, String path) {
Pattern r = Pattern.compile(pattern1);
Matcher m = r.matcher(path);
boolean found = m.find();
if (!found)
return null;
String noun = toCamelCase(m.group(1));
String verb = getVerb(op);
if (verb.equals("create"))
noun = singularize(noun);
return verb + noun;
}
private static String handlePattern2(String op, String path) {
Pattern r = Pattern.compile(pattern2);
Matcher m = r.matcher(path);
boolean found = m.find();
if (!found)
return null;
String noun = toCamelCase(singularize(m.group(1)));
return getVerb(op) + noun;
}
private static String handlePattern3(String op, String path) {
Pattern r = Pattern.compile(pattern3);
Matcher m = r.matcher(path);
boolean found = m.find();
if (!found)
return null;
String noun = toCamelCase(singularize(m.group(1)));
String relation = toCamelCase(m.group(3));
return op + noun + relation;
}
private static String handlePattern4(String op, String path) {
Pattern r = Pattern.compile(pattern4);
Matcher m = r.matcher(path);
boolean found = m.find();
if (!found)
return null;
String entity = toCamelCase(singularize(m.group(1)));
String relation = m.group(3);
return getVerb(op) + entity + toCamelCase(singularize(relation)) + "ById";
}
private static String handlePattern5(String op, String path) {
Pattern r = Pattern.compile(pattern5);
Matcher m = r.matcher(path);
boolean found = m.find();
if (!found)
return null;
String entity = toCamelCase(m.group(1));
String searchMethod = m.group(2);
r = Pattern.compile("findBy([a-zA-Z0-9]+)");
m = r.matcher(searchMethod);
if (m.find())
return "search" + entity + "By" + toCamelCase(m.group(1));
return "search" + entity + "By" + toCamelCase(searchMethod);
}
public static String singularize(String word) {
Inflector i = new Inflector();
return i.singularize(word);
}
public static boolean isSingular(String word) {
Inflector i = new Inflector();
return i.singularize(word).equals(word);
}
public static String getVerb(String op) {
return httpMethodVerb.get(op);
}
public static String convert(String op, String path) {
String result = handlePattern1(op, path);
if (result == null) {
result = handlePattern2(op, path);
if (result == null) {
result = handlePattern3(op, path);
if (result == null) {
result = handlePattern4(op, path);
if (result == null) {
result = handlePattern5(op, path);
}
}
}
}
return result;
}
private static String toCamelCase(String phrase) {
List<String> words = new ArrayList<>();
for (String word : phrase.split("_"))
words.add(word);
for (int i = 0; i < words.size(); i++) {
String word = words.get(i);
String firstLetter = word.substring(0, 1).toUpperCase();
word = firstLetter + word.substring(1);
words.set(i, word);
}
return String.join("", words.toArray(new String[words.size()]));
}
}