Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,53 @@ The `templateType` option will default to `SupportingFiles`, so the option for `

Excluding `SupportingFiles`, each of the above options may result in multiple files. API related types create a file per API. Model related types create a file for each model.

### Directory-based file discovery with `filesDir`

If you have many additional template files, listing each one individually under `files` can become unwieldy. The `filesDir` option lets you point to a directory whose structure determines template types automatically:

```yaml
templateDir: my_custom_templates
filesDir: my_extra_templates/
```

Where `my_extra_templates/` is organized like this:

```
my_extra_templates/
README.md # -> SupportingFiles (root = default)
LICENSE.mustache # -> SupportingFiles, output as "LICENSE"
api/
custom_api.mustache # -> templateType: API
model/
validators.mustache # -> templateType: Model
apiDocs/
api_readme.mustache # -> templateType: APIDocs
modelDocs/
model_readme.mustache # -> templateType: ModelDocs
apiTests/
api_test.mustache # -> templateType: APITests
modelTests/
model_test.mustache # -> templateType: ModelTests
supportingFiles/
build.gradle.mustache # -> SupportingFiles
scripts/
check.sh # -> SupportingFiles, folder: "scripts"
custom_output_dir/
deploy.sh # -> SupportingFiles, folder: "custom_output_dir"
```

The rules are:

* **Root files** (directly inside `filesDir`) are treated as `SupportingFiles` with no output folder.
* **Recognized subdirectory names** (case-insensitive: `api`, `model`, `apiDocs`, `modelDocs`, `apiTests`, `modelTests`, `supportingFiles`) map files to the corresponding `templateType`.
* **Unrecognized subdirectory names** (e.g. `custom_output_dir/`) are treated as `SupportingFiles` with the directory path used as the output `folder`.
* **`.mustache` suffixes** are stripped from the destination filename (e.g. `build.gradle.mustache` outputs as `build.gradle`). Non-mustache files are copied as-is.
* **Nested directories under `supportingFiles/`** preserve their path as the output folder (e.g. `supportingFiles/scripts/check.sh` outputs to `scripts/check.sh`).

You can use `filesDir` together with `files`. When both are specified, explicit `files` entries take precedence over auto-discovered ones if there is a template path conflict.

`filesDir` follows symbolic links. Be careful not to create symlink cycles, as no infinite loop protection is provided.

Note that user-defined templates will merge with built-in template definitions. If a supporting file with the sample template file path exists, it will be replaced with the user-defined template, otherwise the user-defined template will be added to the list of template files to compile. If the generator's built-in template is `model_docs.mustache` and you define `model-docs.mustache`, this will result in duplicated model docs (if `destinationFilename` differs) or undefined behavior as whichever template compiles last will overwrite the previous model docs (if `destinationFilename` matches the extension or suffix in the generator's code).

## Custom Generator (and Template)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import org.openapitools.codegen.api.TemplateDefinition;
import org.openapitools.codegen.api.TemplateFileType;

import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;

/**
* Represents a serialization helper of {@link org.openapitools.codegen.config.GeneratorSettings} and {@link org.openapitools.codegen.config.WorkflowSettings}. When used to deserialize any available Jackson binding input,
Expand All @@ -39,31 +41,151 @@ public class DynamicSettings {
private WorkflowSettings workflowSettings;

/**
* Gets the list of template files allowing user redefinition and addition of templating files
* Gets the list of template files allowing user redefinition and addition of templating files.
* This includes files from both the {@code files} map and the {@code filesDir} directory.
* When both are specified, explicit {@code files} entries take precedence over auto-discovered
* ones from {@code filesDir} if there is a template path conflict.
*
* @return A list of template files
*/
public List<TemplateDefinition> getFiles() {
if (files == null) return new ArrayList<>();

return files.entrySet().stream().map(kvp -> {
TemplateDefinition file = kvp.getValue();
String templateFile = kvp.getKey();
String destination = file.getDestinationFilename();
if (TemplateFileType.SupportingFiles.equals(file.getTemplateType()) && StringUtils.isBlank(destination)) {
// this special case allows definitions such as LICENSE:{}
destination = templateFile;
// Collect explicitly defined files
Map<String, TemplateDefinition> result = new LinkedHashMap<>();

if (files != null) {
for (Map.Entry<String, TemplateDefinition> kvp : files.entrySet()) {
TemplateDefinition file = kvp.getValue();
String templateFile = kvp.getKey();
String destination = file.getDestinationFilename();
if (TemplateFileType.SupportingFiles.equals(file.getTemplateType()) && StringUtils.isBlank(destination)) {
// this special case allows definitions such as LICENSE:{}
destination = templateFile;
}
TemplateDefinition definition = new TemplateDefinition(templateFile, file.getFolder(), destination);
definition.setTemplateType(file.getTemplateType());
result.put(templateFile, definition);
}
}

// Discover files from filesDir, if specified
if (StringUtils.isNotBlank(filesDir)) {
List<TemplateDefinition> discovered = discoverFilesFromDirectory(filesDir);
for (TemplateDefinition def : discovered) {
// Explicit files entries take precedence
result.putIfAbsent(def.getTemplateFile(), def);
}
TemplateDefinition definition = new TemplateDefinition(templateFile, file.getFolder(), destination);
definition.setTemplateType(file.getTemplateType());
return definition;
}).collect(Collectors.toList());
}

return new ArrayList<>(result.values());
}

@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
@JsonProperty("files")
private Map<String, TemplateDefinition> files;

@JsonProperty("filesDir")
private String filesDir;

// Maps subdirectory names (lowercase) to TemplateFileType for auto-discovery
private static final Map<String, TemplateFileType> DIRECTORY_TYPE_MAPPING;
static {
Map<String, TemplateFileType> m = new HashMap<>();
m.put("api", TemplateFileType.API);
m.put("model", TemplateFileType.Model);
m.put("apidocs", TemplateFileType.APIDocs);
m.put("modeldocs", TemplateFileType.ModelDocs);
m.put("apitests", TemplateFileType.APITests);
m.put("modeltests", TemplateFileType.ModelTests);
m.put("supportingfiles", TemplateFileType.SupportingFiles);
DIRECTORY_TYPE_MAPPING = Collections.unmodifiableMap(m);
}

/**
* Scans a directory and discovers template files, mapping subdirectory names to template types.
* <ul>
* <li>Files at the root of the directory default to {@link TemplateFileType#SupportingFiles}</li>
* <li>Files under subdirectories named after a {@link TemplateFileType} (case-insensitive) are
* assigned that type (e.g. {@code api/}, {@code model/}, {@code apiDocs/})</li>
* <li>Files under unrecognized subdirectory names are treated as {@link TemplateFileType#SupportingFiles}
* with the subdirectory path used as the output folder</li>
* <li>For API/Model types, only immediate children of the type directory are included</li>
* <li>For SupportingFiles (including root and unrecognized dirs), nesting is preserved as the output folder</li>
* </ul>
*
* @param dirPath path to the directory to scan
* @return list of discovered template definitions
*/
static List<TemplateDefinition> discoverFilesFromDirectory(String dirPath) {
List<TemplateDefinition> result = new ArrayList<>();
Path baseDir = Paths.get(dirPath).normalize();

if (!Files.isDirectory(baseDir)) {
return result;
}

try {
Files.walkFileTree(baseDir, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs) {
if (!Files.isRegularFile(filePath)) {
return FileVisitResult.CONTINUE;
}

Path relative = baseDir.relativize(filePath);
String templateFile = relative.toString().replace('\\', '/');
String fileName = filePath.getFileName().toString();

// Determine the destination filename: strip .mustache suffix if present
String destinationFilename = fileName.endsWith(".mustache")
? fileName.substring(0, fileName.length() - ".mustache".length())
: fileName;

// Determine template type and folder from directory structure
TemplateFileType templateType = TemplateFileType.SupportingFiles;
String folder = "";

if (relative.getNameCount() == 1) {
// File at root of filesDir -> SupportingFiles, no folder
templateType = TemplateFileType.SupportingFiles;
} else {
// Has at least one parent directory
String topDir = relative.getName(0).toString();
String topDirLower = topDir.toLowerCase(Locale.ROOT);
TemplateFileType mappedType = DIRECTORY_TYPE_MAPPING.get(topDirLower);

if (mappedType != null) {
templateType = mappedType;

if (mappedType == TemplateFileType.SupportingFiles) {
// For supportingFiles subdir, preserve nested structure as folder
if (relative.getNameCount() > 2) {
Path folderPath = relative.subpath(1, relative.getNameCount() - 1);
folder = folderPath.toString().replace('\\', '/');
}
}
// For API/Model/etc types, folder is left empty (generator handles placement)
} else {
// Unrecognized subdirectory -> SupportingFiles with the full relative dir as folder
templateType = TemplateFileType.SupportingFiles;
Path folderPath = relative.subpath(0, relative.getNameCount() - 1);
folder = folderPath.toString().replace('\\', '/');
}
}

TemplateDefinition definition = new TemplateDefinition(templateFile, folder, destinationFilename);
definition.setTemplateType(templateType);
result.add(definition);

return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
throw new RuntimeException("Failed to scan filesDir: " + dirPath, e);
}

return result;
}

/**
* Gets the {@link org.openapitools.codegen.config.GeneratorSettings} included in the config object.
*
Expand Down
Loading