Skip to content

Commit f8dacd8

Browse files
committed
feat(cli): add options to control client generator output behavior
1 parent be1fd4f commit f8dacd8

File tree

5 files changed

+365
-17
lines changed

5 files changed

+365
-17
lines changed

chanx/cli/main.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,33 @@ def cli() -> None:
9393
default=False,
9494
help="Skip README.md generation",
9595
)
96+
@click.option(
97+
"--clear-output",
98+
is_flag=True,
99+
default=False,
100+
help="Remove entire output directory before generation",
101+
)
102+
@click.option(
103+
"--override-base",
104+
is_flag=True,
105+
default=False,
106+
help="Regenerate base files even if they already exist",
107+
)
108+
@click.option(
109+
"--no-clear-channels",
110+
is_flag=True,
111+
default=False,
112+
help="Keep existing channel folders instead of clearing them",
113+
)
96114
def generate_client(
97115
schema: str,
98116
output: Path,
99117
formatter: str | None,
100118
no_format: bool,
101119
no_readme: bool,
120+
clear_output: bool,
121+
override_base: bool,
122+
no_clear_channels: bool,
102123
) -> None:
103124
"""
104125
Generate a type-safe WebSocket client from an AsyncAPI schema.
@@ -127,6 +148,15 @@ def generate_client(
127148
# Skip README generation
128149
chanx generate-client --schema asyncapi.json --output ./myclient --no-readme
129150
151+
# Clear entire output directory before generation
152+
chanx generate-client --schema asyncapi.json --output ./myclient --clear-output
153+
154+
# Force regenerate base files
155+
chanx generate-client --schema asyncapi.json --output ./myclient --override-base
156+
157+
# Keep existing channel folders (don't clear them)
158+
chanx generate-client --schema asyncapi.json --output ./myclient --no-clear-channels
159+
130160
# With URL
131161
chanx generate-client --schema https://example.com/api/asyncapi.json --output ./myclient
132162
"""
@@ -138,6 +168,9 @@ def generate_client(
138168
schema_path=schema,
139169
output_dir=str(output),
140170
generate_readme=not no_readme,
171+
clear_output=clear_output,
172+
override_base=override_base,
173+
clear_channels=not no_clear_channels,
141174
)
142175

143176
# Display parsed info

chanx/client_generator/generator.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ def __init__(
4141
schema_path: str,
4242
output_dir: str,
4343
generate_readme: bool = True,
44+
clear_output: bool = False,
45+
override_base: bool = False,
46+
clear_channels: bool = True,
4447
):
4548
"""
4649
Initialize client generator.
@@ -49,10 +52,16 @@ def __init__(
4952
schema_path: Path to AsyncAPI schema file
5053
output_dir: Output directory for generated code
5154
generate_readme: Whether to generate README.md file
55+
clear_output: Whether to remove entire output directory before generation
56+
override_base: Whether to regenerate base files even if they exist
57+
clear_channels: Whether to clear channel folders (except base) before generation
5258
"""
5359
self.schema_path = schema_path
5460
self.output_dir = Path(output_dir)
5561
self.generate_readme = generate_readme
62+
self.clear_output = clear_output
63+
self.override_base = override_base
64+
self.clear_channels = clear_channels
5665

5766
# Load and parse schema
5867
schema_dict = SchemaLoader.load(schema_path)
@@ -91,9 +100,18 @@ def generate(self) -> None:
91100

92101
def _create_directory_structure(self) -> None:
93102
"""Create output directory structure."""
94-
# Remove existing directory if it exists
95103
if self.output_dir.exists():
96-
shutil.rmtree(self.output_dir)
104+
if self.clear_output:
105+
# Remove entire directory
106+
shutil.rmtree(self.output_dir)
107+
elif self.clear_channels:
108+
# Remove everything except base folder
109+
for item in self.output_dir.iterdir():
110+
if item.name != "base":
111+
if item.is_dir():
112+
shutil.rmtree(item)
113+
else:
114+
item.unlink()
97115

98116
# Create directories
99117
self.output_dir.mkdir(parents=True, exist_ok=True)
@@ -109,11 +127,12 @@ def _create_directory_structure(self) -> None:
109127

