Замените модель в схеме openapi внешним значением Rust во время генерации

У меня есть спецификация openapi сервера, где одна конечная точка возвращает список Thing, но где Thing — чрезвычайно сложный и большой объект, созданный на основе другой спецификации openapi. Невозможно (читай: возможно, но это худший вариант по множеству причин) добавить эту спецификацию сервера в определение openapi или ссылаться на объект по пути. В любом случае эта спецификация генерирует большие библиотеки/пакеты/кейты моделей на различных языках, к которым будет иметь доступ код клиента/сервера, созданный на основе этой спецификации.

В частности, мне хотелось бы сгенерировать серверный код на Rust. А потом еще кое-что о клиентах на других языках, но я чувствую, что если смогу решить эту проблему с генерацией сервера Rust, я буду знать, что делать. В этой спецификации сервера он ссылается на Thing. Я бы хотел, чтобы генератор не создавал эту модель и не заменял любую ссылку на нее на Thing, найденную в our_crate::thing::Thing.

Прежде всего, я не совсем уверен, что написать для схемы. Я пробовал эти подходы, и третий кажется... лучшим из худших?

components:
  schemas:
    Thing1:
      description: Doesn't generate anything because it's a free-form object.
      type: object
      properties: {}
    Thing2:
      description: |
        Obviously generates a struct with a `String` field with any of the below approaches.
      type: object
      properties:
        dummy_field:
          type: string
    Thing3:
      description: |
        Assigns it the type `HashMap<String, serde_json::Value>` or a `HashMap`
        of `String` to a wrapper of `serde_json::Value`, depending on generator.
      type: object
      additionalProperties: true
    Thing4:
      description: Tried in conjunction with a schema/type mapping; same result as 3.
      type: object
      format: thing4
      additionalProperties: true

В официальной документации говорится, что нужно использовать аргумент --import-mappings для CLI или в конфигурации, передаваемой с -c, вот так:

generatorName: rust-axum
skipSchemaValidation: true
additionalProperties:
  generateAliasAsModel: true
importMappings:
  Thing: "our_crate::thing::Thing"

Это не влияет ни на генераторы rust-axum, ни на rust-server, а выходные данные соответствуют описанию схемы выше. В той же документации позже говорится, что следует использовать --schema-mappings в сочетании с --type-mappings практически в том же сценарии, и моя попытка сделать это со схемой, как в Thing4 выше.

generatorName: rust-server
skipSchemaValidation: true
additionalProperties:
  generateAliasAsModel: true
schemaMappings:
  thing: "our_crate::thing::Thing"
typeMappings:
  "object+thing": "thing"

создает пустые структуры.

Так что я немного растерялся на этом этапе. Я прочитал множество других сообщений SO об этом, и все они имеют ответы, примерно соответствующие документам. Я не очень хорошо знаком с Java или написанием шаблонов усов, но в этот момент у меня возникает ощущение, что это проблема с самим генератором, или я могу каким-то образом изменить шаблон, чтобы учитывать это сопоставление.

Итак, мой вопрос: на чем мне следует сосредоточить свои усилия сейчас? Может ли индивидуальный шаблон добиться этого?

Или нужно менять сам генератор? Я заметил это как в генераторе rust-axum, так и в генераторе rust-server. об этой опции сопоставления импорта упоминается только одно, и оба они пусты. И запуск его с параметрами отладки действительно показывает, что об этом пользовательском сопоставлении не упоминается. В моем очень ограниченном понимании этой системы шаблоны усов могут получить доступ к этим переменным только при выполнении своей задачи.

Иначе, есть ли способ, который я еще не придумал, как сделать эту, казалось бы, простую и, вероятно, очень распространенную вещь?

Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
0
0
93
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В конечном итоге это решение, которое работает для меня. Я все равно был бы очень признателен за точку зрения на общую проблему от кого-то, кто делал это раньше. Документация расплывчата до такой степени, что вводит в заблуждение по ряду ее аспектов, и ни один результат поиска, который я смог найти, не достиг уровня полного описания решения. У меня до сих пор много вопросов, например: «Действительно ли это так и должно быть сделано?» Я надеюсь, что любой, кому предстоит решить ту же проблему, немедленно найдет этот ответ и найдет его полезным, а не разочарованием, которое я испытал.

В любом случае, моя схема OpenAPI была структурирована следующим образом.

├── openapi.yaml
├── parameters
│   ├── path
│   └── query
├── path_items
│   ├── things.yaml
└── schemas
    ├── errors
    └── responses

