Coverage for formkit_ninja / parser / node_factory.py: 19.67%

45 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-02-20 04:40 +0000

1""" 

2Factory for parsing FormKit nodes from structured inputs. 

3 

4This factory uses the NodeRegistry to resolve node types and create instances. 

5It falls back to FormKitNode.parse_obj for backward compatibility. 

6""" 

7 

8from __future__ import annotations 

9 

10import json 

11from typing import Any, cast 

12 

13from formkit_ninja import formkit_schema 

14from formkit_ninja.formkit_schema import FormKitNode 

15from formkit_ninja.parser.node_registry import NodeRegistry, default_registry 

16 

17 

18class FormKitNodeFactory: 

19 """ 

20 Create FormKit node instances from dict or JSON input. 

21 

22 Uses NodeRegistry to resolve node types when possible, falling back to 

23 FormKitNode.parse_obj for backward compatibility. 

24 """ 

25 

26 def __init__(self, registry: NodeRegistry | None = None) -> None: 

27 """ 

28 Initialize the factory with an optional registry. 

29 

30 Args: 

31 registry: Optional NodeRegistry instance. Defaults to default_registry. 

32 """ 

33 self.registry = registry or default_registry 

34 

35 @staticmethod 

36 def from_dict(data: dict[str, Any]) -> formkit_schema.FormKitType: 

37 """ 

38 Create a FormKit node from a dictionary. 

39 

40 Uses the registry to resolve node types when possible, falling back 

41 to FormKitNode.parse_obj for backward compatibility. 

42 

43 Args: 

44 data: Dictionary containing node data (must include "$formkit" key for formkit nodes) 

45 

46 Returns: 

47 A FormKit node instance 

48 

49 Raises: 

50 ValueError: If the data is invalid or cannot be parsed 

51 """ 

52 # Try to use registry if this is a formkit node 

53 node: formkit_schema.FormKitType | None = None 

54 if "$formkit" in data: 

55 formkit_type = data["$formkit"] 

56 node_class = default_registry.get_formkit_node_class(formkit_type) 

57 

58 if node_class is not None: 

59 try: 

60 # Use the registered class directly 

61 node = node_class.parse_obj(data) 

62 

63 # Recursively parse children if present 

64 # This is necessary because Pydantic models define children as FormKitSchemaProps 

65 # but we need the specific node types 

66 if "children" in data and hasattr(node, "children"): 

67 children_in = data["children"] 

68 if isinstance(children_in, list): 

69 children_out: list[Any] = [] 

70 for child_data in children_in: 

71 if isinstance(child_data, dict): 

72 # Recursively use the factory 

73 children_out.append(FormKitNodeFactory.from_dict(child_data)) 

74 elif isinstance(child_data, str): 

75 children_out.append(child_data) 

76 

77 # mypy doesn't know that node.children is a list of FormKitType 

78 node.children = children_out # type: ignore[assignment] 

79 

80 return cast(formkit_schema.FormKitType, node) 

81 except Exception: 

82 # If direct parsing fails, fall back to FormKitNode.parse_obj 

83 # This maintains backward compatibility 

84 pass 

85 

86 # Fall back to original behavior for backward compatibility 

87 try: 

88 node = FormKitNode.parse_obj(data).__root__ # type: ignore[assignment] 

89 except Exception as exc: 

90 raise ValueError("Invalid FormKit node data") from exc 

91 return cast(formkit_schema.FormKitType, node) 

92 

93 @staticmethod 

94 def from_json(payload: str) -> formkit_schema.FormKitType: 

95 """ 

96 Create a FormKit node from a JSON string. 

97 

98 Args: 

99 payload: JSON string containing node data 

100 

101 Returns: 

102 A FormKit node instance 

103 

104 Raises: 

105 ValueError: If the JSON is invalid or cannot be parsed 

106 """ 

107 try: 

108 data = json.loads(payload) 

109 except json.JSONDecodeError as exc: 

110 raise ValueError("Invalid JSON for FormKit node") from exc 

111 if not isinstance(data, dict): 

112 raise ValueError("FormKit node JSON must be an object") 

113 return FormKitNodeFactory.from_dict(data)