110128
def _generate_base(self) -> None:
111129
"""Generate base client class."""
112-
# Copy the base directory from the client generator to the output directory
113130
source_base_dir = self.client_generator_path / "base"
114131
target_base_dir = self.output_dir / "base"
115132

116-
shutil.copytree(source_base_dir, target_base_dir, dirs_exist_ok=True)
133+
# Only regenerate base if it doesn't exist or override_base is True
134+
if self.override_base or not target_base_dir.exists():
135+
shutil.copytree(source_base_dir, target_base_dir, dirs_exist_ok=True)
117136

118137
def _generate_shared_schemas(self) -> None:
119138
"""Generate shared message schemas used across multiple channels."""
@@ -194,7 +213,7 @@ def _generate_channel_messages(
194213
if m.payload and m.payload.title
195214
)
196215
)
197-
lines.append(f'IncomingMessage = {" | ".join(incoming_titles)}')
216+
lines.append(f"IncomingMessage = {' | '.join(incoming_titles)}")
198217
exported_classes.append("IncomingMessage")
199218

200219
if outgoing_messages:
@@ -205,7 +224,7 @@ def _generate_channel_messages(
205224
if m.payload and m.payload.title
206225
)
207226
)
208-
lines.append(f'OutgoingMessage = {" | ".join(outgoing_titles)}')
227+
lines.append(f"OutgoingMessage = {' | '.join(outgoing_titles)}")
209228
exported_classes.append("OutgoingMessage")
210229

211230
if lines:

docs/user-guide/client-generator.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ Basic Usage
219219
``--no-readme``
220220
Skip README.md generation
221221

222+
``--clear-output``
223+
Remove entire output directory before generation
224+
225+
``--override-base``
226+
Regenerate base files even if they already exist
227+
228+
``--no-clear-channels``
229+
Keep existing channel folders instead of clearing them
230+
231+
.. note::
232+
By default, regeneration keeps the ``base/`` folder (preserving customizations) and clears channel folders. Use ``--clear-output`` for a fresh start or ``--override-base`` to update base files.
233+
222234
Examples
223235
~~~~~~~~
224236

@@ -273,6 +285,24 @@ Examples
273285
--output ./myclient \
274286
--no-readme
275287
288+
**Fresh Regeneration (Clear Everything):**
289+
290+
.. code-block:: bash
291+
292+
chanx generate-client \
293+
--schema asyncapi.json \
294+
--output ./myclient \
295+
--clear-output
296+
297+
**Update Base Files:**
298+
299+
.. code-block:: bash
300+
301+
chanx generate-client \
302+
--schema asyncapi.json \
303+
--output ./myclient \
304+
--override-base
305+
276306
277307
Generated Client Structure
278308
--------------------------

tests/client_generation/test_cli.py

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ def test_generate_client_help(self) -> None:
159159
assert "--formatter" in result.output
160160
assert "--no-format" in result.output
161161
assert "--no-readme" in result.output
162+
assert "--clear-output" in result.output
163+
assert "--override-base" in result.output
164+
assert "--no-clear-channels" in result.output
162165

163166
def test_generate_client_with_custom_formatter(self) -> None:
164167
"""Test client generation with custom formatter command."""
@@ -206,8 +209,8 @@ def test_generate_client_formatter_not_found(self) -> None:
206209
or "Formatter" in result.output
207210
)
208211

209-
def test_generate_client_overwrites_existing_directory(self) -> None:
210-
"""Test that generating to existing directory overwrites it."""
212+
def test_generate_client_default_keeps_base(self) -> None:
213+
"""Test that default generation keeps base folder but clears channels."""
211214
output_dir = self.tmp_path / "client"
212215

213216
# Generate once
@@ -224,7 +227,11 @@ def test_generate_client_overwrites_existing_directory(self) -> None:
224227
)
225228
assert result1.exit_code == 0
226229

