View Source OpenAPI.Processor.Naming (OpenAPI Generator v0.1.0-rc.0)

Default implementation for naming-related callbacks

This module contains the default implementations for:

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

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: []
  ]
]

Link to this section 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 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

Link to this section Types

@type module_and_type() :: {module :: module(), type :: atom()}
@type raw_module_and_type() :: {module :: String.t() | nil, type :: String.t()}

Link to this section Default Implementations

Link to this function

operation_function(state, operation_spec)

View Source
@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.

Link to this function

operation_modules(state, operation_spec)

View Source
@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 is false)
  • 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 tag bar => Bar.foo
  • Operation foo/bar with tag baz => Baz.foo_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

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.

Link to this function

schema_module_and_type(state, schema_spec)

View Source
@spec schema_module_and_type(OpenAPI.Processor.State.t(), OpenAPI.Spec.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:

  1. 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()
  2. Merge settings are applied based on the original names of the schemas by merge_schema/2:

    SimpleUser.t()  =>  User.simple()
  3. Rename settings are applied based on the merged module names by rename_schema/2:

    UserPreferences.t()  =>  UserSettings.t()
  4. Group settings are applied based on the renamed module names by group_schema/2:

    UserSettings.t()  =>  User.Settings.t()
  5. 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

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).

Link to this section Functions

Link to this function

group_schema(raw_module_and_type, state)

View Source

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

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

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

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).

Link to this function

merge_schema(raw_module_and_type, state)

View Source

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

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

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.

Link to this function

normalize_identifier(input)

View Source
@spec normalize_identifier(String.t()) :: String.t()

Normalize an identifier into snake_case

example

Example

iex> normalize_identifier("get-/customer/purchases/{date}_byId")
"get_customer_purchases_date_by_id"
Link to this function

raw_schema_module_and_type(arg1)

View Source
@spec raw_schema_module_and_type(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.

Link to this function

readable_content_type(content_type)

View Source
@spec readable_content_type(String.t()) :: String.t()

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).

Link to this function

rename_schema(raw_module_and_type, state)

View Source

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

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

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.