Feature Spec: Output Formatter¶
Feature ID: FE-08 Status: Ready for Implementation Priority: P1 Parent: Tech Design v1.0 Section 8.5 SRS Requirements: FR-DISC-004
1. Description¶
The Output Formatter provides TTY-adaptive output rendering for apcore-cli. It detects whether stdout is connected to a terminal or a pipe, and defaults to rich table formatting for TTY sessions or JSON for non-TTY contexts. It is used by Discovery (list, describe) and can be used by exec output formatting.
2. Requirements Traceability¶
| Req ID | SRS Ref | Description |
|---|---|---|
| FR-08-01 | FR-DISC-004 | TTY-adaptive output format selection with --format override. |
| FR-08-02 | FR-DISC-001 | Table rendering for module lists using rich.table.Table. |
| FR-08-03 | FR-DISC-003 | Syntax-highlighted JSON rendering using rich.syntax.Syntax. |
3. Module Path¶
apcore_cli/output.py
4. Implementation Details¶
4.1 Function: resolve_format¶
Signature: resolve_format(explicit_format: str | None) -> str
Logic steps:
1. If explicit_format is not None: return explicit_format.
2. If sys.stdout.isatty(): return "table".
3. Else: return "json".
4.2 Function: format_module_list¶
Signature: format_module_list(modules: list[ModuleDefinition], format: str, filter_tags: tuple[str, ...] = ()) -> None
Logic steps:
1. If format == "table":
a. Create table = rich.table.Table(title="Modules").
b. Add columns: "ID", "Description", "Tags".
c. For each module:
- desc = _truncate(module.description, 80).
- tags = ", ".join(module.tags).
- table.add_row(module.canonical_id, desc, tags).
d. If no modules and filter_tags:
- Print f"No modules found matching tags: {', '.join(filter_tags)}.".
e. Elif no modules:
- Print "No modules found.".
f. Print table via Console().print(table).
2. If format == "json":
a. Build result = [{"id": m.canonical_id, "description": m.description, "tags": m.tags} for m in modules].
b. Print json.dumps(result, indent=2).
4.3 Function: format_module_detail¶
Signature: format_module_detail(module_def: ModuleDefinition, format: str) -> None
Logic steps:
1. If format == "table":
a. Print rich.panel.Panel(f"Module: {module_def.canonical_id}").
b. Print "\nDescription:\n {module_def.description}\n".
c. If module_def.input_schema:
- Print "\nInput Schema:".
- Print rich.syntax.Syntax(json.dumps(module_def.input_schema, indent=2), "json", theme="monokai").
d. If module_def.output_schema:
- Print "\nOutput Schema:".
- Print rich.syntax.Syntax(json.dumps(module_def.output_schema, indent=2), "json", theme="monokai").
e. If module_def.annotations and non-empty:
- Print "\nAnnotations:".
- For each (k, v): print f" {k}: {v}".
f. Collect x_fields = {k: v for k, v in vars(module_def).items() if k.startswith("x_") or k.startswith("x-")}.
g. If x_fields:
- Print "\nExtension Metadata:".
- For each (k, v): print f" {k}: {v}".
h. If module_def.tags:
- Print f"\nTags: {', '.join(module_def.tags)}".
2. If format == "json":
a. Build dict with all non-None fields.
b. Print json.dumps(result, indent=2).
4.4 Function: format_exec_result¶
Signature: format_exec_result(result: Any, format: str | None = None) -> None
Logic steps:
1. If result is a dict or list: print json.dumps(result, indent=2, default=str).
2. If result is a str: print result directly.
3. If result is None: print nothing (empty stdout).
4. Otherwise: print str(result).
4.5 Helper: _truncate¶
def _truncate(text: str, max_length: int = 80) -> str:
if len(text) <= max_length:
return text
return text[:max_length - 3] + "..."
5. Parameter Validation¶
| Parameter | Type | Valid Values | Invalid Handling | SRS Reference |
|---|---|---|---|---|
explicit_format |
str \| None |
"table", "json", None |
None triggers TTY detection. Invalid values should be caught by Click upstream. |
FR-DISC-004 |
modules |
list |
List of ModuleDefinition objects. |
Empty list: display "No modules found." message. | FR-DISC-001 |
result (exec) |
Any |
Any JSON-serializable type, string, or None. | Non-serializable: use default=str fallback. |
— |
6. Error Handling¶
| Condition | Behavior | SRS Reference |
|---|---|---|
| Empty module list | Display "No modules found." (table) or [] (JSON). Exit 0. |
FR-DISC-001 AF-1 |
| Result not JSON-serializable | Use default=str in json.dumps. |
— |
| Terminal without color support | rich auto-detects. Can be forced via NO_COLOR=1 env var. |
NFR-PRT-002 |
7. Verification¶
| Test ID | Description | Expected Result |
|---|---|---|
| T-OUT-01 | resolve_format(None) in TTY |
Returns "table". |
| T-OUT-02 | resolve_format(None) in non-TTY |
Returns "json". |
| T-OUT-03 | resolve_format("json") in TTY |
Returns "json" (explicit overrides TTY). |
| T-OUT-04 | format_module_list with 2 modules, format="table" |
Rich table with 2 rows. |
| T-OUT-05 | format_module_list with 0 modules, format="table" |
"No modules found." message. |
| T-OUT-06 | format_module_list with modules, format="json" |
Valid JSON array. |
| T-OUT-07 | format_module_detail with full metadata, format="table" |
Syntax-highlighted schemas, annotations, tags. |
| T-OUT-08 | format_module_detail with minimal metadata (no output_schema) |
Output Schema section omitted. |
| T-OUT-09 | format_exec_result with dict |
JSON-formatted dict. |
| T-OUT-10 | format_exec_result with None |
Empty stdout. |
| T-OUT-11 | Description truncation at 80 chars | 77 chars + "..." for text > 80 chars. |