Skip to content

Commit

Permalink
fix: handle client generation in a dir containing multiple app spec t…
Browse files Browse the repository at this point in the history
…ypes
  • Loading branch information
neilcampbell committed Dec 24, 2024
1 parent 1e0e39c commit f457878
Show file tree
Hide file tree
Showing 10 changed files with 382 additions and 141 deletions.
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 9 additions & 18 deletions src/algokit/cli/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import click

from algokit.core.generate import load_generators, run_generator
from algokit.core.typed_client_generation import ClientGenerator
from algokit.core.typed_client_generation import AppSpecsNotFoundError, ClientGenerator

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -161,20 +161,11 @@ def generate_client(
"One of --language or --output is required to determine the client language to generate"
)

if not app_spec_path_or_dir.is_dir():
app_specs = [app_spec_path_or_dir]
else:
patterns = ["application.json", "*.arc32.json", "*.arc56.json"]

app_specs = []
for pattern in patterns:
app_specs.extend(app_spec_path_or_dir.rglob(pattern))

app_specs = list(set(app_specs))
app_specs.sort()
if not app_specs:
raise click.ClickException("No app specs found")
for app_spec in app_specs:
output_path = generator.resolve_output_path(app_spec, output_path_pattern)
if output_path is not None:
generator.generate(app_spec, output_path)
try:
generator.generate_all(
app_spec_path_or_dir,
output_path_pattern,
raise_on_failure=False,
)
except AppSpecsNotFoundError as ex:
raise click.ClickException("No app specs found") from ex
25 changes: 9 additions & 16 deletions src/algokit/cli/project/link.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
import typing
from dataclasses import dataclass
from itertools import chain
from pathlib import Path

