View Source OpenAPI.Processor.Naming (OpenAPI Generator v0.2.0)
Default implementation for naming-related callbacks
This module contains the default implementations for:
OpenAPI.Processor.operation_function_name/2
OpenAPI.Processor.operation_module_names/2
OpenAPI.Processor.schema_module_and_type/2
It also includes several helper functions that are used by the default implementations. Library authors implementing their own naming-related callbacks may find these helpful.
Configuration
All configuration offered by the functions in this module lives under the naming
key of the
active configuration profile. For example (default values shown):
# config/config.exs
config :oapi_generator, default: [
naming: [
default_operation_module: Operations,
group: [],
merge: [],
rename: []
]
]
Summary
Default Implementations
Choose the name of an operation client function based on its operation ID
Choose the names of modules containing the given operation
Choose the name of the schema's module and type
Functions
Group schema modules into configured namespaces
Merge schemas based on configured pairs of patterns and replacements
Normalize an identifier into CamelCase or snake_case
Choose a starting schema module and type name based on title and context
Turn a content type (ex. "application/json"
) into a readable type (ex. "json"
)
Rename schema modules based on configured patterns
Default Implementations
@spec operation_function(OpenAPI.Processor.State.t(), OpenAPI.Spec.Path.Operation.t()) :: atom()
Choose the name of an operation client function based on its operation ID
Default implementation of OpenAPI.Processor.operation_function_name/2
.
In this implementation, the operation ID is split up by slash characters with only the last portion taken (ex. "repos/get" becomes "get"), assuming that the module name will use the remaining portions. Then the value is normalized to be atom-friendly.
Note that this function creates new atoms, and should not be run in a production environment.
@spec operation_modules(OpenAPI.Processor.State.t(), OpenAPI.Spec.Path.Operation.t()) :: [module()]
Choose the names of modules containing the given operation
Default implementation of OpenAPI.Processor.operation_module_names/2
.
This generator generates a set of modules with functions in them according to some normalization rules:
- Operation tags and IDs are normalized for spaces, slashes, etc.
- Operation tags are used to generate modules that group operation functions (unless
naming.operation_use_tags
isfalse
) - Operation IDs with slashes will be split, with the initial segments (everything except the last segment) used as segments of a module
Examples:
- Operation
foo
with tagbar
=>Bar.foo
- Operation
foo/bar
with tagbaz
=>Foo.bar
andBaz.bar
- Operation
foo/bar
without tags =>Foo.bar
Each operation may exist in multiple modules depending on the quantity of tags and the format
of the operation ID. If the operation does not have slashes in its ID and does not have any
tags, then the configured :default_operation_module
or [output.base_module].Operations
becomes the module by default.
Configuration
Use naming.default_operation_module
to configure the catch-all module name. Note that the
configured name should not include the base module, if it is set in output.base_module
. The
following configuration would result in a module named MyClientLibrary.Operations
:
config :oapi_generator, default: [
naming: [
default_operation_module: Operations,
operation_use_tags: true
],
output: [
base_module: MyClientLibrary
]
]
Set naming.operation_use_tags
to false
to disable the use of tags when creating modules.
@spec schema_module_and_type( OpenAPI.Processor.State.t(), OpenAPI.Processor.Schema.t() ) :: {module() | nil, atom()}
Choose the name of the schema's module and type
Default implementation of OpenAPI.Processor.schema_module_and_type/2
.
Most of the configuration of this project relates to the manipulation of schema names. It is important to understand the order of operations. As an example, imagine an OpenAPI description has the following schemas:
#/components/schemas/simple-user
#/components/schemas/user
#/components/schemas/user-preferences
And the following configuration:
config :oapi_generator, default: [
naming: [
group: [User],
merge: [{"SimpleUser", "User"}]
rename: [{~r/Preferences/, "Settings"}]
],
output: [
base_module: Example
]
]
In this case, naming would proceed as follows:
Schemas in the OpenAPI descriptions are turned into Elixir modules based on their location, context, or title by
raw_schema_module_and_type/1
:#/components/schemas/simple-user => SimpleUser.t() #/components/schemas/user => User.t() #/components/schemas/user-preferences => UserPreferences.t()
Merge settings are applied based on the original names of the schemas by
merge_schema/2
:SimpleUser.t() => User.simple()
Rename settings are applied based on the merged module names by
rename_schema/2
:UserPreferences.t() => UserSettings.t()
Group settings are applied based on the renamed module names by
group_schema/2
:UserSettings.t() => User.Settings.t()
The base module is applied to get the final names:
User.simple() => Example.User.simple() User.t() => Example.User.t() User.Settings.t() => Example.User.Settings.t()
Collapsing
Note that User.simple()
and User.t()
will end up in the same file as a result of the merge,
sharing the same struct for their responses (with distinct typespecs).
Types
Functions
@spec group_schema(raw_module_and_type(), OpenAPI.Processor.State.t()) :: raw_module_and_type()
Group schema modules into configured namespaces
This function accepts a tuple with the module and type of a schema as strings, along with the processor state, and returns a modified tuple according to the configured groups.
Discussion
Schemas in an OpenAPI description can have extensively long names. For example, GitHub has a
schema called actions-cache-usage-by-repository
. Along with all other actions-related schemas,
we can cut down the top-level module namespace by grouping on Actions
or even further:
group: [
Actions,
Actions.CacheUsage
]
Even simple renaming and groups can take a raw OpenAPI description and turn it into a library that feels friendly to users.
Configuration
Module namespaces can be configured as a list of modules in the naming.group
key of a
configuration profile:
config :oapi_generator, default: [
naming: [
group: [
Author,
Author.Bio
Comment,
# ...
]
]
]
Examples
The configuration above includes three module namespaces for grouping: Author
, Author.Bio
,
and Comment
. These rules would create the following transformations (types omitted because
they do not change):
AuthorAvatar => Author.Avatar
AuthorBio => Author.Bio
AuthorBioUpdate => Author.Bio.Update
PostComment => PostComment
Note that the desired grouping must appear at the start of the module name: PostComment
is
unaffected by the Comment
group configuration. As a result, it is also important that Author
appear in the configuration before Author.Bio
, otherwise Author.Bio
would fail to match the
beginning of AuthorBioUpdate
resulting in Author.BioUpdate
(since the Author
configuration
would still match afterwards).
@spec merge_schema(raw_module_and_type(), OpenAPI.Processor.State.t()) :: raw_module_and_type()
Merge schemas based on configured pairs of patterns and replacements
This function accepts a tuple with the module and type of a schema as strings, along with the processor state, and returns a modified tuple according to the configured merges.
Discussion
OpenAPI descriptions may have multiple schemas that are closely related or even duplicated. Merging gives the power to consolidate these schemas into a single struct that is easy to use.
For example, the GitHub API description used to have schemas repository
, full-repository
,
and nullable-repository
. While the "full" repository added additional properties, the
"nullable" variant was just that: all of the same properties, but the schema was nullable. This
kind of oddity in the OpenAPI specification is exactly what makes most generated code difficult
to use.
The following merge settings would help clean this up:
merge: [
{"FullRepository", "Repository"},
{~r/^Nullable/, ""}
]
In the first line, we tell the generator to merge FullRepository
into Repository
(the
original module names based on the names of the schemas). Because the destination module appears
at the end of the original module, the word "Repository" will be dropped from the type:
FullRepository => Repository :: Repository.full()
This renaming of the type is automatic for prefixes and suffixes. If no overlap is found, then the full (underscored) schema name will be used for the type:
SimpleUser => User :: User.simple()
PullRequestSimple => PullRequest :: PullRequest.simple()
MySchema => Unrelated :: Unrelated.my_schema()
If the destination module is later renamed or grouped, the merged schemas will processed in the same way.
Configuration
Merges are configured as a list of tuples in the naming.merge
key of a configuration profile:
config :oapi_generator, default: [
naming: [
merge: [
{"PrivateUser", "User"},
{~r/Simple$/, ""}
]
]
]
If the first element of the tuple is a string, it will be compared for an exact match to the
schema's module name. If the first element of the tuple is a regular express, it will be
compared to the schema's module name using Regex.match?/2
. If it matches, the module name will
be replaced with the second element of the tuple.
After the module name replacement, the type name may be modified. If new the module name is the first or last part of the original module name, the leftover portion will be used as the type. For example, with the configuration above, the following transformations take place:
PrivateUser.t() => User.private()
UserSimple.t() => User.simple()
In the case that the new module name is not a prefix or suffix of the original, the entire underscored original module name is used as the new type.
Normalize an identifier into CamelCase or snake_case
Example
iex> normalize_identifier("get-/customer/purchases/{date}_byId")
"get_customer_purchases_date_by_id"
iex> normalize_identifier("openAPISpec", :camel)
"OpenAPISpec"
iex> normalize_identifier("get-/customer/purchases/{date}_byId", :lower_camel)
"getCustomerPurchasesDateById"
@spec raw_schema_module_and_type( OpenAPI.Processor.State.t(), OpenAPI.Processor.Schema.t(), OpenAPI.Spec.Schema.t() ) :: {module :: String.t() | nil, type :: String.t()}
Choose a starting schema module and type name based on title and context
Returns a tuple containing the {module, type}
, such as {"MySchema", "t"}
.
This function does not consider schema renaming or merging. It uses the title, context, and
location of the schema within the specification to determine an initial set of names. Schemas
located in components/schemas
are named based on their key in the schemas
map, so a schema
located at components/schemas/my_schema
will become MySchema.t()
. If a schema has a
context attached (such as a request body or response body for an operation) then it will be
named based on the operation. Finally, if a schema has a defined title, this will be used as
the name. If none of this information is available, {nil, "map"}
is returned.
Callers of this function will almost certainly want to perform further processing.
Turn a content type (ex. "application/json"
) into a readable type (ex. "json"
)
This is used by the default implementation of the schema module/type name function while constructing the type of a request or response body that is otherwise unnamed. If an unknown content type is passed, this function returns an empty string to avoid including the content type in the name (although this could cause collisions).
@spec rename_schema(raw_module_and_type(), OpenAPI.Processor.State.t()) :: raw_module_and_type()
Rename schema modules based on configured patterns
This function accepts a tuple with the module and type of a schema as strings, along with the processor state, and returns a modified tuple according to the configured replacements.
Configuration
Module replacements can be configured as a list of tuples in the naming.rename
key of a
configuration profile:
config :oapi_generator, default: [
naming: [
rename: [
{"Api", "API"},
{~r/^Bio/, "Author.Bio"},
# ...
]
]
]
The contents of each tuple will be fed into String.replace/3
, for example:
> String.replace("MyApiResponse", "Api", "API")
Examples
In the configuration above, there are two replacements configured: the string pattern "Api"
will be replaced with "API"
, and the regular expression pattern ^Bio
will be replaced with
"Author.Bio"
. These rules would create the following transformations (types omitted because
they do not change):
MyApiResponse => MyAPIResponse
Apiary => APIary
BioUpdate => Author.BioUpdate
EditorBio => EditorBio
Note that replacements can have unintended side-effects. For example, while we correctly
capitalized MyApiResponse
using the "Api"
pattern, we also replaced APIary
. Regular
expressions lend more powerful and precise replacement patterns. This includes the ability to
use capture expressions (ex. ~r/(Api)([A-Z]|$)/
) and replacements that reference those
captures (ex. "API\\2"
). See String.replace/3
for more information.