Expand Dumas JSON utility nodes
This commit is contained in:
61
README.md
61
README.md
@@ -1,12 +1,45 @@
|
||||
# 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
|
||||
|
||||
- `Dumas JSON String to Object`
|
||||
- Converts a JSON string into a JSON object so downstream nodes can consume structured data.
|
||||
- Lives in the `Dumas/JSON` category.
|
||||
- Input: `json_string`
|
||||
- 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
|
||||
|
||||
@@ -26,16 +59,28 @@
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
MIT.
|
||||
|
||||
@@ -1,6 +1,91 @@
|
||||
import copy
|
||||
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:
|
||||
CATEGORY = "Dumas/JSON"
|
||||
RETURN_TYPES = ("JSON",)
|
||||
@@ -23,10 +108,144 @@ class DumasJSONStringToObjectNode:
|
||||
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 = {
|
||||
"DumasJSONStringToObject": DumasJSONStringToObjectNode,
|
||||
"DumasJSONObjectToString": DumasJSONObjectToStringNode,
|
||||
"DumasJSONGetValue": DumasJSONGetValueNode,
|
||||
"DumasJSONSetValue": DumasJSONSetValueNode,
|
||||
"DumasJSONMergeObjects": DumasJSONMergeObjectsNode,
|
||||
"DumasJSONKeys": DumasJSONKeysNode,
|
||||
"DumasJSONArrayLength": DumasJSONArrayLengthNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"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
|
||||
|
||||
from dumas_json_nodes import DumasJSONStringToObjectNode
|
||||
from dumas_json_nodes import (
|
||||
DumasJSONArrayLengthNode,
|
||||
DumasJSONGetValueNode,
|
||||
DumasJSONKeysNode,
|
||||
DumasJSONMergeObjectsNode,
|
||||
DumasJSONObjectToStringNode,
|
||||
DumasJSONStringToObjectNode,
|
||||
DumasJSONSetValueNode,
|
||||
)
|
||||
|
||||
|
||||
class DumasJSONStringToObjectNodeTests(unittest.TestCase):
|
||||
class DumasJSONNodeTests(unittest.TestCase):
|
||||
def test_parses_valid_json(self):
|
||||
node = DumasJSONStringToObjectNode()
|
||||
|
||||
@@ -18,6 +25,54 @@ class DumasJSONStringToObjectNodeTests(unittest.TestCase):
|
||||
|
||||
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__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user