Add JSONLoaderDynamic node with JS frontend for auto-discovered outputs

Dynamic node reads JSON keys and exposes them as outputs automatically
via 32 AnyType slots managed by a JS extension (show/hide/rename).
Includes /json_manager/get_keys API route, bool-safe type handling,
and workflow save/reload support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 15:47:22 +01:00
parent a4717dfab6
commit e841e9b76b
4 changed files with 306 additions and 2 deletions

View File

@@ -3,7 +3,10 @@ import os
import pytest
from json_loader import to_float, to_int, get_batch_item, read_json_data
from json_loader import (
to_float, to_int, get_batch_item, read_json_data,
JSONLoaderDynamic, MAX_DYNAMIC_OUTPUTS,
)
class TestToFloat:
@@ -75,3 +78,88 @@ class TestReadJsonData:
p = tmp_path / "ok.json"
p.write_text(json.dumps({"key": "val"}))
assert read_json_data(str(p)) == {"key": "val"}
class TestJSONLoaderDynamic:
def _make_json(self, tmp_path, data):
p = tmp_path / "test.json"
p.write_text(json.dumps(data))
return str(p)
def test_known_keys(self, tmp_path):
path = self._make_json(tmp_path, {"name": "alice", "age": 30, "score": 9.5})
loader = JSONLoaderDynamic()
result = loader.load_dynamic(path, 1, output_keys="name,age,score")
assert result[0] == "alice"
assert result[1] == 30
assert result[2] == 9.5
def test_empty_output_keys(self, tmp_path):
path = self._make_json(tmp_path, {"name": "alice"})
loader = JSONLoaderDynamic()
result = loader.load_dynamic(path, 1, output_keys="")
assert len(result) == MAX_DYNAMIC_OUTPUTS
assert all(v == "" for v in result)
def test_pads_to_max(self, tmp_path):
path = self._make_json(tmp_path, {"a": "1", "b": "2"})
loader = JSONLoaderDynamic()
result = loader.load_dynamic(path, 1, output_keys="a,b")
assert len(result) == MAX_DYNAMIC_OUTPUTS
assert result[0] == "1"
assert result[1] == "2"
assert all(v == "" for v in result[2:])
def test_type_preservation_int(self, tmp_path):
path = self._make_json(tmp_path, {"count": 42})
loader = JSONLoaderDynamic()
result = loader.load_dynamic(path, 1, output_keys="count")
assert result[0] == 42
assert isinstance(result[0], int)
def test_type_preservation_float(self, tmp_path):
path = self._make_json(tmp_path, {"rate": 3.14})
loader = JSONLoaderDynamic()
result = loader.load_dynamic(path, 1, output_keys="rate")
assert result[0] == 3.14
assert isinstance(result[0], float)
def test_type_preservation_str(self, tmp_path):
path = self._make_json(tmp_path, {"label": "hello"})
loader = JSONLoaderDynamic()
result = loader.load_dynamic(path, 1, output_keys="label")
assert result[0] == "hello"
assert isinstance(result[0], str)
def test_bool_becomes_string(self, tmp_path):
path = self._make_json(tmp_path, {"flag": True, "off": False})
loader = JSONLoaderDynamic()
result = loader.load_dynamic(path, 1, output_keys="flag,off")
assert result[0] == "true"
assert result[1] == "false"
assert isinstance(result[0], str)
def test_missing_key_returns_empty_string(self, tmp_path):
path = self._make_json(tmp_path, {"a": "1"})
loader = JSONLoaderDynamic()
result = loader.load_dynamic(path, 1, output_keys="a,nonexistent")
assert result[0] == "1"
assert result[1] == ""
def test_missing_file_returns_all_empty(self, tmp_path):
loader = JSONLoaderDynamic()
result = loader.load_dynamic(str(tmp_path / "nope.json"), 1, output_keys="a,b")
assert len(result) == MAX_DYNAMIC_OUTPUTS
assert result[0] == ""
assert result[1] == ""
def test_batch_data(self, tmp_path):
path = self._make_json(tmp_path, {
"batch_data": [
{"sequence_number": 1, "x": "first"},
{"sequence_number": 2, "x": "second"},
]
})
loader = JSONLoaderDynamic()
result = loader.load_dynamic(path, 2, output_keys="x")
assert result[0] == "second"