Skip to content

Commit

Permalink
feat: add support for ARC56 typed client generation (#595)
Browse files Browse the repository at this point in the history
* feat: add support for ARC56 typed client generation
  • Loading branch information
neilcampbell authored Nov 25, 2024
1 parent 84d7690 commit 72807c3
Show file tree
Hide file tree
Showing 14 changed files with 67 additions and 20 deletions.
2 changes: 1 addition & 1 deletion docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ algokit generate [OPTIONS] COMMAND [ARGS]...

### client

Create a typed ApplicationClient from an ARC-32 application.json
Create a typed ApplicationClient from an ARC-32/56 application.json

Supply the path to an application specification file or a directory to recursively search
for "application.json" files
Expand Down
8 changes: 4 additions & 4 deletions docs/features/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The `algokit generate` [command](../cli/index.md#generate) is used to generate c

## 1. Typed clients

The `algokit generate client` [command](../cli/index.md#client) can be used to generate a typed client from an [ARC-0032](https://arc.algorand.foundation/ARCs/arc-0032) application specification with both Python and TypeScript available as target languages.
The `algokit generate client` [command](../cli/index.md#client) can be used to generate a typed client from an [ARC-0032](https://arc.algorand.foundation/ARCs/arc-0032) or [ARC-0056](https://github.com/algorandfoundation/ARCs/pull/258) application specification with both Python and TypeScript available as target languages.

### Prerequisites

Expand All @@ -15,7 +15,7 @@ Each generated client will also have a dependency on `algokit-utils` libraries f

### Input file / directory

You can either specify a path to a ARC-0032 JSON file, or to a directory that is recursively scanned for `application.json` or `*.arc32.json` file(s).
You can either specify a path to an ARC-0032 JSON file, an ARC-0056 JSON file or to a directory that is recursively scanned for `application.json`, `*.arc32.json`, `*.arc56.json` file(s).

### Output tokens

Expand All @@ -24,8 +24,8 @@ The output path is interpreted as relative to the current working directory, how

There are two tokens available for use with the `-o`, `--output` [option](../cli/index.md#-o---output-):

- `{contract_name}`: This will resolve to a name based on the ARC-0032 contract name, formatted appropriately for the target language.
- `{app_spec_dir}`: This will resolve to the parent directory of the `application.json` or `*.arc32.json` file which can be useful to output a client relative to its source file.
- `{contract_name}`: This will resolve to a name based on the ARC-0032/ARC-0056 contract name, formatted appropriately for the target language.
- `{app_spec_dir}`: This will resolve to the parent directory of the `application.json`, `*.arc32.json`, `*.arc56.json` file which can be useful to output a client relative to its source file.

### Version Pinning

Expand Down
4 changes: 2 additions & 2 deletions src/algokit/cli/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def generate_group() -> None:
def generate_client(
output_path_pattern: str | None, app_spec_path_or_dir: Path, language: str | None, version: str | None
) -> None:
"""Create a typed ApplicationClient from an ARC-32 application.json
"""Create a typed ApplicationClient from an ARC-32/56 application.json
Supply the path to an application specification file or a directory to recursively search
for "application.json" files"""
Expand All @@ -164,7 +164,7 @@ def generate_client(
if not app_spec_path_or_dir.is_dir():
app_specs = [app_spec_path_or_dir]
else:
patterns = ["application.json", "*.arc32.json"]
patterns = ["application.json", "*.arc32.json", "*.arc56.json"]

app_specs = []
for pattern in patterns:
Expand Down
9 changes: 5 additions & 4 deletions src/algokit/cli/project/link.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import typing
from dataclasses import dataclass
from itertools import chain
from pathlib import Path

import click
Expand Down Expand Up @@ -85,12 +86,12 @@ 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)
app_specs = list(contract_project_root.rglob("application.json")) + list(
contract_project_root.rglob("*.arc32.json")
)
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:
click.secho(
f"WARNING: No application.json | *.arc32.json files found in {contract_project_root}. Skipping...",
f"WARNING: No application.json | *.arc32.json | *.arc56.json files found in {contract_project_root}. "
"Skipping...",
fg="yellow",
)
return
Expand Down
5 changes: 4 additions & 1 deletion src/algokit/core/typed_client_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ def create_for_extension(cls, extension: str, version: str | None) -> "ClientGen
def resolve_output_path(self, app_spec: Path, output_path_pattern: str | None) -> Path | None:
try:
application_json = json.loads(app_spec.read_text())
contract_name: str = application_json["contract"]["name"]
try:
contract_name: str = application_json["name"] # ARC-56
except KeyError:
contract_name = application_json["contract"]["name"] # ARC-32
except Exception:
logger.error(f"Couldn't parse contract name from {app_spec}", exc_info=True)
return None
Expand Down
25 changes: 25 additions & 0 deletions tests/generate/test_generate_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ def arc32_json(cwd: Path, dir_with_app_spec_factory: DirWithAppSpecFactory) -> P
return dir_with_app_spec_factory(cwd, "app.arc32.json")


@pytest.fixture()
def arc56_json(cwd: Path, dir_with_app_spec_factory: DirWithAppSpecFactory) -> Path:
return dir_with_app_spec_factory(cwd, "app.arc56.json")


@pytest.fixture(autouse=True)
def which_mock(mocker: MockerFixture) -> WhichMock:
which_mock = WhichMock()
Expand Down Expand Up @@ -211,6 +216,26 @@ def test_generate_client_python_arc32_filename(
assert proc_mock.called[3].command == _get_python_generate_command(None, arc32_json, expected_output_path).split()


@pytest.mark.parametrize(
("options", "expected_output_path"),
[
("-o client.py", "client.py"),
],
)
def test_generate_client_python_arc56_filename(
proc_mock: ProcMock, arc56_json: Path, options: str, expected_output_path: Path
) -> None:
proc_mock.should_bad_exit_on(["poetry", "show", PYTHON_PYPI_PACKAGE, "--tree"])
proc_mock.should_bad_exit_on(["pipx", "list", "--short"])

result = invoke(f"generate client {options} {arc56_json.name}", cwd=arc56_json.parent)

assert result.exit_code == 0
verify(_normalize_output(result.output), options=NamerFactory.with_parameters(*options.split()))
assert len(proc_mock.called) == 4 # noqa: PLR2004
assert proc_mock.called[3].command == _get_python_generate_command(None, arc56_json, expected_output_path).split()


@pytest.mark.usefixtures("mock_platform_system")
@pytest.mark.parametrize(
("options", "expected_output_path"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
DEBUG: Searching for project installed client generator
DEBUG: Running 'poetry show algokit-client-generator --tree' in '{current_working_directory}'
DEBUG: poetry: STDOUT
DEBUG: poetry: STDERR
DEBUG: Running 'pipx --version' in '{current_working_directory}'
DEBUG: pipx: STDOUT
DEBUG: pipx: STDERR
DEBUG: Searching for globally installed client generator
DEBUG: Running 'pipx list --short' in '{current_working_directory}'
DEBUG: pipx: STDOUT
DEBUG: pipx: STDERR
DEBUG: No matching installed client generator found, run client generator via pipx
Generating Python client code for application specified in {current_working_directory}/app.arc56.json and writing to client.py
DEBUG: Running 'pipx run --spec=algokit-client-generator algokitgen-py -a {current_working_directory}/app.arc56.json -o client.py' in '{current_working_directory}'
DEBUG: pipx: STDOUT
DEBUG: pipx: STDERR
STDOUT
STDERR
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Options:
-h, --help Show this message and exit.

Commands:
client Create a typed ApplicationClient from an ARC-32 application.json
client Create a typed ApplicationClient from an ARC-32/56 application.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ Options:
-h, --help Show this message and exit.

Commands:
client Create a typed ApplicationClient from an ARC-32 application.json
client Create a typed ApplicationClient from an ARC-32/56 application.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Options:
-h, --help Show this message and exit.

Commands:
client Create a typed ApplicationClient from an ARC-32 application.json
client Create a typed ApplicationClient from an ARC-32/56 application.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ Options:
-h, --help Show this message and exit.

Commands:
client Create a typed ApplicationClient from an ARC-32...
client Create a typed ApplicationClient from an ARC-32/56...
smart-contract Generates a new smart contract
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Options:
-h, --help Show this message and exit.

Commands:
client Create a typed ApplicationClient from an ARC-32 application.json
client Create a typed ApplicationClient from an ARC-32/56 application.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ Options:
-h, --help Show this message and exit.

Commands:
client Create a typed ApplicationClient from an ARC-32...
client Create a typed ApplicationClient from an ARC-32/56...
smart-contract Generator command description is not supplied.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
WARNING: No application.json | *.arc32.json files found in <cwd>/projects/project3. Skipping...
WARNING: No application.json | *.arc32.json | *.arc56.json files found in <cwd>/projects/project3. Skipping...
✅ 1/2: Finished processing contract_project_3
WARNING: No application.json | *.arc32.json files found in <cwd>/projects/project5. Skipping...
WARNING: No application.json | *.arc32.json | *.arc56.json files found in <cwd>/projects/project5. Skipping...
✅ 2/2: Finished processing contract_project_5

1 comment on commit 72807c3

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/algokit
   __init__.py15753%6–13, 17–24, 32–34
   __main__.py440%1–7
src/algokit/cli
   __init__.py47394%31–34
   codespace.py50982%28, 114, 137, 150–155
   completions.py108992%63–64, 83, 93–99
   dispenser.py121199%77
   doctor.py53394%146–148
   explore.py631576%35–40, 42–47, 85–92, 113
   generate.py70396%76–77, 155
   goal.py54591%85, 96, 107–109
   init.py3102492%491–492, 497–498, 501, 522, 525–527, 538, 542, 600, 626, 655, 688, 697–699, 702–707, 720, 739, 751–752
   localnet.py1643280%67, 88–115, 164–168, 212, 233, 248–258, 271, 322, 343–344
   task.py34391%25–28
src/algokit/cli/common
   utils.py37295%137, 139
src/algokit/cli/project
   bootstrap.py32197%33
   deploy.py1172083%47, 49, 102, 125, 147–149, 270, 277, 291–299, 302–311
   link.py911682%61, 66–67, 102–106, 116–121, 149–150, 219–220, 224
   list.py33585%21–23, 51–56
   run.py46296%38, 174
src/algokit/cli/tasks
   analyze.py81199%81
   assets.py821384%65–66, 72, 74–75, 105, 119, 125–126, 132, 134, 136–137
   ipfs.py51884%52, 80, 92, 94–95, 105–107
   mint.py1061586%51, 73, 100–103, 108, 113, 131–132, 158, 335–339
   send_transaction.py651085%52–53, 57, 89, 158, 170–174
   sign_transaction.py59886%21, 28–30, 71–72, 109, 123
   transfer.py39392%26, 90, 117
   utils.py1144660%29–37, 43–46, 78–79, 103–104, 128–136, 155–165, 212, 261–262, 282–293, 300–302, 324
   vanity_address.py561082%41, 45–48, 112, 114, 121–123
   wallet.py79495%21, 66, 136, 162
src/algokit/core
   codespace.py1756861%34–37, 41–44, 48–71, 111–112, 125–133, 191, 200–202, 210, 216–217, 229–236, 251–298, 311–313, 338–344, 348, 395
   conf.py57984%12, 24, 28, 36, 38, 73–75, 80
   dispenser.py2022687%92, 124–125, 142–150, 192–193, 199–201, 219–220, 260–261, 319, 333–335, 346–347, 357, 370, 385
   doctor.py65789%67–69, 92–94, 134
   generate.py50394%44, 85, 103
   goal.py65494%21, 36–37, 47
   init.py671085%53, 57–62, 70, 81, 88, 108–109
   log_handlers.py68790%50–51, 63, 112–116, 125
   proc.py45198%99
   sandbox.py2762392%32, 89–92, 97, 101–103, 174, 222–229, 240, 611, 627, 652, 660
   typed_client_generation.py1732088%65–67, 106–111, 135, 138–141, 159, 162–165, 232, 235–238
   utils.py1504073%50–51, 57–69, 125–131, 155, 158, 164–177, 206–208, 237–240, 262
src/algokit/core/_vendor/auth0/authentication
   token_verifier.py15711129%16, 45, 58, 73–85, 98–107, 119–124, 136–137, 140, 170, 178–180, 190–199, 206–213, 227–236, 258, 280–287, 314–323, 333–444
src/algokit/core/compilers
   python.py28582%19–20, 25, 49–50
src/algokit/core/config_commands
   container_engine.py412149%24, 29–31, 47–76
   version_prompt.py921485%37–38, 68, 87–90, 108, 118–125, 148
src/algokit/core/project
   __init__.py53394%50, 86, 145
   bootstrap.py120893%47, 126–127, 149, 176, 207–209
   deploy.py69987%108–111, 120–122, 126, 131
   run.py1321390%83, 88, 97–98, 133–134, 138–139, 143, 147, 277–278, 293
src/algokit/core/tasks
   analyze.py93397%105–112, 187
   ipfs.py63789%58–64, 140, 144, 146, 152
   nfd.py491373%25, 31, 34–41, 70–72, 99–101
   vanity_address.py903462%49–50, 54, 59–75, 92–108, 128–131
   wallet.py71593%37, 129, 155–157
src/algokit/core/tasks/mint
   mint.py74988%123–133
   models.py921782%50, 52, 57, 71–74, 81–90
TOTAL495077284% 

Tests Skipped Failures Errors Time
502 0 💤 0 ❌ 0 🔥 24.324s ⏱️

Please sign in to comment.