Expand Dumas JSON utility nodes
This commit is contained in:
61
README.md
61
README.md
@@ -1,12 +1,45 @@
|
|||||||
# DumasNodes
|
# DumasNodes
|
||||||
|
|
||||||
`DumasNodes` is a ComfyUI custom-node pack for generic utility workflows under the Dumas brand. The repo starts by replicating the JSON string parsing helper from [`a-und-b/ComfyUI_JSON_Helper`](https://github.com/a-und-b/ComfyUI_JSON_Helper) and rebadging it into a reusable `Dumas` namespace.
|
`DumasNodes` is a ComfyUI custom-node pack for generic utility workflows under the Dumas brand.
|
||||||
|
|
||||||
## Included Nodes
|
## Included Nodes
|
||||||
|
|
||||||
- `Dumas JSON String to Object`
|
- `Dumas JSON String to Object`
|
||||||
- Converts a JSON string into a JSON object so downstream nodes can consume structured data.
|
- Input: `json_string`
|
||||||
- Lives in the `Dumas/JSON` category.
|
- Output: parsed `JSON`
|
||||||
|
- Use it to turn raw JSON text into a structured object.
|
||||||
|
|
||||||
|
- `Dumas JSON Object to String`
|
||||||
|
- Inputs: `json_object`, `pretty`, `sort_keys`
|
||||||
|
- Output: serialized `STRING`
|
||||||
|
- Use it to compact or pretty-print JSON for prompts, logs, or file output.
|
||||||
|
|
||||||
|
- `Dumas JSON Get Value`
|
||||||
|
- Inputs: `json_object`, `path`
|
||||||
|
- Output: `JSON`
|
||||||
|
- Reads a nested value using dot paths like `user.profile.name` or `items.0.id`.
|
||||||
|
|
||||||
|
- `Dumas JSON Set Value`
|
||||||
|
- Inputs: `json_object`, `path`, `value_json`
|
||||||
|
- Output: updated `JSON`
|
||||||
|
- Writes a parsed JSON value into a nested path without mutating the original object.
|
||||||
|
|
||||||
|
- `Dumas JSON Merge Objects`
|
||||||
|
- Inputs: `base_object`, `overlay_object`
|
||||||
|
- Output: merged `JSON`
|
||||||
|
- Deep-merges dictionaries so overlay values replace or extend the base object.
|
||||||
|
|
||||||
|
- `Dumas JSON Keys`
|
||||||
|
- Input: `json_object`
|
||||||
|
- Outputs: `keys` as JSON array, `count` as integer
|
||||||
|
- Lists the top-level keys of an object and reports how many there are.
|
||||||
|
|
||||||
|
- `Dumas JSON Array Length`
|
||||||
|
- Input: `json_array`
|
||||||
|
- Output: `INT`
|
||||||
|
- Returns the length of a JSON array.
|
||||||
|
|
||||||
|
All nodes live in the `Dumas/JSON` category.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -26,16 +59,28 @@
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Add `Dumas JSON String to Object` anywhere you need to bridge plain-text JSON output into structured JSON input.
|
Use dot paths for nested access:
|
||||||
|
|
||||||
|
```text
|
||||||
|
user.profile.name
|
||||||
|
items.0.id
|
||||||
|
settings.render.width
|
||||||
|
```
|
||||||
|
|
||||||
|
`Dumas JSON Set Value` expects `value_json` to be valid JSON, so strings should be quoted:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Dumas"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"enabled": true, "retries": 3}
|
||||||
|
```
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
This repo is intended to grow into a broader set of Dumas-branded generic utility nodes, including JSON helpers and adjacent data-manipulation tools.
|
This repo is intended to grow into a broader set of Dumas-branded generic utility nodes, including JSON helpers and adjacent data-manipulation tools.
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
The first node is derived from the upstream `ComfyUI_JSON_Helper` project by `a-und-b`, then repackaged and branded for the Dumas node collection.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT.
|
MIT.
|
||||||
|
|||||||
@@ -1,6 +1,91 @@
|
|||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_json_path(path):
|
||||||
|
if not path.strip():
|
||||||
|
return []
|
||||||
|
|
||||||
|
segments = []
|
||||||
|
for raw_segment in path.split("."):
|
||||||
|
segment = raw_segment.strip()
|
||||||
|
if not segment:
|
||||||
|
continue
|
||||||
|
if segment.isdigit():
|
||||||
|
segments.append(int(segment))
|
||||||
|
else:
|
||||||
|
segments.append(segment)
|
||||||
|
return segments
|
||||||
|
|
||||||
|
|
||||||
|
def _get_json_path_value(data, path):
|
||||||
|
current = data
|
||||||
|
for segment in _parse_json_path(path):
|
||||||
|
if isinstance(segment, int):
|
||||||
|
if not isinstance(current, list) or segment >= len(current):
|
||||||
|
return None
|
||||||
|
current = current[segment]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not isinstance(current, dict) or segment not in current:
|
||||||
|
return None
|
||||||
|
current = current[segment]
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def _set_json_path_value(data, path, value):
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
segments = _parse_json_path(path)
|
||||||
|
if not segments:
|
||||||
|
return value
|
||||||
|
|
||||||
|
result = copy.deepcopy(data)
|
||||||
|
current = result
|
||||||
|
|
||||||
|
for index, segment in enumerate(segments[:-1]):
|
||||||
|
next_segment = segments[index + 1]
|
||||||
|
if isinstance(segment, int):
|
||||||
|
if not isinstance(current, list):
|
||||||
|
return None
|
||||||
|
while segment >= len(current):
|
||||||
|
current.append({} if not isinstance(next_segment, int) else [])
|
||||||
|
current = current[segment]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if segment not in current or not isinstance(current[segment], (dict, list)):
|
||||||
|
current[segment] = [] if isinstance(next_segment, int) else {}
|
||||||
|
current = current[segment]
|
||||||
|
|
||||||
|
last_segment = segments[-1]
|
||||||
|
if isinstance(last_segment, int):
|
||||||
|
if not isinstance(current, list):
|
||||||
|
return None
|
||||||
|
while last_segment >= len(current):
|
||||||
|
current.append(None)
|
||||||
|
current[last_segment] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
if not isinstance(current, dict):
|
||||||
|
return None
|
||||||
|
current[last_segment] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_json_objects(base_object, overlay_object):
|
||||||
|
if not isinstance(base_object, dict) or not isinstance(overlay_object, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
merged = copy.deepcopy(base_object)
|
||||||
|
for key, value in overlay_object.items():
|
||||||
|
if isinstance(merged.get(key), dict) and isinstance(value, dict):
|
||||||
|
merged[key] = _merge_json_objects(merged[key], value)
|
||||||
|
else:
|
||||||
|
merged[key] = copy.deepcopy(value)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
class DumasJSONStringToObjectNode:
|
class DumasJSONStringToObjectNode:
|
||||||
CATEGORY = "Dumas/JSON"
|
CATEGORY = "Dumas/JSON"
|
||||||
RETURN_TYPES = ("JSON",)
|
RETURN_TYPES = ("JSON",)
|
||||||
@@ -23,10 +108,144 @@ class DumasJSONStringToObjectNode:
|
|||||||
return (None,)
|
return (None,)
|
||||||
|
|
||||||
|
|
||||||
|
class DumasJSONObjectToStringNode:
|
||||||
|
CATEGORY = "Dumas/JSON"
|
||||||
|
RETURN_TYPES = ("STRING",)
|
||||||
|
FUNCTION = "convert_object_to_string"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"json_object": ("JSON",),
|
||||||
|
"pretty": ("BOOLEAN", {"default": True}),
|
||||||
|
"sort_keys": ("BOOLEAN", {"default": False}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_object_to_string(self, json_object, pretty, sort_keys):
|
||||||
|
indent = 2 if pretty else None
|
||||||
|
separators = None if pretty else (",", ":")
|
||||||
|
return (json.dumps(json_object, indent=indent, sort_keys=sort_keys, separators=separators),)
|
||||||
|
|
||||||
|
|
||||||
|
class DumasJSONGetValueNode:
|
||||||
|
CATEGORY = "Dumas/JSON"
|
||||||
|
RETURN_TYPES = ("JSON",)
|
||||||
|
FUNCTION = "get_value"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"json_object": ("JSON",),
|
||||||
|
"path": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_value(self, json_object, path):
|
||||||
|
return (_get_json_path_value(json_object, path),)
|
||||||
|
|
||||||
|
|
||||||
|
class DumasJSONSetValueNode:
|
||||||
|
CATEGORY = "Dumas/JSON"
|
||||||
|
RETURN_TYPES = ("JSON",)
|
||||||
|
FUNCTION = "set_value"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"json_object": ("JSON",),
|
||||||
|
"path": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
"value_json": ("STRING", {"multiline": True, "default": "null"}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_value(self, json_object, path, value_json):
|
||||||
|
try:
|
||||||
|
value = json.loads(value_json)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
print(f"[DumasNodes] Error decoding JSON value: {exc}")
|
||||||
|
return (None,)
|
||||||
|
|
||||||
|
return (_set_json_path_value(json_object, path, value),)
|
||||||
|
|
||||||
|
|
||||||
|
class DumasJSONMergeObjectsNode:
|
||||||
|
CATEGORY = "Dumas/JSON"
|
||||||
|
RETURN_TYPES = ("JSON",)
|
||||||
|
FUNCTION = "merge_objects"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"base_object": ("JSON",),
|
||||||
|
"overlay_object": ("JSON",),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def merge_objects(self, base_object, overlay_object):
|
||||||
|
return (_merge_json_objects(base_object, overlay_object),)
|
||||||
|
|
||||||
|
|
||||||
|
class DumasJSONKeysNode:
|
||||||
|
CATEGORY = "Dumas/JSON"
|
||||||
|
RETURN_TYPES = ("JSON", "INT")
|
||||||
|
RETURN_NAMES = ("keys", "count")
|
||||||
|
FUNCTION = "list_keys"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"json_object": ("JSON",),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def list_keys(self, json_object):
|
||||||
|
if not isinstance(json_object, dict):
|
||||||
|
return (None, 0)
|
||||||
|
keys = list(json_object.keys())
|
||||||
|
return (keys, len(keys))
|
||||||
|
|
||||||
|
|
||||||
|
class DumasJSONArrayLengthNode:
|
||||||
|
CATEGORY = "Dumas/JSON"
|
||||||
|
RETURN_TYPES = ("INT",)
|
||||||
|
FUNCTION = "get_length"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"json_array": ("JSON",),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_length(self, json_array):
|
||||||
|
if not isinstance(json_array, list):
|
||||||
|
return (0,)
|
||||||
|
return (len(json_array),)
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"DumasJSONStringToObject": DumasJSONStringToObjectNode,
|
"DumasJSONStringToObject": DumasJSONStringToObjectNode,
|
||||||
|
"DumasJSONObjectToString": DumasJSONObjectToStringNode,
|
||||||
|
"DumasJSONGetValue": DumasJSONGetValueNode,
|
||||||
|
"DumasJSONSetValue": DumasJSONSetValueNode,
|
||||||
|
"DumasJSONMergeObjects": DumasJSONMergeObjectsNode,
|
||||||
|
"DumasJSONKeys": DumasJSONKeysNode,
|
||||||
|
"DumasJSONArrayLength": DumasJSONArrayLengthNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"DumasJSONStringToObject": "Dumas JSON String to Object",
|
"DumasJSONStringToObject": "Dumas JSON String to Object",
|
||||||
|
"DumasJSONObjectToString": "Dumas JSON Object to String",
|
||||||
|
"DumasJSONGetValue": "Dumas JSON Get Value",
|
||||||
|
"DumasJSONSetValue": "Dumas JSON Set Value",
|
||||||
|
"DumasJSONMergeObjects": "Dumas JSON Merge Objects",
|
||||||
|
"DumasJSONKeys": "Dumas JSON Keys",
|
||||||
|
"DumasJSONArrayLength": "Dumas JSON Array Length",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from dumas_json_nodes import DumasJSONStringToObjectNode
|
from dumas_json_nodes import (
|
||||||
|
DumasJSONArrayLengthNode,
|
||||||
|
DumasJSONGetValueNode,
|
||||||
|
DumasJSONKeysNode,
|
||||||
|
DumasJSONMergeObjectsNode,
|
||||||
|
DumasJSONObjectToStringNode,
|
||||||
|
DumasJSONStringToObjectNode,
|
||||||
|
DumasJSONSetValueNode,
|
||||||
|
)
|
||||||
|
|
||||||
|
class DumasJSONNodeTests(unittest.TestCase):
|
||||||
class DumasJSONStringToObjectNodeTests(unittest.TestCase):
|
|
||||||
def test_parses_valid_json(self):
|
def test_parses_valid_json(self):
|
||||||
node = DumasJSONStringToObjectNode()
|
node = DumasJSONStringToObjectNode()
|
||||||
|
|
||||||
@@ -18,6 +25,54 @@ class DumasJSONStringToObjectNodeTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(result, (None,))
|
self.assertEqual(result, (None,))
|
||||||
|
|
||||||
|
def test_object_to_string_can_pretty_print(self):
|
||||||
|
node = DumasJSONObjectToStringNode()
|
||||||
|
|
||||||
|
result = node.convert_object_to_string({"b": 1, "a": 2}, True, True)
|
||||||
|
|
||||||
|
self.assertEqual(result, ('{\n "a": 2,\n "b": 1\n}',))
|
||||||
|
|
||||||
|
def test_get_value_reads_nested_paths(self):
|
||||||
|
node = DumasJSONGetValueNode()
|
||||||
|
|
||||||
|
result = node.get_value({"items": [{"id": "a1"}]}, "items.0.id")
|
||||||
|
|
||||||
|
self.assertEqual(result, ("a1",))
|
||||||
|
|
||||||
|
def test_set_value_writes_nested_paths(self):
|
||||||
|
node = DumasJSONSetValueNode()
|
||||||
|
|
||||||
|
result = node.set_value({"user": {"name": "Chris"}}, "user.name", '"Dumas"')
|
||||||
|
|
||||||
|
self.assertEqual(result, ({"user": {"name": "Dumas"}},))
|
||||||
|
|
||||||
|
def test_merge_objects_deep_merges_dictionaries(self):
|
||||||
|
node = DumasJSONMergeObjectsNode()
|
||||||
|
|
||||||
|
result = node.merge_objects(
|
||||||
|
{"user": {"name": "Chris", "flags": {"admin": False}}},
|
||||||
|
{"user": {"flags": {"admin": True}, "role": "owner"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
({"user": {"name": "Chris", "flags": {"admin": True}, "role": "owner"}},),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_keys_returns_key_list_and_count(self):
|
||||||
|
node = DumasJSONKeysNode()
|
||||||
|
|
||||||
|
result = node.list_keys({"name": "Dumas", "kind": "nodes"})
|
||||||
|
|
||||||
|
self.assertEqual(result, (["name", "kind"], 2))
|
||||||
|
|
||||||
|
def test_array_length_returns_length(self):
|
||||||
|
node = DumasJSONArrayLengthNode()
|
||||||
|
|
||||||
|
result = node.get_length([1, 2, 3, 4])
|
||||||
|
|
||||||
|
self.assertEqual(result, (4,))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user