227-
# Create a dummy file
230+
# Create a custom file in base
231+
base_custom = output_dir / "base" / "custom.txt"
232+
base_custom.write_text("Custom base file")
233+
234+
# Create a dummy file in root
228235
dummy_file = output_dir / "dummy.txt"
229236
dummy_file.write_text("This should be removed")
230237

@@ -242,9 +249,132 @@ def test_generate_client_overwrites_existing_directory(self) -> None:
242249
)
243250
assert result2.exit_code == 0
244251

245-
# Dummy file should be removed
252+
# Base custom file should still exist
253+
assert base_custom.exists()
254+
# Root dummy file should be removed
246255
assert not dummy_file.exists()
247256

257+
def test_generate_client_clear_output(self) -> None:
258+
"""Test that --clear-output removes entire directory."""
259+
output_dir = self.tmp_path / "client"
260+
261+
# Generate once
262+
result1 = self.runner.invoke(
263+
cli,
264+
[
265+
"generate-client",
266+
"--schema",
267+
str(self.schema_path),
268+
"--output",
269+
str(output_dir),
270+
"--no-format",
271+
],
272+
)
273+
assert result1.exit_code == 0
274+
275+
# Create a custom file in base
276+
base_custom = output_dir / "base" / "custom.txt"
277+
base_custom.write_text("Custom base file")
278+
279+
# Generate again with --clear-output
280+
result2 = self.runner.invoke(
281+
cli,
282+
[
283+
"generate-client",
284+
"--schema",
285+
str(self.schema_path),
286+
"--output",
287+
str(output_dir),
288+
"--no-format",
289+
"--clear-output",
290+
],
291+
)
292+
assert result2.exit_code == 0
293+
294+
# Base custom file should be removed (entire directory was cleared)
295+
assert not base_custom.exists()
296+
297+
def test_generate_client_override_base(self) -> None:
298+
"""Test that --override-base regenerates base files."""
299+
output_dir = self.tmp_path / "client"
300+
301+
# Generate once
302+
result1 = self.runner.invoke(
303+
cli,
304+
[
305+
"generate-client",
306+
"--schema",
307+
str(self.schema_path),
308+
"--output",
309+
str(output_dir),
310+
"--no-format",
311+
],
312+
)
313+
assert result1.exit_code == 0
314+
315+
# Modify a file in base
316+
client_file = output_dir / "base" / "client.py"
317+
original_content = client_file.read_text()
318+
client_file.write_text("# Modified content")
319+
320+
# Generate again with --override-base
321+
result2 = self.runner.invoke(
322+
cli,
323+
[
324+
"generate-client",
325+
"--schema",
326+
str(self.schema_path),
327+
"--output",
328+
str(output_dir),
329+
"--no-format",
330+
"--override-base",
331+
],
332+
)
333+
assert result2.exit_code == 0
334+
335+
# Base client.py should be restored to original
336+
assert client_file.read_text() == original_content
337+
338+
def test_generate_client_no_clear_channels(self) -> None:
339+
"""Test that --no-clear-channels keeps existing channel files."""
340+
output_dir = self.tmp_path / "client"
341+
342+
# Generate once
343+
result1 = self.runner.invoke(
344+
cli,
345+
[
346+
"generate-client",
347+
"--schema",
348+
str(self.schema_path),
349+
"--output",
350+
str(output_dir),
351+
"--no-format",
352+
],
353+
)
354+
assert result1.exit_code == 0
355+
356+
# Create a custom file in a channel
357+
channel_custom = output_dir / "chat" / "custom.txt"
358+
channel_custom.write_text("Custom channel file")
359+
360+
# Generate again with --no-clear-channels
361+
result2 = self.runner.invoke(
362+
cli,
363+
[
364+
"generate-client",
365+
"--schema",
366+
str(self.schema_path),
367+
"--output",
368+
str(output_dir),
369+
"--no-format",
370+
"--no-clear-channels",
371+
],
372+
)
373+
assert result2.exit_code == 0
374+
375+
# Custom channel file should still exist
376+
assert channel_custom.exists()
377+
248378
def test_generate_client_formatter_timeout(self) -> None:
249379
"""Test that formatter timeout is handled gracefully."""
250380
import subprocess

0 commit comments

Comments
 (0)