Остаток данных Spring: изменение идентификатора операции в спецификации OpenAPI

#spring-data-rest #openapi #springdoc

#остаток весенних данных #openapi #springdoc

Вопрос:

Я пытаюсь сгенерировать openapi.yaml моей службы Spring Data Rest, чтобы мы могли легко генерировать клиентский код с помощью генератора typescript-angular. К сожалению, имена сгенерированных сервисов и методов являются … меньше, чем идеально. Мы получаем разные контроллеры для объекта, для «поиска» и другой для отношения. Кроме того, имена функций в сгенерированных сервисах чрезвычайно длинные, не добавляя много информации / преимуществ. Вот пример:

 paths:
  /pricingPlans:
    get:
      tags:
      - pricing-plan-entity-controller
      description: get-pricingplan
      operationId: getCollectionResource-pricingplan-get_1
 

С помощью этого openapi.yaml мы получаем a PricingPlanEntityControllerService с функцией getCollectionResource-pricingplan-get_1 , которая просто смешна. Мы хотели бы изменить это на PricingPlanService и getAll .

 @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?

Комментарии:

1. Ваши описания кажутся неполными. Когда вы говорите, что мы получаем разные контроллеры для объекта, для «поиска» и другого для отношения: вот как встроен spring-data-rest. Убедитесь, что вы предоставили более точную информацию, если вам действительно нужна помощь.

2. Я просто описывал, каков результат, и тот факт, что это не так, как мне бы хотелось, кажется неявно понятным, иначе я бы не публиковал здесь. Если это было не так, я прошу прощения.

Ответ №1:

Обратите внимание, что ваш подход с использованием @Operation аннотации для настройки идентификатора операции не работает для большинства репозиториев spring-data-rest: причина в том, что операции генерируются внутри фреймворка, и у вас нет никакого способа добавить аннотации.

Простой способ, который работает во всех случаях, — это использовать OpenApiCustomiser , чтобы изменить значение любой части сгенерированной спецификации OpenAPI, как описано в документации .

     @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 ...");
                });
    }
 

Комментарии:

1. Спасибо за это решение! Не мог SDR прочитать аннотации к методам репозитория и добавить их в место, где генерируются операции?

2. Не уверен, что этот метод хорош. Имена генерируются частично путем добавления числа в конце, которое увеличивается каждый раз, чтобы сделать идентификаторы операций уникальными. Что, если я добавлю конечную точку или удалю ее? Это приведет к перетасовке порядка, и мой перевод будет нарушен.

Ответ №2:

@mimi78 указал мне, как настроить сгенерированную спецификацию OpenAPI. Спасибо за это! Я был обеспокоен методом простого добавления переводов идентификаторов операций 1: 1, поскольку внутреннее / исходное имя может изменяться по мере добавления или удаления конечных точек. Я придумал решение, которое генерирует идентификатор операции из шаблона пути (например /products/{id}/vendor ) и метода HTTP. Я думаю, что это должно обеспечить стабильное именование, понятное для человека и лучше подходящее для клиентских генераторов, которые основывают свой код на идентификаторе операции.

Я хотел поделиться этим решением на случай, если однажды оно понадобится кому-то еще:

 @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 resu<
    }

    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()]));
    }

}