import click
Expand All @@ -11,7 +10,7 @@
from algokit.core import questionary_extensions
from algokit.core.conf import get_algokit_config
from algokit.core.project import ProjectType, get_project_configs
from algokit.core.typed_client_generation import ClientGenerator
from algokit.core.typed_client_generation import AppSpecsNotFoundError, ClientGenerator

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -86,25 +85,19 @@ def _link_projects(
"""
output_path_pattern = f"{frontend_clients_path}/{{contract_name}}.{'ts' if language == 'typescript' else 'py'}"
generator = ClientGenerator.create_for_language(language, version=version)
file_patterns = ["application.json", "*.arc32.json", "*.arc56.json"]
app_specs = list(chain.from_iterable(contract_project_root.rglob(pattern) for pattern in file_patterns))
if not app_specs:

try:
generator.generate_all(
contract_project_root,
output_path_pattern,
raise_on_failure=fail_fast,
)
except AppSpecsNotFoundError:
click.secho(
f"WARNING: No application.json | *.arc32.json | *.arc56.json files found in {contract_project_root}. "
"Skipping...",
fg="yellow",
)
return

for app_spec in app_specs:
output_path = generator.resolve_output_path(app_spec, output_path_pattern)
if output_path is None:
if fail_fast:
raise click.ClickException(f"Error generating client for {app_spec}")

logger.warning(f"Error generating client for {app_spec}")
continue
generator.generate(app_spec, output_path)


def _prompt_contract_project() -> ContractArtifacts | None:
Expand Down
55 changes: 53 additions & 2 deletions src/algokit/core/typed_client_generation.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import abc
import enum
import json
import logging
import re
import shutil # noqa: F401
from functools import reduce
from itertools import chain
from pathlib import Path
from typing import ClassVar

Expand All @@ -26,6 +29,15 @@ def _snake_case(s: str) -> str:
return re.sub(r"[-\s]", "_", s).lower()


class AppSpecType(enum.Enum):
ARC32 = "arc32"
ARC56 = "arc56"


class AppSpecsNotFoundError(Exception):
pass


class ClientGenerator(abc.ABC):
language: ClassVar[str]
extension: ClassVar[str]
Expand Down Expand Up @@ -55,13 +67,15 @@ def create_for_language(cls, language: str, version: str | None) -> "ClientGener
def create_for_extension(cls, extension: str, version: str | None) -> "ClientGenerator":
return cls._by_extension[extension](version)

def resolve_output_path(self, app_spec: Path, output_path_pattern: str | None) -> Path | None:
def resolve_output_path(self, app_spec: Path, output_path_pattern: str | None) -> tuple[Path, AppSpecType] | None:
try:
application_json = json.loads(app_spec.read_text())
try:
contract_name: str = application_json["name"] # ARC-56
app_spec_type: AppSpecType = AppSpecType.ARC56
except KeyError:
contract_name = application_json["contract"]["name"] # ARC-32
app_spec_type = AppSpecType.ARC32
except Exception:
logger.error(f"Couldn't parse contract name from {app_spec}", exc_info=True)
return None
Expand All @@ -73,7 +87,7 @@ def resolve_output_path(self, app_spec: Path, output_path_pattern: str | None) -
if output_path.exists() and not output_path.is_file():
logger.error(f"Could not output to {output_path} as it already exists and is a directory")
return None
return output_path
return (output_path, app_spec_type)

@abc.abstractmethod
def generate(self, app_spec: Path, output: Path) -> None: ...
Expand All @@ -88,6 +102,43 @@ def find_generate_command(self, version: str | None) -> list[str]: ...
def format_contract_name(self, contract_name: str) -> str:
return contract_name

def generate_all(
self,
app_spec_path_or_dir: Path,
output_path_pattern: str | None,
*,
raise_on_failure: bool, # TODO: NC - Maybe we should return the error instead?
) -> None:
if not app_spec_path_or_dir.is_dir():
app_specs = [app_spec_path_or_dir]
else:
file_patterns = ["application.json", "*.arc32.json", "*.arc56.json"]
app_specs = list(set(chain.from_iterable(app_spec_path_or_dir.rglob(pattern) for pattern in file_patterns)))
app_specs.sort()
if not app_specs:
raise AppSpecsNotFoundError

def accumulate_items_to_generate(
acc: dict[Path, tuple[Path, AppSpecType]], app_spec: Path
) -> dict[Path, tuple[Path, AppSpecType]]:
output_path_result = self.resolve_output_path(app_spec, output_path_pattern)
if output_path_result is None:
if raise_on_failure:
raise click.ClickException(f"Error generating client for {app_spec}")
return acc
(output_path, app_spec_type) = output_path_result
if output_path in acc:
# ARC-56 app specs take precedence over ARC-32 app specs
if acc[output_path][1] == AppSpecType.ARC32 and app_spec_type == AppSpecType.ARC56:
acc[output_path] = (app_spec, app_spec_type)
else:
acc[output_path] = (app_spec, app_spec_type)
return acc

items_to_generate: dict[Path, tuple[Path, AppSpecType]] = reduce(accumulate_items_to_generate, app_specs, {})
for output_path, (app_spec, _) in items_to_generate.items():
self.generate(app_spec, output_path)


class PythonClientGenerator(ClientGenerator, language="python", extension=".py"):
def generate(self, app_spec: Path, output: Path) -> None:
Expand Down
75 changes: 75 additions & 0 deletions tests/generate/app.arc32.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"hints": {
"hello(string)string": {
"call_config": {
"no_op": "CALL"
}
},
"hello_world_check(string)void": {
"call_config": {
"no_op": "CALL"
}
}
},
"source": {
"approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMQp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sNgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAyYmVjZTExIC8vICJoZWxsbyhzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGJmOWMxZWRmIC8vICJoZWxsb193b3JsZF9jaGVjayhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDQKZXJyCm1haW5fbDQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CnR4bmEgQXBwbGljYXRpb25BcmdzIDEKY2FsbHN1YiBoZWxsb3dvcmxkY2hlY2tfMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpjYWxsc3ViIGhlbGxvXzIKc3RvcmUgMApwdXNoYnl0ZXMgMHgxNTFmN2M3NSAvLyAweDE1MWY3Yzc1CmxvYWQgMApjb25jYXQKbG9nCmludGNfMSAvLyAxCnJldHVybgptYWluX2w2Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2wxMgp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sMTEKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDUgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KYm56IG1haW5fbDEwCmVycgptYWluX2wxMDoKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KYXNzZXJ0CmNhbGxzdWIgZGVsZXRlXzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDExOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGVfMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTI6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIHVwZGF0ZQp1cGRhdGVfMDoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKcHVzaGludCBUTVBMX1VQREFUQUJMRSAvLyBUTVBMX1VQREFUQUJMRQovLyBDaGVjayBhcHAgaXMgdXBkYXRhYmxlCmFzc2VydApyZXRzdWIKCi8vIGRlbGV0ZQpkZWxldGVfMToKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKcHVzaGludCBUTVBMX0RFTEVUQUJMRSAvLyBUTVBMX0RFTEVUQUJMRQovLyBDaGVjayBhcHAgaXMgZGVsZXRhYmxlCmFzc2VydApyZXRzdWIKCi8vIGhlbGxvCmhlbGxvXzI6CnByb3RvIDEgMQpwdXNoYnl0ZXMgMHggLy8gIiIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjJjMjAgLy8gIkhlbGxvLCAiCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBoZWxsb193b3JsZF9jaGVjawpoZWxsb3dvcmxkY2hlY2tfMzoKcHJvdG8gMSAwCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApwdXNoYnl0ZXMgMHg1NzZmNzI2YzY0IC8vICJXb3JsZCIKPT0KYXNzZXJ0CnJldHN1Yg==",
"clear": "I3ByYWdtYSB2ZXJzaW9uIDgKcHVzaGludCAwIC8vIDAKcmV0dXJu"
},
"state": {
"global": {
"num_byte_slices": 0,
"num_uints": 0
},
"local": {
"num_byte_slices": 0,
"num_uints": 0
}
},
"schema": {
"global": {
"declared": {},
"reserved": {}
},
"local": {
"declared": {},
"reserved": {}
}
},
"contract": {
"name": "HelloWorldApp",
"methods": [
{
"name": "hello",
"args": [
{
"type": "string",
"name": "name"
}
],
"returns": {
"type": "string"
},
"desc": "Returns Hello, {name}"
},
{
"name": "hello_world_check",
"args": [
{
"type": "string",
"name": "name"
}
],
"returns": {
"type": "void"
},
"desc": "Asserts {name} is \"World\""
}
],
"networks": {}
},
"bare_call_config": {
"delete_application": "CALL",
"no_op": "CREATE",
"update_application": "CALL"
}
}
95 changes: 95 additions & 0 deletions tests/generate/app.arc56.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"name": "HelloWorldApp",
"structs": {},
"methods": [
{
"name": "hello",
"args": [
{
"type": "string",
"name": "name"
}
],
"returns": {
"type": "string"
},
"actions": {
"create": [],
"call": [
"NoOp"
]
},
"readonly": false,
"events": [],
"recommendations": {}
}
],
"arcs": [
22,
28
],
"networks": {},
"state": {
"schema": {
"global": {
"ints": 0,
"bytes": 0
},
"local": {
"ints": 0,
"bytes": 0
}
},
"keys": {
"global": {},
"local": {},
"box": {}
},
"maps": {
"global": {},
"local": {},
"box": {}
}
},
"bareActions": {
"create": [
"NoOp"
],
"call": []
},
"sourceInfo": {
"approval": {
"sourceInfo": [
{
"pc": [
35
],
"errorMessage": "OnCompletion is not NoOp"
},
{
"pc": [
75
],
"errorMessage": "can only call when creating"
},
{
"pc": [
38
],
"errorMessage": "can only call when not creating"
}
],
"pcOffsetMethod": "none"
},
"clear": {
"sourceInfo": [],
"pcOffsetMethod": "none"
}
},
"source": {
"approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5hcHByb3ZhbF9wcm9ncmFtOgogICAgaW50Y2Jsb2NrIDAgMQogICAgY2FsbHN1YiBfX3B1eWFfYXJjNF9yb3V0ZXJfXwogICAgcmV0dXJuCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkLmNvbnRyYWN0LkhlbGxvV29ybGQuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHByb3RvIDAgMQogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A1CiAgICBwdXNoYnl0ZXMgMHgwMmJlY2UxMSAvLyBtZXRob2QgImhlbGxvKHN0cmluZylzdHJpbmciCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyCiAgICBpbnRjXzAgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjYKICAgIC8vIEBhYmltZXRob2QoKQogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NQogICAgLy8gY2xhc3MgSGVsbG9Xb3JsZChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgZXh0cmFjdCAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZC9jb250cmFjdC5weTo2CiAgICAvLyBAYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgaGVsbG8KICAgIGR1cAogICAgbGVuCiAgICBpdG9iCiAgICBleHRyYWN0IDYgMgogICAgc3dhcAogICAgY29uY2F0CiAgICBwdXNoYnl0ZXMgMHgxNTFmN2M3NQogICAgc3dhcAogICAgY29uY2F0CiAgICBsb2cKICAgIGludGNfMSAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A1OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHR4biBPbkNvbXBsZXRpb24KICAgIGJueiBfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDkKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDk6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NQogICAgLy8gY2xhc3MgSGVsbG9Xb3JsZChBUkM0Q29udHJhY3QpOgogICAgaW50Y18wIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZC5jb250cmFjdC5IZWxsb1dvcmxkLmhlbGxvKG5hbWU6IGJ5dGVzKSAtPiBieXRlczoKaGVsbG86CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6Ni03CiAgICAvLyBAYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBoZWxsbyhzZWxmLCBuYW1lOiBTdHJpbmcpIC0+IFN0cmluZzoKICAgIHByb3RvIDEgMQogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjgKICAgIC8vIHJldHVybiAiSGVsbG8sICIgKyBuYW1lCiAgICBwdXNoYnl0ZXMgIkhlbGxvLCAiCiAgICBmcmFtZV9kaWcgLTEKICAgIGNvbmNhdAogICAgcmV0c3ViCg==",
"clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5jbGVhcl9zdGF0ZV9wcm9ncmFtOgogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgo="
},
"events": [],
"templateVariables": {}
}
Loading

0 comments on commit f457878

Please sign in to comment.