Coverage for src/prosemark/app/add_node.py: 100%
41 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
1"""AddNode use case for adding content nodes to the binder."""
3from pathlib import Path
4from typing import TYPE_CHECKING
6from prosemark.domain.models import BinderItem, NodeId
8if TYPE_CHECKING: # pragma: no cover
9 from prosemark.ports.binder_repo import BinderRepo
10 from prosemark.ports.clock import Clock
11 from prosemark.ports.console_port import ConsolePort
12 from prosemark.ports.id_generator import IdGenerator
13 from prosemark.ports.logger import Logger
14 from prosemark.ports.node_repo import NodeRepo
17class AddNode:
18 """Add a new content node to the binder hierarchy."""
20 def __init__(
21 self,
22 *,
23 binder_repo: 'BinderRepo',
24 node_repo: 'NodeRepo',
25 id_generator: 'IdGenerator',
26 clock: 'Clock',
27 console: 'ConsolePort',
28 logger: 'Logger',
29 ) -> None:
30 """Initialize the AddNode use case.
32 Args:
33 binder_repo: Repository for binder operations.
34 node_repo: Repository for node operations.
35 id_generator: Generator for unique node IDs.
36 clock: Clock for timestamps.
37 console: Console output port.
38 logger: Logger port.
40 """
41 self.binder_repo = binder_repo
42 self.node_repo = node_repo
43 self.id_generator = id_generator
44 self.clock = clock
45 self.console = console
46 self.logger = logger
48 def execute(
49 self,
50 *,
51 title: str,
52 parent_id: NodeId | None = None,
53 position: int | None = None,
54 project_path: Path | None = None,
55 ) -> NodeId:
56 """Add a new node to the binder.
58 Args:
59 title: Title for the new node.
60 parent_id: Optional parent node ID for nested placement.
61 position: Optional position within parent's children.
62 project_path: Project directory path.
64 Returns:
65 The ID of the newly created node.
67 """
68 project_path = project_path or Path.cwd()
69 self.logger.info('Adding node: %s', title)
71 # Load existing binder
72 binder = self.binder_repo.load()
74 # Generate new node ID
75 node_id = self.id_generator.new()
77 # Create the node files
78 self.node_repo.create(node_id, title, None)
80 # Create binder item
81 new_item = BinderItem(display_title=title, node_id=node_id, children=[])
83 # Add to binder hierarchy
84 if parent_id:
85 # Find parent and add as child
86 parent_item = self._find_item(binder.roots, parent_id)
87 if not parent_item:
88 self.console.print_error(f'Parent node {parent_id.value} not found')
89 return node_id
91 if position is not None and 0 <= position <= len(parent_item.children):
92 parent_item.children.insert(position, new_item)
93 else:
94 parent_item.children.append(new_item)
95 # Add as root item
96 elif position is not None and 0 <= position <= len(binder.roots):
97 binder.roots.insert(position, new_item)
98 else:
99 binder.roots.append(new_item)
101 # Save updated binder
102 self.binder_repo.save(binder)
104 self.console.print_success(f'Added "{title}" ({node_id.value})')
105 self.logger.info('Node added: %s', node_id.value)
107 return node_id
109 def _find_item(self, items: list[BinderItem], node_id: NodeId) -> BinderItem | None:
110 """Find an item by node ID in the hierarchy.
112 Args:
113 items: List of binder items to search.
114 node_id: Node ID to find.
116 Returns:
117 The binder item if found, None otherwise.
119 """
120 for item in items:
121 if item.node_id and item.node_id.value == node_id.value:
122 return item
123 found = self._find_item(item.children, node_id)
124 if found:
125 return found
126 return None