где схема, которую я хотел преобразовать в импорт типа из внешнего ящика, ссылалась на путь через:

# path_items/things.yaml
get:
  operationId: GetThingsPage
  description: Get a single page of things.
  security:
    - ThingsAuth:
        - things:read
  parameters:
    - $ref: "../parameters/path/thing_id.yaml"
    - $ref: "../parameters/query/user.yaml"
    - $ref: "../parameters/query/page_token.yaml"
  responses:
    200:
      description: OK
      content:
        application/json:
          schema:
            $ref: "../schemas/responses/getthingspageresponse.yaml"
    400:
      description: BAD_REQUEST
      content:
        application/json:
          schema:
            $ref: "../schemas/errors/error.yaml"
    401:
      description: UNAUTHORIZED
      content:
        application/json:
          schema:
            $ref: "../schemas/errors/error.yaml"
    403:
      description: FORBIDDEN
      content:
        application/json:
          schema:
            $ref: "../schemas/errors/error.yaml"
    500:
      description: INTERNAL_SERVER_ERROR
      content:
        application/json:
          schema:
            $ref: "../schemas/errors/error.yaml"
# schemas/responses/getthingspageresponse.yaml
type: object
properties:
  next_page_token:
    name: next_page_token
    type: string
    description: |
      If present, used to fetch the next set of results, otherwise it's the last page.
  things:
    description: Page of things.
    type: array
    items:
      $ref: "../../openapi.yaml#/components/schemas/Thing"
required:
  - things

и наконец,

# openapi.yaml
components:
  schemas:
    Thing:
      # All of the different ways I tried to express that this is
      # a "placeholder," intended to be specified in some mapping option
      # as an import.

То, что сначала попало в шкаф, было

# openapi.yaml
components:
  schemas:
    Thing:
      description: A reference to `Thing` as defined in our global models.
      type: ExternalThingSchema

а затем typeMapping (в конфигурации, передаваемой в CLI с опцией -c):

packageName: "things-internal-server"
skipSchemaValidation: true
additionalProperties:
  generateAliasAsModel: true
typeMappings:
  ExternalThingSchema: "the_crate::models::Thing"

В результате был создан код Rust, который наконец осознал мою внешнюю зависимость:

/// A reference to `Thing` as defined in our global models.
#[derive(Debug, Clone, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))]
pub struct Thing(the_crate::models::Thing);

Но наличие этой оболочки newtype раздражает, поэтому я подумал о том, чтобы переместить внешний тип непосредственно в тело ответа:

# schemas/responses/getthingspageresponse.yaml
type: object
properties:
  next_page_token:
    type: string
    description: |
      If present, used to fetch the next set of results, otherwise it's the last page.
  things:
    description: Page of things.
    type: array
    items:
      description: A reference to `Thing` as defined in our global models.
      type: ExternalThingSchema
required:
  - things

Это не работает: генератор сообщает, что «[main] ERROR o.o.codegen.utils.ModelUtils - Undefined array inner type for 'null'. Default to String», а в журналах debugModel мы видим, что назначенный тип — это Vec<String>.

Итак, в конце концов кажется (и я не могу сказать почему), что ExternalThingSchema можно упомянуть только в файле openapi.yaml верхнего уровня:

components:
  schemas:
    GetThingsPageResponseBody:
      type: object
      properties:
        next_page_token:
          description:  If present, used to fetch the next set of results, otherwise it's the last page.
          type: string
        things:
          description: Page of things.
          type: array
          items:
            description: A reference to `Thing` as defined in our global models.
            type: ExternalThingSchema
      required:
        - things

производит

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)]
#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))]
pub struct GetThingsPageResponseBody {
/// If present, used to fetch the next set of results, otherwise it's the last page.
    #[serde(rename = "next_page_token")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub next_page_token: Option<String>,

/// Page of things.
    #[serde(rename = "things")]
    pub things: Vec<the_crate::models::Thing>,

}

наконец-то это то, чего я хотел.

NB: Даже с этим typeMappings, и хотя он генерирует то, что я хочу, генератор все равно выдает предупреждения:

[main] WARN  o.o.codegen.DefaultCodegen - Unknown type found in the schema: ExternalThingSchema. To map it, please use the schema mapping option (e.g. --schema-mappings in CLI)

Передача всего возможного schemaMappings не имеет никакого эффекта, и меня это не волнует.

Другие вопросы по теме