Compare commits
27 Commits
cca7d61666
...
master
Author | SHA1 | Date | |
---|---|---|---|
e562e30e5e
|
|||
8aaceba484
|
|||
c74d615131
|
|||
8ccb2c3a66
|
|||
ad73ea6508
|
|||
875b1b6344
|
|||
ea52c99fee
|
|||
dbf2762982
|
|||
0f4a9fcaa7
|
|||
bd079b42d9
|
|||
bd0d74ee6a
|
|||
2208079355
|
|||
98eac68929
|
|||
39619e7d6b
|
|||
33ab092a06
|
|||
21a9753d46
|
|||
e8ddee5ab2
|
|||
2f90ccba6f
|
|||
d6e1e85ea1
|
|||
ed913ab3a3
|
|||
73575a43a7
|
|||
1c5d7af552
|
|||
f371aa281c
|
|||
ce65dee71f
|
|||
f371f16e2f
|
|||
f381edfff3
|
|||
6d2c08878d
|
26
build.zig
26
build.zig
@@ -2,11 +2,26 @@ const std = @import("std");
|
|||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) void {
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
const nice = b.addModule("nice", .{
|
const nice = b.addModule("nice", .{
|
||||||
.source_file = .{ .path = "src/nice.zig" },
|
.root_source_file = b.path("src/nice.zig"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tests = b.addTest(.{
|
||||||
|
.name = "nice-unit-tests",
|
||||||
|
.root_source_file = b.path("tests/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
tests.root_module.addImport("nice", nice);
|
||||||
|
|
||||||
|
const run_main_tests = b.addRunArtifact(tests);
|
||||||
|
const test_step = b.step("test", "Run tests");
|
||||||
|
test_step.dependOn(&b.addInstallArtifact(tests, .{}).step);
|
||||||
|
test_step.dependOn(&run_main_tests.step);
|
||||||
|
|
||||||
add_examples(b, .{
|
add_examples(b, .{
|
||||||
.target = target,
|
.target = target,
|
||||||
.nice_mod = nice,
|
.nice_mod = nice,
|
||||||
@@ -14,7 +29,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ExampleOptions = struct {
|
const ExampleOptions = struct {
|
||||||
target: std.zig.CrossTarget,
|
target: std.Build.ResolvedTarget,
|
||||||
nice_mod: *std.Build.Module,
|
nice_mod: *std.Build.Module,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,20 +41,21 @@ const Example = struct {
|
|||||||
const examples = [_]Example{
|
const examples = [_]Example{
|
||||||
.{ .name = "parse", .file = "examples/parse.zig" },
|
.{ .name = "parse", .file = "examples/parse.zig" },
|
||||||
.{ .name = "stream", .file = "examples/stream.zig" },
|
.{ .name = "stream", .file = "examples/stream.zig" },
|
||||||
|
.{ .name = "reify", .file = "examples/reify.zig" },
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn add_examples(b: *std.build, options: ExampleOptions) void {
|
pub fn add_examples(b: *std.Build, options: ExampleOptions) void {
|
||||||
const example_step = b.step("examples", "build examples");
|
const example_step = b.step("examples", "build examples");
|
||||||
|
|
||||||
inline for (examples) |example| {
|
inline for (examples) |example| {
|
||||||
const ex_exe = b.addExecutable(.{
|
const ex_exe = b.addExecutable(.{
|
||||||
.name = example.name,
|
.name = example.name,
|
||||||
.root_source_file = .{ .path = example.file },
|
.root_source_file = b.path(example.file),
|
||||||
.target = options.target,
|
.target = options.target,
|
||||||
.optimize = .Debug,
|
.optimize = .Debug,
|
||||||
});
|
});
|
||||||
|
|
||||||
ex_exe.addModule("nice", options.nice_mod);
|
ex_exe.root_module.addImport("nice", options.nice_mod);
|
||||||
const install = b.addInstallArtifact(ex_exe, .{});
|
const install = b.addInstallArtifact(ex_exe, .{});
|
||||||
example_step.dependOn(&install.step);
|
example_step.dependOn(&install.step);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
.{
|
.{
|
||||||
.name = "nice-data",
|
.name = "nice",
|
||||||
.version = "0.1.0-pre",
|
.version = "0.1.0-pre",
|
||||||
.dependencies = .{},
|
.dependencies = .{},
|
||||||
|
.paths = .{
|
||||||
|
"src",
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"license",
|
||||||
|
"readme.md",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
// This example is dedicated to the public domain or, where that is not possible,
|
||||||
|
// licensed under CC0-1.0, available at https://spdx.org/licenses/CC0-1.0.html
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const nice = @import("nice");
|
const nice = @import("nice");
|
||||||
|
104
examples/reify.zig
Normal file
104
examples/reify.zig
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// This example is dedicated to the public domain or, where that is not possible,
|
||||||
|
// licensed under CC0-1.0, available at https://spdx.org/licenses/CC0-1.0.html
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const nice = @import("nice");
|
||||||
|
|
||||||
|
const Enum = enum { first, second, third };
|
||||||
|
const TagUnion = union(Enum) { first: []const u8, second: i32, third: void };
|
||||||
|
|
||||||
|
const Example = struct {
|
||||||
|
useful: bool,
|
||||||
|
number: i32,
|
||||||
|
string: []const u8,
|
||||||
|
longstring: [:0]const u8,
|
||||||
|
tuple: struct { bool, i8 },
|
||||||
|
enume: Enum,
|
||||||
|
taggart: TagUnion,
|
||||||
|
voidtag: TagUnion,
|
||||||
|
exist: ?bool,
|
||||||
|
again: ?bool,
|
||||||
|
array: [5]i16,
|
||||||
|
nested: [3]struct { index: usize, title: []const u8 },
|
||||||
|
default: u64 = 0xDEADCAFE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const source =
|
||||||
|
\\useful: true
|
||||||
|
\\number: 0x9001
|
||||||
|
\\string: > salutations, earthen oblate spheroid
|
||||||
|
\\
|
||||||
|
\\longstring:
|
||||||
|
\\ | If, at first, you don't think this string has
|
||||||
|
\\ + multiple lines, then perhaps you are the one who is
|
||||||
|
\\ # yeah, let's add a newline here
|
||||||
|
\\ > wrong.
|
||||||
|
\\ # and a trailing newline for good measure
|
||||||
|
\\ >
|
||||||
|
\\tuple: [ no, 127 ]
|
||||||
|
\\enume: .second
|
||||||
|
\\taggart: {.first: string a thing}
|
||||||
|
\\voidtag: .third
|
||||||
|
\\list:
|
||||||
|
\\ - I am a list item
|
||||||
|
\\exist: null
|
||||||
|
\\again: true
|
||||||
|
\\array: [ 1, 2, 3, 4, 5 ]
|
||||||
|
\\nested:
|
||||||
|
\\ - { index: 1, title: none }
|
||||||
|
\\ - { index: 2, title: such }
|
||||||
|
\\ - { index: 3, title: luck }
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
var diagnostics = nice.Diagnostics{};
|
||||||
|
var loaded = nice.parseBufferTo(Example, allocator, source, &diagnostics, .{}) catch |err| {
|
||||||
|
std.debug.print("row:{d} col:{d}: {s}\n", .{
|
||||||
|
diagnostics.row,
|
||||||
|
diagnostics.line_offset,
|
||||||
|
diagnostics.message,
|
||||||
|
});
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
defer loaded.deinit();
|
||||||
|
|
||||||
|
std.debug.print("{s} {{\n", .{@typeName(Example)});
|
||||||
|
std.debug.print(" useful: {}\n", .{loaded.value.useful});
|
||||||
|
std.debug.print(" number: {d}\n", .{loaded.value.number});
|
||||||
|
std.debug.print(" string: {s}\n", .{loaded.value.string});
|
||||||
|
std.debug.print(" longstring: {s}\n", .{loaded.value.longstring});
|
||||||
|
std.debug.print(" tuple: {{ {}, {d} }}\n", .{ loaded.value.tuple[0], loaded.value.tuple[1] });
|
||||||
|
std.debug.print(" enume: .{s}\n", .{@tagName(loaded.value.enume)});
|
||||||
|
std.debug.print(" taggart: ", .{});
|
||||||
|
switch (loaded.value.taggart) {
|
||||||
|
.first => |val| std.debug.print(".first = {s}\n", .{val}),
|
||||||
|
.second => |val| std.debug.print(".second = {d}\n", .{val}),
|
||||||
|
.third => std.debug.print(".third\n", .{}),
|
||||||
|
}
|
||||||
|
std.debug.print(" voidtag: ", .{});
|
||||||
|
switch (loaded.value.voidtag) {
|
||||||
|
.first => |val| std.debug.print(".first = {s}\n", .{val}),
|
||||||
|
.second => |val| std.debug.print(".second = {d}\n", .{val}),
|
||||||
|
.third => std.debug.print(".third\n", .{}),
|
||||||
|
}
|
||||||
|
std.debug.print(" exist: {?}\n", .{loaded.value.exist});
|
||||||
|
std.debug.print(" again: {?}\n", .{loaded.value.again});
|
||||||
|
std.debug.print(" array: [ ", .{});
|
||||||
|
for (loaded.value.array) |item| {
|
||||||
|
std.debug.print("{d}, ", .{item});
|
||||||
|
}
|
||||||
|
std.debug.print("]\n", .{});
|
||||||
|
std.debug.print(" nested: [\n", .{});
|
||||||
|
for (loaded.value.nested) |item| {
|
||||||
|
std.debug.print(" {{ index: {d}, title: {s} }}\n", .{ item.index, item.title });
|
||||||
|
}
|
||||||
|
std.debug.print(" ]\n", .{});
|
||||||
|
std.debug.print(" default: 0x{X}\n", .{loaded.value.default});
|
||||||
|
std.debug.print("}}\n", .{});
|
||||||
|
}
|
@@ -1,3 +1,6 @@
|
|||||||
|
// This example is dedicated to the public domain or, where that is not possible,
|
||||||
|
// licensed under CC0-1.0, available at https://spdx.org/licenses/CC0-1.0.html
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const nice = @import("nice");
|
const nice = @import("nice");
|
||||||
|
7
license
Normal file
7
license
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright 2023 torque@epicyclic.dev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
73
readme.md
73
readme.md
@@ -47,7 +47,7 @@ A scalar value is a sequence of valid UTF-8 codepoints. Scalars cannot contain l
|
|||||||
|
|
||||||
A string value is very similar to a scalar value, except that it is started by a leader character sequence and ended with trailer character sequence. Strings may be spread across multiple lines (here, we call each line a string fragment), and each fragment must start with a leader and end with the trailer. Strings fragments respect leading whitespace (after the leader sequence), unlike scalars. The trailer may be used to include trailing whitespace in a fragment. Comments may be interspersed between the fragments that compose a string (demonstrated below).
|
A string value is very similar to a scalar value, except that it is started by a leader character sequence and ended with trailer character sequence. Strings may be spread across multiple lines (here, we call each line a string fragment), and each fragment must start with a leader and end with the trailer. Strings fragments respect leading whitespace (after the leader sequence), unlike scalars. The trailer may be used to include trailing whitespace in a fragment. Comments may be interspersed between the fragments that compose a string (demonstrated below).
|
||||||
|
|
||||||
The string leader sequence consists of an ASCII character followed by a single ASCII space. The space must be omitted if the fragment contains no other characters (because otherwise it would be trailing whitespace, which is forbidden). The leader sequence defines how the lines of the string are concatenated together, as follows:
|
The string leader sequence consists of an ASCII character followed by a single ASCII space. The space must be omitted if the fragment contains no other characters (because otherwise it would be trailing whitespace, which is forbidden). The leader sequence defines how the fragments of the string are concatenated together, as follows:
|
||||||
|
|
||||||
- `| ` specifies that this fragment of the string should be directly concatenated onto the previous fragment.
|
- `| ` specifies that this fragment of the string should be directly concatenated onto the previous fragment.
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ The string leader sequence consists of an ASCII character followed by a single A
|
|||||||
|
|
||||||
Note that the leader of the first fragment of a string has no effect on the string, and may be any of the three options, but using `| ` is recommended.
|
Note that the leader of the first fragment of a string has no effect on the string, and may be any of the three options, but using `| ` is recommended.
|
||||||
|
|
||||||
The standard fragment trailer is just the normal literal linefeed character `"\n"`, as shown in the examples above. However, because Nice does not permit trailing whitespace syntactically, a string fragment may use the pipe character `|` as a trailer. If the last character in a string fragment is `|`, it will be stripped from the string while the rest of the line is preserved. Note that due to this, if a string fragment needs to end with a pipe character, it must be doubled, as the last one will be stripped from the fragment.
|
The standard fragment trailer is just the normal literal linefeed character `"\n"`, as shown in the examples above. However, because Nice does not permit trailing whitespace syntactically, a string fragment may use the pipe character `|` as a trailer. If the last character in a string fragment is `|`, it will be stripped from the fragment while preserving the rest of the line. If a string fragment needs to end with a pipe character, the pipe must be doubled, as the last `|` will be stripped from the fragment.
|
||||||
|
|
||||||
```nice
|
```nice
|
||||||
| lots of |
|
| lots of |
|
||||||
@@ -119,7 +119,7 @@ parses to the following JSON structure:
|
|||||||
["a list", "containing", "", "several values"]
|
["a list", "containing", "", "several values"]
|
||||||
```
|
```
|
||||||
|
|
||||||
There are a couple of new concepts here. The first new concept is demonstrated in the second value, which is an inline string. This is a standard fragment string that appears on the same line after another introducer (either a list item introducer, as in this example, or a map key introducer, which will be demonstrated in the section describing maps). The only difference between an inline string and a normal string as discussed above is that the inline string may is composed of only a single fragment (meaning it cannot be spread across multiple lines). The string leader used has no effect on an inline string, since the leader is not applied.
|
There are a couple of new concepts here. The first new concept is demonstrated in the second value, which is an inline string. This is a standard string fragment that appears on the same line after another introducer (either a list item introducer, as in this example, or a map key introducer, which will be demonstrated in the section describing maps). The only difference between an inline string and a normal string as discussed above is that the inline string is composed of only a single fragment (meaning it cannot be spread across multiple lines). The string leader used has no effect on an inline string, since the leader is not applied.
|
||||||
|
|
||||||
The other new concept is structural indentation. The fourth list item contains an indented string following a list item introducer that does not contain an inline value. Because the string sequence is indented, it belongs to the list item introduced immediately before it. Note that an indented sequence following an introducer that contains an inline value is a syntactic error. That is, the following document **cannot** be parsed:
|
The other new concept is structural indentation. The fourth list item contains an indented string following a list item introducer that does not contain an inline value. Because the string sequence is indented, it belongs to the list item introduced immediately before it. Note that an indented sequence following an introducer that contains an inline value is a syntactic error. That is, the following document **cannot** be parsed:
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ The Nice document is similar in layout to its indented JSON counterpart but cont
|
|||||||
|
|
||||||
Inline lists allow a list to be specified in a more concise form on a line following another item introducer (either a list item introducer or a map item introducer). They consist of a comma-separated sequence of scalars within a pair of square brackets (`[` and `]`). Inline lists may also contain other inline lists and inline maps (discussed later), but they cannot contain strings. Whitespace before and after values in an inline list is ignored, though whitespace within a value is preserved. Inline list values may not contain commas. For reasons related to intellectual bankruptcy, `[]` and `[ ]` are distinct values, just as they are in NestedText. `[]` represents an empty list, while `[ ]` represents a list containing a single empty string. As is hopefully suggested by the name, an inline list *must* be specified on a single line.
|
Inline lists allow a list to be specified in a more concise form on a line following another item introducer (either a list item introducer or a map item introducer). They consist of a comma-separated sequence of scalars within a pair of square brackets (`[` and `]`). Inline lists may also contain other inline lists and inline maps (discussed later), but they cannot contain strings. Whitespace before and after values in an inline list is ignored, though whitespace within a value is preserved. Inline list values may not contain commas. For reasons related to intellectual bankruptcy, `[]` and `[ ]` are distinct values, just as they are in NestedText. `[]` represents an empty list, while `[ ]` represents a list containing a single empty string. As is hopefully suggested by the name, an inline list *must* be specified on a single line.
|
||||||
|
|
||||||
Inline lists are provided for when a document may benefit to having horizontal layout rather than vertical. It can also be used tactically to improve readability in other ways, but should not, in general, be preferred over standard lists. Here's the previous example, with a bit less indentation thanks to use of inline lists:
|
Inline lists are provided for when some parts of a document may benefit from having horizontal layout rather than vertical layout. It can also be used tactically to improve readability in other ways, but should not, in general, be preferred over standard lists. Here's the previous example, with a bit less indentation thanks to use of inline lists:
|
||||||
|
|
||||||
```nice
|
```nice
|
||||||
- start the parent
|
- start the parent
|
||||||
@@ -195,9 +195,9 @@ Hopefully you agree that readability suffers when a more complex hierarchy is ja
|
|||||||
|
|
||||||
### Maps
|
### Maps
|
||||||
|
|
||||||
A map is a data structure consisting of a sequence of pairs, with each pair being composed of a key and value. A map may represent a general-purpose pair-based data structure such as a hashtable, or it may represent a strictly defined data type with a fixed number of named fields, like a C `struct`. The keys of the map are exclusively scalars, but the corresponding values may be any Nice type, including scalars, strings, lists, or other maps.
|
A map is a data structure consisting of a sequence of pairs, with each pair being composed of a key and value. A map may represent a general-purpose pair-based data structure such as a hashtable, or it may represent a strictly defined data type with a fixed number of named fields, like a C `struct`. The keys of the map are exclusively scalars, but the corresponding values may be any Nice type or scalar, including scalars, strings, lists, or other maps.
|
||||||
|
|
||||||
A map item is introduced by the key scalar. A key scalar is a scalar value that is terminated with an ASCII colon followed by a space `: `. The `:` is removed from the end of the key scalar when parsing. Key scalars may not begin with a sequence that is used for introducing a different type, which means that map keys cannot start with `#` (comments), `- ` (list item introducer), `+ `, `| `, `> `, (string fragment leaders) `[` (inline lists), or `{` (inline maps). Note that `-`, `+`, `|`, and `>` without a following space may be used to begin map keys unambiguously. However `#`, `[`, and `{` are always forbidden. Additionally, key scalars may not contain a colon `:`. Comments may intersperse map pairs. As with the other introducers, if the key scalar is the only item on a line, it must not have a trailing space.
|
A map item is introduced by the key scalar. A key scalar is a scalar value that is terminated with an ASCII colon followed by a space `: `. The `:` is removed from the end of the key scalar when parsing. Key scalars may not begin with a sequence that is used for introducing a different type, which means that map keys cannot start with `#` (comments), `- ` (list item introducer), `+ `, `| `, `> ` (string fragment leaders), `[` (inline lists), or `{` (inline maps). `-`, `+`, `|`, and `>` without a following space may be used to begin map keys unambiguously, but `#`, `[`, and `{` are always forbidden. Additionally, key scalars may not contain a colon `:`. Comments may intersperse map pairs. As with the other introducers, if the key scalar is the only item on a line, it must not have a trailing space.
|
||||||
|
|
||||||
Enough talk, have an example:
|
Enough talk, have an example:
|
||||||
|
|
||||||
@@ -208,9 +208,9 @@ a string:
|
|||||||
+ from a map
|
+ from a map
|
||||||
inline string: | hello from a map
|
inline string: | hello from a map
|
||||||
a list:
|
a list:
|
||||||
- 1
|
- true
|
||||||
- 2
|
- false
|
||||||
- 3
|
- null
|
||||||
inline list: [ 1, 2, 3 ]
|
inline list: [ 1, 2, 3 ]
|
||||||
a map:
|
a map:
|
||||||
nested:
|
nested:
|
||||||
@@ -225,14 +225,16 @@ This maps to the following JSON structure:
|
|||||||
"a scalar": "value",
|
"a scalar": "value",
|
||||||
"a string": "hello from a map",
|
"a string": "hello from a map",
|
||||||
"inline string": "hello from a map",
|
"inline string": "hello from a map",
|
||||||
"a list": [1, 2, 3],
|
"a list": ["true", "false", "null"],
|
||||||
"inline list": [1, 2, 3],
|
"inline list": ["1", "2", "3"],
|
||||||
"a map": { "nested": { "several": "levels" } },
|
"a map": { "nested": { "several": "levels" } },
|
||||||
"an empty value": ""
|
"an empty value": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Serialized maps are inherently ordered, but the data structures they represent do not necessarily preserve order. Nice preserves the order of the map keys as they were encountered in the document. ASCII spaces following the key scalar will be ignored, allowing adjacent values to be justified. The key scalar itself may not contain trailing or leading whitespace. A line only ever contains a single key scalar, unlike YAML. Maps must be nested using structural indentation.
|
Serialized maps are inherently ordered, but the data structures they represent do not necessarily preserve order. Nice guarantees that the order of the map keys, as they were encountered in the document, is preserved. Serialized maps can also represent multiple entries that have the same key. This is not generally useful (if you need to have multiple values for a given key, its corresponding value should be a list) and cannot typically be represented by a map data structure. The Nice parser can be configured to produce a parse error when a duplicate key is encountered (the default behavior) or it can preserve either only first encountered duplicate value or only the last encountered duplicate value (in this case, the map order preserves the index of the last encountered duplicate, which may be less efficient if many duplicates exist, since it requires performing an ordered remove on the previously encountered instance).
|
||||||
|
|
||||||
|
ASCII spaces following the key scalar will be ignored, allowing adjacent values to be justified. The key scalar itself may not contain trailing or leading whitespace. A line only ever contains a single key scalar, unlike YAML. Maps must be nested using structural indentation.
|
||||||
|
|
||||||
```nice
|
```nice
|
||||||
fully aligned: value: 1
|
fully aligned: value: 1
|
||||||
@@ -246,18 +248,34 @@ values: value: 2
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- inline maps
|
### Inline Maps
|
||||||
|
|
||||||
|
The final syntactic construct is the inline map, which is, as its name hopefully suggests, the map equivalent of an inline list. An inline map is introduced by an opening curly brace `{` and closed by an opposing brace `}`. An inline map consists of a sequence of key-value pairs with the keys being separated from the values by the `:` character. An inline map may contain scalars, inline lists, and other inline maps as values, and all of its keys must be scalars. As with inline lists, whitespace surrounding values is ignored, and whitespace preceding keys is also ignored (there must be no whitespace between the key and its following `:`).
|
||||||
|
|
||||||
|
```nice
|
||||||
|
an example: { this: is, an inline: map }
|
||||||
|
nests:
|
||||||
|
- { a list: [ of, { inline: maps } ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"an example": {"this": "is", "an inline": "map"},
|
||||||
|
"nests": [
|
||||||
|
{ "a list": [ "of", { "inline": "maps" } ] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Restrictions
|
## Restrictions
|
||||||
|
|
||||||
Nice documents must be encoded in valid UTF-8. They must use `LF`-only newlines (`CR` characters are forbidden). Tabs and spaces cannot be mixed for indentation. Indentation *must* adhere to a consistent quantum. Nonprinting ASCII characters are forbidden (specifically, any character less than `0x20` (space) except for `0x09` (horizontal tab) and `0x0A` (newline)). Trailing whitespace, including lines consisting only of whitespace, is forbidden, although empty lines are permitted. Some keys and values cannot be represented (for example, map keys cannot start with the character `#`, though map values can).
|
Nice documents must be encoded in valid UTF-8 with no BOM. They must use `LF`-only newlines (`CR` characters are forbidden). Tabs and spaces cannot be mixed for indentation. Indentation *must* adhere to a consistent quantum throughout the whole document, including on comment lines. Nonprinting ASCII characters are forbidden (specifically, any character less than `0x20` (space) except for `0x09` (horizontal tab) and `0x0A` (newline)). Trailing whitespace, including lines consisting only of whitespace, is forbidden, although empty lines are permitted. Some keys and values cannot be represented (for example, map keys cannot start with the character `#`, though map values can).
|
||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
|
|
||||||
### Let the Application Interpret Data Types (Bring Your Own Schema)
|
### Let the Application Interpret Data Types (Bring Your Own Schema)
|
||||||
|
|
||||||
An arbitrarily structured data format with strict types adds complexity to the parser and cannot possibly cover all necessary types needed for every possible application. For example, numbers in JSON are represented by a sequence of ASCII characters, but they are defined by the format to be restricted to specifying double precision floating point numbers. Of course, it is possible to generate a numeric ASCII sequence that does not fit into a double precision floating point number. If an application needs to represent a 64-bit integer in JSON without producing technically invalid JSON, the value must be serialized as a string, which places the burden of decoding it on the application, since the format cannot represent it as a direct numeric value. The same is true of an RFC 3339 datetime. It's not possible for a format to account for every possible data type that an application may need, so don't bother. Users are encouraged to parse Nice documents directly into well-defined, typed structures. If you're interested, the NestedText documentation contains a [highly comprehensive explanation of why having explicit data types in your serialization format is a futile pursuit][only-strings].
|
An arbitrarily structured data format with strict types adds complexity to the parser and cannot possibly cover all necessary types needed for every possible application. For example, numbers in JSON are represented by a sequence of ASCII characters, but they are defined by the format to be restricted to specifying double precision floating point numbers. Of course, it is possible to generate a numeric ASCII sequence that does not fit into a double precision floating point number. If an application needs to represent a 64-bit integer in JSON without producing technically invalid JSON, the value must be serialized as a string, which places the burden of decoding it on the application, since the format cannot represent it as a direct numeric value. The same is true of an RFC 3339 datetime. It's not possible for a format to account for every possible data type that an application may need, so don't bother. Users are encouraged to parse Nice documents directly into well-defined, typed structures. If you're interested, the NestedText documentation contains [several examples of why having strict data types in your serialization format is not as useful as you think][only-strings].
|
||||||
|
|
||||||
Nice explicitly differentiates between bare scalars and strings so that `null` may be disambiguated and interpreted differently from `"null"`.
|
Nice explicitly differentiates between bare scalars and strings so that `null` may be disambiguated and interpreted differently from `"null"`.
|
||||||
|
|
||||||
@@ -267,13 +285,24 @@ Nice is not, and does not try to be, a general-purpose data serialization format
|
|||||||
|
|
||||||
### There's No Need to Conquer the World
|
### There's No Need to Conquer the World
|
||||||
|
|
||||||
Nice has no exhaustive specification or formal grammar. The parser is handwritten, and there are pretty much guaranteed to be some strange edge cases that weren't considered when writing it. Standardization is a good thing, generally speaking, but it's not a goal here. Perhaps this driven by the author's indolence more than deep philosophical zealotry. On the other hand, this paragraph is under the philosophy section.
|
Nice has no exhaustive specification or formal grammar. The parser is handwritten, and there are pretty much guaranteed to be some strange edge cases that weren't considered when writing it. Standardization is a good thing, generally speaking, but it's not a goal here. Perhaps this is driven by the author's indolence more than deep philosophical zealotry. On the other hand, this paragraph is under the philosophy section.
|
||||||
|
|
||||||
# The Implementation
|
# The Implementation
|
||||||
|
|
||||||
|
The Reference™ Nice parser/deserializer is this Zig library. It contains a handwritten nonrecursive parser to a generic data structure (`nice.Value`, a tagged union that can represent a scalar, a string, a list of these generic values, or a map of scalars to these generic values). The included example scripts demonstrate how to use the API. See `examples/parse.zig` for one-shot parsing from a slice. `examples/stream.zig` demonstrates how to parse streaming data that does not require loading a whole document into memory at once. This is slower but will generally have a lower peak memory usage (though that is mainly driven by the size of the document).
|
||||||
|
|
||||||
|
`nice.Value` has a method to recursively be converted into a strongly
|
||||||
|
typed user-defined structure. Zig's compile-time reflection is used to generate code to perform appropriate type conversion. There a variety of options which can be used to control specific details of the conversion, which are governed by `nice.parser.Options`. `examples/reify.zig` demonstrates basic use of this functionality.
|
||||||
|
|
||||||
|
A reference to a `nice.Diagnostics` object with a lifecycle at least as long as the parser must always be provided when parsing. If the source document could not be parsed, this diagnostic object will contain a human-readable explanation of the invalid syntax in the source document that caused the parser to error.
|
||||||
|
|
||||||
|
## Memory Strategy
|
||||||
|
|
||||||
|
The parser wraps a user-provided allocator in an arena, which is used for all internal allocations. All parsed values are copied into the arena rather than storing references to the source document. The parse result contains a reference to the arena, which can be used to free all of the data allocated during parsing.
|
||||||
|
|
||||||
# Disclaimer
|
# Disclaimer
|
||||||
|
|
||||||
Yeah, it's entirely possible you hate this and think it's not in fact a nice format. That's fine, but, unfortunately, you forgot to make a time machine and make me name it something else. And yeah, this is probably impossible to search for.
|
It's entirely possible you hate this and think it's not, in fact, a nice data format. That's fine, but, unfortunately, you forgot to make a time machine and go back in time to make me name it something else. And yeah, this is probably impossible to search for.
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
@@ -281,6 +310,16 @@ Q: This is so similar to NestedText, why on earth didn't you just implement that
|
|||||||
|
|
||||||
A: in my opinion, it's extremely stupid that NestedText does not support indentation using tabs. Also, trailing whitespace is 100% satanic (in the bad way). And if an implementation is going to diverge there, it might as well roll in some other ideas, call it a new format, and just ruin the world with one more slightly-incompatible thing.
|
A: in my opinion, it's extremely stupid that NestedText does not support indentation using tabs. Also, trailing whitespace is 100% satanic (in the bad way). And if an implementation is going to diverge there, it might as well roll in some other ideas, call it a new format, and just ruin the world with one more slightly-incompatible thing.
|
||||||
|
|
||||||
|
Q: Why is this documentation kind of bad?
|
||||||
|
|
||||||
|
A: I'll be honest, I ran out of steam while writing it. For a format that probably nobody besides me will ever use because there's so much open source code in the world that anything without heavy marketing tends to die in obscurity, it's a lot of work to write down the things I already know. But I have put an FAQ section here, while also indicating nobody has ever asked questions about this. Hmm.
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
What are you going to do, steal my open-source code? Oh, noooooooooo. Here, let me help you.
|
||||||
|
|
||||||
|
Library is licensed MIT, examples are Public Domain/CC0. See file headers and the file `license` in the source tree for details.
|
||||||
|
|
||||||
[NestedText]: https://nestedtext.org
|
[NestedText]: https://nestedtext.org
|
||||||
[only-strings]: https://nestedtext.org/en/latest/alternatives.html#only-strings
|
[only-strings]: https://nestedtext.org/en/latest/alternatives.html#only-strings
|
||||||
[YAML]: https://yaml.org
|
[YAML]: https://yaml.org
|
||||||
|
@@ -1,3 +1,13 @@
|
|||||||
|
// Copyright 2023 torque@epicyclic.dev
|
||||||
|
//
|
||||||
|
// Licensed under the MIT/Expat license. You may not use this file except in
|
||||||
|
// compliance with the license. You may obtain a copy of the license at
|
||||||
|
//
|
||||||
|
// https://spdx.org/licenses/MIT.html
|
||||||
|
//
|
||||||
|
// This software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
// CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const Diagnostics = @import("./parser.zig").Diagnostics;
|
const Diagnostics = @import("./parser.zig").Diagnostics;
|
||||||
|
64
src/nice.zig
64
src/nice.zig
@@ -1,63 +1,12 @@
|
|||||||
// Heavily inspired by, but not quite compatible with, NestedText. Key differences:
|
// Copyright 2023 torque@epicyclic.dev
|
||||||
//
|
//
|
||||||
// - Doesn't support multiline keys (this means map keys cannot start with
|
// Licensed under the MIT/Expat license. You may not use this file except in
|
||||||
// ' ', \t, #, {, [, |, or >, and they cannot contain :)
|
// compliance with the license. You may obtain a copy of the license at
|
||||||
// - Allows using tabs for indentation (but not mixed tabs/spaces)
|
|
||||||
// - Indentation must be quantized consistently throughout the document. e.g.
|
|
||||||
// every nested layer being exactly 2 spaces past its parent. Tabs may
|
|
||||||
// only use one tab per indentation level.
|
|
||||||
// - Allows flow-style lists, maps, and strings on the same line as map keys or
|
|
||||||
// list items (i.e. the following are legal):
|
|
||||||
//
|
//
|
||||||
// key: {inline: map}
|
// https://spdx.org/licenses/MIT.html
|
||||||
// key: [inline, list]
|
|
||||||
// key: > inline string
|
|
||||||
// - {map: item}
|
|
||||||
// - [list, item]
|
|
||||||
// - > inline string
|
|
||||||
//
|
//
|
||||||
// The string case retains the possibility of having an inline map value starting
|
// This software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
// with {, [, or >
|
// CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// - a map keys and list item dashes must be followed by a value or an indented
|
|
||||||
// section to reduce parser quantum state. This means that
|
|
||||||
//
|
|
||||||
// foo:
|
|
||||||
// bar: baz
|
|
||||||
//
|
|
||||||
// or
|
|
||||||
//
|
|
||||||
// -
|
|
||||||
// - qux
|
|
||||||
//
|
|
||||||
// are not valid. This can be represented with an inline empty string after foo:
|
|
||||||
//
|
|
||||||
// foo: >
|
|
||||||
// bar: baz
|
|
||||||
//
|
|
||||||
// or
|
|
||||||
//
|
|
||||||
// - >
|
|
||||||
// - qux
|
|
||||||
//
|
|
||||||
// - newlines are strictly LF, if the parser finds CR, it is an error
|
|
||||||
// - blank lines may not contain any whitespace characters except the single LF
|
|
||||||
// - Additional string indicator `|` for soft-wrapped strings, i.e.
|
|
||||||
//
|
|
||||||
// key: | this is not special
|
|
||||||
// key:
|
|
||||||
// | these lines are
|
|
||||||
// | soft-wrapped
|
|
||||||
//
|
|
||||||
// soft-wrapped lines are joined with a ' ' instead of a newline character.
|
|
||||||
// Like multiline strings, the final space is stripped (I guess this is a very
|
|
||||||
// janky way to add trailing whitespace to a string).
|
|
||||||
//
|
|
||||||
// - terminated strings to allow trailing whitespace:
|
|
||||||
// | this string has trailing whitespace |
|
|
||||||
// > and so does this one |
|
|
||||||
// - The parser is both strict and probably sloppy and may have weird edge
|
|
||||||
// cases since I'm slinging code, not writing a spec. For example, tabs are
|
|
||||||
// not trimmed from the values of inline lists/maps
|
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
@@ -65,6 +14,7 @@ pub const buffers = @import("./linebuffer.zig");
|
|||||||
pub const tokenizer = @import("./tokenizer.zig");
|
pub const tokenizer = @import("./tokenizer.zig");
|
||||||
pub const parser = @import("./parser.zig");
|
pub const parser = @import("./parser.zig");
|
||||||
pub const parseBuffer = parser.parseBuffer;
|
pub const parseBuffer = parser.parseBuffer;
|
||||||
|
pub const parseBufferTo = parser.parseBufferTo;
|
||||||
pub const StreamParser = parser.StreamParser;
|
pub const StreamParser = parser.StreamParser;
|
||||||
pub const Document = parser.Document;
|
pub const Document = parser.Document;
|
||||||
pub const Value = parser.Value;
|
pub const Value = parser.Value;
|
||||||
|
@@ -1,3 +1,13 @@
|
|||||||
|
// Copyright 2023 torque@epicyclic.dev
|
||||||
|
//
|
||||||
|
// Licensed under the MIT/Expat license. You may not use this file except in
|
||||||
|
// compliance with the license. You may obtain a copy of the license at
|
||||||
|
//
|
||||||
|
// https://spdx.org/licenses/MIT.html
|
||||||
|
//
|
||||||
|
// This software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
// CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const buffers = @import("./linebuffer.zig");
|
const buffers = @import("./linebuffer.zig");
|
||||||
@@ -40,7 +50,7 @@ pub const Options = struct {
|
|||||||
// If an empty document is parsed, this defines what value type should be the
|
// If an empty document is parsed, this defines what value type should be the
|
||||||
// resulting document root object. The default behavior is to emit an error if the
|
// resulting document root object. The default behavior is to emit an error if the
|
||||||
// document is empty.
|
// document is empty.
|
||||||
default_object: enum { string, list, map, fail } = .fail,
|
default_object: enum { scalar, list, map, fail } = .fail,
|
||||||
|
|
||||||
// Only used by the parseTo family of functions.
|
// Only used by the parseTo family of functions.
|
||||||
// If false, and a mapping contains additional keys that do not map to the fields of
|
// If false, and a mapping contains additional keys that do not map to the fields of
|
||||||
@@ -51,11 +61,11 @@ pub const Options = struct {
|
|||||||
ignore_extra_fields: bool = true,
|
ignore_extra_fields: bool = true,
|
||||||
|
|
||||||
// Only used by the parseTo family of functions.
|
// Only used by the parseTo family of functions.
|
||||||
// If true, if a struct field is an optional type and the corresponding mapping key
|
// If true, if a struct field has a default value associated with it and the
|
||||||
// does not exist, the object field will be set to `null`. By default, if the
|
// corresponding mapping key does not exist, the object field will be set to the
|
||||||
// parsed document is missing a mapping key for a given field, an error will be
|
// default value. By default, this behavior is enabled, allowing succinct
|
||||||
// raised instead.
|
// representation of objects that have default fields.
|
||||||
treat_omitted_as_null: bool = false,
|
allow_omitting_default_values: bool = true,
|
||||||
|
|
||||||
// Only used by the parseTo family of functions.
|
// Only used by the parseTo family of functions.
|
||||||
// If true, strings may be coerced into other scalar types, like booleans or
|
// If true, strings may be coerced into other scalar types, like booleans or
|
||||||
@@ -70,19 +80,28 @@ pub const Options = struct {
|
|||||||
// an error if the destination is a boolean type. By default, these comparisons are
|
// an error if the destination is a boolean type. By default, these comparisons are
|
||||||
// case-sensitive. See the `case_insensitive_scalar_coersion` option to change
|
// case-sensitive. See the `case_insensitive_scalar_coersion` option to change
|
||||||
// this.
|
// this.
|
||||||
boolean_scalars: struct { truthy: []const []const u8, falsy: []const []const u8 } = .{
|
truthy_boolean_scalars: []const []const u8 = &.{ "true", "True", "yes", "on" },
|
||||||
.truthy = &.{ "true", "True", "yes", "on" },
|
falsy_boolean_scalars: []const []const u8 = &.{ "false", "False", "no", "off" },
|
||||||
.falsy = &.{ "false", "False", "no", "off" },
|
|
||||||
},
|
|
||||||
|
|
||||||
// Only used by the parseTo family of functions.
|
// Only used by the parseTo family of functions.
|
||||||
// A list of strings. Scalars in the doucment that match any of the values listed
|
// A list of strings. Scalars in the document that match any of the values listed
|
||||||
// will be parsed to optional `null`. Any other scalar value will be parsed as the
|
// will be parsed to optional `null`. Any other scalar value will be parsed as the
|
||||||
// optional child type if the destination type is an optional. By default, these
|
// optional child type if the destination type is an optional. By default, these
|
||||||
// comparisons are case-sensitive. See the `case_insensitive_scalar_coersion`
|
// comparisons are case-sensitive. See the `case_insensitive_scalar_coersion`
|
||||||
// option to change this.
|
// option to change this.
|
||||||
null_scalars: []const []const u8 = &.{ "null", "nil", "None" },
|
null_scalars: []const []const u8 = &.{ "null", "nil", "None" },
|
||||||
|
|
||||||
|
// Only used by the parseTo family of functions.
|
||||||
|
// Choose whether to strip the leading `.` off of expected enum values. By default,
|
||||||
|
// `.enum_field` will be parsed into the enum field `enum_field`, which makes them
|
||||||
|
// look like source code enum literals. Any enum value missing the leading `.` will
|
||||||
|
// result in a conversion error. If set to false, no preprocessing will be done
|
||||||
|
// and enum values will be converted from the literal scalar/string. These two styles
|
||||||
|
// cannot be mixed in a single document. Note that this setting also affects how
|
||||||
|
// tagged unions are parsed (specifically, the union's field name must also have the
|
||||||
|
// leading `.` if this option is enabled.)
|
||||||
|
expect_enum_dot: bool = true,
|
||||||
|
|
||||||
// Only used by the parseTo family of functions.
|
// Only used by the parseTo family of functions.
|
||||||
// Perform ASCII-case-insensitive comparisons for scalars (i.e. `TRUE` in a document
|
// Perform ASCII-case-insensitive comparisons for scalars (i.e. `TRUE` in a document
|
||||||
// will match `true` in the boolean scalars. Unicode case folding is not currently
|
// will match `true` in the boolean scalars. Unicode case folding is not currently
|
||||||
@@ -136,6 +155,7 @@ pub fn parseBufferTo(
|
|||||||
options: Options,
|
options: Options,
|
||||||
) !Parsed(T) {
|
) !Parsed(T) {
|
||||||
var doc = try parseBuffer(allocator, buffer, diagnostics, options);
|
var doc = try parseBuffer(allocator, buffer, diagnostics, options);
|
||||||
|
errdefer doc.deinit();
|
||||||
return try doc.convertTo(T, options);
|
return try doc.convertTo(T, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,3 +1,13 @@
|
|||||||
|
// Copyright 2023 torque@epicyclic.dev
|
||||||
|
//
|
||||||
|
// Licensed under the MIT/Expat license. You may not use this file except in
|
||||||
|
// compliance with the license. You may obtain a copy of the license at
|
||||||
|
//
|
||||||
|
// https://spdx.org/licenses/MIT.html
|
||||||
|
//
|
||||||
|
// This software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
// CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const tokenizer = @import("../tokenizer.zig");
|
const tokenizer = @import("../tokenizer.zig");
|
||||||
@@ -49,7 +59,7 @@ pub const State = struct {
|
|||||||
|
|
||||||
switch (state.mode) {
|
switch (state.mode) {
|
||||||
.initial => switch (options.default_object) {
|
.initial => switch (options.default_object) {
|
||||||
.string => state.document.root = Value.emptyString(),
|
.scalar => state.document.root = Value.emptyScalar(),
|
||||||
.list => state.document.root = Value.newList(arena_alloc),
|
.list => state.document.root = Value.newList(arena_alloc),
|
||||||
.map => state.document.root = Value.newMap(arena_alloc),
|
.map => state.document.root = Value.newMap(arena_alloc),
|
||||||
.fail => {
|
.fail => {
|
||||||
@@ -60,7 +70,7 @@ pub const State = struct {
|
|||||||
},
|
},
|
||||||
.value => switch (state.value_stack.getLast().*) {
|
.value => switch (state.value_stack.getLast().*) {
|
||||||
// we have an in-progress string, finish it.
|
// we have an in-progress string, finish it.
|
||||||
.string => |*string| string.* = try state.string_builder.toOwnedSlice(arena_alloc),
|
.string => |*string| string.* = try state.string_builder.toOwnedSliceSentinel(arena_alloc, 0),
|
||||||
// if we have a dangling -, attach an empty scalar to it
|
// if we have a dangling -, attach an empty scalar to it
|
||||||
.list => |*list| if (state.expect_shift == .indent) try list.append(Value.emptyScalar()),
|
.list => |*list| if (state.expect_shift == .indent) try list.append(Value.emptyScalar()),
|
||||||
// if we have a dangling "key:", attach an empty scalar to it
|
// if we have a dangling "key:", attach an empty scalar to it
|
||||||
@@ -175,7 +185,7 @@ pub const State = struct {
|
|||||||
|
|
||||||
if (firstpass and line.shift == .dedent) {
|
if (firstpass and line.shift == .dedent) {
|
||||||
// copy the string into the document proper
|
// copy the string into the document proper
|
||||||
string.* = try state.string_builder.toOwnedSlice(arena_alloc);
|
string.* = try state.string_builder.toOwnedSliceSentinel(arena_alloc, 0);
|
||||||
|
|
||||||
var dedent_depth = line.shift.dedent;
|
var dedent_depth = line.shift.dedent;
|
||||||
while (dedent_depth > 0) : (dedent_depth -= 1)
|
while (dedent_depth > 0) : (dedent_depth -= 1)
|
||||||
@@ -189,9 +199,9 @@ pub const State = struct {
|
|||||||
.in_line => |in_line| switch (in_line) {
|
.in_line => |in_line| switch (in_line) {
|
||||||
.empty => unreachable,
|
.empty => unreachable,
|
||||||
inline .line_string, .space_string, .concat_string => |str, tag| {
|
inline .line_string, .space_string, .concat_string => |str, tag| {
|
||||||
if (tag == .line_string)
|
if (comptime tag == .line_string)
|
||||||
try state.string_builder.append(arena_alloc, '\n');
|
try state.string_builder.append(arena_alloc, '\n');
|
||||||
if (tag == .space_string)
|
if (comptime tag == .space_string)
|
||||||
try state.string_builder.append(arena_alloc, ' ');
|
try state.string_builder.append(arena_alloc, ' ');
|
||||||
try state.string_builder.appendSlice(arena_alloc, str);
|
try state.string_builder.appendSlice(arena_alloc, str);
|
||||||
},
|
},
|
||||||
@@ -792,7 +802,10 @@ pub const State = struct {
|
|||||||
return error.DuplicateKey;
|
return error.DuplicateKey;
|
||||||
},
|
},
|
||||||
.use_first => {},
|
.use_first => {},
|
||||||
.use_last => gop.value_ptr.* = value,
|
.use_last => {
|
||||||
|
_ = map.orderedRemove(key);
|
||||||
|
map.putAssumeCapacityNoClobber(key, value);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
gop.value_ptr.* = value;
|
gop.value_ptr.* = value;
|
||||||
|
@@ -1,4 +1,19 @@
|
|||||||
|
// Copyright 2023 torque@epicyclic.dev
|
||||||
|
//
|
||||||
|
// Licensed under the MIT/Expat license. You may not use this file except in
|
||||||
|
// compliance with the license. You may obtain a copy of the license at
|
||||||
|
//
|
||||||
|
// https://spdx.org/licenses/MIT.html
|
||||||
|
//
|
||||||
|
// This software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
// CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const hasFn = if (@hasDecl(std.meta, "trait")) struct {
|
||||||
|
fn hasFn(comptime T: type, comptime name: []const u8) bool {
|
||||||
|
return std.meta.trait.hasFn(name)(T);
|
||||||
|
}
|
||||||
|
}.hasFn else std.meta.hasFn;
|
||||||
|
|
||||||
const Options = @import("../parser.zig").Options;
|
const Options = @import("../parser.zig").Options;
|
||||||
|
|
||||||
@@ -41,7 +56,7 @@ pub fn Parsed(comptime T: type) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const Value = union(enum) {
|
pub const Value = union(enum) {
|
||||||
pub const String = []const u8;
|
pub const String = [:0]const u8;
|
||||||
pub const Map = std.StringArrayHashMap(Value);
|
pub const Map = std.StringArrayHashMap(Value);
|
||||||
pub const List = std.ArrayList(Value);
|
pub const List = std.ArrayList(Value);
|
||||||
pub const TagType = @typeInfo(Value).Union.tag_type.?;
|
pub const TagType = @typeInfo(Value).Union.tag_type.?;
|
||||||
@@ -53,6 +68,10 @@ pub const Value = union(enum) {
|
|||||||
map: Map,
|
map: Map,
|
||||||
inline_map: Map,
|
inline_map: Map,
|
||||||
|
|
||||||
|
pub fn FieldConverter(comptime T: type) type {
|
||||||
|
return *const fn (Value, std.mem.Allocator, Options) error{BadValue}!T;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn convertTo(self: Value, comptime T: type, allocator: std.mem.Allocator, options: Options) !T {
|
pub fn convertTo(self: Value, comptime T: type, allocator: std.mem.Allocator, options: Options) !T {
|
||||||
switch (@typeInfo(T)) {
|
switch (@typeInfo(T)) {
|
||||||
.Void => {
|
.Void => {
|
||||||
@@ -67,14 +86,14 @@ pub const Value = union(enum) {
|
|||||||
inline .scalar, .string => |str, tag| {
|
inline .scalar, .string => |str, tag| {
|
||||||
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
||||||
if (options.case_insensitive_scalar_coersion) {
|
if (options.case_insensitive_scalar_coersion) {
|
||||||
for (options.boolean_strings.truthy) |check|
|
for (options.truthy_boolean_scalars) |check|
|
||||||
if (std.ascii.eqlIgnoreCase(str, check)) return true;
|
if (std.ascii.eqlIgnoreCase(str, check)) return true;
|
||||||
for (options.boolean_strings.falsy) |check|
|
for (options.falsy_boolean_scalars) |check|
|
||||||
if (std.ascii.eqlIgnoreCase(str, check)) return false;
|
if (std.ascii.eqlIgnoreCase(str, check)) return false;
|
||||||
} else {
|
} else {
|
||||||
for (options.boolean_strings.truthy) |check|
|
for (options.truthy_boolean_scalars) |check|
|
||||||
if (std.mem.eql(u8, str, check)) return true;
|
if (std.mem.eql(u8, str, check)) return true;
|
||||||
for (options.boolean_strings.falsy) |check|
|
for (options.falsy_boolean_scalars) |check|
|
||||||
if (std.mem.eql(u8, str, check)) return false;
|
if (std.mem.eql(u8, str, check)) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +115,7 @@ pub const Value = union(enum) {
|
|||||||
switch (self) {
|
switch (self) {
|
||||||
inline .scalar, .string => |str, tag| {
|
inline .scalar, .string => |str, tag| {
|
||||||
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
||||||
return try std.fmt.parseFloat(T, str, 0);
|
return try std.fmt.parseFloat(T, str);
|
||||||
},
|
},
|
||||||
else => return error.BadValue,
|
else => return error.BadValue,
|
||||||
}
|
}
|
||||||
@@ -108,16 +127,32 @@ pub const Value = union(enum) {
|
|||||||
// type to use for this? the problem is that it becomes
|
// type to use for this? the problem is that it becomes
|
||||||
// invasive into downstream code. Ultimately this should
|
// invasive into downstream code. Ultimately this should
|
||||||
// probably be solved in the zig stdlib or similar.
|
// probably be solved in the zig stdlib or similar.
|
||||||
// TODO: This also doesn't handle sentinels properly.
|
|
||||||
switch (self) {
|
switch (self) {
|
||||||
.scalar, .string => |str| return if (ptr.child == u8) str else error.BadValue,
|
.scalar, .string => |str| {
|
||||||
.list, .inline_list => |lst| {
|
if (comptime ptr.child == u8) {
|
||||||
var result = try std.ArrayList(ptr.child).initCapacity(allocator, lst.items.len);
|
if (comptime ptr.sentinel) |sentinel|
|
||||||
errdefer result.deinit();
|
if (comptime @as(*align(1) const ptr.child, @ptrCast(sentinel)).* != 0)
|
||||||
for (lst.items) |item| {
|
return error.BadValue;
|
||||||
result.appendAssumeCapacity(try item.convertTo(ptr.child, allocator, options));
|
|
||||||
|
return str;
|
||||||
|
} else {
|
||||||
|
return error.BadValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.list, .inline_list => |lst| {
|
||||||
|
const result = try allocator.alloc(ptr.child, lst.items.len + @intFromBool(ptr.sentinel != null));
|
||||||
|
|
||||||
|
for (result[0..lst.items.len], lst.items) |*res, item| {
|
||||||
|
res.* = try item.convertTo(ptr.child, allocator, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime ptr.sentinel) |sentinel| {
|
||||||
|
const sval = @as(*align(1) const ptr.child, @ptrCast(sentinel)).*;
|
||||||
|
result[lst.items.len] = sval;
|
||||||
|
return result[0..lst.items.len :sval];
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
return result.toOwnedSlice();
|
|
||||||
},
|
},
|
||||||
else => return error.BadValue,
|
else => return error.BadValue,
|
||||||
}
|
}
|
||||||
@@ -128,7 +163,7 @@ pub const Value = union(enum) {
|
|||||||
result.* = try self.convertTo(ptr.child, allocator, options);
|
result.* = try self.convertTo(ptr.child, allocator, options);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
else => @compileError("Cannot deserialize into many-pointer or c-pointer " ++ @typeName(T)), // do not support many or C item pointers.
|
else => @compileError("Cannot deserialize into many-pointer or c-pointer " ++ @typeName(T)),
|
||||||
},
|
},
|
||||||
.Array => |arr| {
|
.Array => |arr| {
|
||||||
// TODO: There is ambiguity here because a document expecting a list
|
// TODO: There is ambiguity here because a document expecting a list
|
||||||
@@ -136,7 +171,6 @@ pub const Value = union(enum) {
|
|||||||
// type to use for this? the problem is that it becomes
|
// type to use for this? the problem is that it becomes
|
||||||
// invasive into downstream code. Ultimately this should
|
// invasive into downstream code. Ultimately this should
|
||||||
// probably be solved in the zig stdlib or similar.
|
// probably be solved in the zig stdlib or similar.
|
||||||
// TODO: This also doesn't handle sentinels properly.
|
|
||||||
switch (self) {
|
switch (self) {
|
||||||
.scalar, .string => |str| {
|
.scalar, .string => |str| {
|
||||||
if (arr.child == u8 and str.len == arr.len) {
|
if (arr.child == u8 and str.len == arr.len) {
|
||||||
@@ -146,21 +180,19 @@ pub const Value = union(enum) {
|
|||||||
} else return error.BadValue;
|
} else return error.BadValue;
|
||||||
},
|
},
|
||||||
.list, .inline_list => |lst| {
|
.list, .inline_list => |lst| {
|
||||||
var storage = try std.ArrayList(arr.child).initCapacity(allocator, arr.len);
|
if (lst.items.len != arr.len) return error.BadValue;
|
||||||
defer storage.deinit();
|
|
||||||
for (lst.items) |item| {
|
|
||||||
storage.appendAssumeCapacity(try item.convertTo(arr.child, allocator, options));
|
|
||||||
}
|
|
||||||
// this may result in a big stack allocation, which is not ideal
|
|
||||||
var result: T = undefined;
|
var result: T = undefined;
|
||||||
@memcpy(&result, storage.items);
|
for (&result, lst.items) |*res, item| {
|
||||||
|
res.* = try item.convertTo(arr.child, allocator, options);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
else => return error.BadValue,
|
else => return error.BadValue,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.Struct => |stt| {
|
.Struct => |stt| {
|
||||||
if (comptime std.meta.trait.hasFn("deserializeNice")(T))
|
if (comptime hasFn(T, "deserializeNice"))
|
||||||
return T.deserializeNice(self, allocator, options);
|
return T.deserializeNice(self, allocator, options);
|
||||||
|
|
||||||
if (stt.is_tuple) {
|
if (stt.is_tuple) {
|
||||||
@@ -168,8 +200,8 @@ pub const Value = union(enum) {
|
|||||||
.list, .inline_list => |list| {
|
.list, .inline_list => |list| {
|
||||||
if (list.items.len != stt.fields.len) return error.BadValue;
|
if (list.items.len != stt.fields.len) return error.BadValue;
|
||||||
var result: T = undefined;
|
var result: T = undefined;
|
||||||
inline for (stt.fields, 0..) |field, idx| {
|
inline for (stt.fields, &result, list.items) |field, *res, item| {
|
||||||
result[idx] = try list.items[idx].convertTo(field.type, allocator, options);
|
res.* = try item.convertTo(field.type, allocator, options);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
@@ -181,33 +213,28 @@ pub const Value = union(enum) {
|
|||||||
.map, .inline_map => |map| {
|
.map, .inline_map => |map| {
|
||||||
var result: T = undefined;
|
var result: T = undefined;
|
||||||
|
|
||||||
if (options.ignore_extra_fields) {
|
if (!options.ignore_extra_fields and (map.count() > stt.fields.len))
|
||||||
inline for (stt.fields) |field| {
|
return error.BadValue;
|
||||||
if (map.get(field.name)) |value| {
|
|
||||||
@field(result, field.name) = try value.convertTo(field.type, allocator, options);
|
var use_count: usize = 0;
|
||||||
} else if (options.treat_omitted_as_null and @typeInfo(field.type) == .Optional) {
|
inline for (stt.fields) |field| {
|
||||||
@field(result, field.name) = null;
|
if (map.get(field.name)) |val| {
|
||||||
|
if (comptime hasFn(T, "niceFieldConverter") and T.niceFieldConverter(field.name) != null) {
|
||||||
|
@field(result, field.name) = try T.niceFieldConverter(field.name).?(val, allocator, options);
|
||||||
} else {
|
} else {
|
||||||
return error.BadValue;
|
@field(result, field.name) = try val.convertTo(field.type, allocator, options);
|
||||||
}
|
}
|
||||||
}
|
use_count += 1;
|
||||||
} else {
|
} else if (options.allow_omitting_default_values) {
|
||||||
// we could iterate over each map key and do an exhaustive
|
if (comptime field.default_value) |def|
|
||||||
// comparison with each struct field name. This would save
|
@field(result, field.name) = @as(*align(1) const field.type, @ptrCast(def)).*
|
||||||
// memory and it would probably be a fair amount faster for
|
else
|
||||||
// small structs.
|
return error.BadValue;
|
||||||
var clone = try map.clone();
|
} else return error.BadValue;
|
||||||
defer clone.deinit();
|
|
||||||
inline for (stt.fields) |field| {
|
|
||||||
if (clone.fetchSwapRemove(field.name)) |kv| {
|
|
||||||
@field(result, field.name) = try kv.value.convertTo(field.type, allocator, options);
|
|
||||||
} else if (options.treat_omitted_as_null and @typeInfo(field.type) == .Optional) {
|
|
||||||
@field(result, field.name) = null;
|
|
||||||
} else return error.BadValue;
|
|
||||||
}
|
|
||||||
// there were extra fields in the data
|
|
||||||
if (clone.count() > 0) return error.BadValue;
|
|
||||||
}
|
}
|
||||||
|
// there were extra fields in the data
|
||||||
|
if (!options.ignore_extra_fields and (map.count() > use_count))
|
||||||
|
return error.BadValue;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
@@ -215,13 +242,20 @@ pub const Value = union(enum) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
.Enum => {
|
.Enum => {
|
||||||
if (comptime std.meta.trait.hasFn("deserializeNice")(T))
|
if (comptime hasFn(T, "deserializeNice"))
|
||||||
return T.deserializeNice(self, allocator, options);
|
return T.deserializeNice(self, allocator, options);
|
||||||
|
|
||||||
switch (self) {
|
switch (self) {
|
||||||
inline .scalar, .string => |str, tag| {
|
inline .scalar, .string => |str, tag| {
|
||||||
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
||||||
if (std.meta.stringToEnum(T, str)) |value| return value;
|
const name = if (options.expect_enum_dot) blk: {
|
||||||
|
if (str.len > 0 and str[0] == '.')
|
||||||
|
break :blk str[1..]
|
||||||
|
else
|
||||||
|
return error.BadValue;
|
||||||
|
} else str;
|
||||||
|
|
||||||
|
if (std.meta.stringToEnum(T, name)) |value| return value;
|
||||||
if (options.allow_numeric_enums) {
|
if (options.allow_numeric_enums) {
|
||||||
const parsed = std.fmt.parseInt(@typeInfo(T).Enum.tag_type, str, 10) catch
|
const parsed = std.fmt.parseInt(@typeInfo(T).Enum.tag_type, str, 10) catch
|
||||||
return error.BadValue;
|
return error.BadValue;
|
||||||
@@ -233,25 +267,52 @@ pub const Value = union(enum) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
.Union => |unn| {
|
.Union => |unn| {
|
||||||
if (comptime std.meta.trait.hasFn("deserializeNice")(T))
|
if (comptime hasFn(T, "deserializeNice"))
|
||||||
return T.deserializeNice(self, allocator, options);
|
return T.deserializeNice(self, allocator, options);
|
||||||
|
|
||||||
if (unn.tag_type == null) @compileError("Cannot deserialize into untagged union " ++ @typeName(T));
|
if (unn.tag_type == null) @compileError("Cannot deserialize into untagged union " ++ @typeName(T));
|
||||||
|
|
||||||
switch (self) {
|
switch (self) {
|
||||||
.map, .inline_map => |map| {
|
.map, .inline_map => |map| {
|
||||||
// a union may not ever be deserialized from a map with more than one value
|
// a union may not ever be deserialized from a map with more
|
||||||
|
// (or less) than one value
|
||||||
if (map.count() != 1) return error.BadValue;
|
if (map.count() != 1) return error.BadValue;
|
||||||
const key = map.keys()[0];
|
const key = map.keys()[0];
|
||||||
|
const name = if (options.expect_enum_dot) blk: {
|
||||||
|
if (key.len > 0 and key[0] == '.')
|
||||||
|
break :blk key[1..]
|
||||||
|
else
|
||||||
|
return error.BadValue;
|
||||||
|
} else key;
|
||||||
|
|
||||||
inline for (unn.fields) |field| {
|
inline for (unn.fields) |field| {
|
||||||
if (std.mem.eql(u8, key, field.name))
|
if (std.mem.eql(u8, name, field.name))
|
||||||
return @unionInit(T, field.name, try map.get(key).?.convertTo(field.type, allocator, options));
|
return @unionInit(T, field.name, try map.get(key).?.convertTo(field.type, allocator, options));
|
||||||
}
|
}
|
||||||
return error.BadValue;
|
return error.BadValue;
|
||||||
},
|
},
|
||||||
// TODO: if the field is a 0 width type like void, we could parse it
|
inline .scalar, .string => |str, tag| {
|
||||||
// directly from a scalar/string value (i.e. a name with no
|
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
||||||
// corresponding value)
|
const name = if (options.expect_enum_dot) blk: {
|
||||||
|
if (str.len > 0 and str[0] == '.')
|
||||||
|
break :blk str[1..]
|
||||||
|
else
|
||||||
|
return error.BadValue;
|
||||||
|
} else str;
|
||||||
|
|
||||||
|
inline for (unn.fields) |field| {
|
||||||
|
if (@sizeOf(field.type) != 0) continue;
|
||||||
|
// this logic may be a little off: comtime_int,
|
||||||
|
// comptime_float, and type will all have size 0 because
|
||||||
|
// they can't be used at runtime. On the other hand, trying
|
||||||
|
// to use them here should result in a compile error? Also,
|
||||||
|
// it's a 0 sized type so initializing it as undefined
|
||||||
|
// shouldn't be a problem. As far as I know.
|
||||||
|
if (std.mem.eql(u8, name, field.name))
|
||||||
|
return @unionInit(T, field.name, undefined);
|
||||||
|
}
|
||||||
|
return error.BadValue;
|
||||||
|
},
|
||||||
else => return error.BadValue,
|
else => return error.BadValue,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -260,10 +321,10 @@ pub const Value = union(enum) {
|
|||||||
inline .scalar, .string => |str, tag| {
|
inline .scalar, .string => |str, tag| {
|
||||||
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
||||||
if (options.case_insensitive_scalar_coersion) {
|
if (options.case_insensitive_scalar_coersion) {
|
||||||
for (options.null_strings) |check|
|
for (options.null_scalars) |check|
|
||||||
if (std.ascii.eqlIgnoreCase(str, check)) return null;
|
if (std.ascii.eqlIgnoreCase(str, check)) return null;
|
||||||
} else {
|
} else {
|
||||||
for (options.null_strings) |check|
|
for (options.null_scalars) |check|
|
||||||
if (std.mem.eql(u8, str, check)) return null;
|
if (std.mem.eql(u8, str, check)) return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +346,7 @@ pub const Value = union(enum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline fn _fromScalarOrString(alloc: std.mem.Allocator, comptime classification: TagType, input: []const u8) !Value {
|
inline fn _fromScalarOrString(alloc: std.mem.Allocator, comptime classification: TagType, input: []const u8) !Value {
|
||||||
return @unionInit(Value, @tagName(classification), try alloc.dupe(u8, input));
|
return @unionInit(Value, @tagName(classification), try alloc.dupeZ(u8, input));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn emptyScalar() Value {
|
pub inline fn emptyScalar() Value {
|
||||||
|
@@ -1,3 +1,13 @@
|
|||||||
|
// Copyright 2023 torque@epicyclic.dev
|
||||||
|
//
|
||||||
|
// Licensed under the MIT/Expat license. You may not use this file except in
|
||||||
|
// compliance with the license. You may obtain a copy of the license at
|
||||||
|
//
|
||||||
|
// https://spdx.org/licenses/MIT.html
|
||||||
|
//
|
||||||
|
// This software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
// CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const Diagnostics = @import("./parser.zig").Diagnostics;
|
const Diagnostics = @import("./parser.zig").Diagnostics;
|
||||||
|
5
tests/main.zig
Normal file
5
tests/main.zig
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
comptime {
|
||||||
|
if (@import("builtin").is_test) {
|
||||||
|
_ = @import("./reify.zig");
|
||||||
|
}
|
||||||
|
}
|
144
tests/reify.zig
Normal file
144
tests/reify.zig
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const nice = @import("nice");
|
||||||
|
|
||||||
|
fn reifyScalar(comptime scalar: []const u8, expected: anytype) !void {
|
||||||
|
try reifyScalarWithOptions(scalar, expected, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reifyScalarWithOptions(comptime scalar: []const u8, expected: anytype, options: nice.parser.Options) !void {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var diagnostics = nice.Diagnostics{};
|
||||||
|
const parsed = try nice.parseBufferTo(
|
||||||
|
@TypeOf(expected),
|
||||||
|
allocator,
|
||||||
|
scalar ++ "\n",
|
||||||
|
&diagnostics,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
defer parsed.deinit();
|
||||||
|
|
||||||
|
try std.testing.expectEqual(expected, parsed.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify integer" {
|
||||||
|
try reifyScalar("123", @as(u8, 123));
|
||||||
|
try reifyScalar("0123", @as(u8, 123));
|
||||||
|
try reifyScalar("1_23", @as(u8, 123));
|
||||||
|
try reifyScalar("-01_23", @as(i8, -123));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify hexadecimal" {
|
||||||
|
try reifyScalar("0x123", @as(i64, 0x123));
|
||||||
|
try reifyScalar("0x0123", @as(i64, 0x123));
|
||||||
|
try reifyScalar("0x01_23", @as(i64, 0x123));
|
||||||
|
try reifyScalar("-0x01_23", @as(i64, -0x123));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify octal" {
|
||||||
|
try reifyScalar("0o123", @as(i64, 0o123));
|
||||||
|
try reifyScalar("0o0123", @as(i64, 0o123));
|
||||||
|
try reifyScalar("0o01_23", @as(i64, 0o123));
|
||||||
|
try reifyScalar("-0o01_23", @as(i64, -0o123));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify binary" {
|
||||||
|
try reifyScalar("0b1011", @as(i5, 0b1011));
|
||||||
|
try reifyScalar("0b01011", @as(i5, 0b1011));
|
||||||
|
try reifyScalar("0b010_11", @as(i5, 0b1011));
|
||||||
|
try reifyScalar("-0b010_11", @as(i5, -0b1011));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify float" {
|
||||||
|
try reifyScalar("0.25", @as(f32, 0.25));
|
||||||
|
try reifyScalar("0.2_5", @as(f32, 0.25));
|
||||||
|
try reifyScalar("00.250", @as(f32, 0.25));
|
||||||
|
try reifyScalar("-0.25", @as(f32, -0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify hexfloat" {
|
||||||
|
try reifyScalar("0x0.25", @as(f64, 0x0.25));
|
||||||
|
try reifyScalar("0x0.2_5", @as(f64, 0x0.25));
|
||||||
|
try reifyScalar("0x0.250p1", @as(f64, 0x0.25p1));
|
||||||
|
try reifyScalar("-0x0.25", @as(f64, -0x0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify true" {
|
||||||
|
try reifyScalar("true", true);
|
||||||
|
try reifyScalar("True", true);
|
||||||
|
try reifyScalar("yes", true);
|
||||||
|
try reifyScalar("on", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify false" {
|
||||||
|
try reifyScalar("false", false);
|
||||||
|
try reifyScalar("False", false);
|
||||||
|
try reifyScalar("no", false);
|
||||||
|
try reifyScalar("off", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify custom true" {
|
||||||
|
const options = nice.parser.Options{ .truthy_boolean_scalars = &.{"correct"} };
|
||||||
|
try reifyScalarWithOptions("correct", true, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify true case insensitive" {
|
||||||
|
try std.testing.expectError(error.BadValue, reifyScalar("TRUE", true));
|
||||||
|
const options = nice.parser.Options{ .case_insensitive_scalar_coersion = true };
|
||||||
|
try reifyScalarWithOptions("TRUE", true, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify custom false" {
|
||||||
|
const options = nice.parser.Options{ .falsy_boolean_scalars = &.{"incorrect"} };
|
||||||
|
try reifyScalarWithOptions("incorrect", false, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify false case insensitive" {
|
||||||
|
try std.testing.expectError(error.BadValue, reifyScalar("FALSE", false));
|
||||||
|
const options = nice.parser.Options{ .case_insensitive_scalar_coersion = true };
|
||||||
|
try reifyScalarWithOptions("FALSE", false, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify null" {
|
||||||
|
try reifyScalar("null", @as(?u8, null));
|
||||||
|
try reifyScalar("nil", @as(?u8, null));
|
||||||
|
try reifyScalar("None", @as(?u8, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify custom null" {
|
||||||
|
const options = nice.parser.Options{ .null_scalars = &.{"nothing"} };
|
||||||
|
try reifyScalarWithOptions("nothing", @as(?u8, null), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify null case insensitive" {
|
||||||
|
// this is a little weird because when the null string mismatches, it will try to
|
||||||
|
// parse the child optional type and produce either a value or an error from that,
|
||||||
|
// so the error received depends on whether or not the optional child type fails to
|
||||||
|
// parse the given value.
|
||||||
|
try std.testing.expectError(error.InvalidCharacter, reifyScalar("NULL", @as(?u8, null)));
|
||||||
|
const options = nice.parser.Options{ .case_insensitive_scalar_coersion = true };
|
||||||
|
try reifyScalarWithOptions("NULL", @as(?u8, null), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify void" {
|
||||||
|
// A void scalar cannot exist on its own as it is not distinguishable from an empty
|
||||||
|
// document.
|
||||||
|
const Void = struct { void: void };
|
||||||
|
try reifyScalar("void:", Void{ .void = void{} });
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify void scalar" {
|
||||||
|
const options = nice.parser.Options{ .default_object = .scalar };
|
||||||
|
try reifyScalarWithOptions("", void{}, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify enum" {
|
||||||
|
const Enum = enum { one, two };
|
||||||
|
try reifyScalar(".one", Enum.one);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reify enum no dot" {
|
||||||
|
const options = nice.parser.Options{ .expect_enum_dot = false };
|
||||||
|
const Enum = enum { one, two };
|
||||||
|
try reifyScalarWithOptions("two", Enum.two, options);
|
||||||
|
}
|
Reference in New Issue
Block a user