json-to-toon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dce3c47c0ea7444146b729dea48cbb14a7a229e54b93ea88cdbadf0b17e4cb2c
4
+ data.tar.gz: afb955c2c2e23d89cc17c8f59aaef420df0fa8c1d33df491ade236b386ba5a0d
5
+ SHA512:
6
+ metadata.gz: d282c4fdbe66511c3348a0ec69fadefd0ab0c68fb3752846ff42258bd7ce4cdd5e6d83856208f315a9f241d6a48ec62edbb03ebcb050079b6e5c358f60e6476a
7
+ data.tar.gz: 175b6822ca8893d779c972810b97de44087328ed06aa07e8a3988178ca5611c4a9cfa0fa0044cef4bde376ef2409e6782899356f2f1a03759a1ae2813131994c
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) [2025] [Daniele Frisanco]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,147 @@
1
+ JsonToToon
2
+
3
+ JsonToToon is a unique Ruby gem designed to convert standard JSON strings into a custom, highly structured text format (dubbed the "Toon Format") optimized for human readability and specific parsing needs.
4
+
5
+ Unlike standard JSON or YAML, the Toon Format includes structural metadata like array counts and keys for tabular data, making it useful for configuration systems or structured data display.
6
+
7
+ ✨ Features
8
+
9
+ - **Deep Symbolization**: All JSON keys are deeply symbolized internally for easy processing.
10
+ - **Tree Structure Conversion**: JSON is converted into a hierarchical `DataNode` tree (`ObjectNode`, `ArrayNode`, `ValueNode`).
11
+ - **Custom Toon Format Output**: Converts the structure into a custom, indented string output.
12
+ - **Tabular Array Detection**: Automatically detects arrays containing homogenous records (hash keys are all the same, values are primitive) and outputs them using a concise tabular syntax: `array_key[count]{key1,key2}:`.
13
+ - **Clean Error Handling**: Uses custom `JsonToToon::ConversionError` for invalid input.
14
+ - **Deeply Nested JSONs Handling**: The gem is specifically designed to handle and accurately represent highly complex and **deeply nested** JSON structures in the Toon Format.
15
+
16
+ ⚙️ Installation
17
+
18
+ Add this line to your application's `Gemfile`:
19
+ ```ruby
20
+ gem 'json-to-toon'
21
+ ```
22
+
23
+ And then execute:
24
+ ```bash
25
+ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+ ```bash
30
+ gem install json-to-toon
31
+ ```
32
+
33
+ 🚀 Usage
34
+
35
+ The primary function of the gem is the `JsonToToon.convert` class method, which takes a JSON string and returns the formatted Toon string.
36
+
37
+ Simple Example
38
+
39
+ ```ruby
40
+ require 'json-to-toon'
41
+
42
+ json_string = '{"title": "The Report", "version": 1.0}'
43
+ toon_output = JsonToToon.convert(json_string)
44
+
45
+ puts toon_output
46
+
47
+ # Output:
48
+ # title: The Report
49
+ # version: 1.0
50
+ ```
51
+
52
+ 📝 What is the Toon Format?
53
+
54
+ This gem converts JSON into the Toon Format, a custom, structured data serialization format designed for specialized hierarchical data display.
55
+
56
+ The format is characterized by:
57
+
58
+ - Two-space indentation.
59
+
60
+ - The use of special notation for arrays, particularly when detecting tabular data (arrays of uniform hashes).
61
+
62
+ - Keys containing structural metadata (like the count and keys in `team_members[2]{id,role}:`).
63
+
64
+ - You can find the full specification and details on this format here: [Toon Format Specification](https://toonformat.dev/).
65
+
66
+ Complex Example (Demonstrating Tabular Arrays and Nesting)
67
+
68
+ Given this input JSON:
69
+ ```json
70
+ {
71
+ "project_details": {
72
+ "name": "Project Alpha",
73
+ "status": "In Progress"
74
+ },
75
+ "team_members": [
76
+ { "id": 1, "role": "Engineer" },
77
+ { "id": 2, "role": "Designer" }
78
+ ],
79
+ "tasks": [
80
+ "documentation",
81
+ "refactoring"
82
+ ]
83
+ }
84
+ ```
85
+
86
+ The output "Toon Format" string will be:
87
+ ```ruby
88
+ require 'json-to-toon'
89
+
90
+ complex_json = File.read('input.json') # Assume file reading here
91
+
92
+ toon_output = JsonToToon.convert(complex_json)
93
+
94
+ puts toon_output
95
+
96
+ # Output:
97
+ # project_details:
98
+ # name: Project Alpha
99
+ # status: In Progress
100
+ # team_members[2]{id,role}:
101
+ # 1,Engineer
102
+ # 2,Designer
103
+ # tasks:
104
+ # - documentation
105
+ # - refactoring
106
+ ```
107
+ Notice the key differences:
108
+
109
+ 1. `project_details` is indented normally.
110
+
111
+ 2. `team_members` is detected as a tabular array (`[2]{id,role}:`) and its items are output as comma-separated values (CSV style).
112
+
113
+ 3. `tasks` (an array of simple values) is output as a standard dashed list.
114
+
115
+ 🛑 Error Handling
116
+
117
+ The gem uses custom exceptions for easy handling within your application:
118
+
119
+ | Exception | Base Class | Triggered When
120
+ |-|-|-|
121
+ `ArgumentError` | `StandardError` | Input is not a String.
122
+ `JsonToToon::ConversionError` | `JsonToToon::Error` | The input string is syntactically invalid JSON (wraps `JSON::ParserError`).
123
+
124
+ Example of catching a conversion failure:
125
+ ```ruby
126
+ require 'json-to-toon'
127
+
128
+ begin
129
+ JsonToToon.convert('{broken:')
130
+ rescue JsonToToon::ConversionError => e
131
+ puts "Conversion failed: #{e.message}"
132
+ end
133
+
134
+ # Output: Conversion failed: Invalid JSON input: expected ',' or '}' after object key and value
135
+ ```
136
+
137
+ 🧑‍💻 Development
138
+
139
+ After checking out the repository, run `bundle install` to install dependencies.
140
+
141
+ To run the tests and linting:
142
+
143
+ ```bash
144
+ bundle exec rspec # Runs the test suite
145
+
146
+ bundle exec rubocop -a # Runs the linter and auto-corrects offenses
147
+ ```
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'json_to_toon/version'
4
+ require_relative 'json_to_toon/converter'
5
+
6
+ module JsonToToon
7
+ def self.convert(json_string)
8
+ Converter.convert(json_string)
9
+ end
10
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'container_node'
4
+
5
+ module JsonToToon
6
+ class ArrayNode < ContainerNode
7
+ def initialize(parent = nil)
8
+ super
9
+ end
10
+
11
+ def standard_tabular_array?
12
+ arr = @children
13
+
14
+ return true if arr.empty?
15
+
16
+ first_node = arr[0]
17
+ return false unless first_node.is_a?(ObjectNode)
18
+
19
+ first_hash_keys = first_node.keys.sort
20
+
21
+ arr.all? do |item_node|
22
+ next false unless item_node.is_a?(ObjectNode)
23
+ next false unless item_node.keys.sort == first_hash_keys
24
+
25
+ item_node.children.all? do |key_value_node|
26
+ value_node = key_value_node.value_node
27
+ value_node.is_a?(ValueNode)
28
+ end
29
+ end
30
+ end
31
+
32
+ def single_indent(spaces = 2)
33
+ ' ' * (@indentation_level * spaces)
34
+ end
35
+
36
+ def to_s
37
+ output = +''
38
+ if standard_tabular_array?
39
+ unless @children.empty?
40
+ type = "{#{@children.first&.keys&.join(',')}}"
41
+ output = type.to_s
42
+ end
43
+ output += ":\n"
44
+
45
+ output += @children.map do |item|
46
+ "#{indent}#{item.children.map { |c| c.to_s.chomp.lstrip }.join(',')}"
47
+ end.join("\n")
48
+ return output
49
+ end
50
+
51
+ output = +':'
52
+ output_inline = true
53
+
54
+ child_strings = @children.map do |child|
55
+ output_inline = false if child.is_a?(ObjectNode) || child.is_a?(ArrayNode)
56
+ child.to_s.chomp.lstrip
57
+ end
58
+ if output_inline
59
+ output = +'' if parent.is_a?(ArrayNode)
60
+ output << ' '
61
+ output << child_strings.join(',')
62
+ else
63
+ output << "\n#{indent}- "
64
+ output << child_strings.join("\n#{indent}- ")
65
+ end
66
+
67
+ output << "\n"
68
+
69
+ output
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data_node'
4
+
5
+ module JsonToToon
6
+ class ContainerNode < DataNode
7
+ attr_reader :children
8
+
9
+ def initialize(parent = nil)
10
+ super
11
+ @children = []
12
+ end
13
+
14
+ def add_child(child)
15
+ raise ArgumentError, 'Can only add objects of type DataNode as children.' unless child.is_a?(DataNode)
16
+
17
+ @children << child
18
+ end
19
+
20
+ def to_s
21
+ raise NotImplementedError, "#{self.class} must implement the 'to_s' method for specific formatting."
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'node_builder'
5
+ require_relative 'errors'
6
+
7
+ module JsonToToon
8
+ class Converter
9
+ def self.convert(json_string)
10
+ raise ArgumentError, 'Input must be a String.' unless json_string.is_a?(String)
11
+
12
+ data_hash = JSON.parse(json_string, symbolize_names: true)
13
+
14
+ root_node = NodeBuilder.build(data_hash)
15
+ root_node.to_s
16
+ rescue JSON::ParserError => e
17
+ raise ConversionError, "Invalid JSON structure provided: #{e.message}"
18
+ rescue StandardError => e
19
+ raise Error, "An unexpected error occurred during conversion: #{e.message}"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonToToon
4
+ class DataNode
5
+ attr_accessor :parent
6
+
7
+ attr_reader :indentation_level
8
+
9
+ def initialize(parent = nil)
10
+ @parent = parent
11
+ @indentation_level = parent ? parent.indentation_level + 1 : 0
12
+ end
13
+
14
+ def to_s
15
+ raise NotImplementedError, "#{self.class} must implement the 'to_s' method."
16
+ end
17
+
18
+ def add_child(child)
19
+ raise NotImplementedError, "#{self.class} does not support adding children."
20
+ end
21
+
22
+ def indent(spaces = 2)
23
+ ' ' * (@indentation_level * spaces)
24
+ end
25
+
26
+ def root
27
+ current = self
28
+ current = current.parent until current.parent.nil?
29
+ current
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonToToon
4
+ class Error < StandardError; end
5
+ class ConversionError < Error; end
6
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'object_node'
4
+ require_relative 'array_node'
5
+ require_relative 'value_node'
6
+
7
+ module JsonToToon
8
+ class NodeBuilder
9
+ def self.build(data, parent = nil)
10
+ case data
11
+ when Hash
12
+ node = ObjectNode.new(parent)
13
+ data.each do |key, value|
14
+ child_node = self.build(value, node)
15
+
16
+ node.add_key_value(key, child_node)
17
+ end
18
+
19
+ node
20
+
21
+ when Array
22
+ node = ArrayNode.new(parent)
23
+
24
+ data.each do |item|
25
+ child_node = self.build(item, node)
26
+
27
+ node.add_child(child_node)
28
+ end
29
+
30
+ node
31
+
32
+ when String, Numeric, TrueClass, FalseClass, NilClass
33
+ ValueNode.new(data, parent)
34
+ else
35
+ ValueNode.new(data.to_s, parent)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'container_node'
4
+
5
+ module JsonToToon
6
+ class ObjectNode < ContainerNode
7
+ class KeyedValue
8
+ attr_reader :key, :value_node
9
+
10
+ def initialize(key, value_node)
11
+ @key = key.to_s
12
+ @value_node = value_node
13
+ end
14
+
15
+ def to_s
16
+ @value_node.to_s
17
+ end
18
+ end
19
+
20
+ def initialize(parent = nil)
21
+ super
22
+ end
23
+
24
+ def keys
25
+ @children.map(&:key)
26
+ end
27
+
28
+ def add_key_value(key, value_node)
29
+ raise ArgumentError, 'Can only add DataNode objects as values.' unless value_node.is_a?(DataNode)
30
+
31
+ @children << KeyedValue.new(key, value_node)
32
+ end
33
+
34
+ def to_s
35
+ return indent.to_s if @children.empty?
36
+
37
+ output = if parent
38
+ "#{indent}\n"
39
+ else
40
+ String.new(encoding: Encoding::UTF_8)
41
+ end
42
+
43
+ child_strings = @children.map do |keyed_value|
44
+ value_indent = ' ' * @indentation_level
45
+ value_string = keyed_value.value_node.to_s.chomp
46
+ if keyed_value.value_node.is_a?(ArrayNode)
47
+ "#{value_indent}#{keyed_value.key}[#{keyed_value.value_node.children.size}]#{value_string.lstrip}"
48
+
49
+ else
50
+
51
+ newline = ''
52
+ if keyed_value.value_node.is_a?(ObjectNode) && !keyed_value.value_node.children.empty?
53
+ newline = "\n#{' ' * keyed_value.value_node.indentation_level}"
54
+ end
55
+
56
+ "#{value_indent}#{keyed_value.key}: #{newline}#{value_string.lstrip}"
57
+ end
58
+ end
59
+
60
+ output << child_strings.join("\n")
61
+
62
+ output
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data_node'
4
+
5
+ module JsonToToon
6
+ class ValueNode < DataNode
7
+ attr_reader :value
8
+
9
+ def initialize(value, parent = nil)
10
+ super(parent)
11
+ @value = value
12
+ end
13
+
14
+ def escape(value)
15
+ return 'null' if value.nil?
16
+ return value unless value.is_a?(String)
17
+ return "\"#{value.gsub('"', '\"')}\"" if value.match(/"/)
18
+ return "\"#{value}\"" if value.match(/,/)
19
+ return "\"#{value}\"" if value.match(/^(-\s)/)
20
+ return "\"#{value.gsub("\n", '\n')}\"" if value.match(/\n/)
21
+
22
+ value
23
+ end
24
+
25
+ def to_s
26
+ formatted_value = case @value
27
+ when String
28
+ escape(@value)
29
+ when Numeric, TrueClass, FalseClass
30
+ @value.to_s
31
+ when NilClass
32
+ 'null'
33
+ else
34
+ @value.to_s
35
+ end
36
+
37
+ "#{indent}#{formatted_value}"
38
+ end
39
+
40
+ def add_child(_child)
41
+ raise NoMethodError, "A #{self.class} cannot have children."
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonToToon
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json-to-toon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniele Frisanco
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-11-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.61'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.61'
83
+ description: Transforms JSON into the custom, human-readable Toon Format. Features
84
+ deep nesting support and optimized tabular array display.
85
+ email:
86
+ - daniele.frisanco@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - LICENSE.txt
92
+ - README.md
93
+ - lib/json-to-toon.rb
94
+ - lib/json_to_toon/array_node.rb
95
+ - lib/json_to_toon/container_node.rb
96
+ - lib/json_to_toon/converter.rb
97
+ - lib/json_to_toon/data_node.rb
98
+ - lib/json_to_toon/errors.rb
99
+ - lib/json_to_toon/node_builder.rb
100
+ - lib/json_to_toon/object_node.rb
101
+ - lib/json_to_toon/value_node.rb
102
+ - lib/json_to_toon/version.rb
103
+ homepage: https://github.com/danielefrisanco/json-to-toon
104
+ licenses:
105
+ - MIT
106
+ metadata:
107
+ homepage_uri: https://github.com/danielefrisanco/json-to-toon
108
+ source_code_uri: https://github.com/danielefrisanco/json-to-toon
109
+ changelog_uri: https://github.com/danielefrisanco/json-to-toon/CHANGELOG.md
110
+ rubygems_mfa_required: 'true'
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.3.26
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Transforms JSON into the custom, human-readable Toon Format.
130
+ test_files: []