diff --git a/bin/configs/motoko-petstore.yaml b/bin/configs/motoko-petstore.yaml
new file mode 100644
index 000000000000..181ce1d043cf
--- /dev/null
+++ b/bin/configs/motoko-petstore.yaml
@@ -0,0 +1,9 @@
+generatorName: motoko
+outputDir: samples/client/petstore/motoko
+inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
+templateDir: modules/openapi-generator/src/main/resources/motoko
+artifactId: petstore-client
+artifactVersion: 1.0.0
+additionalProperties:
+ hideGenerationTimestamp: "true"
+ useDfx: false
diff --git a/docs/generators.md b/docs/generators.md
index d9d5bafcaf3c..aff4a007f7d3 100644
--- a/docs/generators.md
+++ b/docs/generators.md
@@ -45,6 +45,7 @@ The following generators are available:
* [k6 (beta)](generators/k6.md)
* [kotlin](generators/kotlin.md)
* [lua (beta)](generators/lua.md)
+* [motoko (beta)](generators/motoko.md)
* [n4js (beta)](generators/n4js.md)
* [nim (beta)](generators/nim.md)
* [objc](generators/objc.md)
diff --git a/docs/generators/motoko.md b/docs/generators/motoko.md
new file mode 100644
index 000000000000..b5817c3d76db
--- /dev/null
+++ b/docs/generators/motoko.md
@@ -0,0 +1,255 @@
+---
+title: Documentation for the motoko Generator
+---
+
+## METADATA
+
+| Property | Value | Notes |
+| -------- | ----- | ----- |
+| generator name | motoko | pass this to the generate command after -g |
+| generator stability | BETA | |
+| generator type | CLIENT | |
+| generator language | Motoko | |
+| generator default templating engine | mustache | |
+| helpTxt | Generates a Motoko OpenAPI client module. | |
+
+## CONFIG OPTIONS
+These options may be applied as additional-properties (cli) or configOptions (plugins). Refer to [configuration docs](https://openapi-generator.tech/docs/configuration) for more details.
+
+| Option | Description | Values | Default |
+| ------ | ----------- | ------ | ------- |
+|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false|
+|disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|
**false** The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications. **true** Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default. |true|
+|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
+|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response. With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|**false** No changes to the enums are made, this is the default option. **true** With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case. |false|
+|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|**true** The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document. **false** The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing. |true|
+|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
+|projectName|Project name for generated code| |null|
+|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
+|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
+|useDfx|Use ic:aaaaa-aa imports (dfx toolchain). Mutually exclusive with useIcp.| |false|
+|useIcp|Use ic:aaaaa-aa imports and generate icp.yaml (icp-cli toolchain; replaces dfx). Mutually exclusive with useDfx.| |false|
+
+## IMPORT MAPPING
+
+| Type/Alias | Imports |
+| ---------- | ------- |
+
+
+## INSTANTIATION TYPES
+
+| Type/Alias | Instantiated By |
+| ---------- | --------------- |
+
+
+## LANGUAGE PRIMITIVES
+
+
+Any
+Blob
+Bool
+Char
+Error
+Float
+Int
+Int16
+Int32
+Int64
+Int8
+Nat
+Nat16
+Nat32
+Nat64
+Nat8
+None
+Null
+Principal
+Region
+Text
+
+
+## RESERVED WORDS
+
+
+actor
+and
+any
+assert
+async
+await
+blob
+bool
+break
+case
+catch
+char
+class
+continue
+debug
+do
+else
+error
+false
+float
+for
+func
+if
+import
+in
+int
+int16
+int32
+int64
+int8
+label
+let
+loop
+map
+module
+nat
+nat16
+nat32
+nat64
+nat8
+none
+not
+null
+object
+or
+principal
+private
+public
+query
+region
+return
+shared
+switch
+system
+text
+throw
+true
+try
+type
+var
+while
+with
+
+
+## FEATURE SET
+
+
+### Client Modification Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|BasePath|✓|ToolingExtension
+|Authorizations|✗|ToolingExtension
+|UserAgent|✗|ToolingExtension
+|MockServer|✗|ToolingExtension
+
+### Data Type Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Custom|✗|OAS2,OAS3
+|Int32|✓|OAS2,OAS3
+|Int64|✓|OAS2,OAS3
+|Float|✓|OAS2,OAS3
+|Double|✓|OAS2,OAS3
+|Decimal|✗|ToolingExtension
+|String|✓|OAS2,OAS3
+|Byte|✓|OAS2,OAS3
+|Binary|✓|OAS2,OAS3
+|Boolean|✓|OAS2,OAS3
+|Date|✓|OAS2,OAS3
+|DateTime|✓|OAS2,OAS3
+|Password|✓|OAS2,OAS3
+|File|✓|OAS2
+|Uuid|✗|
+|Array|✓|OAS2,OAS3
+|Null|✗|OAS3
+|AnyType|✗|OAS2,OAS3
+|Object|✓|OAS2,OAS3
+|Maps|✓|ToolingExtension
+|CollectionFormat|✓|OAS2
+|CollectionFormatMulti|✓|OAS2
+|Enum|✓|OAS2,OAS3
+|ArrayOfEnum|✓|ToolingExtension
+|ArrayOfModel|✓|ToolingExtension
+|ArrayOfCollectionOfPrimitives|✓|ToolingExtension
+|ArrayOfCollectionOfModel|✓|ToolingExtension
+|ArrayOfCollectionOfEnum|✓|ToolingExtension
+|MapOfEnum|✓|ToolingExtension
+|MapOfModel|✓|ToolingExtension
+|MapOfCollectionOfPrimitives|✓|ToolingExtension
+|MapOfCollectionOfModel|✓|ToolingExtension
+|MapOfCollectionOfEnum|✓|ToolingExtension
+
+### Documentation Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Readme|✓|ToolingExtension
+|Model|✓|ToolingExtension
+|Api|✓|ToolingExtension
+
+### Global Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Host|✓|OAS2,OAS3
+|BasePath|✓|OAS2,OAS3
+|Info|✓|OAS2,OAS3
+|Schemes|✗|OAS2,OAS3
+|PartialSchemes|✓|OAS2,OAS3
+|Consumes|✓|OAS2
+|Produces|✓|OAS2
+|ExternalDocumentation|✓|OAS2,OAS3
+|Examples|✓|OAS2,OAS3
+|XMLStructureDefinitions|✗|OAS2,OAS3
+|MultiServer|✗|OAS3
+|ParameterizedServer|✗|OAS3
+|ParameterStyling|✗|OAS3
+|Callbacks|✗|OAS3
+|LinkObjects|✗|OAS3
+
+### Parameter Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Path|✓|OAS2,OAS3
+|Query|✓|OAS2,OAS3
+|Header|✓|OAS2,OAS3
+|Body|✓|OAS2
+|FormUnencoded|✗|OAS2
+|FormMultipart|✗|OAS2
+|Cookie|✗|OAS3
+
+### Schema Support Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Simple|✓|OAS2,OAS3
+|Composite|✓|OAS2,OAS3
+|Polymorphism|✗|OAS2,OAS3
+|Union|✗|OAS3
+|allOf|✗|OAS2,OAS3
+|anyOf|✓|OAS3
+|oneOf|✓|OAS3
+|not|✗|OAS3
+
+### Security Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|BasicAuth|✓|OAS2,OAS3
+|ApiKey|✓|OAS2,OAS3
+|OpenIDConnect|✗|OAS3
+|BearerToken|✓|OAS3
+|OAuth2_Implicit|✗|OAS2,OAS3
+|OAuth2_Password|✗|OAS2,OAS3
+|OAuth2_ClientCredentials|✗|OAS2,OAS3
+|OAuth2_AuthorizationCode|✗|OAS2,OAS3
+|SignatureAuth|✗|OAS3
+|AWSV4Signature|✗|ToolingExtension
+
+### Wire Format Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|JSON|✓|OAS2,OAS3
+|XML|✗|OAS2,OAS3
+|PROTOBUF|✗|ToolingExtension
+|Custom|✗|OAS2,OAS3
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/MotokoClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/MotokoClientCodegen.java
new file mode 100644
index 000000000000..a6d4a72d8435
--- /dev/null
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/MotokoClientCodegen.java
@@ -0,0 +1,1253 @@
+/*
+ * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openapitools.codegen.languages;
+
+import org.openapitools.codegen.*;
+import org.openapitools.codegen.meta.GeneratorMetadata;
+import org.openapitools.codegen.meta.Stability;
+import org.openapitools.codegen.meta.features.*;
+import org.openapitools.codegen.model.ModelMap;
+import org.openapitools.codegen.model.ModelsMap;
+import org.openapitools.codegen.model.OperationsMap;
+import org.openapitools.codegen.utils.ModelUtils;
+import io.swagger.v3.oas.models.media.Schema;
+
+import java.io.File;
+import java.util.*;
+
+public class MotokoClientCodegen extends DefaultCodegen implements CodegenConfig {
+ public static final String PROJECT_NAME = "projectName";
+ public static final String USE_DFX = "useDfx";
+ public static final String USE_ICP = "useIcp";
+
+ protected String projectName = "OpenAPI";
+ protected boolean useDfx = false;
+ protected boolean useIcp = false;
+
+ @Override
+ public CodegenType getTag() {
+ return CodegenType.CLIENT;
+ }
+
+ @Override
+ public String getName() {
+ return "motoko";
+ }
+
+ @Override
+ public String getHelp() {
+ return "Generates a Motoko client (beta).";
+ }
+
+ public MotokoClientCodegen() {
+ super();
+
+ modifyFeatureSet(features -> features
+ .includeDocumentationFeatures(DocumentationFeature.Readme)
+ .wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON))
+ .securityFeatures(EnumSet.noneOf(SecurityFeature.class))
+ .excludeGlobalFeatures(
+ GlobalFeature.XMLStructureDefinitions,
+ GlobalFeature.Callbacks,
+ GlobalFeature.LinkObjects,
+ GlobalFeature.ParameterStyling
+ )
+ .excludeSchemaSupportFeatures(
+ SchemaSupportFeature.Polymorphism
+ )
+ .excludeParameterFeatures(
+ ParameterFeature.Cookie
+ )
+ .includeClientModificationFeatures(
+ ClientModificationFeature.BasePath
+ )
+ );
+
+ generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata)
+ .stability(Stability.BETA)
+ .build();
+
+ outputFolder = "generated-code" + File.separator + "motoko";
+ modelTemplateFiles.put("model.mustache", ".mo");
+ apiTemplateFiles.put("api.mustache", ".mo");
+ embeddedTemplateDir = templateDir = "motoko";
+ apiPackage = "src/Apis";
+ modelPackage = "src/Models";
+ supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
+ supportingFiles.add(new SupportingFile("mops.toml.mustache", "", "mops.toml"));
+ supportingFiles.add(new SupportingFile("Config.mustache", "src", "Config.mo"));
+
+ // Motoko language-specific primitives (don't need imports)
+ // All builtin types are listed to prevent naming clashes with OpenAPI models
+ languageSpecificPrimitives.clear();
+ languageSpecificPrimitives.addAll(Arrays.asList(
+ "Text", "Char", "Bool",
+ "Int", "Int8", "Int16", "Int32", "Int64",
+ "Nat", "Nat8", "Nat16", "Nat32", "Nat64",
+ "Float", "Blob", "Any", "Null", "Principal",
+ "Region", "Error", "None"
+ ));
+
+ // Motoko reserved words
+ // Based on Motoko language specification
+ reservedWords.addAll(Arrays.asList(
+ "actor", "and", "assert", "async", "await", "break", "case", "catch", "class",
+ "continue", "debug", "do", "else", "false", "for", "func", "if",
+ "in", "import", "module", "not", "null", "object", "or", "label",
+ "let", "loop", "private", "public", "query", "return", "shared", "switch",
+ "system", "throw", "true", "try", "type", "var", "while", "with"
+ ));
+ // Add lowercase versions of all primitives (isReservedWord() converts to lowercase)
+ languageSpecificPrimitives.forEach(p -> reservedWords.add(p.toLowerCase(Locale.ROOT)));
+ // Add "map" since Map is a parameterized type that could conflict with user models
+ reservedWords.add("map");
+
+ // Motoko type mappings
+ typeMapping.clear();
+ typeMapping.putAll(Map.of(
+ "string", "Text",
+ "char", "Char",
+ "boolean", "Bool",
+ "int", "Int",
+ "integer", "Int",
+ "long", "Int",
+ "float", "Float",
+ "double", "Float",
+ "number", "Float",
+ "date", "Text"
+ ));
+ typeMapping.putAll(Map.of(
+ "DateTime", "Text",
+ "password", "Text",
+ "file", "Blob",
+ "binary", "Blob",
+ "ByteArray", "Blob",
+ "UUID", "Text",
+ "URI", "Text",
+ "array", "Array", // Handled in getTypeDeclaration to produce [T] syntax
+ "map", "Map", // Maps use the red-black tree based Map from core/pure/Map
+ "object", "Any"
+ ));
+ // Map AnyType (from additionalProperties/free-form objects) to Text as a placeholder
+ // TODO: Better support for additionalProperties - see related TODO in postProcessModels
+ typeMapping.put("AnyType", "Text");
+
+ cliOptions.add(CliOption.newString(PROJECT_NAME, "Project name for generated code"));
+ cliOptions.add(CliOption.newBoolean(USE_DFX, "Use ic:aaaaa-aa imports (dfx toolchain). Mutually exclusive with useIcp.", useDfx));
+ cliOptions.add(CliOption.newBoolean(USE_ICP, "Use ic:aaaaa-aa imports and generate icp.yaml (icp-cli toolchain; replaces dfx). Mutually exclusive with useDfx.", useIcp));
+
+ // Enable inline enum resolution to create model files for inline enum parameters
+ // This ensures type-safe enum variants instead of raw Text types
+ inlineSchemaOption.put("RESOLVE_INLINE_ENUMS", "true");
+ }
+
+ @Override
+ public void processOpts() {
+ super.processOpts();
+
+ if (additionalProperties.containsKey(PROJECT_NAME)) {
+ setProjectName((String) additionalProperties.get(PROJECT_NAME));
+ }
+
+ if (additionalProperties.containsKey(USE_DFX)) {
+ setUseDfx(convertPropertyToBooleanAndWriteBack(USE_DFX));
+ }
+ additionalProperties.put(USE_DFX, useDfx);
+
+ if (additionalProperties.containsKey(USE_ICP)) {
+ setUseIcp(convertPropertyToBooleanAndWriteBack(USE_ICP));
+ }
+ if (useDfx && useIcp) {
+ throw new IllegalArgumentException("useDfx and useIcp are mutually exclusive — pick one toolchain");
+ }
+ additionalProperties.put(USE_ICP, useIcp);
+ if (useIcp) {
+ supportingFiles.add(new SupportingFile("icp.yaml.mustache", "", "icp.yaml"));
+ }
+ // useImportedInterface: true when either useDfx or useIcp — both use moc with ic:aaaaa-aa imports
+ additionalProperties.put("useImportedInterface", useDfx || useIcp);
+ }
+
+ @Override
+ public String escapeReservedWord(String name) {
+ // Escape reserved words and Motoko primitives/core types by appending underscore
+ if (this.reservedWordsMappings().containsKey(name)) {
+ return this.reservedWordsMappings().get(name);
+ }
+ return name + "_";
+ }
+
+ @Override
+ public String toModelName(String name) {
+ // Apply parent sanitization first
+ name = super.toModelName(name);
+
+ // Check if model name conflicts with reserved words or Motoko primitives
+ if (isReservedWord(name)) {
+ return escapeReservedWord(name);
+ }
+
+ return name;
+ }
+
+ @Override
+ public String toModelFilename(String name) {
+ // Filename should match the model name
+ return toModelName(name);
+ }
+
+ @Override
+ public String toVarName(String name) {
+ // Sanitize name but keep it as snake_case (convert hyphens to underscores)
+ name = name.replace("-", "_");
+
+ // Handle reserved words by appending underscore
+ if (isReservedWord(name)) {
+ name = escapeReservedWord(name);
+ }
+
+ // Ensure valid Motoko identifier (starts with letter or underscore)
+ if (!name.isEmpty() && !Character.isJavaIdentifierStart(name.charAt(0))) {
+ name = "_" + name;
+ }
+
+ return name;
+ }
+
+ public void setProjectName(String projectName) {
+ this.projectName = projectName;
+ }
+
+ public String getProjectName() {
+ return projectName;
+ }
+
+ public void setUseDfx(boolean useDfx) {
+ this.useDfx = useDfx;
+ }
+
+ public boolean getUseDfx() {
+ return useDfx;
+ }
+
+ public void setUseIcp(boolean useIcp) {
+ this.useIcp = useIcp;
+ }
+
+ public boolean getUseIcp() {
+ return useIcp;
+ }
+
+ @Override
+ public String toModelImport(String name) {
+ // For Motoko, imports are relative to the current Models directory
+ // Just return the model name without package prefix
+ return name;
+ }
+
+ @Override
+ public String toEnumVarName(String name, String datatype) {
+ // Check for custom mapping
+ if (enumNameMapping.containsKey(name)) {
+ return enumNameMapping.get(name);
+ }
+
+ // Handle empty string
+ if (name == null || name.isEmpty()) {
+ return "empty";
+ }
+
+ // For purely numeric values (Candid-style), wrap with underscores
+ if (name.matches("^\\d+$")) {
+ return "_" + name + "_";
+ }
+
+ // Lowercase for Motoko variant convention (idiomatic, though not required)
+ String enumVarName = name.toLowerCase(Locale.ROOT);
+
+ // Handle leading minus sign (negative numbers) before generic replacement,
+ // so -1 becomes "minus_1" rather than colliding with 1 -> "_1"
+ if (enumVarName.startsWith("-")) {
+ enumVarName = "minus_" + enumVarName.substring(1);
+ }
+
+ // Replace remaining special characters with underscores
+ // Motoko identifiers: [a-zA-Z_][a-zA-Z0-9_]*
+ enumVarName = enumVarName.replaceAll("[^a-zA-Z0-9_]", "_");
+
+ // Remove consecutive underscores
+ enumVarName = enumVarName.replaceAll("_+", "_");
+
+ // Remove leading/trailing underscores
+ enumVarName = enumVarName.replaceAll("^_+|_+$", "");
+
+ // If name starts with a number (but not purely numeric), prefix with underscore
+ if (enumVarName.matches("^\\d.*")) {
+ enumVarName = "_" + enumVarName;
+ }
+
+ // Fallback for invalid names after sanitization
+ if (enumVarName.isEmpty()) {
+ enumVarName = "value_" + Math.abs(name.hashCode());
+ }
+
+ // Escape reserved words
+ if (isReservedWord(enumVarName)) {
+ return escapeReservedWord(enumVarName);
+ }
+
+ return enumVarName;
+ }
+
+ @Override
+ public String toEnumName(CodegenProperty property) {
+ // Check for custom mapping
+ if (enumNameMapping.containsKey(property.name)) {
+ return enumNameMapping.get(property.name);
+ }
+
+ // Use the property's base name to create enum type name
+ String enumName = toModelName(property.baseName);
+
+ // Avoid collision with property variable name by checking if they would be identical
+ // For example, if property is "status", enum type should be "Status" not "status"
+ if (enumName.equals(property.name)) {
+ enumName = enumName + "Enum";
+ }
+
+ // Check for reserved word collision
+ if (isReservedWord(enumName)) {
+ enumName = escapeReservedWord(enumName);
+ }
+
+ return enumName;
+ }
+
+ @Override
+ public String getTypeDeclaration(io.swagger.v3.oas.models.media.Schema schema) {
+ // Handle array types: convert to Motoko syntax [ElementType]
+ String result;
+ if (ModelUtils.isArraySchema(schema)) {
+ io.swagger.v3.oas.models.media.Schema inner = ModelUtils.getSchemaItems(schema);
+ result = "[" + getTypeDeclaration(inner) + "]";
+
+ return result;
+ } else if (ModelUtils.isMapSchema(schema)) {
+ // Handle map types: convert to Motoko Map syntax Map
+ io.swagger.v3.oas.models.media.Schema inner = ModelUtils.getAdditionalProperties(schema);
+ result = "Map";
+
+ return result;
+ }
+
+ return super.getTypeDeclaration(schema);
+ }
+
+ @Override
+ public String getSchemaType(io.swagger.v3.oas.models.media.Schema schema) {
+ // Handle array types first, before calling super.getSchemaType()
+ // This is critical because super.getSchemaType() returns "array" without the element type
+ if (ModelUtils.isArraySchema(schema)) {
+ String inner = getSchemaType(ModelUtils.getSchemaItems(schema));
+ return "[" + inner + "]";
+ } else if (ModelUtils.isMapSchema(schema)) {
+ io.swagger.v3.oas.models.media.Schema inner = ModelUtils.getAdditionalProperties(schema);
+ return "Map";
+ }
+
+ String openAPIType = super.getSchemaType(schema);
+ String type;
+ // Check if we have a type mapping for this OpenAPI type
+ if (typeMapping.containsKey(openAPIType)) {
+ type = typeMapping.get(openAPIType);
+ // If it's a language-specific primitive, return it directly
+ if (languageSpecificPrimitives.contains(type)) {
+ return type;
+ }
+ } else {
+ type = openAPIType;
+ }
+ // Otherwise, convert to model name
+ return toModelName(type);
+ }
+
+ @Override
+ public String toInstantiationType(io.swagger.v3.oas.models.media.Schema schema) {
+ if (ModelUtils.isArraySchema(schema)) {
+ String inner = getSchemaType(ModelUtils.getSchemaItems(schema));
+ return "[" + inner + "]";
+ } else if (ModelUtils.isMapSchema(schema)) {
+ io.swagger.v3.oas.models.media.Schema inner = ModelUtils.getAdditionalProperties(schema);
+ return "Map";
+ }
+ return null;
+ }
+
+ @Override
+ public void postProcessParameter(CodegenParameter parameter) {
+ super.postProcessParameter(parameter);
+
+ // Fix dataType for arrays and maps that may have slipped through as bare types
+ // This happens when the dataType is set before our getSchemaType is called
+ if ("array".equals(parameter.dataType)) {
+ // Try to reconstruct the array type from the parameter
+ if (parameter.isArray && parameter.items != null) {
+ // items is a CodegenProperty, not CodegenParameter - just use its dataType
+ String old = parameter.dataType;
+ parameter.dataType = "[" + parameter.items.dataType + "]";
+ }
+ }
+
+ // Check if this is an integer parameter with minimum >= 0 constraint
+ // Convert to Nat type for type safety (unsigned integers)
+ if (parameter.dataType != null) {
+ String dataType = parameter.dataType;
+ boolean isIntType = "Int".equals(dataType) || dataType.matches("Int\\d+");
+
+ if (isIntType && parameter.minimum != null) {
+ try {
+ java.math.BigDecimal minimum = new java.math.BigDecimal(parameter.minimum);
+ if (minimum.compareTo(java.math.BigDecimal.ZERO) >= 0) {
+ // Convert Int to Nat (unsigned)
+ parameter.dataType = "Nat";
+ parameter.vendorExtensions.put("x-is-unsigned", true);
+ parameter.vendorExtensions.put("x-original-type", dataType);
+ }
+ } catch (NumberFormatException e) {
+ // Ignore invalid minimum values
+ }
+ }
+ }
+
+ // For enum parameters, ensure enumVars are built for template use
+ if (Boolean.TRUE.equals(parameter.isEnum) && parameter.allowableValues != null) {
+ @SuppressWarnings("unchecked")
+ List values = (List) parameter.allowableValues.get("values");
+
+ if (values != null && !values.isEmpty()) {
+ List> enumVars = new ArrayList<>();
+ for (int i = 0; i < values.size(); i++) {
+ Object value = values.get(i);
+ Map enumVar = new HashMap<>();
+
+ // Get the variant name using toEnumVarName
+ String variantName = toEnumVarName(String.valueOf(value), parameter.dataType);
+
+ enumVar.put("name", variantName);
+ enumVar.put("value", String.valueOf(value));
+ enumVar.put("isString", value instanceof String);
+
+ // Mark the last item
+ if (i == values.size() - 1) {
+ enumVar.put("-last", true);
+ }
+
+ enumVars.add(enumVar);
+ }
+ parameter.allowableValues.put("enumVars", enumVars);
+ }
+ }
+ }
+
+ @Override
+ public CodegenModel fromModel(String name, Schema schema) {
+ CodegenModel model = super.fromModel(name, schema);
+
+ // For standalone enum schemas, ensure enumVars are built
+ if (Boolean.TRUE.equals(model.isEnum) && model.allowableValues != null) {
+ @SuppressWarnings("unchecked")
+ List values = (List) model.allowableValues.get("values");
+
+ if (values != null && !values.isEmpty()) {
+ List> enumVars = new ArrayList<>();
+ for (int i = 0; i < values.size(); i++) {
+ Object value = values.get(i);
+ Map enumVar = new HashMap<>();
+
+ // Get the variant name using toEnumVarName
+ String variantName = toEnumVarName(String.valueOf(value), model.dataType);
+
+ enumVar.put("name", variantName);
+ enumVar.put("value", String.valueOf(value));
+ enumVar.put("isString", value instanceof String);
+
+ // Mark the last item
+ if (i == values.size() - 1) {
+ enumVar.put("-last", true);
+ }
+
+ enumVars.add(enumVar);
+ }
+ model.allowableValues.put("enumVars", enumVars);
+ }
+ }
+
+ // Handle oneOf schemas - generate as discriminated unions (variant types)
+ if (model.getComposedSchemas() != null && model.getComposedSchemas().getOneOf() != null) {
+ List oneOfList = model.getComposedSchemas().getOneOf();
+
+ if (!oneOfList.isEmpty()) {
+ model.vendorExtensions.put("x-is-oneof", true);
+
+ // Build variant cases for oneOf options
+ List> oneOfVariants = new ArrayList<>();
+ boolean hasUnsignedVariants = false;
+
+ for (int i = 0; i < oneOfList.size(); i++) {
+ CodegenProperty oneOfProp = oneOfList.get(i);
+
+ // Check if this is an inline enum - expand it into multiple unit variants
+ if (Boolean.TRUE.equals(oneOfProp.isEnum) && oneOfProp.allowableValues != null) {
+ @SuppressWarnings("unchecked")
+ List enumValues = (List) oneOfProp.allowableValues.get("values");
+
+ if (enumValues != null && !enumValues.isEmpty()) {
+ // Expand inline enum into multiple unit variants
+ for (Object enumValue : enumValues) {
+ Map variant = new HashMap<>();
+ String variantName = toEnumVarName(String.valueOf(enumValue), oneOfProp.dataType);
+
+ variant.put("name", variantName);
+ variant.put("hasType", false); // Unit variant (no associated data)
+ variant.put("isUnsigned", false);
+ variant.put("needsConversion", false);
+ variant.put("enumValue", String.valueOf(enumValue));
+ variant.put("isStringEnum", enumValue instanceof String);
+
+ // Type classification flags (not used for unit variants, but added for consistency)
+ variant.put("isNumericType", false);
+ variant.put("isEnumType", false);
+ variant.put("isObjectType", false);
+
+ oneOfVariants.add(variant);
+ }
+ continue; // Skip regular processing for this enum option
+ }
+ }
+
+ // Regular oneOf option (not an inline enum)
+ Map variant = new HashMap<>();
+
+ // Determine variant name and type
+ String variantName = getOneOfVariantName(oneOfProp);
+ String variantType = oneOfProp.dataType;
+ String jsonType = variantType; // JSON-facing type (may differ for Nat->Int)
+
+ // For integer types with minimum >= 0, convert to Nat in user-facing type
+ boolean isUnsigned = Boolean.TRUE.equals(oneOfProp.vendorExtensions.get("x-is-unsigned"));
+ if (isUnsigned || ("Nat".equals(variantType))) {
+ // User-facing uses Nat, JSON-facing uses Int
+ variantType = "Nat";
+ jsonType = "Int";
+ isUnsigned = true;
+ hasUnsignedVariants = true;
+ }
+
+ variant.put("name", variantName);
+ variant.put("dataType", variantType);
+ variant.put("jsonType", jsonType);
+ variant.put("hasType", true); // Typed variant (has associated data)
+ variant.put("isUnsigned", isUnsigned);
+ variant.put("needsConversion", !variantType.equals(jsonType));
+
+ // Add type classification flags for toText() generation
+ boolean isNumericType = false;
+ boolean isEnumType = false;
+ boolean isObjectType = false;
+
+ if (isUnsigned || "Nat".equals(variantType) || "Int".equals(variantType)) {
+ // Numeric types: Int, Nat
+ isNumericType = true;
+ } else if (Boolean.TRUE.equals(oneOfProp.isEnum) ||
+ (variantType != null && variantType.endsWith("Enum")) ||
+ Boolean.TRUE.equals(oneOfProp.vendorExtensions.get("x-is-enum")) ||
+ (oneOfProp.allowableValues != null && !oneOfProp.allowableValues.isEmpty())) {
+ // Enum types: detected by isEnum flag, "Enum" suffix, vendor extension, or allowableValues
+ isEnumType = true;
+ } else if (oneOfProp.complexType != null || Boolean.TRUE.equals(oneOfProp.isModel)) {
+ // Complex object/record types
+ isObjectType = true;
+ } else {
+ // Default to object type for unknown cases
+ isObjectType = true;
+ }
+
+ variant.put("isNumericType", isNumericType);
+ variant.put("isEnumType", isEnumType);
+ variant.put("isObjectType", isObjectType);
+
+ oneOfVariants.add(variant);
+ }
+
+ // Mark the last variant
+ if (!oneOfVariants.isEmpty()) {
+ oneOfVariants.get(oneOfVariants.size() - 1).put("-last", true);
+ }
+
+ model.vendorExtensions.put("oneOfVariants", oneOfVariants);
+
+ // Set flag if any variant needs Int import for unsigned conversion
+ if (hasUnsignedVariants) {
+ model.vendorExtensions.put("x-has-unsigned-fields", true);
+ }
+ }
+ }
+
+ return model;
+ }
+
+ /**
+ * Generate a variant name for a oneOf option.
+ * For simple types, use the type name. For enum strings, use the enum value.
+ */
+ private String getOneOfVariantName(CodegenProperty prop) {
+ // For enum types, use the first enum value or a sanitized name
+ if (Boolean.TRUE.equals(prop.isEnum) && prop.allowableValues != null) {
+ @SuppressWarnings("unchecked")
+ List values = (List) prop.allowableValues.get("values");
+ if (values != null && !values.isEmpty()) {
+ // Use the enum values directly as variant names
+ // This handles string enums like ["up", "down"]
+ return toEnumVarName(String.valueOf(values.get(0)), prop.dataType);
+ }
+ }
+
+ // For reference types, use the referenced type name
+ if (prop.complexType != null) {
+ return toVarName(prop.complexType);
+ }
+
+ // For primitive types, use the base type name
+ String baseName = prop.baseName != null ? prop.baseName : prop.dataType;
+ if (baseName != null) {
+ return toVarName(baseName);
+ }
+
+ // Default to "integer", "string", etc. for simple types
+ return toVarName(prop.dataType);
+ }
+
+ @Override
+ public CodegenProperty fromProperty(String name, Schema propertySchema, boolean required, boolean schemaIsFromAdditionalProperties) {
+ CodegenProperty property = super.fromProperty(name, propertySchema, required, schemaIsFromAdditionalProperties);
+
+ // Check if this is an integer type with minimum >= 0 constraint
+ // Convert to Nat type for type safety (unsigned integers)
+ if (property != null && property.dataType != null) {
+ String dataType = property.dataType;
+
+ // Check if it's an Int type (including parameterized types in arrays/maps)
+ boolean isIntType = "Int".equals(dataType) || dataType.matches("Int\\d+");
+
+ if (isIntType && propertySchema != null) {
+ // Check minimum constraint
+ java.math.BigDecimal minimum = propertySchema.getMinimum();
+
+ if (minimum != null && minimum.compareTo(java.math.BigDecimal.ZERO) >= 0) {
+ // Convert Int to Nat (unsigned)
+ // Note: This creates Janus-like behavior where JSON has Int but Motoko uses Nat
+ property.dataType = "Nat";
+
+ // Add vendor extension to signal this conversion in templates
+ property.vendorExtensions.put("x-is-unsigned", true);
+ property.vendorExtensions.put("x-original-type", dataType);
+ }
+ }
+ }
+
+ return property;
+ }
+
+ @Override
+ public ModelsMap postProcessModelsEnum(ModelsMap objs) {
+ // Call parent to process enums with default logic
+ objs = super.postProcessModelsEnum(objs);
+
+ return objs;
+ }
+
+ @Override
+ public ModelsMap postProcessModels(ModelsMap objs) {
+ // Call parent first (which calls postProcessModelsEnum internally)
+ objs = super.postProcessModels(objs);
+
+ // Track enum models to add to imports
+ Set enumModelNames = new HashSet<>();
+
+ // Check if we need to import Map
+ boolean needsMapImport = false;
+
+ // Collect field mappings for JSON serialization
+ Map>> fieldMappings = new HashMap<>();
+
+ // Check all model properties for Map usage and enum references
+ List models = objs.getModels();
+
+ // FIRST PASS: Collect all enum model names
+ if (models != null) {
+ for (ModelMap modelMap : models) {
+ org.openapitools.codegen.CodegenModel model = modelMap.getModel();
+ if (model != null && Boolean.TRUE.equals(model.isEnum)) {
+ enumModelNames.add(model.classname);
+ }
+ }
+ }
+
+ // SECOND PASS: Process all models with full enum knowledge
+ if (models != null) {
+ for (ModelMap modelMap : models) {
+ org.openapitools.codegen.CodegenModel model = modelMap.getModel();
+
+ if (model != null) {
+ // Mark enum models for conditional template logic
+ if (Boolean.TRUE.equals(model.isEnum)) {
+ model.vendorExtensions.put("x-is-motoko-enum", true);
+ }
+
+ // Track if this model has any enum fields (needs JSON sub-module)
+ boolean hasEnumFields = false;
+ // Track if this model has any unsigned (Nat) fields
+ boolean hasUnsignedFields = false;
+
+ if (model.vars != null) {
+ // Collect field name mappings
+ List> fieldEscapeMappings = new ArrayList<>();
+
+ for (org.openapitools.codegen.CodegenProperty prop : model.vars) {
+ // Check for Map usage
+ if (prop.dataType != null && prop.dataType.contains("Map<")) {
+ needsMapImport = true;
+ }
+
+ // Check for unsigned fields (Nat types converted from Int)
+ if (Boolean.TRUE.equals(prop.vendorExtensions.get("x-is-unsigned"))) {
+ hasUnsignedFields = true;
+ }
+
+ // Collect escaped field names (where Motoko name differs from JSON name)
+ if (!prop.baseName.equals(prop.name)) {
+ Map mapping = new HashMap<>();
+ mapping.put("motokoName", prop.name);
+ mapping.put("jsonName", prop.baseName);
+ fieldEscapeMappings.add(mapping);
+ }
+
+ // Handle enum properties
+ if (Boolean.TRUE.equals(prop.isEnum)) {
+ // TODO: Inline enums need proper implementation to generate separate model files
+ // For now, keep them as Text type
+ // This is an inline enum - would need to generate a separate model for it
+ // String enumTypeName = toEnumName(prop);
+ // prop.datatypeWithEnum = enumTypeName;
+ // prop.dataType = enumTypeName;
+ // enumModelNames.add(enumTypeName);
+ } else if (Boolean.TRUE.equals(prop.isEnumRef)) {
+ // This is a reference to an existing enum
+ // The datatypeWithEnum should already be set by DefaultCodegen
+ if (prop.datatypeWithEnum != null) {
+ prop.vendorExtensions.put("x-is-motoko-enum", true);
+ prop.vendorExtensions.put("xIsMotokoEnum", true);
+ prop.vendorExtensions.put("x-motoko-enum-type", prop.datatypeWithEnum);
+ prop.vendorExtensions.put("xMotokoEnumType", prop.datatypeWithEnum);
+ enumModelNames.add(prop.datatypeWithEnum);
+ hasEnumFields = true;
+ }
+ } else if (enumModelNames.contains(prop.dataType)) {
+ // Property's dataType matches an enum model name
+ // This catches cases where isEnumRef might not be set correctly
+ prop.vendorExtensions.put("x-is-motoko-enum", true);
+ prop.vendorExtensions.put("xIsMotokoEnum", true);
+ prop.vendorExtensions.put("x-motoko-enum-type", prop.dataType);
+ prop.vendorExtensions.put("xMotokoEnumType", prop.dataType);
+ hasEnumFields = true;
+ }
+ }
+
+ // Store field mappings if any
+ if (!fieldEscapeMappings.isEmpty()) {
+ fieldMappings.put(model.classname, fieldEscapeMappings);
+ model.vendorExtensions.put("x-has-field-mappings", true);
+ model.vendorExtensions.put("x-field-mappings", fieldEscapeMappings);
+ }
+ }
+
+ // Mark models that need JSON sub-modules (have enum fields)
+ if (hasEnumFields) {
+ model.vendorExtensions.put("x-needs-json-module", true);
+ model.vendorExtensions.put("xNeedsJsonModule", true);
+ }
+
+ // Mark models that have unsigned (Nat) fields (need Int import for conversion)
+ if (hasUnsignedFields) {
+ model.vendorExtensions.put("x-has-unsigned-fields", true);
+ }
+ }
+ }
+ }
+
+ // Store field mappings in context for templates
+ objs.put("fieldMappings", fieldMappings);
+ objs.put("hasAnyMappings", !fieldMappings.isEmpty());
+
+ // Mark imports that are mapped types (primitives) or array/map types so they can be filtered out
+ List> imports = objs.getImports();
+ if (imports != null) {
+ for (Map im : imports) {
+ String importName = im.get("import");
+ // Check if this import is a primitive/mapped type or array/map type
+ if (importName != null) {
+ // TODO: Support additionalProperties by modeling as Map or similar
+ // Currently schemas with additionalProperties: true are not fully supported.
+ // Future work: Add a field like "additionalProperties: ?Map" to models.
+ boolean isMappedType = typeMapping.containsKey(importName) ||
+ typeMapping.containsValue(importName) ||
+ languageSpecificPrimitives.contains(importName) ||
+ importName.startsWith("[") ||
+ importName.contains("<") || // Filter out parameterized types like "Map"
+ "AnyType".equals(importName); // Filter out AnyType - not yet implemented
+ if (isMappedType) {
+ im.put("isMappedType", "true");
+ }
+
+ // Mark enum imports
+ if (enumModelNames.contains(importName)) {
+ im.put("isEnum", "true");
+ }
+ }
+ }
+ }
+
+ // Add Map import if needed
+ // NOTE: Model name escaping is implemented in toModelName() and tested in
+ // samples/client/type-coverage/motoko-test with a user-defined "Map" model.
+ // User model "Map" is escaped to "Map_" while Map refers to the core type.
+ if (needsMapImport) {
+ if (imports == null) {
+ imports = new ArrayList<>();
+ objs.put("imports", imports);
+ }
+ imports.add(Map.of(
+ "import", "Map",
+ "isMap", "true",
+ "isMappedType", "true" // Prevent it from being imported as a model
+ ));
+ }
+
+ return objs;
+ }
+
+ /**
+ * Check if a type is a primitive or mapped type (not a generated model).
+ * Uses languageSpecificPrimitives and typeMapping as the single source of truth.
+ */
+ private boolean isPrimitiveOrMappedType(String type) {
+ if (type == null) {
+ return false;
+ }
+ // Check if it's a language-specific primitive
+ if (languageSpecificPrimitives.contains(type)) {
+ return true;
+ }
+ // Check if it's in typeMapping (like Any for object)
+ if (typeMapping.containsValue(type)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Map postProcessAllModels(Map objs) {
+ // Call parent first
+ objs = super.postProcessAllModels(objs);
+
+ // Collect all models from all ModelsMap objects
+ List allModels = new ArrayList<>();
+ for (ModelsMap modelsMap : objs.values()) {
+ List modelMaps = modelsMap.getModels();
+ if (modelMaps != null) {
+ for (ModelMap modelMap : modelMaps) {
+ CodegenModel model = modelMap.getModel();
+ if (model != null) {
+ allModels.add(model);
+ }
+ }
+ }
+ }
+
+ // THIRD PASS: Detect transitive enum references (models containing fields that reference models with enums)
+ // Iterate until we reach a fixed point (no new models marked as having enum fields)
+ boolean changed = true;
+ while (changed) {
+ changed = false;
+ for (CodegenModel model : allModels) {
+ // Skip if already marked as having enum fields or if it's an enum itself
+ if (!Boolean.TRUE.equals(model.vendorExtensions.get("xNeedsJsonModule"))
+ && !Boolean.TRUE.equals(model.isEnum)) {
+
+ boolean hasTransitiveEnums = false;
+
+ if (model.vars != null) {
+ for (CodegenProperty prop : model.vars) {
+ // Check if this field's type is a model that has enum fields
+ if (prop.dataType != null && !prop.isEnum && !prop.isEnumRef) {
+ // Look for the referenced model
+ for (CodegenModel refModel : allModels) {
+ if (refModel.classname.equals(prop.dataType)
+ && Boolean.TRUE.equals(refModel.vendorExtensions.get("xNeedsJsonModule"))) {
+ hasTransitiveEnums = true;
+ break;
+ }
+ }
+ }
+ if (hasTransitiveEnums) break;
+ }
+ }
+
+ if (hasTransitiveEnums) {
+ model.vendorExtensions.put("x-needs-json-module", true);
+ model.vendorExtensions.put("xNeedsJsonModule", true);
+ changed = true;
+ }
+ }
+ }
+ }
+
+ // FOURTH PASS: Mark models that have no enum fields (neither direct nor transitive) and no unsigned fields for identity optimization
+ for (CodegenModel model : allModels) {
+ if (!Boolean.TRUE.equals(model.isEnum)
+ && !Boolean.TRUE.equals(model.vendorExtensions.get("xNeedsJsonModule"))
+ && !Boolean.TRUE.equals(model.vendorExtensions.get("x-has-unsigned-fields"))) {
+ // Model has no enum fields and no unsigned fields - can use identity transform
+ model.vendorExtensions.put("x-no-enum-fields", true);
+ model.vendorExtensions.put("xNoEnumFields", true);
+ }
+ }
+
+ // FIFTH PASS: Mark properties that reference models needing JSON conversion
+ for (CodegenModel model : allModels) {
+ if (model.vars != null) {
+ for (CodegenProperty prop : model.vars) {
+ // Check if this property references a model that needs JSON conversion
+ if (prop.dataType != null && !prop.isEnum && !prop.isEnumRef) {
+ for (CodegenModel refModel : allModels) {
+ if (refModel.classname.equals(prop.dataType)
+ && Boolean.TRUE.equals(refModel.vendorExtensions.get("xNeedsJsonModule"))) {
+ // This property references a model that needs JSON conversion
+ prop.vendorExtensions.put("x-needs-json-conversion", true);
+ prop.vendorExtensions.put("xNeedsJsonConversion", true);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // SIXTH PASS: Count transformed fields for record-update optimization
+ for (CodegenModel model : allModels) {
+ // Skip enum models - they don't have record fields
+ if (Boolean.TRUE.equals(model.isEnum)) {
+ continue;
+ }
+
+ if (model.vars != null && !model.vars.isEmpty()) {
+ int totalFields = model.vars.size();
+ int transformedFields = 0;
+
+ // Count fields that need transformation
+ for (CodegenProperty prop : model.vars) {
+ // A field needs transformation if it's an enum reference or needs JSON conversion
+ // Check both isEnumRef and the vendor extension we set
+ boolean needsTransform = Boolean.TRUE.equals(prop.isEnumRef) ||
+ Boolean.TRUE.equals(prop.vendorExtensions.get("xIsMotokoEnum")) ||
+ Boolean.TRUE.equals(prop.vendorExtensions.get("xNeedsJsonConversion"));
+ if (needsTransform) {
+ transformedFields++;
+ }
+ }
+
+ // Store the counts
+ model.vendorExtensions.put("x-total-fields", totalFields);
+ model.vendorExtensions.put("x-transform-count", transformedFields);
+
+ // Determine optimization strategy:
+ // - If transformedFields == 0: use identity (already handled by xNoEnumFields)
+ // - If 0 < transformedFields < totalFields: use record-update syntax (with)
+ // - If transformedFields == totalFields: construct fresh record
+ if (transformedFields > 0 && transformedFields < totalFields) {
+ model.vendorExtensions.put("x-has-partial-transform", true);
+ model.vendorExtensions.put("xHasPartialTransform", true);
+ }
+ }
+ }
+
+ return objs;
+ }
+
+ /**
+ * Collect enum variant mappings for JSON serialization.
+ * Returns a list of mappings where Motoko variant name differs from OpenAPI value.
+ */
+ @Override
+ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) {
+ OperationsMap result = super.postProcessOperationsWithModels(objs, allModels);
+
+ // Parse security schemes from OpenAPI spec to enable authentication support
+ Map securitySchemes = new HashMap<>();
+ if (openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getSecuritySchemes() != null) {
+ securitySchemes = openAPI.getComponents().getSecuritySchemes();
+ }
+
+ // Determine which authentication types are used
+ boolean usesBearerAuth = false;
+ boolean usesApiKeyAuth = false;
+ boolean usesBasicAuth = false;
+ String apiKeyHeaderName = null;
+ String apiKeyQueryName = null;
+
+ for (Map.Entry entry : securitySchemes.entrySet()) {
+ io.swagger.v3.oas.models.security.SecurityScheme scheme = entry.getValue();
+
+ if (io.swagger.v3.oas.models.security.SecurityScheme.Type.HTTP.equals(scheme.getType())) {
+ if ("bearer".equalsIgnoreCase(scheme.getScheme())) {
+ usesBearerAuth = true;
+ } else if ("basic".equalsIgnoreCase(scheme.getScheme())) {
+ usesBasicAuth = true;
+ }
+ } else if (io.swagger.v3.oas.models.security.SecurityScheme.Type.APIKEY.equals(scheme.getType())) {
+ usesApiKeyAuth = true;
+ if (io.swagger.v3.oas.models.security.SecurityScheme.In.HEADER.equals(scheme.getIn())) {
+ apiKeyHeaderName = scheme.getName();
+ } else if (io.swagger.v3.oas.models.security.SecurityScheme.In.QUERY.equals(scheme.getIn())) {
+ apiKeyQueryName = scheme.getName();
+ }
+ }
+ }
+
+ // Add authentication context to operations for use in templates
+ result.put("usesBearerAuth", usesBearerAuth);
+ result.put("usesApiKeyAuth", usesApiKeyAuth);
+ result.put("usesBasicAuth", usesBasicAuth);
+ result.put("apiKeyHeaderName", apiKeyHeaderName);
+ result.put("apiKeyQueryName", apiKeyQueryName);
+
+ // Response code processing is implemented in api.mustache template
+ // - HTTP status codes are checked before parsing (2xx vs 4xx/5xx)
+ // - Error responses use generated error models when available
+ // - Structured error details are included in thrown errors
+
+ // Collect all enum types and models with escaped fields for global API context
+ List> allEnumTypes = new ArrayList<>();
+ List> allModelsWithEscapedFields = new ArrayList<>();
+
+ if (allModels != null) {
+ for (ModelMap modelMap : allModels) {
+ CodegenModel model = modelMap.getModel();
+
+ if (model != null) {
+ // Collect enum types with mappings
+ if (Boolean.TRUE.equals(model.isEnum) &&
+ Boolean.TRUE.equals(model.vendorExtensions.get("x-has-enum-mappings"))) {
+ Map enumInfo = new HashMap<>();
+ enumInfo.put("name", model.classname);
+ enumInfo.put("mappings", model.vendorExtensions.get("x-enum-mappings"));
+ allEnumTypes.add(enumInfo);
+ }
+
+ // Collect models with escaped field names
+ if (Boolean.TRUE.equals(model.vendorExtensions.get("x-has-field-mappings"))) {
+ Map modelInfo = new HashMap<>();
+ modelInfo.put("name", model.classname);
+ modelInfo.put("mappings", model.vendorExtensions.get("x-field-mappings"));
+ allModelsWithEscapedFields.add(modelInfo);
+ }
+ }
+ }
+ }
+
+ // Mark first/last items for template iteration (Mustache uses these for comma handling)
+ // Clear any existing flags first
+ for (Map enumInfo : allEnumTypes) {
+ enumInfo.remove("-last");
+ enumInfo.remove("-first");
+ }
+ for (Map modelInfo : allModelsWithEscapedFields) {
+ modelInfo.remove("-last");
+ modelInfo.remove("-first");
+ }
+ // Set flags on boundary items
+ if (!allEnumTypes.isEmpty()) {
+ allEnumTypes.get(allEnumTypes.size() - 1).put("-last", true);
+ }
+ if (!allModelsWithEscapedFields.isEmpty()) {
+ allModelsWithEscapedFields.get(0).put("-first", true);
+ allModelsWithEscapedFields.get(allModelsWithEscapedFields.size() - 1).put("-last", true);
+ }
+
+ // Store in context for API template
+ result.put("allEnumTypes", allEnumTypes);
+ result.put("allModelsWithEscapedFields", allModelsWithEscapedFields);
+ result.put("hasAnyMappings", !allEnumTypes.isEmpty() || !allModelsWithEscapedFields.isEmpty());
+
+ // Also add to additionalProperties for supporting files (EnumMappings.mustache)
+ additionalProperties.put("allEnumTypes", allEnumTypes);
+ additionalProperties.put("allModelsWithEscapedFields", allModelsWithEscapedFields);
+ additionalProperties.put("hasAnyMappings", !allEnumTypes.isEmpty() || !allModelsWithEscapedFields.isEmpty());
+
+ // Check if we need to import Map
+ boolean needsMapImport = false;
+
+ // Fix array types in operations
+ org.openapitools.codegen.model.OperationMap operations = result.getOperations();
+ if (operations != null) {
+ for (org.openapitools.codegen.CodegenOperation op : operations.getOperation()) {
+ // Fix return type if it's a bare "array"
+ if ("array".equals(op.returnType)) {
+ if (op.returnContainer != null && op.returnContainer.equals("array")) {
+ op.returnType = "[" + op.returnBaseType + "]";
+ }
+ }
+
+ // Mark operations with array return types for special handling in template
+ if (op.returnContainer != null && op.returnContainer.equals("array")) {
+ op.vendorExtensions.put("x-return-is-array", true);
+ op.vendorExtensions.put("x-return-base-type", op.returnBaseType);
+
+ // Check if array element type is a primitive/mapped type
+ boolean isElementPrimitive = isPrimitiveOrMappedType(op.returnBaseType);
+ op.vendorExtensions.put("x-return-array-element-is-primitive", isElementPrimitive);
+ }
+
+ // Mark operations with primitive/mapped return types (non-model types)
+ if (op.returnType != null && op.returnContainer == null) {
+ boolean isPrimitive = isPrimitiveOrMappedType(op.returnType);
+ op.vendorExtensions.put("x-return-is-primitive", isPrimitive);
+ }
+
+ // Mark operations with map return types for special handling in template
+ if (op.returnContainer != null && op.returnContainer.equals("map")) {
+ op.vendorExtensions.put("x-return-is-map", true);
+ if (op.returnBaseType != null) {
+ op.vendorExtensions.put("x-return-map-value-type", op.returnBaseType);
+ }
+ needsMapImport = true;
+ }
+
+ // Check if return type uses Map
+ if (op.returnType != null && op.returnType.contains("Map<")) {
+ needsMapImport = true;
+ }
+
+ // Check body parameter for special handling
+ if (op.bodyParam != null) {
+ org.openapitools.codegen.CodegenParameter bodyParam = op.bodyParam;
+
+ // Handle array body parameters
+ if (bodyParam.isArray && bodyParam.items != null) {
+ bodyParam.vendorExtensions.put("x-body-is-array", true);
+ bodyParam.vendorExtensions.put("x-body-base-type", bodyParam.items.dataType);
+
+ // Check if array element type is primitive
+ boolean isElementPrimitive = isPrimitiveOrMappedType(bodyParam.items.dataType);
+ bodyParam.vendorExtensions.put("x-body-array-element-is-primitive", isElementPrimitive);
+ } else if (bodyParam.dataType != null) {
+ // Handle primitive body parameters
+ boolean isPrimitive = isPrimitiveOrMappedType(bodyParam.dataType);
+ bodyParam.vendorExtensions.put("x-body-is-primitive", isPrimitive);
+ }
+ }
+
+ // Check if any parameters use Map
+ if (op.allParams != null) {
+ for (org.openapitools.codegen.CodegenParameter param : op.allParams) {
+ if (param.dataType != null && param.dataType.contains("Map<")) {
+ needsMapImport = true;
+ break;
+ }
+ }
+ }
+
+ // Mark oneOf parameters with x-is-oneof-type vendor extension
+ // This tells api.mustache to use .toText() instead of .toJSON() for URL parameters
+ // We need to mark parameters in allParams, queryParams, and pathParams lists
+ if (allModels != null) {
+ List> paramLists = new ArrayList<>();
+ if (op.allParams != null) paramLists.add(op.allParams);
+ if (op.queryParams != null) paramLists.add(op.queryParams);
+ if (op.pathParams != null) paramLists.add(op.pathParams);
+
+ for (List paramList : paramLists) {
+ for (org.openapitools.codegen.CodegenParameter param : paramList) {
+ if (param.dataType != null) {
+ // Check if this parameter's type is a oneOf model
+ for (ModelMap modelMap : allModels) {
+ CodegenModel model = modelMap.getModel();
+ if (model != null && model.classname.equals(param.dataType)) {
+ if (Boolean.TRUE.equals(model.vendorExtensions.get("x-is-oneof"))) {
+ param.vendorExtensions.put("x-is-oneof-type", true);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Mark imports that are mapped types (primitives) or array/map types so they can be filtered out
+ List> imports = result.getImports();
+ if (imports != null) {
+ for (Map im : imports) {
+ // Get the classname field - this is what we use in the template
+ String className = im.get("classname");
+ // Check if this classname is a key in typeMapping (meaning it's a primitive/mapped type)
+ // OR if it starts with '[' (array/map type) which shouldn't be imported
+ if (className != null) {
+ boolean isMappedType = typeMapping.containsKey(className) ||
+ className.startsWith("[") ||
+ className.contains("<") || // Filter out parameterized types like "Map"
+ "AnyType".equals(className); // Filter out AnyType - not yet implemented
+ // In Mustache, only add the key if it's true (for conditional sections)
+ if (isMappedType) {
+ im.put("isMappedType", "true");
+ }
+ }
+ }
+ }
+
+ // Add Map import if needed
+ // NOTE: Model name escaping is implemented in toModelName() and tested in
+ // samples/client/type-coverage/motoko-test with a user-defined "Map" model.
+ // User model "Map" is escaped to "Map_" while Map refers to the core type.
+ if (needsMapImport) {
+ if (imports == null) {
+ imports = new ArrayList<>();
+ result.put("imports", imports);
+ }
+ imports.add(Map.of(
+ "import", "Map",
+ "classname", "Map",
+ "isMap", "true",
+ "isMappedType", "true" // Prevent it from being imported as a model
+ ));
+ }
+
+ return result;
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
index 04da8d20435b..cb28e2c7abf6 100644
--- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
+++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
@@ -90,6 +90,7 @@ org.openapitools.codegen.languages.KotlinWiremockServerCodegen
org.openapitools.codegen.languages.KtormSchemaCodegen
org.openapitools.codegen.languages.LuaClientCodegen
org.openapitools.codegen.languages.MarkdownDocumentationCodegen
+org.openapitools.codegen.languages.MotokoClientCodegen
org.openapitools.codegen.languages.MysqlSchemaCodegen
org.openapitools.codegen.languages.N4jsClientCodegen
org.openapitools.codegen.languages.NimClientCodegen
diff --git a/modules/openapi-generator/src/main/resources/motoko/Config.mustache b/modules/openapi-generator/src/main/resources/motoko/Config.mustache
new file mode 100644
index 000000000000..faaa9a6faee6
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/motoko/Config.mustache
@@ -0,0 +1,36 @@
+// Config.mo — shared configuration types and default config for {{appName}}
+
+module {
+ public type Auth = {
+ #bearer : Text;
+ #apiKey : Text;
+ #basicAuth : { user : Text; password : Text };
+ };
+
+ type http_header = { name : Text; value : Text };
+ type http_request_result = { status : Nat; headers : [http_header]; body : Blob };
+
+ public type Config = {
+ baseUrl : Text;
+ auth : ?Auth;
+ max_response_bytes : ?Nat64;
+ transform : ?{
+ function : shared query ({ response : http_request_result; context : Blob }) -> async http_request_result;
+ context : Blob;
+ };
+ is_replicated : ?Bool;
+ cycles : Nat;
+ };
+
+ /// Default configuration for {{appName}}.
+ /// Customize with record update syntax:
+ /// { defaultConfig with auth = ?#bearer "my-token" }
+ public let defaultConfig : Config = {
+ baseUrl = "{{basePath}}";
+ auth = null;
+ max_response_bytes = null;
+ transform = null;
+ is_replicated = null;
+ cycles = 30_000_000_000;
+ };
+}
diff --git a/modules/openapi-generator/src/main/resources/motoko/README.mustache b/modules/openapi-generator/src/main/resources/motoko/README.mustache
new file mode 100644
index 000000000000..96de34200bff
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/motoko/README.mustache
@@ -0,0 +1,103 @@
+# {{appName}}
+
+{{#appDescription}}
+{{{appDescription}}}
+{{/appDescription}}
+
+This Motoko client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project.
+
+- API version: {{appVersion}}
+{{^hideGenerationTimestamp}}
+- Build date: {{generatedDate}}
+{{/hideGenerationTimestamp}}
+- Generator version: {{generatorVersion}}
+- Build package: {{generatorClass}}
+
+## Models
+
+{{#models}}
+{{#model}}
+- {{classname}}
+{{/model}}
+{{/models}}
+
+## APIs
+
+{{#apiInfo}}
+{{#apis}}
+{{#operations}}
+- {{classname}}
+{{/operations}}
+{{/apis}}
+{{/apiInfo}}
+
+## Installation
+
+This is a Motoko module that can be used in your Internet Computer project.
+
+## Usage
+
+Import the generated API modules in your Motoko code:
+
+```motoko
+import SomeApi "mo:{{artifactId}}/Apis/SomeApi";
+// or using destructuring for specific functions
+import { someFunction } "mo:{{artifactId}}/Apis/SomeApi";
+```
+
+Configure and call the API:
+
+```motoko
+import { defaultConfig } "mo:{{artifactId}}/Config";
+
+// Use the default config as-is, or customize specific fields:
+let config = { defaultConfig with auth = ?#bearer "my-token" };
+
+let result = await* SomeApi.someFunction(config, ...);
+```
+
+The `defaultConfig` has `baseUrl` pre-set to the API's base URL, `cycles = 30_000_000_000`, and all optional fields set to `null`.
+
+Alternatively, use the suite-based API to bind config once and call multiple functions without threading it through each call:
+
+```motoko
+import { SomeApi } "mo:{{artifactId}}/Apis/SomeApi";
+
+let api = SomeApi(config);
+let result = await api.someFunction(...);
+let other = await api.anotherFunction(...);
+```
+
+### HTTP Outcalls and Cycles
+
+The generated API client makes HTTP outcalls using the Internet Computer's management canister. HTTP outcalls require cycles to execute.
+
+**Important:** Before calling any API endpoints, ensure your canister has sufficient cycles:
+
+{{#useIcp}}
+For local development with icp-cli:
+```bash
+icp network start -d # local replica auto-seeds cycles — no fabrication needed
+icp deploy # build + deploy
+```
+
+For mainnet:
+```bash
+icp cycles balance -n ic # check balance
+icp deploy -e ic # deploy to mainnet
+```
+{{/useIcp}}
+{{^useIcp}}
+For local development:
+```bash
+# Get your canister ID
+CANISTER_ID=$(dfx canister id your_canister_name)
+
+# Add cycles (100 trillion cycles for testing)
+dfx ledger fabricate-cycles --canister "$CANISTER_ID" --amount 100000000000000
+```
+
+For production deployment, you'll need to fund your canister with cycles through the NNS or cycles wallet.
+{{/useIcp}}
+
+Each HTTP outcall typically costs around 20-50 million cycles depending on the request/response size.
diff --git a/modules/openapi-generator/src/main/resources/motoko/api.mustache b/modules/openapi-generator/src/main/resources/motoko/api.mustache
new file mode 100644
index 000000000000..ab92ad16ec9b
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/motoko/api.mustache
@@ -0,0 +1,285 @@
+// {{classname}}.mo
+{{#description}}
+/// {{{description}}}
+{{/description}}
+
+import Text "mo:core/Text";
+import Int "mo:core/Int";
+import Blob "mo:core/Blob";
+import Array "mo:core/Array";
+import Error "mo:core/Error";
+import Base64 "mo:core/Base64";
+import { JSON } "mo:serde";
+{{#useImportedInterface}}
+// FIXME: destructuring on `actor` types is not implemented yet for shared functions
+// type error [M0114], object pattern cannot consume actor type
+import { type http_request_args; type http_request_result; type http_header } "ic:aaaaa-aa";
+import Mgnt__ = "ic:aaaaa-aa";
+{{/useImportedInterface}}
+{{#imports}}
+{{#isMap}}import { type Map; fromIter } "mo:core/pure/Map";
+{{/isMap}}{{^isMappedType}}import { type {{classname}}; JSON = {{classname}} } "../Models/{{classname}}";
+{{/isMappedType}}
+{{/imports}}
+import { type Config } "../Config";
+
+module {
+{{#useImportedInterface}}
+ let http_request = Mgnt__.http_request;
+
+{{/useImportedInterface}}
+{{^useImportedInterface}}
+ // Management Canister interface for HTTP outcalls
+ // Based on https://github.com/dfinity/interface-spec/blob/master/spec/ic.did
+ type http_header = {
+ name : Text;
+ value : Text;
+ };
+
+ type http_method = {
+ #get;
+ #head;
+ #post;
+ #put; // Non-replicated only (is_replicated forced to ?false in generated code)
+ #delete; // Non-replicated only (is_replicated forced to ?false in generated code)
+ };
+
+ type http_request_args = {
+ url : Text;
+ max_response_bytes : ?Nat64;
+ method : http_method;
+ headers : [http_header];
+ body : ?Blob;
+ transform : ?{
+ function : shared query ({ response : http_request_result; context : Blob }) -> async http_request_result;
+ context : Blob;
+ };
+ is_replicated : ?Bool;
+ };
+
+ type http_request_result = {
+ status : Nat;
+ headers : [http_header];
+ body : Blob;
+ };
+
+ let http_request = (actor "aaaaa-aa" : actor { http_request : (http_request_args) -> async http_request_result }).http_request;
+
+{{/useImportedInterface}}
+
+{{#operations}}
+{{#operation}}
+ {{#summary}}
+ /// {{{summary}}}
+ {{/summary}}
+ {{#notes}}
+ ///
+ /// {{{notes}}}
+ {{/notes}}
+ public func {{operationId}}(config : Config{{#allParams}}, {{paramName}} : {{#isNullable}}?{{/isNullable}}{{dataType}}{{/allParams}}) : async* {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}(){{/returnType}} {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "{{path}}"{{#pathParams}}
+ |> Text.replace(_, #text "{{=<% %>=}}{<%baseName%>}<%={{ }}=%>", {{#vendorExtensions.x-is-oneof-type}}{{{dataType}}}.toText({{paramName}}){{/vendorExtensions.x-is-oneof-type}}{{^vendorExtensions.x-is-oneof-type}}{{#isEnum}}{{{dataType}}}.toJSON({{paramName}}){{/isEnum}}{{#isEnumRef}}{{{dataType}}}.toJSON({{paramName}}){{/isEnumRef}}{{^isEnum}}{{^isEnumRef}}{{#isModel}}{{{dataType}}}.toJSON({{paramName}}){{/isModel}}{{^isModel}}{{#isString}}{{paramName}}{{/isString}}{{^isString}}{{#isInteger}}Int.toText({{paramName}}){{/isInteger}}{{^isInteger}}debug_show({{paramName}}){{/isInteger}}{{/isString}}{{/isModel}}{{/isEnumRef}}{{/isEnum}}{{/vendorExtensions.x-is-oneof-type}}){{/pathParams}}{{#queryParams}}{{#-first}}
+ # "?"{{/-first}}{{^-first}} # "&"{{/-first}} # "{{baseName}}=" # {{#vendorExtensions.x-is-oneof-type}}{{{dataType}}}.toText({{paramName}}){{/vendorExtensions.x-is-oneof-type}}{{^vendorExtensions.x-is-oneof-type}}{{#isEnum}}{{{dataType}}}.toJSON({{paramName}}){{/isEnum}}{{#isEnumRef}}{{{dataType}}}.toJSON({{paramName}}){{/isEnumRef}}{{^isEnum}}{{^isEnumRef}}{{#isModel}}{{{dataType}}}.toJSON({{paramName}}){{/isModel}}{{^isModel}}{{#isString}}{{paramName}}{{/isString}}{{^isString}}{{#isInteger}}Int.toText({{paramName}}){{/isInteger}}{{^isInteger}}debug_show({{paramName}}){{/isInteger}}{{/isString}}{{/isModel}}{{/isEnumRef}}{{/isEnum}}{{/vendorExtensions.x-is-oneof-type}}{{/queryParams}};
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ {{#apiKeyQueryName}}
+ case (?#apiKey(key)) {
+ let separator = if (Text.contains(baseUrl__, #text "?")) "&" else "?";
+ baseUrl__ # separator # "{{apiKeyQueryName}}=" # key
+ };
+ {{/apiKeyQueryName}}
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }{{#headerParams}},
+ { name = "{{baseName}}"; value = {{#isEnum}}{{{dataType}}}.toJSON({{paramName}}){{/isEnum}}{{#isEnumRef}}{{{dataType}}}.toJSON({{paramName}}){{/isEnumRef}}{{^isEnum}}{{^isEnumRef}}{{#isModel}}{{{dataType}}}.toJSON({{paramName}}){{/isModel}}{{^isModel}}{{#isString}}{{paramName}}{{/isString}}{{^isString}}debug_show({{paramName}}){{/isString}}{{/isModel}}{{/isEnumRef}}{{/isEnum}} }{{/headerParams}}
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ {{#apiKeyHeaderName}}
+ // API key goes in header
+ [{ name = "{{apiKeyHeaderName}}"; value = key }]
+ {{/apiKeyHeaderName}}
+ {{^apiKeyHeaderName}}
+ // API key goes in query parameter, not header
+ []
+ {{/apiKeyHeaderName}}
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}};
+ headers;
+ {{#isPut}}is_replicated = ?false; // PUT requires non-replicated mode on IC
+ {{/isPut}}{{#isDelete}}is_replicated = ?false; // DELETE requires non-replicated mode on IC
+ {{/isDelete}}body = {{#bodyParam}}do ? {
+ let jsonValue = {{#vendorExtensions.x-body-is-array}}{{#vendorExtensions.x-body-array-element-is-primitive}}{{paramName}}{{/vendorExtensions.x-body-array-element-is-primitive}}{{^vendorExtensions.x-body-array-element-is-primitive}}Array.map<{{{vendorExtensions.x-body-base-type}}}, {{{vendorExtensions.x-body-base-type}}}.JSON>({{paramName}}, {{{vendorExtensions.x-body-base-type}}}.toJSON){{/vendorExtensions.x-body-array-element-is-primitive}}{{/vendorExtensions.x-body-is-array}}{{^vendorExtensions.x-body-is-array}}{{#vendorExtensions.x-body-is-primitive}}{{paramName}}{{/vendorExtensions.x-body-is-primitive}}{{^vendorExtensions.x-body-is-primitive}}{{dataType}}.toJSON({{paramName}}){{/vendorExtensions.x-body-is-primitive}}{{/vendorExtensions.x-body-is-array}};
+ let candidBlob = to_candid(jsonValue);
+ let #ok(jsonText) = JSON.toText(candidBlob, [], null) else throw Error.reject("Failed to serialize to JSON");
+ Text.encodeUtf8(jsonText)
+ }{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}};
+ };
+
+ // Call the management canister's http_request method with cycles
+ {{#returnType}}let response : http_request_result = {{/returnType}}{{^returnType}}ignore {{/returnType}}await (with cycles) http_request(request);
+
+ {{#returnType}}
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ {{#vendorExtensions.x-return-is-array}}
+ {{#vendorExtensions.x-return-array-element-is-primitive}}
+ from_candid(_) : ?[{{{vendorExtensions.x-return-base-type}}}] |>
+ (switch (_) {
+ case (?result) result;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ {{/vendorExtensions.x-return-array-element-is-primitive}}
+ {{^vendorExtensions.x-return-array-element-is-primitive}}
+ from_candid(_) : ?[{{{vendorExtensions.x-return-base-type}}}.JSON] |>
+ (switch (_) {
+ case (?jsonArray) {
+ let converted = Array.filterMap<{{{vendorExtensions.x-return-base-type}}}.JSON, {{{vendorExtensions.x-return-base-type}}}>(jsonArray, {{{vendorExtensions.x-return-base-type}}}.fromJSON);
+ if (converted.size() != jsonArray.size()) {
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert some array elements to {{{vendorExtensions.x-return-base-type}}}");
+ };
+ converted
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ {{/vendorExtensions.x-return-array-element-is-primitive}}
+ {{/vendorExtensions.x-return-is-array}}
+ {{^vendorExtensions.x-return-is-array}}
+ {{#vendorExtensions.x-return-is-primitive}}
+ from_candid(_) : ?{{{returnType}}} |>
+ (switch (_) {
+ case (?result) result;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ {{/vendorExtensions.x-return-is-primitive}}
+ {{^vendorExtensions.x-return-is-primitive}}
+ {{#vendorExtensions.x-return-is-map}}
+ from_candid(_) : ?[(Text, {{{vendorExtensions.x-return-map-value-type}}})] |>
+ (switch (_) {
+ case (?pairs) fromIter(pairs.values(), Text.compare);
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ {{/vendorExtensions.x-return-is-map}}
+ {{^vendorExtensions.x-return-is-map}}
+ from_candid(_) : ?{{{returnType}}}.JSON |>
+ (switch (_) {
+ case (?jsonValue) {
+ switch ({{{returnType}}}.fromJSON(jsonValue)) {
+ case (?value) value;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert response to {{{returnType}}}");
+ }
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ {{/vendorExtensions.x-return-is-map}}
+ {{/vendorExtensions.x-return-is-primitive}}
+ {{/vendorExtensions.x-return-is-array}}
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+ {{#responses}}
+ {{^is2xx}}
+ {{#dataType}}
+ // Try parsing {{code}} response as {{{dataType}}}
+ if (response.status == {{code}}) {
+ let errorDetail = if (responseText != "") {
+ switch (JSON.fromText(responseText, null)) {
+ case (#ok(blob)) {
+ let parsedJson : ?{{{dataType}}}.JSON = from_candid(blob);
+ switch (parsedJson) {
+ case (?jsonValue) {
+ switch ({{{dataType}}}.fromJSON(jsonValue)) {
+ case (?err) " - " # debug_show(err);
+ case null " - " # responseText;
+ }
+ };
+ case null " - " # responseText;
+ };
+ };
+ case (#err(_)) " - " # responseText;
+ };
+ } else { "" };
+ throw Error.reject("HTTP {{code}}: {{message}}" # errorDetail);
+ };
+ {{/dataType}}
+ {{^dataType}}
+ // {{code}}: {{message}} (no response body model defined)
+ if (response.status == {{code}}) {
+ throw Error.reject("HTTP {{code}}: {{message}}");
+ };
+ {{/dataType}}
+ {{/is2xx}}
+ {{/responses}}
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ {{/returnType}}
+ };
+
+{{/operation}}
+{{/operations}}
+
+ let operations__ = {
+{{#operations}}
+{{#operation}}
+ {{operationId}};
+{{/operation}}
+{{/operations}}
+ };
+
+ public module class {{classname}}(config : Config) {
+{{#operations}}
+{{#operation}}
+ {{#summary}}
+ /// {{{summary}}}
+ {{/summary}}
+ {{#notes}}
+ ///
+ /// {{{notes}}}
+ {{/notes}}
+ public func {{operationId}}({{#allParams}}{{^-first}}, {{/-first}}{{paramName}} : {{#isNullable}}?{{/isNullable}}{{dataType}}{{/allParams}}) : async {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}(){{/returnType}} {
+ await* operations__.{{operationId}}(config{{#allParams}}, {{paramName}}{{/allParams}})
+ };
+
+{{/operation}}
+{{/operations}}
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/motoko/enum.mustache b/modules/openapi-generator/src/main/resources/motoko/enum.mustache
new file mode 100644
index 000000000000..5a4352f8c082
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/motoko/enum.mustache
@@ -0,0 +1,22 @@
+{{#models}}
+{{#model}}
+// {{classname}}.mo
+{{#description}}
+/// {{{description}}}
+{{/description}}
+/// Enum values: {{#allowableValues}}{{#enumVars}}#{{name}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}
+
+module {
+ public type {{classname}} = {
+{{#allowableValues}}
+{{#enumVars}}
+ {{#enumDescription}}
+ /// {{{enumDescription}}}
+ {{/enumDescription}}
+ #{{name}};
+{{/enumVars}}
+{{/allowableValues}}
+ };
+}
+{{/model}}
+{{/models}}
diff --git a/modules/openapi-generator/src/main/resources/motoko/icp.yaml.mustache b/modules/openapi-generator/src/main/resources/motoko/icp.yaml.mustache
new file mode 100644
index 000000000000..d1fcbc0856ef
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/motoko/icp.yaml.mustache
@@ -0,0 +1,20 @@
+# icp.yaml — generated by OpenAPI Generator (motoko)
+#
+# This file configures icp-cli (https://cli.internetcomputer.org) for a canister
+# that imports the {{artifactId}} generated client library.
+#
+# Usage:
+# icp network start -d # start local replica
+# icp deploy # build + deploy + sync
+# icp network stop # stop when done
+#
+# For mainnet: icp deploy -e ic
+
+canisters:
+ - name: {{#projectName}}{{projectName}}{{/projectName}}{{^projectName}}my_canister{{/projectName}}
+ recipe:
+ type: "@dfinity/motoko@v4.1.0"
+ configuration:
+ main: src/main.mo
+ # Declare the candid interface once generated:
+ # candid: {{#projectName}}{{projectName}}{{/projectName}}{{^projectName}}my_canister{{/projectName}}.did
diff --git a/modules/openapi-generator/src/main/resources/motoko/model.mustache b/modules/openapi-generator/src/main/resources/motoko/model.mustache
new file mode 100644
index 000000000000..65ba1e5e5adb
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/motoko/model.mustache
@@ -0,0 +1,209 @@
+{{#models}}
+{{#model}}
+{{#description}}
+/// {{{description}}}
+{{/description}}
+{{/model}}
+{{#imports}}
+{{#isMap}}import { type Map } "mo:core/pure/Map";
+{{/isMap}}{{^isMappedType}}
+import { type {{import}}; JSON = {{import}} } "./{{import}}";
+{{/isMappedType}}
+{{/imports}}
+{{#model}}{{#vendorExtensions.x-has-unsigned-fields}}
+import Int "mo:core/Int";
+{{/vendorExtensions.x-has-unsigned-fields}}
+
+{{#isEnum}}
+// {{classname}}.mo
+/// Enum values: {{#allowableValues}}{{#enumVars}}#{{name}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}
+
+module {
+ // User-facing type: type-safe variants for application code
+ public type {{classname}} = {
+{{#allowableValues}}
+{{#enumVars}}
+ {{#enumDescription}}
+ /// {{{enumDescription}}}
+ {{/enumDescription}}
+ #{{name}};
+{{/enumVars}}
+{{/allowableValues}}
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer {{classname}} type
+ public type JSON = {{dataType}};
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : {{classname}}) : JSON =
+ switch (value) {
+{{#allowableValues}}
+{{#enumVars}}
+ case (#{{name}}) {{#isString}}"{{value}}"{{/isString}}{{^isString}}{{value}}{{/isString}};
+{{/enumVars}}
+{{/allowableValues}}
+ };
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?{{classname}} =
+ switch (json) {
+{{#allowableValues}}
+{{#enumVars}}
+ case {{#isString}}"{{value}}"{{/isString}}{{^isString}}({{value}}){{/isString}} ?#{{name}};
+{{/enumVars}}
+{{/allowableValues}}
+ case _ null;
+ };
+ }
+}
+{{/isEnum}}
+{{^isEnum}}
+{{#vendorExtensions.x-is-oneof}}
+// {{classname}}.mo
+import Runtime "mo:core/Runtime";
+
+module {
+ // User-facing type: discriminated union (oneOf)
+ public type {{classname}} = {
+{{#vendorExtensions.oneOfVariants}}
+ {{#hasType}}#{{name}} : {{dataType}}{{/hasType}}{{^hasType}}#{{name}}{{/hasType}};
+{{/vendorExtensions.oneOfVariants}}
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // Convert oneOf variant to Text for URL parameters
+ public func toText(value : {{classname}}) : Text =
+ switch (value) {
+{{#vendorExtensions.oneOfVariants}}
+ case ({{#hasType}}#{{name}}(v){{/hasType}}{{^hasType}}#{{name}}{{/hasType}}) {{#hasType}}{{#isNumericType}}Int.toText(v){{/isNumericType}}{{#isEnumType}}{{dataType}}.toJSON(v){{/isEnumType}}{{#isObjectType}}Runtime.unreachable(){{/isObjectType}}{{/hasType}}{{^hasType}}"{{enumValue}}"{{/hasType}};
+{{/vendorExtensions.oneOfVariants}}
+ };
+
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer {{classname}} type
+ public type JSON = {
+{{#vendorExtensions.oneOfVariants}}
+ {{#hasType}}#{{name}} : {{jsonType}}{{/hasType}}{{^hasType}}#{{name}}{{/hasType}};
+{{/vendorExtensions.oneOfVariants}}
+ };
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : {{classname}}) : JSON =
+ switch (value) {
+{{#vendorExtensions.oneOfVariants}}
+ case ({{#hasType}}#{{name}}(v){{/hasType}}{{^hasType}}#{{name}}{{/hasType}}) {{#hasType}}#{{name}}{{#needsConversion}}(v){{/needsConversion}}{{^needsConversion}}(v){{/needsConversion}}{{/hasType}}{{^hasType}}#{{name}}{{/hasType}};
+{{/vendorExtensions.oneOfVariants}}
+ };
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?{{classname}} =
+ switch (json) {
+{{#vendorExtensions.oneOfVariants}}
+ case ({{#hasType}}#{{name}}(v){{/hasType}}{{^hasType}}#{{name}}{{/hasType}}) {{#hasType}}{{#isUnsigned}}if (v < 0) null else ?#{{name}}(Int.abs(v)){{/isUnsigned}}{{^isUnsigned}}?#{{name}}(v){{/isUnsigned}}{{/hasType}}{{^hasType}}?#{{name}}{{/hasType}};
+{{/vendorExtensions.oneOfVariants}}
+ };
+ }
+}
+{{/vendorExtensions.x-is-oneof}}
+{{^vendorExtensions.x-is-oneof}}
+// {{classname}}.mo
+
+module {
+ // User-facing type: what application code uses
+ public type {{classname}} = {
+{{#vars}}
+ {{#description}}
+ /// {{{description}}}
+ {{/description}}
+ {{name}} : {{^required}}?{{/required}}{{{dataType}}};
+{{/vars}}
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer {{classname}} type
+ public type JSON = {
+{{#vars}}
+ {{name}} : {{^required}}?{{/required}}{{#isEnumRef}}{{{datatypeWithEnum}}}.JSON{{/isEnumRef}}{{^isEnumRef}}{{#vendorExtensions.xNeedsJsonConversion}}{{{dataType}}}.JSON{{/vendorExtensions.xNeedsJsonConversion}}{{^vendorExtensions.xNeedsJsonConversion}}{{#vendorExtensions.x-is-unsigned}}Int{{/vendorExtensions.x-is-unsigned}}{{^vendorExtensions.x-is-unsigned}}{{{dataType}}}{{/vendorExtensions.x-is-unsigned}}{{/vendorExtensions.xNeedsJsonConversion}}{{/isEnumRef}};
+{{/vars}}
+ };
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : {{classname}}) : JSON = {{#vendorExtensions.xNoEnumFields}}value{{/vendorExtensions.xNoEnumFields}}{{^vendorExtensions.xNoEnumFields}}{{#vendorExtensions.xHasPartialTransform}}{ value with
+{{#vars}}
+{{#isEnumRef}}
+ {{name}} = {{#required}}{{{datatypeWithEnum}}}.toJSON(value.{{name}}){{/required}}{{^required}}do ? { {{{datatypeWithEnum}}}.toJSON(value.{{name}}!) }{{/required}};
+{{/isEnumRef}}
+{{^isEnumRef}}
+{{#vendorExtensions.xNeedsJsonConversion}}
+ {{name}} = {{#required}}{{{dataType}}}.toJSON(value.{{name}}){{/required}}{{^required}}do ? { {{{dataType}}}.toJSON(value.{{name}}!) }{{/required}};
+{{/vendorExtensions.xNeedsJsonConversion}}
+{{/isEnumRef}}
+{{/vars}}
+ }{{/vendorExtensions.xHasPartialTransform}}{{^vendorExtensions.xHasPartialTransform}}{
+{{#vars}}
+ {{name}} = {{#required}}{{#isEnumRef}}{{{datatypeWithEnum}}}.toJSON(value.{{name}}){{/isEnumRef}}{{^isEnumRef}}{{#vendorExtensions.xNeedsJsonConversion}}{{{dataType}}}.toJSON(value.{{name}}){{/vendorExtensions.xNeedsJsonConversion}}{{^vendorExtensions.xNeedsJsonConversion}}value.{{name}}{{/vendorExtensions.xNeedsJsonConversion}}{{/isEnumRef}}{{/required}}{{^required}}{{#isEnumRef}}do ? { {{{datatypeWithEnum}}}.toJSON(value.{{name}}!) }{{/isEnumRef}}{{^isEnumRef}}{{#vendorExtensions.xNeedsJsonConversion}}do ? { {{{dataType}}}.toJSON(value.{{name}}!) }{{/vendorExtensions.xNeedsJsonConversion}}{{^vendorExtensions.xNeedsJsonConversion}}value.{{name}}{{/vendorExtensions.xNeedsJsonConversion}}{{/isEnumRef}}{{/required}};
+{{/vars}}
+ }{{/vendorExtensions.xHasPartialTransform}}{{/vendorExtensions.xNoEnumFields}};
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?{{classname}} {{#vendorExtensions.xNoEnumFields}}= ?json{{/vendorExtensions.xNoEnumFields}}{{^vendorExtensions.xNoEnumFields}}{{#vendorExtensions.xHasPartialTransform}}{
+{{#vars}}
+{{#required}}
+{{#isEnumRef}}
+ let ?{{name}} = {{{datatypeWithEnum}}}.fromJSON(json.{{name}}) else return null;
+{{/isEnumRef}}
+{{^isEnumRef}}
+{{#vendorExtensions.xNeedsJsonConversion}}
+ let ?{{name}} = {{{dataType}}}.fromJSON(json.{{name}}) else return null;
+{{/vendorExtensions.xNeedsJsonConversion}}
+{{/isEnumRef}}
+{{/required}}
+{{/vars}}
+ ?{ json with
+{{#vars}}
+{{#isEnumRef}}
+ {{name}}{{#required}}{{/required}}{{^required}} = do ? { {{{datatypeWithEnum}}}.fromJSON(json.{{name}}!)! }{{/required}};
+{{/isEnumRef}}
+{{^isEnumRef}}
+{{#vendorExtensions.xNeedsJsonConversion}}
+ {{name}}{{#required}}{{/required}}{{^required}} = do ? { {{{dataType}}}.fromJSON(json.{{name}}!)! }{{/required}};
+{{/vendorExtensions.xNeedsJsonConversion}}
+{{^vendorExtensions.xNeedsJsonConversion}}
+{{#vendorExtensions.x-is-unsigned}}
+ {{name}}{{#required}} = if (json.{{name}} < 0) return null else Int.abs(json.{{name}}){{/required}}{{^required}} = switch (json.{{name}}) { case (?v) if (v < 0) null else ?Int.abs(v); case null null }{{/required}};
+{{/vendorExtensions.x-is-unsigned}}
+{{/vendorExtensions.xNeedsJsonConversion}}
+{{/isEnumRef}}
+{{/vars}}
+ }
+ }{{/vendorExtensions.xHasPartialTransform}}{{^vendorExtensions.xHasPartialTransform}}{
+{{#vars}}
+{{#required}}
+{{#isEnumRef}}
+ let ?{{name}} = {{{datatypeWithEnum}}}.fromJSON(json.{{name}}) else return null;
+{{/isEnumRef}}
+{{^isEnumRef}}
+{{#vendorExtensions.xNeedsJsonConversion}}
+ let ?{{name}} = {{{dataType}}}.fromJSON(json.{{name}}) else return null;
+{{/vendorExtensions.xNeedsJsonConversion}}
+{{/isEnumRef}}
+{{/required}}
+{{/vars}}
+ ?{
+{{#vars}}
+ {{name}}{{#required}}{{^isEnumRef}}{{^vendorExtensions.xNeedsJsonConversion}}{{#vendorExtensions.x-is-unsigned}} = if (json.{{name}} < 0) return null else Int.abs(json.{{name}}){{/vendorExtensions.x-is-unsigned}}{{^vendorExtensions.x-is-unsigned}} = json.{{name}}{{/vendorExtensions.x-is-unsigned}}{{/vendorExtensions.xNeedsJsonConversion}}{{/isEnumRef}}{{/required}}{{^required}} = {{#isEnumRef}}do ? { {{{datatypeWithEnum}}}.fromJSON(json.{{name}}!)! }{{/isEnumRef}}{{^isEnumRef}}{{#vendorExtensions.xNeedsJsonConversion}}do ? { {{{dataType}}}.fromJSON(json.{{name}}!)! }{{/vendorExtensions.xNeedsJsonConversion}}{{^vendorExtensions.xNeedsJsonConversion}}{{#vendorExtensions.x-is-unsigned}}do ? { let v = json.{{name}}!; if (v < 0) return null else Int.abs(v) }{{/vendorExtensions.x-is-unsigned}}{{^vendorExtensions.x-is-unsigned}}json.{{name}}{{/vendorExtensions.x-is-unsigned}}{{/vendorExtensions.xNeedsJsonConversion}}{{/isEnumRef}}{{/required}};
+{{/vars}}
+ }
+ }{{/vendorExtensions.xHasPartialTransform}}{{/vendorExtensions.xNoEnumFields}};
+ }
+}
+{{/vendorExtensions.x-is-oneof}}
+{{/isEnum}}
+{{/model}}
+{{/models}}
diff --git a/modules/openapi-generator/src/main/resources/motoko/mops.toml.mustache b/modules/openapi-generator/src/main/resources/motoko/mops.toml.mustache
new file mode 100644
index 000000000000..53ac65734ea0
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/motoko/mops.toml.mustache
@@ -0,0 +1,19 @@
+[toolchain]
+moc = "1.4.1"
+
+[package]
+name = "{{artifactId}}"
+version = "{{artifactVersion}}"
+description = "Generated Motoko client for {{appName}}"
+{{#artifactRepoUrl}}repository = "{{artifactRepoUrl}}"
+{{/artifactRepoUrl}}license = "Apache-2.0"
+files = ["src/Config.mo", "src/Apis/**/*.mo", "src/Models/**/*.mo"]
+
+[dependencies]
+core = "2.4.0"
+serde = "3.5.0"
+"cbor@4.1.0" = "4.1.0"
+"itertools@0.2.2" = "0.2.2" # because for serde
+base = "0.16.0" # because serde uses json.mo submodule
+xtended-numbers = "2.3.0"
+"sha2@0.1.6" = "0.1.6"
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/motoko/MotokoClientCodegenModelTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/motoko/MotokoClientCodegenModelTest.java
new file mode 100644
index 000000000000..ef8bf0ac9d87
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/motoko/MotokoClientCodegenModelTest.java
@@ -0,0 +1,46 @@
+package org.openapitools.codegen.motoko;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.media.*;
+import io.swagger.v3.parser.util.SchemaTypeUtil;
+import org.openapitools.codegen.*;
+import org.openapitools.codegen.languages.MotokoClientCodegen;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("static-method")
+public class MotokoClientCodegenModelTest {
+
+ @Test(description = "convert a simple java model")
+ public void simpleModelTest() {
+ final Schema schema = new Schema()
+ .description("a sample model")
+ .addProperty("id", new IntegerSchema().format(SchemaTypeUtil.INTEGER64_FORMAT))
+ .addProperty("name", new StringSchema())
+ .addRequiredItem("id")
+ .addRequiredItem("name");
+ final DefaultCodegen codegen = new MotokoClientCodegen();
+ OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", schema);
+ codegen.setOpenAPI(openAPI);
+
+ final CodegenModel cm = codegen.fromModel("sample", schema);
+
+ Assert.assertEquals(cm.name, "sample");
+ Assert.assertEquals(cm.classname, "Sample");
+ Assert.assertEquals(cm.description, "a sample model");
+ Assert.assertEquals(cm.vars.size(), 2);
+
+ final CodegenProperty property1 = cm.vars.get(0);
+ Assert.assertEquals(property1.baseName, "id");
+ Assert.assertEquals(property1.name, "id");
+ Assert.assertTrue(property1.required);
+
+ final CodegenProperty property2 = cm.vars.get(1);
+ Assert.assertEquals(property2.baseName, "name");
+ Assert.assertEquals(property2.name, "name");
+ Assert.assertTrue(property2.required);
+ }
+
+}
+
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/motoko/MotokoClientCodegenOptionsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/motoko/MotokoClientCodegenOptionsTest.java
new file mode 100644
index 000000000000..2602840fa555
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/motoko/MotokoClientCodegenOptionsTest.java
@@ -0,0 +1,44 @@
+package org.openapitools.codegen.motoko;
+
+import org.openapitools.codegen.AbstractOptionsTest;
+import org.openapitools.codegen.CodegenConfig;
+import org.openapitools.codegen.languages.MotokoClientCodegen;
+import org.openapitools.codegen.options.MotokoClientCodegenOptionsProvider;
+
+import static java.lang.Boolean.parseBoolean;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.openapitools.codegen.CodegenConstants.*;
+import static org.openapitools.codegen.languages.MotokoClientCodegen.*;
+import static org.openapitools.codegen.options.MotokoClientCodegenOptionsProvider.*;
+
+public class MotokoClientCodegenOptionsTest extends AbstractOptionsTest {
+ private MotokoClientCodegen codegen = mock(MotokoClientCodegen.class, mockSettings);
+
+ public MotokoClientCodegenOptionsTest() {
+ super(new MotokoClientCodegenOptionsProvider());
+ }
+
+ @Override
+ protected CodegenConfig getCodegenConfig() {
+ return codegen;
+ }
+
+ @SuppressWarnings("unused")
+ @Override
+ protected void verifyOptions() {
+ verify(codegen).setProjectName(PROJECT_NAME_VALUE);
+ verify(codegen).setSortParamsByRequiredFlag(parseBoolean(SORT_PARAMS_VALUE));
+ verify(codegen).setSortModelPropertiesByRequiredFlag(parseBoolean(SORT_MODEL_PROPERTIES_VALUE));
+ verify(codegen).setEnsureUniqueParams(parseBoolean(ENSURE_UNIQUE_PARAMS_VALUE));
+ verify(codegen).setAllowUnicodeIdentifiers(parseBoolean(ALLOW_UNICODE_IDENTIFIERS_VALUE));
+ verify(codegen).setPrependFormOrBodyParameters(parseBoolean(PREPEND_FORM_OR_BODY_PARAMETERS_VALUE));
+ verify(codegen).setLegacyDiscriminatorBehavior(parseBoolean(LEGACY_DISCRIMINATOR_BEHAVIOR_VALUE));
+ verify(codegen).setDisallowAdditionalPropertiesIfNotPresent(parseBoolean(DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT_VALUE));
+ verify(codegen).setEnumUnknownDefaultCase(parseBoolean(ENUM_UNKNOWN_DEFAULT_CASE_VALUE));
+ verify(codegen).setUseDfx(parseBoolean(USE_DFX_VALUE));
+ verify(codegen).setUseIcp(parseBoolean(USE_ICP_VALUE));
+ }
+}
+
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/motoko/MotokoClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/motoko/MotokoClientCodegenTest.java
new file mode 100644
index 000000000000..5975e59608f8
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/motoko/MotokoClientCodegenTest.java
@@ -0,0 +1,22 @@
+package org.openapitools.codegen.motoko;
+
+import org.openapitools.codegen.*;
+import org.openapitools.codegen.languages.MotokoClientCodegen;
+import io.swagger.models.*;
+import io.swagger.parser.SwaggerParser;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class MotokoClientCodegenTest {
+
+ MotokoClientCodegen codegen = new MotokoClientCodegen();
+
+ @Test
+ public void shouldSucceed() throws Exception {
+ codegen.processOpts();
+
+ Assert.assertEquals(codegen.getName(), "motoko");
+ Assert.assertEquals(codegen.getTag(), CodegenType.CLIENT);
+ Assert.assertNotNull(codegen.getHelp());
+ }
+}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/MotokoClientCodegenOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/MotokoClientCodegenOptionsProvider.java
new file mode 100644
index 000000000000..353c9fa41b30
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/MotokoClientCodegenOptionsProvider.java
@@ -0,0 +1,51 @@
+package org.openapitools.codegen.options;
+
+import org.openapitools.codegen.CodegenConstants;
+import org.openapitools.codegen.languages.MotokoClientCodegen;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+public class MotokoClientCodegenOptionsProvider implements OptionsProvider {
+ public static final String PROJECT_NAME_VALUE = "OpenAPI";
+ public static final String SORT_PARAMS_VALUE = "false";
+ public static final String SORT_MODEL_PROPERTIES_VALUE = "false";
+ public static final String ENSURE_UNIQUE_PARAMS_VALUE = "true";
+ public static final String ALLOW_UNICODE_IDENTIFIERS_VALUE = "false";
+ public static final String PREPEND_FORM_OR_BODY_PARAMETERS_VALUE = "true";
+ public static final String LEGACY_DISCRIMINATOR_BEHAVIOR_VALUE = "true";
+ public static final String DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT_VALUE = "true";
+ public static final String ENUM_UNKNOWN_DEFAULT_CASE_VALUE = "false";
+ public static final String USE_DFX_VALUE = "false";
+ public static final String USE_ICP_VALUE = "false";
+
+ @Override
+ public String getLanguage() {
+ return "motoko";
+ }
+
+ @Override
+ public Map createOptions() {
+ ImmutableMap.Builder builder = new ImmutableMap.Builder();
+ return builder
+ .put(MotokoClientCodegen.PROJECT_NAME, PROJECT_NAME_VALUE)
+ .put(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, SORT_PARAMS_VALUE)
+ .put(CodegenConstants.SORT_MODEL_PROPERTIES_BY_REQUIRED_FLAG, SORT_MODEL_PROPERTIES_VALUE)
+ .put(CodegenConstants.ENSURE_UNIQUE_PARAMS, ENSURE_UNIQUE_PARAMS_VALUE)
+ .put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
+ .put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)
+ .put(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, LEGACY_DISCRIMINATOR_BEHAVIOR_VALUE)
+ .put(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT_VALUE)
+ .put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, ENUM_UNKNOWN_DEFAULT_CASE_VALUE)
+ .put(MotokoClientCodegen.USE_DFX, USE_DFX_VALUE)
+ .put(MotokoClientCodegen.USE_ICP, USE_ICP_VALUE)
+ .build();
+ }
+
+ @Override
+ public boolean isServer() {
+ return false;
+ }
+}
+
diff --git a/samples/client/petstore/motoko/.openapi-generator-ignore b/samples/client/petstore/motoko/.openapi-generator-ignore
new file mode 100644
index 000000000000..7484ee590a38
--- /dev/null
+++ b/samples/client/petstore/motoko/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/samples/client/petstore/motoko/.openapi-generator/FILES b/samples/client/petstore/motoko/.openapi-generator/FILES
new file mode 100644
index 000000000000..e9053c270787
--- /dev/null
+++ b/samples/client/petstore/motoko/.openapi-generator/FILES
@@ -0,0 +1,16 @@
+.openapi-generator-ignore
+README.md
+mops.toml
+src/Apis/PetApi.mo
+src/Apis/StoreApi.mo
+src/Apis/UserApi.mo
+src/Config.mo
+src/Models/ApiResponse.mo
+src/Models/Category.mo
+src/Models/FindPetsByStatusStatusParameterInner.mo
+src/Models/Order.mo
+src/Models/OrderStatus.mo
+src/Models/Pet.mo
+src/Models/PetStatus.mo
+src/Models/Tag.mo
+src/Models/User.mo
diff --git a/samples/client/petstore/motoko/.openapi-generator/VERSION b/samples/client/petstore/motoko/.openapi-generator/VERSION
new file mode 100644
index 000000000000..f7962df3e243
--- /dev/null
+++ b/samples/client/petstore/motoko/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.22.0-SNAPSHOT
diff --git a/samples/client/petstore/motoko/README.md b/samples/client/petstore/motoko/README.md
new file mode 100644
index 000000000000..f40e1cfb9eba
--- /dev/null
+++ b/samples/client/petstore/motoko/README.md
@@ -0,0 +1,83 @@
+# OpenAPI Petstore
+
+This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+
+This Motoko client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project.
+
+- API version: 1.0.0
+- Generator version: 7.22.0-SNAPSHOT
+- Build package: org.openapitools.codegen.languages.MotokoClientCodegen
+
+## Models
+
+- ApiResponse
+- Category
+- FindPetsByStatusStatusParameterInner
+- Order
+- OrderStatus
+- Pet
+- PetStatus
+- Tag
+- User
+
+## APIs
+
+- PetApi
+- StoreApi
+- UserApi
+
+## Installation
+
+This is a Motoko module that can be used in your Internet Computer project.
+
+## Usage
+
+Import the generated API modules in your Motoko code:
+
+```motoko
+import SomeApi "mo:petstore-client-nodfx/Apis/SomeApi";
+// or using destructuring for specific functions
+import { someFunction } "mo:petstore-client-nodfx/Apis/SomeApi";
+```
+
+Configure and call the API:
+
+```motoko
+import { defaultConfig } "mo:petstore-client-nodfx/Config";
+
+// Use the default config as-is, or customize specific fields:
+let config = { defaultConfig with auth = ?#bearer "my-token" };
+
+let result = await* SomeApi.someFunction(config, ...);
+```
+
+The `defaultConfig` has `baseUrl` pre-set to the API's base URL, `cycles = 30_000_000_000`, and all optional fields set to `null`.
+
+Alternatively, use the suite-based API to bind config once and call multiple functions without threading it through each call:
+
+```motoko
+import { SomeApi } "mo:petstore-client-nodfx/Apis/SomeApi";
+
+let api = SomeApi(config);
+let result = await api.someFunction(...);
+let other = await api.anotherFunction(...);
+```
+
+### HTTP Outcalls and Cycles
+
+The generated API client makes HTTP outcalls using the Internet Computer's management canister. HTTP outcalls require cycles to execute.
+
+**Important:** Before calling any API endpoints, ensure your canister has sufficient cycles:
+
+For local development:
+```bash
+# Get your canister ID
+CANISTER_ID=$(dfx canister id your_canister_name)
+
+# Add cycles (100 trillion cycles for testing)
+dfx ledger fabricate-cycles --canister "$CANISTER_ID" --amount 100000000000000
+```
+
+For production deployment, you'll need to fund your canister with cycles through the NNS or cycles wallet.
+
+Each HTTP outcall typically costs around 20-50 million cycles depending on the request/response size.
diff --git a/samples/client/petstore/motoko/mops.toml b/samples/client/petstore/motoko/mops.toml
new file mode 100644
index 000000000000..1412e2b4bca1
--- /dev/null
+++ b/samples/client/petstore/motoko/mops.toml
@@ -0,0 +1,18 @@
+[toolchain]
+moc = "1.4.1"
+
+[package]
+name = "petstore-client-nodfx"
+version = "1.0.0"
+description = "Generated Motoko client for OpenAPI Petstore"
+license = "Apache-2.0"
+files = ["src/Config.mo", "src/Apis/**/*.mo", "src/Models/**/*.mo"]
+
+[dependencies]
+core = "2.4.0"
+serde = "3.5.0"
+"cbor@4.1.0" = "4.1.0"
+"itertools@0.2.2" = "0.2.2" # because for serde
+base = "0.16.0" # because serde uses json.mo submodule
+xtended-numbers = "2.3.0"
+"sha2@0.1.6" = "0.1.6"
diff --git a/samples/client/petstore/motoko/src/Apis/PetApi.mo b/samples/client/petstore/motoko/src/Apis/PetApi.mo
new file mode 100644
index 000000000000..3e711fe2fa82
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Apis/PetApi.mo
@@ -0,0 +1,764 @@
+// PetApi.mo
+
+import Text "mo:core/Text";
+import Int "mo:core/Int";
+import Blob "mo:core/Blob";
+import Array "mo:core/Array";
+import Error "mo:core/Error";
+import Base64 "mo:core/Base64";
+import { JSON } "mo:serde";
+import { type ApiResponse; JSON = ApiResponse } "../Models/ApiResponse";
+import { type FindPetsByStatusStatusParameterInner; JSON = FindPetsByStatusStatusParameterInner } "../Models/FindPetsByStatusStatusParameterInner";
+import { type Pet; JSON = Pet } "../Models/Pet";
+import { type Config } "../Config";
+
+module {
+ // Management Canister interface for HTTP outcalls
+ // Based on https://github.com/dfinity/interface-spec/blob/master/spec/ic.did
+ type http_header = {
+ name : Text;
+ value : Text;
+ };
+
+ type http_method = {
+ #get;
+ #head;
+ #post;
+ #put; // Non-replicated only (is_replicated forced to ?false in generated code)
+ #delete; // Non-replicated only (is_replicated forced to ?false in generated code)
+ };
+
+ type http_request_args = {
+ url : Text;
+ max_response_bytes : ?Nat64;
+ method : http_method;
+ headers : [http_header];
+ body : ?Blob;
+ transform : ?{
+ function : shared query ({ response : http_request_result; context : Blob }) -> async http_request_result;
+ context : Blob;
+ };
+ is_replicated : ?Bool;
+ };
+
+ type http_request_result = {
+ status : Nat;
+ headers : [http_header];
+ body : Blob;
+ };
+
+ let http_request = (actor "aaaaa-aa" : actor { http_request : (http_request_args) -> async http_request_result }).http_request;
+
+
+ /// Add a new pet to the store
+ ///
+ ///
+ public func addPet(config : Config, pet : Pet) : async* Pet {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/pet";
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #post;
+ headers;
+ body = do ? {
+ let jsonValue = Pet.toJSON(pet);
+ let candidBlob = to_candid(jsonValue);
+ let #ok(jsonText) = JSON.toText(candidBlob, [], null) else throw Error.reject("Failed to serialize to JSON");
+ Text.encodeUtf8(jsonText)
+ };
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?Pet.JSON |>
+ (switch (_) {
+ case (?jsonValue) {
+ switch (Pet.fromJSON(jsonValue)) {
+ case (?value) value;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert response to Pet");
+ }
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+ // 405: Invalid input (no response body model defined)
+ if (response.status == 405) {
+ throw Error.reject("HTTP 405: Invalid input");
+ };
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+ /// Deletes a pet
+ ///
+ ///
+ public func deletePet(config : Config, petId : Int, apiKey : Text) : async* () {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/pet/{petId}"
+ |> Text.replace(_, #text "{petId}", debug_show(petId));
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" },
+ { name = "api_key"; value = apiKey }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #delete;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ ignore await (with cycles) http_request(request);
+
+ };
+
+ /// Finds Pets by status
+ ///
+ /// Multiple status values can be provided with comma separated strings
+ public func findPetsByStatus(config : Config, status : [FindPetsByStatusStatusParameterInner]) : async* [Pet] {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/pet/findByStatus"
+ # "?" # "status=" # debug_show(status);
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #get;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?[Pet.JSON] |>
+ (switch (_) {
+ case (?jsonArray) {
+ let converted = Array.filterMap(jsonArray, Pet.fromJSON);
+ if (converted.size() != jsonArray.size()) {
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert some array elements to Pet");
+ };
+ converted
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+ // 400: Invalid status value (no response body model defined)
+ if (response.status == 400) {
+ throw Error.reject("HTTP 400: Invalid status value");
+ };
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+ /// Finds Pets by tags
+ ///
+ /// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
+ public func findPetsByTags(config : Config, tags : [Text]) : async* [Pet] {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/pet/findByTags"
+ # "?" # "tags=" # debug_show(tags);
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #get;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?[Pet.JSON] |>
+ (switch (_) {
+ case (?jsonArray) {
+ let converted = Array.filterMap(jsonArray, Pet.fromJSON);
+ if (converted.size() != jsonArray.size()) {
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert some array elements to Pet");
+ };
+ converted
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+ // 400: Invalid tag value (no response body model defined)
+ if (response.status == 400) {
+ throw Error.reject("HTTP 400: Invalid tag value");
+ };
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+ /// Find pet by ID
+ ///
+ /// Returns a single pet
+ public func getPetById(config : Config, petId : Int) : async* Pet {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/pet/{petId}"
+ |> Text.replace(_, #text "{petId}", debug_show(petId));
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #get;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?Pet.JSON |>
+ (switch (_) {
+ case (?jsonValue) {
+ switch (Pet.fromJSON(jsonValue)) {
+ case (?value) value;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert response to Pet");
+ }
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+ // 400: Invalid ID supplied (no response body model defined)
+ if (response.status == 400) {
+ throw Error.reject("HTTP 400: Invalid ID supplied");
+ };
+ // 404: Pet not found (no response body model defined)
+ if (response.status == 404) {
+ throw Error.reject("HTTP 404: Pet not found");
+ };
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+ /// Update an existing pet
+ ///
+ ///
+ public func updatePet(config : Config, pet : Pet) : async* Pet {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/pet";
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #put;
+ headers;
+ body = do ? {
+ let jsonValue = Pet.toJSON(pet);
+ let candidBlob = to_candid(jsonValue);
+ let #ok(jsonText) = JSON.toText(candidBlob, [], null) else throw Error.reject("Failed to serialize to JSON");
+ Text.encodeUtf8(jsonText)
+ };
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?Pet.JSON |>
+ (switch (_) {
+ case (?jsonValue) {
+ switch (Pet.fromJSON(jsonValue)) {
+ case (?value) value;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert response to Pet");
+ }
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+ // 400: Invalid ID supplied (no response body model defined)
+ if (response.status == 400) {
+ throw Error.reject("HTTP 400: Invalid ID supplied");
+ };
+ // 404: Pet not found (no response body model defined)
+ if (response.status == 404) {
+ throw Error.reject("HTTP 404: Pet not found");
+ };
+ // 405: Validation exception (no response body model defined)
+ if (response.status == 405) {
+ throw Error.reject("HTTP 405: Validation exception");
+ };
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+ /// Updates a pet in the store with form data
+ ///
+ ///
+ public func updatePetWithForm(config : Config, petId : Int, name : Text, status : Text) : async* () {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/pet/{petId}"
+ |> Text.replace(_, #text "{petId}", debug_show(petId));
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #post;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ ignore await (with cycles) http_request(request);
+
+ };
+
+ /// uploads an image
+ ///
+ ///
+ public func uploadFile(config : Config, petId : Int, additionalMetadata : Text, file : Blob) : async* ApiResponse {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/pet/{petId}/uploadImage"
+ |> Text.replace(_, #text "{petId}", debug_show(petId));
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #post;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?ApiResponse.JSON |>
+ (switch (_) {
+ case (?jsonValue) {
+ switch (ApiResponse.fromJSON(jsonValue)) {
+ case (?value) value;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert response to ApiResponse");
+ }
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+
+ let operations__ = {
+ addPet;
+ deletePet;
+ findPetsByStatus;
+ findPetsByTags;
+ getPetById;
+ updatePet;
+ updatePetWithForm;
+ uploadFile;
+ };
+
+ public module class PetApi(config : Config) {
+ /// Add a new pet to the store
+ ///
+ ///
+ public func addPet(pet : Pet) : async Pet {
+ await* operations__.addPet(config, pet)
+ };
+
+ /// Deletes a pet
+ ///
+ ///
+ public func deletePet(petId : Int, apiKey : Text) : async () {
+ await* operations__.deletePet(config, petId, apiKey)
+ };
+
+ /// Finds Pets by status
+ ///
+ /// Multiple status values can be provided with comma separated strings
+ public func findPetsByStatus(status : [FindPetsByStatusStatusParameterInner]) : async [Pet] {
+ await* operations__.findPetsByStatus(config, status)
+ };
+
+ /// Finds Pets by tags
+ ///
+ /// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
+ public func findPetsByTags(tags : [Text]) : async [Pet] {
+ await* operations__.findPetsByTags(config, tags)
+ };
+
+ /// Find pet by ID
+ ///
+ /// Returns a single pet
+ public func getPetById(petId : Int) : async Pet {
+ await* operations__.getPetById(config, petId)
+ };
+
+ /// Update an existing pet
+ ///
+ ///
+ public func updatePet(pet : Pet) : async Pet {
+ await* operations__.updatePet(config, pet)
+ };
+
+ /// Updates a pet in the store with form data
+ ///
+ ///
+ public func updatePetWithForm(petId : Int, name : Text, status : Text) : async () {
+ await* operations__.updatePetWithForm(config, petId, name, status)
+ };
+
+ /// uploads an image
+ ///
+ ///
+ public func uploadFile(petId : Int, additionalMetadata : Text, file : Blob) : async ApiResponse {
+ await* operations__.uploadFile(config, petId, additionalMetadata, file)
+ };
+
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Apis/StoreApi.mo b/samples/client/petstore/motoko/src/Apis/StoreApi.mo
new file mode 100644
index 000000000000..ffffe0a3adf5
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Apis/StoreApi.mo
@@ -0,0 +1,399 @@
+// StoreApi.mo
+
+import Text "mo:core/Text";
+import Int "mo:core/Int";
+import Blob "mo:core/Blob";
+import Array "mo:core/Array";
+import Error "mo:core/Error";
+import Base64 "mo:core/Base64";
+import { JSON } "mo:serde";
+import { type Order; JSON = Order } "../Models/Order";
+import { type Map; fromIter } "mo:core/pure/Map";
+import { type Config } "../Config";
+
+module {
+ // Management Canister interface for HTTP outcalls
+ // Based on https://github.com/dfinity/interface-spec/blob/master/spec/ic.did
+ type http_header = {
+ name : Text;
+ value : Text;
+ };
+
+ type http_method = {
+ #get;
+ #head;
+ #post;
+ #put; // Non-replicated only (is_replicated forced to ?false in generated code)
+ #delete; // Non-replicated only (is_replicated forced to ?false in generated code)
+ };
+
+ type http_request_args = {
+ url : Text;
+ max_response_bytes : ?Nat64;
+ method : http_method;
+ headers : [http_header];
+ body : ?Blob;
+ transform : ?{
+ function : shared query ({ response : http_request_result; context : Blob }) -> async http_request_result;
+ context : Blob;
+ };
+ is_replicated : ?Bool;
+ };
+
+ type http_request_result = {
+ status : Nat;
+ headers : [http_header];
+ body : Blob;
+ };
+
+ let http_request = (actor "aaaaa-aa" : actor { http_request : (http_request_args) -> async http_request_result }).http_request;
+
+
+ /// Delete purchase order by ID
+ ///
+ /// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
+ public func deleteOrder(config : Config, orderId : Text) : async* () {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/store/order/{orderId}"
+ |> Text.replace(_, #text "{orderId}", orderId);
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #delete;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ ignore await (with cycles) http_request(request);
+
+ };
+
+ /// Returns pet inventories by status
+ ///
+ /// Returns a map of status codes to quantities
+ public func getInventory(config : Config) : async* Map {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/store/inventory";
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #get;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?[(Text, Int)] |>
+ (switch (_) {
+ case (?pairs) fromIter(pairs.values(), Text.compare);
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+ /// Find purchase order by ID
+ ///
+ /// For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions
+ public func getOrderById(config : Config, orderId : Nat) : async* Order {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/store/order/{orderId}"
+ |> Text.replace(_, #text "{orderId}", debug_show(orderId));
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #get;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?Order.JSON |>
+ (switch (_) {
+ case (?jsonValue) {
+ switch (Order.fromJSON(jsonValue)) {
+ case (?value) value;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert response to Order");
+ }
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+ // 400: Invalid ID supplied (no response body model defined)
+ if (response.status == 400) {
+ throw Error.reject("HTTP 400: Invalid ID supplied");
+ };
+ // 404: Order not found (no response body model defined)
+ if (response.status == 404) {
+ throw Error.reject("HTTP 404: Order not found");
+ };
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+ /// Place an order for a pet
+ ///
+ ///
+ public func placeOrder(config : Config, order : Order) : async* Order {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/store/order";
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #post;
+ headers;
+ body = do ? {
+ let jsonValue = Order.toJSON(order);
+ let candidBlob = to_candid(jsonValue);
+ let #ok(jsonText) = JSON.toText(candidBlob, [], null) else throw Error.reject("Failed to serialize to JSON");
+ Text.encodeUtf8(jsonText)
+ };
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?Order.JSON |>
+ (switch (_) {
+ case (?jsonValue) {
+ switch (Order.fromJSON(jsonValue)) {
+ case (?value) value;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert response to Order");
+ }
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+ // 400: Invalid Order (no response body model defined)
+ if (response.status == 400) {
+ throw Error.reject("HTTP 400: Invalid Order");
+ };
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+
+ let operations__ = {
+ deleteOrder;
+ getInventory;
+ getOrderById;
+ placeOrder;
+ };
+
+ public module class StoreApi(config : Config) {
+ /// Delete purchase order by ID
+ ///
+ /// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
+ public func deleteOrder(orderId : Text) : async () {
+ await* operations__.deleteOrder(config, orderId)
+ };
+
+ /// Returns pet inventories by status
+ ///
+ /// Returns a map of status codes to quantities
+ public func getInventory() : async Map {
+ await* operations__.getInventory(config)
+ };
+
+ /// Find purchase order by ID
+ ///
+ /// For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions
+ public func getOrderById(orderId : Nat) : async Order {
+ await* operations__.getOrderById(config, orderId)
+ };
+
+ /// Place an order for a pet
+ ///
+ ///
+ public func placeOrder(order : Order) : async Order {
+ await* operations__.placeOrder(config, order)
+ };
+
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Apis/UserApi.mo b/samples/client/petstore/motoko/src/Apis/UserApi.mo
new file mode 100644
index 000000000000..a84f76f79b23
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Apis/UserApi.mo
@@ -0,0 +1,610 @@
+// UserApi.mo
+
+import Text "mo:core/Text";
+import Int "mo:core/Int";
+import Blob "mo:core/Blob";
+import Array "mo:core/Array";
+import Error "mo:core/Error";
+import Base64 "mo:core/Base64";
+import { JSON } "mo:serde";
+import { type User; JSON = User } "../Models/User";
+import { type Config } "../Config";
+
+module {
+ // Management Canister interface for HTTP outcalls
+ // Based on https://github.com/dfinity/interface-spec/blob/master/spec/ic.did
+ type http_header = {
+ name : Text;
+ value : Text;
+ };
+
+ type http_method = {
+ #get;
+ #head;
+ #post;
+ #put; // Non-replicated only (is_replicated forced to ?false in generated code)
+ #delete; // Non-replicated only (is_replicated forced to ?false in generated code)
+ };
+
+ type http_request_args = {
+ url : Text;
+ max_response_bytes : ?Nat64;
+ method : http_method;
+ headers : [http_header];
+ body : ?Blob;
+ transform : ?{
+ function : shared query ({ response : http_request_result; context : Blob }) -> async http_request_result;
+ context : Blob;
+ };
+ is_replicated : ?Bool;
+ };
+
+ type http_request_result = {
+ status : Nat;
+ headers : [http_header];
+ body : Blob;
+ };
+
+ let http_request = (actor "aaaaa-aa" : actor { http_request : (http_request_args) -> async http_request_result }).http_request;
+
+
+ /// Create user
+ ///
+ /// This can only be done by the logged in user.
+ public func createUser(config : Config, user : User) : async* () {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/user";
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #post;
+ headers;
+ body = do ? {
+ let jsonValue = User.toJSON(user);
+ let candidBlob = to_candid(jsonValue);
+ let #ok(jsonText) = JSON.toText(candidBlob, [], null) else throw Error.reject("Failed to serialize to JSON");
+ Text.encodeUtf8(jsonText)
+ };
+ };
+
+ // Call the management canister's http_request method with cycles
+ ignore await (with cycles) http_request(request);
+
+ };
+
+ /// Creates list of users with given input array
+ ///
+ ///
+ public func createUsersWithArrayInput(config : Config, user : [User]) : async* () {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/user/createWithArray";
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #post;
+ headers;
+ body = do ? {
+ let jsonValue = Array.map(user, User.toJSON);
+ let candidBlob = to_candid(jsonValue);
+ let #ok(jsonText) = JSON.toText(candidBlob, [], null) else throw Error.reject("Failed to serialize to JSON");
+ Text.encodeUtf8(jsonText)
+ };
+ };
+
+ // Call the management canister's http_request method with cycles
+ ignore await (with cycles) http_request(request);
+
+ };
+
+ /// Creates list of users with given input array
+ ///
+ ///
+ public func createUsersWithListInput(config : Config, user : [User]) : async* () {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/user/createWithList";
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #post;
+ headers;
+ body = do ? {
+ let jsonValue = Array.map(user, User.toJSON);
+ let candidBlob = to_candid(jsonValue);
+ let #ok(jsonText) = JSON.toText(candidBlob, [], null) else throw Error.reject("Failed to serialize to JSON");
+ Text.encodeUtf8(jsonText)
+ };
+ };
+
+ // Call the management canister's http_request method with cycles
+ ignore await (with cycles) http_request(request);
+
+ };
+
+ /// Delete user
+ ///
+ /// This can only be done by the logged in user.
+ public func deleteUser(config : Config, username : Text) : async* () {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/user/{username}"
+ |> Text.replace(_, #text "{username}", username);
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #delete;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ ignore await (with cycles) http_request(request);
+
+ };
+
+ /// Get user by user name
+ ///
+ ///
+ public func getUserByName(config : Config, username : Text) : async* User {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/user/{username}"
+ |> Text.replace(_, #text "{username}", username);
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #get;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?User.JSON |>
+ (switch (_) {
+ case (?jsonValue) {
+ switch (User.fromJSON(jsonValue)) {
+ case (?value) value;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to convert response to User");
+ }
+ };
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+ // 400: Invalid username supplied (no response body model defined)
+ if (response.status == 400) {
+ throw Error.reject("HTTP 400: Invalid username supplied");
+ };
+ // 404: User not found (no response body model defined)
+ if (response.status == 404) {
+ throw Error.reject("HTTP 404: User not found");
+ };
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+ /// Logs user into the system
+ ///
+ ///
+ public func loginUser(config : Config, username : Text, password : Text) : async* Text {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/user/login"
+ # "?" # "username=" # username # "&" # "password=" # password;
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #get;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ let response : http_request_result = await (with cycles) http_request(request);
+
+ // Check HTTP status code before parsing
+ if (response.status >= 200 and response.status < 300) {
+ // Success response (2xx): parse as expected return type
+ (switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to decode response body as UTF-8");
+ }) |>
+ (switch (JSON.fromText(_, null)) {
+ case (#ok(blob)) blob;
+ case (#err(msg)) throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to parse JSON: " # msg);
+ }) |>
+ from_candid(_) : ?Text |>
+ (switch (_) {
+ case (?result) result;
+ case null throw Error.reject("HTTP " # Int.toText(response.status) # ": Failed to deserialize response");
+ })
+ } else {
+ // Error response (4xx, 5xx): parse error models and throw
+ let responseText = switch (Text.decodeUtf8(response.body)) {
+ case (?text) text;
+ case null ""; // Empty body for some errors (e.g., 404)
+ };
+
+ // 400: Invalid username/password supplied (no response body model defined)
+ if (response.status == 400) {
+ throw Error.reject("HTTP 400: Invalid username/password supplied");
+ };
+
+ // Fallback for status codes not defined in OpenAPI spec
+ throw Error.reject("HTTP " # Int.toText(response.status) # ": Unexpected error" #
+ (if (responseText != "") { " - " # responseText } else { "" }));
+ }
+ };
+
+ /// Logs out current logged in user session
+ ///
+ ///
+ public func logoutUser(config : Config) : async* () {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/user/logout";
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #get;
+ headers;
+ body = null;
+ };
+
+ // Call the management canister's http_request method with cycles
+ ignore await (with cycles) http_request(request);
+
+ };
+
+ /// Updated user
+ ///
+ /// This can only be done by the logged in user.
+ public func updateUser(config : Config, username : Text, user : User) : async* () {
+ let {baseUrl; cycles} = config;
+ let baseUrl__ = baseUrl # "/user/{username}"
+ |> Text.replace(_, #text "{username}", username);
+
+ // Add API key as query parameter if using apiKey auth
+ let url = switch (config.auth) {
+ case _ baseUrl__;
+ };
+
+ let baseHeaders = [
+ { name = "Content-Type"; value = "application/json; charset=utf-8" }
+ ];
+
+ // Build authentication headers based on auth type
+ let authHeaders = switch (config.auth) {
+ case (?#bearer(token)) {
+ [{ name = "Authorization"; value = "Bearer " # token }]
+ };
+ case (?#apiKey(key)) {
+ // API key goes in header
+ [{ name = "api_key"; value = key }]
+ };
+ case (?#basicAuth({user; password})) {
+ let encoded = Base64.encode(Text.encodeUtf8(user # ":" # password));
+ [{ name = "Authorization"; value = "Basic " # encoded }]
+ };
+ case null [];
+ };
+
+ let headers = Array.flatten([
+ baseHeaders,
+ authHeaders
+ ]);
+
+ let request : http_request_args = { config with
+ url;
+ method = #put;
+ headers;
+ body = do ? {
+ let jsonValue = User.toJSON(user);
+ let candidBlob = to_candid(jsonValue);
+ let #ok(jsonText) = JSON.toText(candidBlob, [], null) else throw Error.reject("Failed to serialize to JSON");
+ Text.encodeUtf8(jsonText)
+ };
+ };
+
+ // Call the management canister's http_request method with cycles
+ ignore await (with cycles) http_request(request);
+
+ };
+
+
+ let operations__ = {
+ createUser;
+ createUsersWithArrayInput;
+ createUsersWithListInput;
+ deleteUser;
+ getUserByName;
+ loginUser;
+ logoutUser;
+ updateUser;
+ };
+
+ public module class UserApi(config : Config) {
+ /// Create user
+ ///
+ /// This can only be done by the logged in user.
+ public func createUser(user : User) : async () {
+ await* operations__.createUser(config, user)
+ };
+
+ /// Creates list of users with given input array
+ ///
+ ///
+ public func createUsersWithArrayInput(user : [User]) : async () {
+ await* operations__.createUsersWithArrayInput(config, user)
+ };
+
+ /// Creates list of users with given input array
+ ///
+ ///
+ public func createUsersWithListInput(user : [User]) : async () {
+ await* operations__.createUsersWithListInput(config, user)
+ };
+
+ /// Delete user
+ ///
+ /// This can only be done by the logged in user.
+ public func deleteUser(username : Text) : async () {
+ await* operations__.deleteUser(config, username)
+ };
+
+ /// Get user by user name
+ ///
+ ///
+ public func getUserByName(username : Text) : async User {
+ await* operations__.getUserByName(config, username)
+ };
+
+ /// Logs user into the system
+ ///
+ ///
+ public func loginUser(username : Text, password : Text) : async Text {
+ await* operations__.loginUser(config, username, password)
+ };
+
+ /// Logs out current logged in user session
+ ///
+ ///
+ public func logoutUser() : async () {
+ await* operations__.logoutUser(config)
+ };
+
+ /// Updated user
+ ///
+ /// This can only be done by the logged in user.
+ public func updateUser(username : Text, user : User) : async () {
+ await* operations__.updateUser(config, username, user)
+ };
+
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Config.mo b/samples/client/petstore/motoko/src/Config.mo
new file mode 100644
index 000000000000..05bb834b0b81
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Config.mo
@@ -0,0 +1,36 @@
+// Config.mo — shared configuration types and default config for OpenAPI Petstore
+
+module {
+ public type Auth = {
+ #bearer : Text;
+ #apiKey : Text;
+ #basicAuth : { user : Text; password : Text };
+ };
+
+ type http_header = { name : Text; value : Text };
+ type http_request_result = { status : Nat; headers : [http_header]; body : Blob };
+
+ public type Config = {
+ baseUrl : Text;
+ auth : ?Auth;
+ max_response_bytes : ?Nat64;
+ transform : ?{
+ function : shared query ({ response : http_request_result; context : Blob }) -> async http_request_result;
+ context : Blob;
+ };
+ is_replicated : ?Bool;
+ cycles : Nat;
+ };
+
+ /// Default configuration for OpenAPI Petstore.
+ /// Customize with record update syntax:
+ /// { defaultConfig with auth = ?#bearer "my-token" }
+ public let defaultConfig : Config = {
+ baseUrl = "http://petstore.swagger.io/v2";
+ auth = null;
+ max_response_bytes = null;
+ transform = null;
+ is_replicated = null;
+ cycles = 30_000_000_000;
+ };
+}
diff --git a/samples/client/petstore/motoko/src/Models/ApiResponse.mo b/samples/client/petstore/motoko/src/Models/ApiResponse.mo
new file mode 100644
index 000000000000..de5c551fb0ea
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Models/ApiResponse.mo
@@ -0,0 +1,29 @@
+/// Describes the result of uploading an image resource
+
+// ApiResponse.mo
+
+module {
+ // User-facing type: what application code uses
+ public type ApiResponse = {
+ code : ?Int;
+ type_ : ?Text;
+ message : ?Text;
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer ApiResponse type
+ public type JSON = {
+ code : ?Int;
+ type_ : ?Text;
+ message : ?Text;
+ };
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : ApiResponse) : JSON = value;
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?ApiResponse = ?json;
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Models/Category.mo b/samples/client/petstore/motoko/src/Models/Category.mo
new file mode 100644
index 000000000000..0ebb1cd1bff5
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Models/Category.mo
@@ -0,0 +1,27 @@
+/// A category for a pet
+
+// Category.mo
+
+module {
+ // User-facing type: what application code uses
+ public type Category = {
+ id : ?Int;
+ name : ?Text;
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer Category type
+ public type JSON = {
+ id : ?Int;
+ name : ?Text;
+ };
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : Category) : JSON = value;
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?Category = ?json;
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Models/FindPetsByStatusStatusParameterInner.mo b/samples/client/petstore/motoko/src/Models/FindPetsByStatusStatusParameterInner.mo
new file mode 100644
index 000000000000..e21988c8f162
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Models/FindPetsByStatusStatusParameterInner.mo
@@ -0,0 +1,36 @@
+
+// FindPetsByStatusStatusParameterInner.mo
+/// Enum values: #available, #pending, #sold
+
+module {
+ // User-facing type: type-safe variants for application code
+ public type FindPetsByStatusStatusParameterInner = {
+ #available;
+ #pending;
+ #sold;
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer FindPetsByStatusStatusParameterInner type
+ public type JSON = Text;
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : FindPetsByStatusStatusParameterInner) : JSON =
+ switch (value) {
+ case (#available) "available";
+ case (#pending) "pending";
+ case (#sold) "sold";
+ };
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?FindPetsByStatusStatusParameterInner =
+ switch (json) {
+ case "available" ?#available;
+ case "pending" ?#pending;
+ case "sold" ?#sold;
+ case _ null;
+ };
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Models/Order.mo b/samples/client/petstore/motoko/src/Models/Order.mo
new file mode 100644
index 000000000000..205404e566ef
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Models/Order.mo
@@ -0,0 +1,43 @@
+/// An order for a pets from the pet store
+
+import { type OrderStatus; JSON = OrderStatus } "./OrderStatus";
+
+// Order.mo
+
+module {
+ // User-facing type: what application code uses
+ public type Order = {
+ id : ?Int;
+ petId : ?Int;
+ quantity : ?Int;
+ shipDate : ?Text;
+ status : ?OrderStatus;
+ complete : ?Bool;
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer Order type
+ public type JSON = {
+ id : ?Int;
+ petId : ?Int;
+ quantity : ?Int;
+ shipDate : ?Text;
+ status : ?OrderStatus.JSON;
+ complete : ?Bool;
+ };
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : Order) : JSON = { value with
+ status = do ? { OrderStatus.toJSON(value.status!) };
+ };
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?Order {
+ ?{ json with
+ status = do ? { OrderStatus.fromJSON(json.status!)! };
+ }
+ };
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Models/OrderStatus.mo b/samples/client/petstore/motoko/src/Models/OrderStatus.mo
new file mode 100644
index 000000000000..e3337734d90e
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Models/OrderStatus.mo
@@ -0,0 +1,37 @@
+/// Order Status
+
+// OrderStatus.mo
+/// Enum values: #placed, #approved, #delivered
+
+module {
+ // User-facing type: type-safe variants for application code
+ public type OrderStatus = {
+ #placed;
+ #approved;
+ #delivered;
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer OrderStatus type
+ public type JSON = Text;
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : OrderStatus) : JSON =
+ switch (value) {
+ case (#placed) "placed";
+ case (#approved) "approved";
+ case (#delivered) "delivered";
+ };
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?OrderStatus =
+ switch (json) {
+ case "placed" ?#placed;
+ case "approved" ?#approved;
+ case "delivered" ?#delivered;
+ case _ null;
+ };
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Models/Pet.mo b/samples/client/petstore/motoko/src/Models/Pet.mo
new file mode 100644
index 000000000000..2b50f5d2b47f
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Models/Pet.mo
@@ -0,0 +1,47 @@
+/// A pet for sale in the pet store
+
+import { type Category; JSON = Category } "./Category";
+
+import { type PetStatus; JSON = PetStatus } "./PetStatus";
+
+import { type Tag; JSON = Tag } "./Tag";
+
+// Pet.mo
+
+module {
+ // User-facing type: what application code uses
+ public type Pet = {
+ id : ?Int;
+ category : ?Category;
+ name : Text;
+ photoUrls : [Text];
+ tags : ?[Tag];
+ status : ?PetStatus;
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer Pet type
+ public type JSON = {
+ id : ?Int;
+ category : ?Category;
+ name : Text;
+ photoUrls : [Text];
+ tags : ?[Tag];
+ status : ?PetStatus.JSON;
+ };
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : Pet) : JSON = { value with
+ status = do ? { PetStatus.toJSON(value.status!) };
+ };
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?Pet {
+ ?{ json with
+ status = do ? { PetStatus.fromJSON(json.status!)! };
+ }
+ };
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Models/PetStatus.mo b/samples/client/petstore/motoko/src/Models/PetStatus.mo
new file mode 100644
index 000000000000..52aeb1144b5b
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Models/PetStatus.mo
@@ -0,0 +1,37 @@
+/// pet status in the store
+
+// PetStatus.mo
+/// Enum values: #available, #pending, #sold
+
+module {
+ // User-facing type: type-safe variants for application code
+ public type PetStatus = {
+ #available;
+ #pending;
+ #sold;
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer PetStatus type
+ public type JSON = Text;
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : PetStatus) : JSON =
+ switch (value) {
+ case (#available) "available";
+ case (#pending) "pending";
+ case (#sold) "sold";
+ };
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?PetStatus =
+ switch (json) {
+ case "available" ?#available;
+ case "pending" ?#pending;
+ case "sold" ?#sold;
+ case _ null;
+ };
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Models/Tag.mo b/samples/client/petstore/motoko/src/Models/Tag.mo
new file mode 100644
index 000000000000..6cfc8d7c5402
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Models/Tag.mo
@@ -0,0 +1,27 @@
+/// A tag for a pet
+
+// Tag.mo
+
+module {
+ // User-facing type: what application code uses
+ public type Tag = {
+ id : ?Int;
+ name : ?Text;
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer Tag type
+ public type JSON = {
+ id : ?Int;
+ name : ?Text;
+ };
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : Tag) : JSON = value;
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?Tag = ?json;
+ }
+}
diff --git a/samples/client/petstore/motoko/src/Models/User.mo b/samples/client/petstore/motoko/src/Models/User.mo
new file mode 100644
index 000000000000..8e7065147c6b
--- /dev/null
+++ b/samples/client/petstore/motoko/src/Models/User.mo
@@ -0,0 +1,40 @@
+/// A User who is purchasing from the pet store
+
+// User.mo
+
+module {
+ // User-facing type: what application code uses
+ public type User = {
+ id : ?Int;
+ username : ?Text;
+ firstName : ?Text;
+ lastName : ?Text;
+ email : ?Text;
+ password : ?Text;
+ phone : ?Text;
+ /// User Status
+ userStatus : ?Int;
+ };
+
+ // JSON sub-module: everything needed for JSON serialization
+ public module JSON {
+ // JSON-facing Motoko type: mirrors JSON structure
+ // Named "JSON" to avoid shadowing the outer User type
+ public type JSON = {
+ id : ?Int;
+ username : ?Text;
+ firstName : ?Text;
+ lastName : ?Text;
+ email : ?Text;
+ password : ?Text;
+ phone : ?Text;
+ userStatus : ?Int;
+ };
+
+ // Convert User-facing type to JSON-facing Motoko type
+ public func toJSON(value : User) : JSON = value;
+
+ // Convert JSON-facing Motoko type to User-facing type
+ public func fromJSON(json : JSON) : ?User = ?json;
+ }
+}