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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ __pycache__/
env/
venv/
ENV/

# Polytest (temporary files until polytest branch merged to main)
.polytest_algokit-polytest/
45 changes: 37 additions & 8 deletions algokit-configs/openapi-converter/specs/algod.oas3.json
Original file line number Diff line number Diff line change
Expand Up @@ -4054,9 +4054,7 @@
"description": "base64 encoded program bytes"
},
"sourcemap": {
"type": "object",
"properties": {},
"description": "JSON of the source map"
"$ref": "#/components/schemas/SourceMap"
}
}
}
Expand Down Expand Up @@ -4695,8 +4693,7 @@
"id",
"network",
"proto",
"rwd",
"timestamp"
"rwd"
],
"type": "object",
"properties": {
Expand Down Expand Up @@ -4994,6 +4991,7 @@
"type": "integer",
"description": "unique asset identifier",
"x-go-type": "basics.AssetIndex",
"x-algokit-field-rename": "id",
"x-algokit-bigint": true
},
"params": {
Expand Down Expand Up @@ -6551,6 +6549,39 @@
}
},
"description": "Proof of transaction in a block."
},
"SourceMap": {
"type": "object",
"required": [
"version",
"sources",
"names",
"mappings"
],
"properties": {
"version": {
"type": "integer"
},
"sources": {
"description": "A list of original sources used by the \"mappings\" entry.",
"type": "array",
"items": {
"type": "string"
}
},
"names": {
"description": "A list of symbol names used by the \"mappings\" entry.",
"type": "array",
"items": {
"type": "string"
}
},
"mappings": {
"description": "A string with the encoded mapping data.",
"type": "string"
}
},
"description": "Source map for the program"
}
},
"responses": {
Expand Down Expand Up @@ -7333,9 +7364,7 @@
"description": "base64 encoded program bytes"
},
"sourcemap": {
"type": "object",
"properties": {},
"description": "JSON of the source map"
"$ref": "#/components/schemas/SourceMap"
}
}
}
Expand Down
1 change: 1 addition & 0 deletions algokit-configs/openapi-converter/specs/indexer.oas3.json
Original file line number Diff line number Diff line change
Expand Up @@ -3587,6 +3587,7 @@
"index": {
"type": "integer",
"description": "unique asset identifier",
"x-algokit-field-rename": "id",
"x-algokit-bigint": true
},
"deleted": {
Expand Down
2 changes: 2 additions & 0 deletions oas-generator/src/oas_generator/generator/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ class FieldDescriptor:
is_signed_txn: bool
is_optional: bool
is_nullable: bool
inline_object_schema: dict | None = None
inline_meta_name: str | None = None


@dataclass
Expand Down
88 changes: 32 additions & 56 deletions oas-generator/src/oas_generator/generator/template_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ def _build_model_descriptor(self, name: str, schema: Schema, all_schemas: Schema
signed_txn = False
bytes_flag = False
bigint_flag = False
inline_object_schema = None

if is_array and isinstance(items, dict):
if "$ref" in items:
ref_model = ts_pascal_case(items["$ref"].split("/")[-1])
Expand All @@ -209,15 +211,31 @@ def _build_model_descriptor(self, name: str, schema: Schema, all_schemas: Schema
else:
if "$ref" in (prop_schema or {}):
ref_model = ts_pascal_case(prop_schema["$ref"].split("/")[-1])
fmt = prop_schema.get(constants.SchemaKey.FORMAT)
bytes_flag = fmt == "byte" or prop_schema.get(constants.X_ALGOKIT_BYTES_BASE64) is True
bigint_flag = bool(prop_schema.get(constants.X_ALGOKIT_BIGINT) is True)
signed_txn = bool(prop_schema.get(constants.X_ALGOKIT_SIGNED_TXN) is True)
# Check for special codec flags first
elif bool(prop_schema.get(constants.X_ALGOKIT_SIGNED_TXN) is True):
signed_txn = True
# For inline nested objects, store the schema for inline metadata generation
elif (prop_schema.get(constants.SchemaKey.TYPE) == "object" and
"properties" in prop_schema and
"$ref" not in prop_schema and
prop_schema.get(constants.X_ALGOKIT_SIGNED_TXN) is not True):
# Store the inline object schema for metadata generation
inline_object_schema = prop_schema
else:
fmt = prop_schema.get(constants.SchemaKey.FORMAT)
bytes_flag = fmt == "byte" or prop_schema.get(constants.X_ALGOKIT_BYTES_BASE64) is True
bigint_flag = bool(prop_schema.get(constants.X_ALGOKIT_BIGINT) is True)
signed_txn = bool(prop_schema.get(constants.X_ALGOKIT_SIGNED_TXN) is True)

is_optional = prop_name not in required_fields
# Nullable per OpenAPI
is_nullable = bool(prop_schema.get(constants.SchemaKey.NULLABLE) is True)

# Generate inline metadata name for nested objects
inline_meta_name = None
if inline_object_schema:
inline_meta_name = f"{model_name}{ts_pascal_case(canonical)}Meta"

fields.append(
FieldDescriptor(
name=name_camel,
Expand All @@ -230,6 +248,8 @@ def _build_model_descriptor(self, name: str, schema: Schema, all_schemas: Schema
is_signed_txn=signed_txn,
is_optional=is_optional,
is_nullable=is_nullable,
inline_object_schema=inline_object_schema,
inline_meta_name=inline_meta_name,
)
)

Expand Down Expand Up @@ -855,56 +875,27 @@ def generate(
if ts_pascal_case(name) in all_used_types}



# Generate components (only used schemas)
files.update(self.schema_processor.generate_models(output_dir, used_schemas))

if service_class == "AlgodApi":
models_dir = output_dir / constants.DirectoryName.SRC / constants.DirectoryName.MODELS

# Add SuggestedParams custom model
# Generate the custom typed models
files[models_dir / "suggested-params.ts"] = self.renderer.render(
"models/transaction-params/suggested-params.ts.j2",
{"spec": spec},
)

# Custom typed block models
# Block-specific models (prefixed to avoid shape collisions)
files[models_dir / "block-eval-delta.ts"] = self.renderer.render(
"models/block/block-eval-delta.ts.j2",
{"spec": spec},
)
files[models_dir / "block-state-delta.ts"] = self.renderer.render(
"models/block/block-state-delta.ts.j2",
{"spec": spec},
)
files[models_dir / "block-account-state-delta.ts"] = self.renderer.render(
"models/block/block-account-state-delta.ts.j2",
{"spec": spec},
)
# BlockAppEvalDelta is implemented by repurposing application-eval-delta.ts.j2 to new name
files[models_dir / "block-app-eval-delta.ts"] = self.renderer.render(
"models/block/application-eval-delta.ts.j2",
{"spec": spec},
)
files[models_dir / "block_state_proof_tracking_data.ts"] = self.renderer.render(
"models/block/block-state-proof-tracking-data.ts.j2",
{"spec": spec},
)
files[models_dir / "block_state_proof_tracking.ts"] = self.renderer.render(
"models/block/block-state-proof-tracking.ts.j2",
{"spec": spec},
)
files[models_dir / "signed-txn-in-block.ts"] = self.renderer.render(
"models/block/signed-txn-in-block.ts.j2",
"models/custom/suggested-params.ts.j2",
{"spec": spec},
)
files[models_dir / "block.ts"] = self.renderer.render(
"models/block/block.ts.j2",
"models/custom/block.ts.j2",
{"spec": spec},
)
files[models_dir / "get-block.ts"] = self.renderer.render(
"models/block/get-block.ts.j2",
"models/custom/get-block.ts.j2",
{"spec": spec},
)
files[models_dir / "ledger-state-delta.ts"] = self.renderer.render(
"models/custom/ledger-state-delta.ts.j2",
{"spec": spec},
)

Expand All @@ -914,22 +905,8 @@ def generate(
extras = (
"\n"
"export type { SuggestedParams, SuggestedParamsMeta } from './suggested-params';\n"
"export type { BlockEvalDelta } from './block-eval-delta';\n"
"export { BlockEvalDeltaMeta } from './block-eval-delta';\n"
"export type { BlockStateDelta } from './block-state-delta';\n"
"export { BlockStateDeltaMeta } from './block-state-delta';\n"
"export type { BlockAccountStateDelta } from './block-account-state-delta';\n"
"export { BlockAccountStateDeltaMeta } from './block-account-state-delta';\n"
"export type { BlockAppEvalDelta } from './block-app-eval-delta';\n"
"export { BlockAppEvalDeltaMeta } from './block-app-eval-delta';\n"
"export type { BlockStateProofTrackingData } from './block_state_proof_tracking_data';\n"
"export { BlockStateProofTrackingDataMeta } from './block_state_proof_tracking_data';\n"
"export type { BlockStateProofTracking } from './block_state_proof_tracking';\n"
"export { BlockStateProofTrackingMeta } from './block_state_proof_tracking';\n"
"export type { Block } from './block';\n"
"export { BlockMeta } from './block';\n"
"export type { SignedTxnInBlock } from './signed-txn-in-block';\n"
"export { SignedTxnInBlockMeta } from './signed-txn-in-block';\n"
)
files[index_path] = base_index + extras
files.update(self._generate_client_files(output_dir, client_class, service_class))
Expand Down Expand Up @@ -962,7 +939,6 @@ def _generate_runtime(
core_dir / "fetch-http-request.ts": ("base/src/core/fetch-http-request.ts.j2", context),
core_dir / "api-error.ts": ("base/src/core/api-error.ts.j2", context),
core_dir / "request.ts": ("base/src/core/request.ts.j2", context),
core_dir / "serialization.ts": ("base/src/core/serialization.ts.j2", context),
core_dir / "codecs.ts": ("base/src/core/codecs.ts.j2", context),
core_dir / "model-runtime.ts": ("base/src/core/model-runtime.ts.j2", context),
# Project files
Expand Down
32 changes: 20 additions & 12 deletions oas-generator/src/oas_generator/templates/apis/service.ts.j2
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { {% for t in sorted %}{{ t }}Meta{% if not loop.last %}, {% endif %}{% e

{% macro field_type_meta(type_name) -%}
{%- if type_name in import_types -%}
({ kind: 'model', meta: () => {{ type_name }}Meta } as const)
({ kind: 'model', meta: {{ type_name }}Meta } as const)
{%- elif type_name == 'SignedTransaction' -%}
({ kind: 'codec', codecKey: 'SignedTransaction' } as const)
{%- elif type_name == 'Uint8Array' -%}
Expand Down Expand Up @@ -105,8 +105,8 @@ export class {{ service_class_name }} {
{%- endif %}
): Promise<{{ op.responseTsType }}> {
const headers: Record<string, string> = {};
{% set supports_msgpack = op.returnsMsgpack or (op.requestBody and op.requestBody.supportsMsgpack) %}
const responseFormat: BodyFormat = {% if op.forceMsgpackQuery %}'msgpack'{% elif supports_msgpack %}'json'{% else %}'json'{% endif %};
{% set body_format = 'msgpack' if op.forceMsgpackQuery else 'json' %}
const responseFormat: BodyFormat = '{{ body_format }}'
headers['Accept'] = {{ service_class_name }}.acceptFor(responseFormat);

{% if op.requestBody and op.method.upper() not in ['GET', 'HEAD'] %}
Expand All @@ -118,9 +118,11 @@ export class {{ service_class_name }} {
const bodyMeta = {{ meta_expr(op.requestBody.tsType) }};
const mediaType = bodyMeta ? {{ service_class_name }}.mediaFor(responseFormat) : undefined;
if (mediaType) headers['Content-Type'] = mediaType;
const serializedBody = bodyMeta && body !== undefined
? AlgorandSerializer.encode(body, bodyMeta, responseFormat)
: body;
{% if op.requestBody and not meta_expr(op.requestBody.tsType) == 'undefined' %}
const serializedBody = body ? AlgorandSerializer.encode(body, bodyMeta, responseFormat) : undefined;
{% else %}
const serializedBody = body;
{% endif %}
{% endif %}
{% endif %}

Expand All @@ -132,7 +134,11 @@ export class {{ service_class_name }} {
}
{% endif %}

const payload = await this.httpRequest.request<unknown>({
{% if op.responseTsType == 'void' %}
await this.httpRequest.request<void>({
{% else %}
const payload = await this.httpRequest.request<{{'Uint8Array' if body_format == 'msgpack' else 'string'}}>({
{% endif %}
method: '{{ op.method }}',
url: '{{ op.path }}',
path: {
Expand All @@ -158,11 +164,13 @@ export class {{ service_class_name }} {
{% endif %}
});

const responseMeta = {{ meta_expr(op.responseTsType) }};
if (responseMeta) {
return AlgorandSerializer.decode(payload, responseMeta, responseFormat);
}
return payload as {{ op.responseTsType }};
{% if op.responseTsType != 'void' %}
{% if meta_expr(op.responseTsType) == 'undefined' %}
return payload;
{% else %}
return AlgorandSerializer.decode(payload, {{ meta_expr(op.responseTsType) }}, responseFormat);
{% endif %}
{% endif %}
}

{% endfor %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,28 @@
import { decode as msgpackDecode, encode as msgpackEncode } from 'algorand-msgpack'

export function encodeMsgPack<T>(data: T): Uint8Array {
export function encodeMsgPack(data: ApiData): Uint8Array {
return new Uint8Array(msgpackEncode(data, { sortKeys: true, ignoreUndefined: true }));
}

export function decodeMsgPack<T = unknown>(buffer: Uint8Array): T {
const map = msgpackDecode(buffer, { useMap: true }) as unknown;
return mapToObject(map) as T;
type MsgPackDecodeOptions = {
useMap: boolean;
rawBinaryStringKeys: boolean;
rawBinaryStringValues: boolean;
}

/**
* Converts a Map structure from msgpack decoding to a plain object structure.
* Maps are converted to objects recursively, except for the special case
* where the field name is "r" which remains as a Map.
*/
function mapToObject(value: unknown, fieldName?: string): unknown {
// Preserve Uint8Array as-is
if (value instanceof Uint8Array) {
return value;
} else if (value instanceof Map) {
// Special case: keep "r" field as Map
if (fieldName === 'r') {
const newMap = new Map();
for (const [k, v] of value.entries()) {
newMap.set(k, mapToObject(v));
}
return newMap;
}

// Convert Map to object
const obj: Record<string, unknown> = {};
for (const [k, v] of value.entries()) {
obj[k] = mapToObject(v, k);
}
return obj;
} else if (Array.isArray(value)) {
return value.map((item) => mapToObject(item));
}

return value;
export function decodeMsgPack(
buffer: Uint8Array,
options: MsgPackDecodeOptions = { useMap: true, rawBinaryStringKeys: true, rawBinaryStringValues: true },
): Map<number | bigint | Uint8Array, unknown> {
return msgpackDecode(buffer, options) as Map<number | bigint | Uint8Array, unknown>;
}
export type ApiData =
| null
| undefined
| string
| number
| bigint
| boolean
| Uint8Array
| object
| Map<string | number | bigint | Uint8Array, ApiData> // TODO: NC - Do we ever have a string key?
Loading