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