【问题标题】:Spring Data Rest: Change Operation ID in OpenAPI SpecificationSpring Data Rest:在 OpenAPI 规范中更改操作 ID
【发布时间】:2021-03-27 13:48:36
【问题描述】:

我正在尝试生成我的 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,这太荒谬了。我们想将其更改为 PricingPlanServicegetAll

@Tag(name = "pricing-plan")
@CrossOrigin
public interface PricingPlanRepo extends CrudRepository<PricingPlan, UUID> {

    @Override
    Iterable<PricingPlan> findAll();

通过在类级别添加@Tag(name = "pricing-plan"),我们能够将生成的服务的名称更改为PricingPlanService,但无论我们尝试什么,operationId 始终保持不变。

我希望@Operation(operationId = "getAll") 做我们想做的事,但正如我所说:忽略。使用 Spring Data Rest 应用所有这些注释的正确方法是什么?

【问题讨论】:

  • 您的描述似乎不完整。当您说我们为实体、“搜索”和另一个用于关系的控制器提供不同的控制器时:这就是 spring-data-rest 的内置方式。如果您真的需要帮助,请确保提供更准确的信息。
  • 我只是在描述结果是什么,而事实并非我希望它看起来很清楚,否则我不会在这里发帖。如果不是这样,我很抱歉。

标签: spring-data-rest openapi springdoc


【解决方案1】:

请注意,您使用 @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 ...");
                });
    }

【讨论】:

  • 感谢您的解决方案! SDR 不能读取 Repository 方法上的注释并将它们添加到生成操作的位置吗?
  • 不确定这种方法是否很棒。名称部分是通过在末尾附加一个数字来生成的,该数字每次递增以使操作 ID 唯一。如果我添加或删除端点怎么办?这会打乱顺序,我的翻译会被破坏。
【解决方案2】:

@mimi78 向我指出了如何自定义生成的 OpenAPI 规范。感谢那!我担心简单地添加操作 ID 的 1:1 转换的方法,因为内部/原始名称可能会随着端点的添加或删除而改变。我想出了一个从路径模式(例如/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()]));
    }

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-11-05
    • 2021-03-16
    • 1970-01-01
    • 2016-07-22
    • 1970-01-01
    • 2015-05-04
    • 2016-12-07
    相关资源
    最近更新 更多