context_spook 1.6.0 → 1.7.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 +4 -4
- data/README.md +103 -12
- data/Rakefile +0 -1
- data/bin/context_spook +6 -3
- data/context_spook.gemspec +7 -8
- data/lib/context_spook/generator.rb +75 -45
- data/lib/context_spook/output_context.rb +2 -2
- data/lib/context_spook/toon/encoder.rb +628 -0
- data/lib/context_spook/toon/example.md +36 -66
- data/lib/context_spook/toon.rb +13 -15
- data/lib/context_spook/utils.rb +14 -9
- data/lib/context_spook/version.rb +1 -1
- data/spec/context_spook/generator_spec.rb +63 -2
- data/spec/context_spook/toon_encoder_spec.rb +90 -0
- data/spec/context_spook/toon_spec.rb +15 -16
- data/spec/examples/README.md +79 -0
- data/spec/examples/conversions/keyed-tabular-objects.json +19 -0
- data/spec/examples/conversions/keyed-tabular-objects.toon +4 -0
- data/spec/examples/conversions/nested-field-groups.json +28 -0
- data/spec/examples/conversions/nested-field-groups.toon +4 -0
- data/spec/examples/conversions/tabular-arrays.json +22 -0
- data/spec/examples/conversions/tabular-arrays.toon +4 -0
- data/spec/examples/invalid/delimiter-mismatch.toon +3 -0
- data/spec/examples/invalid/length-mismatch.toon +3 -0
- data/spec/examples/invalid/multiple-root-primitives.toon +2 -0
- data/spec/examples/valid/comments.toon +5 -0
- data/spec/examples/valid/delimiter-scoping.toon +5 -0
- data/spec/examples/valid/mixed-arrays.toon +5 -0
- data/spec/examples/valid/nested-objects.toon +9 -0
- data/spec/examples/valid/objects.toon +6 -0
- data/spec/examples/valid/pipe-delimiter.toon +3 -0
- data/spec/examples/valid/primitive-arrays.toon +3 -0
- data/spec/examples/valid/tab-delimiter.toon +3 -0
- data/spec/fixtures/encode/arrays-nested.json +120 -0
- data/spec/fixtures/encode/arrays-objects.json +160 -0
- data/spec/fixtures/encode/arrays-primitive.json +95 -0
- data/spec/fixtures/encode/arrays-tabular.json +182 -0
- data/spec/fixtures/encode/delimiters.json +214 -0
- data/spec/fixtures/encode/objects-keyed.json +160 -0
- data/spec/fixtures/encode/objects.json +288 -0
- data/spec/fixtures/encode/primitives.json +262 -0
- data/spec/fixtures/encode/whitespace.json +54 -0
- metadata +35 -18
|
@@ -1,69 +1,39 @@
|
|
|
1
|
-
# TOON
|
|
1
|
+
# TOON format — quick reference
|
|
2
|
+
#
|
|
3
|
+
# key: value scalar (bare if safe)
|
|
4
|
+
# key[2]: array with length
|
|
5
|
+
# - item list item
|
|
6
|
+
# key[4]{col1,col2}: tabular array (CSV rows)
|
|
7
|
+
# val1,val2 one row per line
|
|
8
|
+
#
|
|
9
|
+
# Quoting: double-quote a string only if it contains
|
|
10
|
+
# , { } [ ] # " \ or starts/ends with whitespace, or looks
|
|
11
|
+
# like a number / true / false / null.
|
|
2
12
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
```json
|
|
13
|
-
{
|
|
14
|
-
"database": {
|
|
15
|
-
"name": "user_management",
|
|
16
|
-
"tables": [
|
|
17
|
-
{
|
|
18
|
-
"name": "users",
|
|
19
|
-
"columns": [
|
|
20
|
-
{
|
|
21
|
-
"name": "id",
|
|
22
|
-
"type": "integer",
|
|
23
|
-
"primary_key": true
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"name": "email",
|
|
27
|
-
"type": "string",
|
|
28
|
-
"unique": true
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
"row_count": 1000
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"name": "orders",
|
|
35
|
-
"columns": [
|
|
36
|
-
{
|
|
37
|
-
"name": "id",
|
|
38
|
-
"type": "integer",
|
|
39
|
-
"primary_key": true
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"name": "user_id",
|
|
43
|
-
"type": "integer"
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
"row_count": 5000
|
|
47
|
-
}
|
|
48
|
-
]
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
**TOON equivalent:**
|
|
54
|
-
```
|
|
13
|
+
files:
|
|
14
|
+
"app/models/user.rb":
|
|
15
|
+
content[4]{linenumber,line}:
|
|
16
|
+
1,require 'json'
|
|
17
|
+
2,""
|
|
18
|
+
3,"# a comment line"
|
|
19
|
+
4,def parse(data)
|
|
20
|
+
size: 42
|
|
21
|
+
lines: 4
|
|
55
22
|
database:
|
|
56
23
|
name: user_management
|
|
57
|
-
tables[2]
|
|
58
|
-
users
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
24
|
+
tables[2]:
|
|
25
|
+
- name: users
|
|
26
|
+
columns[2]:
|
|
27
|
+
- name: id
|
|
28
|
+
type: integer
|
|
29
|
+
primary_key: true
|
|
30
|
+
- name: email
|
|
31
|
+
type: string
|
|
32
|
+
row_count: 1000
|
|
33
|
+
- name: orders
|
|
34
|
+
columns[2]:
|
|
35
|
+
- name: id
|
|
36
|
+
type: integer
|
|
37
|
+
- name: user_id
|
|
38
|
+
type: integer
|
|
39
|
+
row_count: 5000
|
data/lib/context_spook/toon.rb
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'context_spook/toon/encoder'
|
|
2
3
|
|
|
3
4
|
# The ContextSpook::TOON module provides TOON (Token-Oriented Object Notation)
|
|
4
|
-
# serialization functionality for context objects.
|
|
5
|
+
# v4 serialization functionality for context objects.
|
|
5
6
|
module ContextSpook::TOON
|
|
6
|
-
# Converts the context object to TOON format.
|
|
7
|
+
# Converts the context object to TOON v4 format.
|
|
8
|
+
#
|
|
9
|
+
# Uses a fast JSON round-trip (C-optimized) to normalize the data,
|
|
10
|
+
# then walks the canonical Hash/Array tree with our native encoder.
|
|
7
11
|
#
|
|
8
12
|
# @return [String] the TOON-encoded representation of the context
|
|
9
13
|
def to_toon
|
|
10
|
-
|
|
14
|
+
Encoder.encode(as_json)
|
|
11
15
|
end
|
|
12
16
|
memoize method: :to_toon
|
|
13
17
|
|
|
@@ -15,19 +19,13 @@ module ContextSpook::TOON
|
|
|
15
19
|
#
|
|
16
20
|
# @return [Integer] the size in bytes of the TOON representation
|
|
17
21
|
def toon_size
|
|
18
|
-
to_toon.
|
|
22
|
+
to_toon.bytesize
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
#
|
|
22
|
-
# Markdown file that demonstrates the TOON (Token-Oriented Object Notation)
|
|
23
|
-
# format.
|
|
24
|
-
#
|
|
25
|
-
# This method is used to provide a sample representation of how context data
|
|
26
|
-
# can be formatted in TOON, which is an alternative serialization format
|
|
27
|
-
# supported by the ContextSpook library.
|
|
25
|
+
# Streams the TOON v4 output to the given IO.
|
|
28
26
|
#
|
|
29
|
-
# @
|
|
30
|
-
def
|
|
31
|
-
|
|
27
|
+
# @param output [IO] the IO to write to (e.g. STDOUT, File, StringIO)
|
|
28
|
+
def to_toon_stream(output)
|
|
29
|
+
Encoder.encode(as_json, output: output)
|
|
32
30
|
end
|
|
33
31
|
end
|
data/lib/context_spook/utils.rb
CHANGED
|
@@ -6,15 +6,14 @@ module ContextSpook::Utils
|
|
|
6
6
|
# The format_size method converts a byte size value into a human-readable
|
|
7
7
|
# string with binary units.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# etc.) and two decimal places.
|
|
9
|
+
# library to display the size with appropriate IEC prefixes (KiB, MiB,
|
|
10
|
+
# etc.) and one decimal place.
|
|
12
11
|
#
|
|
13
12
|
# @param context_size [ Integer ] the size in bytes to be formatted
|
|
14
13
|
#
|
|
15
14
|
# @return [ String ] the formatted size string with binary units
|
|
16
15
|
def format_size(context_size)
|
|
17
|
-
Tins::Unit.format(context_size, format: '%.
|
|
16
|
+
Tins::Unit.format(context_size, format: '%.1f %U', unit: ?B, prefix: :iec_uc)
|
|
18
17
|
end
|
|
19
18
|
|
|
20
19
|
# The format_tokens method converts a token count into a human-readable
|
|
@@ -28,14 +27,20 @@ module ContextSpook::Utils
|
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
# The estimate_tokens method provides a crude estimation of token counts
|
|
31
|
-
#
|
|
30
|
+
# from either a raw byte count or a string.
|
|
32
31
|
#
|
|
33
|
-
# This follows a heuristic where one token is roughly equivalent to 3.5
|
|
32
|
+
# This follows a heuristic where one token is roughly equivalent to 3.5
|
|
33
|
+
# bytes. A numeric argument is treated as a byte count directly; any other
|
|
34
|
+
# value is converted to a string and measured by its byte size.
|
|
34
35
|
#
|
|
35
|
-
# @param
|
|
36
|
+
# @param text_or_bytesize [ String, Integer ] the content to estimate
|
|
37
|
+
# tokens for, either as a string or a precomputed byte count
|
|
36
38
|
#
|
|
37
39
|
# @return [ Integer ] the estimated number of tokens
|
|
38
|
-
def estimate_tokens(
|
|
39
|
-
|
|
40
|
+
def estimate_tokens(text_or_bytesize)
|
|
41
|
+
unless text_or_bytesize.is_a?(Numeric)
|
|
42
|
+
text_or_bytesize = text_or_bytesize.to_s.bytesize
|
|
43
|
+
end
|
|
44
|
+
(text_or_bytesize.to_f / 3.5).ceil
|
|
40
45
|
end
|
|
41
46
|
end
|
|
@@ -12,7 +12,7 @@ describe ContextSpook::Generator do
|
|
|
12
12
|
expect_any_instance_of(described_class).to receive(:verbose_puts).
|
|
13
13
|
with(/Built.*?31.0 T tokens.*?of JSON context in total/)
|
|
14
14
|
my_context = ContextSpook.generate_context do
|
|
15
|
-
self.token_estimator = -> text { (text.
|
|
15
|
+
self.token_estimator = -> text { (text.bytesize.to_f / Math::PI).ceil }
|
|
16
16
|
context do
|
|
17
17
|
variable foo: 'bar'
|
|
18
18
|
metadata version: '1.0'
|
|
@@ -92,12 +92,73 @@ describe ContextSpook::Generator do
|
|
|
92
92
|
expect_any_instance_of(described_class).to receive(:output_context_size).
|
|
93
93
|
and_call_original
|
|
94
94
|
expect_any_instance_of(described_class).to receive(:verbose_puts).
|
|
95
|
-
with(/Built.*?
|
|
95
|
+
with(/Built.*?KiB.*?KT.*?of JSON context in total/)
|
|
96
96
|
allow_any_instance_of(described_class::Context).to receive(:verbose_puts)
|
|
97
97
|
my_context
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
describe 'manifest mode' do
|
|
102
|
+
let :manifest_context do
|
|
103
|
+
ContextSpook.generate_context(manifest: true, verbose: false) do
|
|
104
|
+
context do
|
|
105
|
+
file 'lib/context_spook.rb'
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
let :full_context do
|
|
111
|
+
ContextSpook.generate_context(manifest: false, verbose: false) do
|
|
112
|
+
context do
|
|
113
|
+
file 'lib/context_spook.rb'
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'defaults to manifest: false' do
|
|
119
|
+
gen = described_class.send(:new)
|
|
120
|
+
expect(gen.manifest).to eq false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'stores manifest: true on Generator' do
|
|
124
|
+
gen = described_class.send(:new, manifest: true)
|
|
125
|
+
expect(gen.manifest).to eq true
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'stamps manifest: true in metadata' do
|
|
129
|
+
expect(manifest_context.metadata[:manifest]).to eq true
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'omits manifest key from metadata when false' do
|
|
133
|
+
expect(full_context.metadata).not_to have_key(:manifest)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'includes content and lines in non-manifest mode' do
|
|
137
|
+
file = full_context.files['lib/context_spook.rb']
|
|
138
|
+
expect(file[:content]).to be_a(Array)
|
|
139
|
+
expect(file[:content].first).to include(:linenumber, :line)
|
|
140
|
+
expect(file[:lines]).to be > 0
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'omits content and lines in manifest mode' do
|
|
144
|
+
file = manifest_context.files['lib/context_spook.rb']
|
|
145
|
+
expect(file).not_to have_key(:content)
|
|
146
|
+
expect(file).not_to have_key(:lines)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'retains metadata fields in manifest mode' do
|
|
150
|
+
file = manifest_context.files['lib/context_spook.rb']
|
|
151
|
+
expect(file[:size]).to be > 0
|
|
152
|
+
expect(file[:tokens]).to be > 0
|
|
153
|
+
expect(file[:content_types]).to be_present
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'produces smaller output in manifest mode' do
|
|
157
|
+
expect(manifest_context.to_json.bytesize).
|
|
158
|
+
to be < full_context.to_json.bytesize
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
101
162
|
describe 'Context' do
|
|
102
163
|
it 'can have variables' do
|
|
103
164
|
expect(my_context.variables[:branch]).to be_present
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
# Conformance spec: drives ContextSpook::TOON::Encoder against the official
|
|
7
|
+
# TOON v4 encode fixtures from the toon-format-spec repository.
|
|
8
|
+
#
|
|
9
|
+
# Each fixture file is a JSON document:
|
|
10
|
+
# { "tests": [ { "name", "input", "expected", ["options"], ["minSpecVersion"] } ] }
|
|
11
|
+
#
|
|
12
|
+
# We load every encode fixture, iterate the test cases, encode via our
|
|
13
|
+
# encoder, and compare the output string.
|
|
14
|
+
#
|
|
15
|
+
# Fixtures with minSpecVersion above our target are skipped.
|
|
16
|
+
|
|
17
|
+
describe ContextSpook::TOON::Encoder do
|
|
18
|
+
FIXTURES_DIR = File.expand_path('../fixtures/encode', __dir__)
|
|
19
|
+
MAX_SPEC_VERSION = '4.4'
|
|
20
|
+
|
|
21
|
+
describe '.new' do
|
|
22
|
+
it 'can output to IO' do
|
|
23
|
+
io = StringIO.new
|
|
24
|
+
encoder = described_class.new(output: io)
|
|
25
|
+
encoder.encode(%w[ hello world ], 0)
|
|
26
|
+
expect(io.string).to eq '[2]: hello,world'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'can output to StringArray' do
|
|
30
|
+
encoder = described_class.new
|
|
31
|
+
encoder.encode(%w[ hello world ], 0)
|
|
32
|
+
expect(encoder.to_s).to eq '[2]: hello,world'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.fixture_files
|
|
37
|
+
Dir[File.join(FIXTURES_DIR, '*.json')].sort
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.version_ok?(fixture)
|
|
41
|
+
return true unless (ver = fixture['minSpecVersion'])
|
|
42
|
+
ver.version <= MAX_SPEC_VERSION.version
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
fixture_files.each do |path|
|
|
46
|
+
data = JSON.parse(File.read(path))
|
|
47
|
+
category = File.basename(path, '.json')
|
|
48
|
+
|
|
49
|
+
describe "fixture: #{category}" do
|
|
50
|
+
data['tests'].each do |test|
|
|
51
|
+
name = test['name']
|
|
52
|
+
input = JSON.parse(JSON.generate(test['input']))
|
|
53
|
+
|
|
54
|
+
# Skip tests for spec versions we haven't implemented yet
|
|
55
|
+
if self.version_ok?(test)
|
|
56
|
+
it name do
|
|
57
|
+
options = test['options'] || {}
|
|
58
|
+
delimiter = options['delimiter'] || ','
|
|
59
|
+
indent = (options['indentSize'] || 2)
|
|
60
|
+
|
|
61
|
+
actual = described_class.encode(
|
|
62
|
+
input, indent: indent, delimiter:
|
|
63
|
+
)
|
|
64
|
+
expected = test['expected']
|
|
65
|
+
|
|
66
|
+
if actual == expected
|
|
67
|
+
# pass
|
|
68
|
+
else
|
|
69
|
+
# Pretty diff for debugging
|
|
70
|
+
expected_lines = expected.lines
|
|
71
|
+
actual_lines = actual.lines
|
|
72
|
+
diff_lines = []
|
|
73
|
+
max_len = [expected_lines.size, actual_lines.size].max
|
|
74
|
+
(0...max_len).each do |i|
|
|
75
|
+
e = expected_lines[i] || '<missing>'
|
|
76
|
+
a = actual_lines[i] || '<missing>'
|
|
77
|
+
diff_lines << " #{i + 1}: expected #{e.inspect} got #{a.inspect}" \
|
|
78
|
+
unless e == a
|
|
79
|
+
end
|
|
80
|
+
raise "TOON mismatch:\n#{diff_lines.join("\n")}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
else
|
|
84
|
+
skip "#{name} (requires spec v#{test['minSpecVersion']}, max: #{MAX_SPEC_VERSION})" do
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -20,22 +20,21 @@ describe ContextSpook::TOON do
|
|
|
20
20
|
|
|
21
21
|
it 'produces valid TOON output' do
|
|
22
22
|
toon_output = my_context.to_toon
|
|
23
|
-
expect(toon_output).to
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
^[ ]{4}size:[ ]120\n
|
|
29
|
-
^[ ]{4}lines:[ ]5\n
|
|
30
|
-
^[ ]{4}content_types\[2\]:[ ]application/x-ruby,text/x-ruby\n
|
|
31
|
-
^commands:\n
|
|
32
|
-
^metadata:\n
|
|
33
|
-
^[ ]{2}version:[ ]"1\.0"\n
|
|
34
|
-
^[ ]{2}format:[ ]JSON\n
|
|
35
|
-
^variables:\n
|
|
36
|
-
^[ ]{2}foo:[ ]bar\z
|
|
37
|
-
}x
|
|
23
|
+
expect(toon_output).to start_with(
|
|
24
|
+
"files:\n" \
|
|
25
|
+
" \"lib/context_spook.rb\":\n" \
|
|
26
|
+
" content[5]{linenumber,line}:\n" \
|
|
27
|
+
" 1,module ContextSpook\n"
|
|
38
28
|
)
|
|
29
|
+
expect(toon_output).to include(" size: 120\n")
|
|
30
|
+
expect(toon_output).to include(" lines: 5\n")
|
|
31
|
+
expect(toon_output)
|
|
32
|
+
.to include(" content_types[2]: application/x-ruby,text/x-ruby\n")
|
|
33
|
+
expect(toon_output).to include("commands:\n")
|
|
34
|
+
expect(toon_output).to include(" version: \"1.0\"\n")
|
|
35
|
+
expect(toon_output).to include(" format: JSON\n")
|
|
36
|
+
expect(toon_output).to include("variables:\n")
|
|
37
|
+
expect(toon_output).to end_with(" foo: bar")
|
|
39
38
|
end
|
|
40
39
|
end
|
|
41
40
|
|
|
@@ -49,7 +48,7 @@ describe ContextSpook::TOON do
|
|
|
49
48
|
it 'matches the size of the TOON output' do
|
|
50
49
|
size = my_context.toon_size
|
|
51
50
|
toon_output = my_context.to_toon
|
|
52
|
-
expect(size).to eq(toon_output.
|
|
51
|
+
expect(size).to eq(toon_output.bytesize)
|
|
53
52
|
end
|
|
54
53
|
end
|
|
55
54
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# TOON Examples
|
|
2
|
+
|
|
3
|
+
Example TOON files, one per format feature – each filename names the feature it demonstrates. All files validate against the [reference implementation](https://github.com/toon-format/toon): valid examples decode in strict mode, invalid examples error, and every conversion pair is generated by the reference encoder and round-trip verified.
|
|
4
|
+
|
|
5
|
+
## Valid Examples
|
|
6
|
+
|
|
7
|
+
### Objects
|
|
8
|
+
|
|
9
|
+
- [`valid/objects.toon`](valid/objects.toon) – Flat object with primitive values
|
|
10
|
+
- Key-value pairs across all data types: string, number, boolean, null
|
|
11
|
+
- Spec: §8
|
|
12
|
+
|
|
13
|
+
- [`valid/nested-objects.toon`](valid/nested-objects.toon) – Multi-level nested objects
|
|
14
|
+
- Indentation-based nesting (2 spaces per level) instead of braces
|
|
15
|
+
- Spec: §8, §12
|
|
16
|
+
|
|
17
|
+
### Arrays
|
|
18
|
+
|
|
19
|
+
- [`valid/primitive-arrays.toon`](valid/primitive-arrays.toon) – Inline primitive arrays
|
|
20
|
+
- Compact inline form `[N]: item1,item2,item3` and the empty array `[]`
|
|
21
|
+
- Spec: §9.1
|
|
22
|
+
|
|
23
|
+
- [`valid/mixed-arrays.toon`](valid/mixed-arrays.toon) – Mixed-type array (list form)
|
|
24
|
+
- One `-` item per element, used wherever neither inline nor tabular form applies
|
|
25
|
+
- Spec: §9.4
|
|
26
|
+
|
|
27
|
+
### Comments
|
|
28
|
+
|
|
29
|
+
- [`valid/comments.toon`](valid/comments.toon) – Full-line `#` comments
|
|
30
|
+
- Stripped by decoders in a lexical pre-pass, before row counting – decode-only; encoders never emit them
|
|
31
|
+
- Spec: §5.1
|
|
32
|
+
|
|
33
|
+
### Delimiters
|
|
34
|
+
|
|
35
|
+
- [`valid/pipe-delimiter.toon`](valid/pipe-delimiter.toon) – Pipe delimiter (`|`)
|
|
36
|
+
- Delimiter marker appears in both header `[N|]` and field list `{field|...}`
|
|
37
|
+
- Spec: §11
|
|
38
|
+
|
|
39
|
+
- [`valid/tab-delimiter.toon`](valid/tab-delimiter.toon) – Tab delimiter (`\t`)
|
|
40
|
+
- Tab character appears in both header and between fields
|
|
41
|
+
- Spec: §11
|
|
42
|
+
|
|
43
|
+
- [`valid/delimiter-scoping.toon`](valid/delimiter-scoping.toon) – Document vs active delimiter
|
|
44
|
+
- Tabular row cells split only on the active delimiter; object field values follow document delimiter quoting
|
|
45
|
+
- Spec: §11
|
|
46
|
+
|
|
47
|
+
## Conversions
|
|
48
|
+
|
|
49
|
+
Side-by-side JSON ↔ TOON pairs, one per tabular form:
|
|
50
|
+
|
|
51
|
+
- [`conversions/tabular-arrays.json`](conversions/tabular-arrays.json) + [`conversions/tabular-arrays.toon`](conversions/tabular-arrays.toon)
|
|
52
|
+
- Uniform arrays of objects collapse into a table: fields declared once in the header, one row per element
|
|
53
|
+
- Spec: §9.3
|
|
54
|
+
|
|
55
|
+
- [`conversions/nested-field-groups.json`](conversions/nested-field-groups.json) + [`conversions/nested-field-groups.toon`](conversions/nested-field-groups.toon)
|
|
56
|
+
- A uniform nested-object column folds into the header (`{id,customer{name,country},total}`) while rows stay flat
|
|
57
|
+
- Spec: §9.3
|
|
58
|
+
|
|
59
|
+
- [`conversions/keyed-tabular-objects.json`](conversions/keyed-tabular-objects.json) + [`conversions/keyed-tabular-objects.toon`](conversions/keyed-tabular-objects.toon)
|
|
60
|
+
- Objects whose values are uniform objects become tables whose entry rows carry their own keys; the colon after the length (`[3:]`) marks the keyed header
|
|
61
|
+
- Spec: §9.5
|
|
62
|
+
|
|
63
|
+
## Invalid Examples
|
|
64
|
+
|
|
65
|
+
Files that intentionally violate TOON syntax rules and MUST error in strict mode:
|
|
66
|
+
|
|
67
|
+
- [`invalid/length-mismatch.toon`](invalid/length-mismatch.toon) – Array length mismatch
|
|
68
|
+
- Declares `[3]` but provides only 2 rows
|
|
69
|
+
- Spec: §14.1
|
|
70
|
+
|
|
71
|
+
- [`invalid/multiple-root-primitives.toon`](invalid/multiple-root-primitives.toon) – Multiple depth-0 primitives
|
|
72
|
+
- Two non-key-value lines at root depth
|
|
73
|
+
- Spec: §5, §14.2
|
|
74
|
+
|
|
75
|
+
- [`invalid/delimiter-mismatch.toon`](invalid/delimiter-mismatch.toon) – Header delimiter mismatch
|
|
76
|
+
- Declares pipe delimiter in brackets (`[N|]`) but uses comma-separated fields (`{a,b}`)
|
|
77
|
+
- Spec: §6
|
|
78
|
+
|
|
79
|
+
For reference test fixtures, see the [`tests/`](../tests/) directory.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"environments": {
|
|
3
|
+
"production": {
|
|
4
|
+
"region": "eu-central-1",
|
|
5
|
+
"replicas": 6,
|
|
6
|
+
"debug": false
|
|
7
|
+
},
|
|
8
|
+
"staging": {
|
|
9
|
+
"region": "eu-central-1",
|
|
10
|
+
"replicas": 2,
|
|
11
|
+
"debug": true
|
|
12
|
+
},
|
|
13
|
+
"development": {
|
|
14
|
+
"region": "local",
|
|
15
|
+
"replicas": 1,
|
|
16
|
+
"debug": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"orders": [
|
|
3
|
+
{
|
|
4
|
+
"id": 1,
|
|
5
|
+
"customer": {
|
|
6
|
+
"name": "Ada",
|
|
7
|
+
"country": "DK"
|
|
8
|
+
},
|
|
9
|
+
"total": 99
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": 2,
|
|
13
|
+
"customer": {
|
|
14
|
+
"name": "Bob",
|
|
15
|
+
"country": "UK"
|
|
16
|
+
},
|
|
17
|
+
"total": 149
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": 3,
|
|
21
|
+
"customer": {
|
|
22
|
+
"name": "Cleo",
|
|
23
|
+
"country": "FR"
|
|
24
|
+
},
|
|
25
|
+
"total": 249
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"users": [
|
|
3
|
+
{
|
|
4
|
+
"id": 1,
|
|
5
|
+
"name": "Ada",
|
|
6
|
+
"role": "admin",
|
|
7
|
+
"active": true
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": 2,
|
|
11
|
+
"name": "Bob",
|
|
12
|
+
"role": "developer",
|
|
13
|
+
"active": true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": 3,
|
|
17
|
+
"name": "Cleo",
|
|
18
|
+
"role": "designer",
|
|
19
|
+
"active": false
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|