feat: scaffold interpres project with TOML parser and CI
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
package interpres
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseScalars(t *testing.T) {
|
||||
tree, err := Parse([]byte(`
|
||||
title = "interpres"
|
||||
count = 42
|
||||
ratio = 3.14
|
||||
enabled = true
|
||||
disabled = false
|
||||
hexv = 0xFF
|
||||
octv = 0o755
|
||||
binv = 0b1010
|
||||
grouped = 1_000_000
|
||||
neg = -17
|
||||
expv = 1e3
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
|
||||
cases := map[string]any{
|
||||
"title": "interpres",
|
||||
"count": int64(42),
|
||||
"ratio": 3.14,
|
||||
"enabled": true,
|
||||
"disabled": false,
|
||||
"hexv": int64(255),
|
||||
"octv": int64(493),
|
||||
"binv": int64(10),
|
||||
"grouped": int64(1000000),
|
||||
"neg": int64(-17),
|
||||
"expv": 1000.0,
|
||||
}
|
||||
for k, want := range cases {
|
||||
if got := tree[k]; got != want {
|
||||
t.Errorf("%s = %#v (%T), want %#v (%T)", k, got, got, want, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfNan(t *testing.T) {
|
||||
tree, err := Parse([]byte("pos = inf\nneg = -inf\nbad = nan\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if v := tree["pos"].(float64); !math.IsInf(v, 1) {
|
||||
t.Errorf("pos = %v, want +Inf", v)
|
||||
}
|
||||
if v := tree["neg"].(float64); !math.IsInf(v, -1) {
|
||||
t.Errorf("neg = %v, want -Inf", v)
|
||||
}
|
||||
if v := tree["bad"].(float64); !math.IsNaN(v) {
|
||||
t.Errorf("bad = %v, want NaN", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStrings(t *testing.T) {
|
||||
tree, err := Parse([]byte(`
|
||||
basic = "a\tb\nc"
|
||||
literal = 'C:\path\no\escape'
|
||||
quote = "say \"hi\""
|
||||
unicode = "\u00e9"
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if tree["basic"] != "a\tb\nc" {
|
||||
t.Errorf("basic = %q", tree["basic"])
|
||||
}
|
||||
if tree["literal"] != `C:\path\no\escape` {
|
||||
t.Errorf("literal = %q", tree["literal"])
|
||||
}
|
||||
if tree["quote"] != `say "hi"` {
|
||||
t.Errorf("quote = %q", tree["quote"])
|
||||
}
|
||||
if tree["unicode"] != "é" {
|
||||
t.Errorf("unicode = %q", tree["unicode"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMultilineString(t *testing.T) {
|
||||
tree, err := Parse([]byte("text = \"\"\"\nfirst\nsecond\"\"\"\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if tree["text"] != "first\nsecond" {
|
||||
t.Errorf("text = %q, want %q", tree["text"], "first\nsecond")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMultilineLineEndingBackslash(t *testing.T) {
|
||||
tree, err := Parse([]byte("text = \"\"\"\\\n one \\\n two\"\"\"\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if tree["text"] != "one two" {
|
||||
t.Errorf("text = %q, want %q", tree["text"], "one two")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTablesAndDottedKeys(t *testing.T) {
|
||||
tree, err := Parse([]byte(`
|
||||
owner.name = "Petr"
|
||||
|
||||
[server]
|
||||
host = "localhost"
|
||||
port = 9090
|
||||
|
||||
[server.tls]
|
||||
enabled = true
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
server := tree["server"].(map[string]any)
|
||||
if server["host"] != "localhost" || server["port"] != int64(9090) {
|
||||
t.Errorf("server = %#v", server)
|
||||
}
|
||||
tls := server["tls"].(map[string]any)
|
||||
if tls["enabled"] != true {
|
||||
t.Errorf("tls = %#v", tls)
|
||||
}
|
||||
owner := tree["owner"].(map[string]any)
|
||||
if owner["name"] != "Petr" {
|
||||
t.Errorf("owner = %#v", owner)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArrayOfTables(t *testing.T) {
|
||||
tree, err := Parse([]byte(`
|
||||
[[forms]]
|
||||
name = "contact"
|
||||
|
||||
[[forms]]
|
||||
name = "feedback"
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
forms := tree["forms"].([]map[string]any)
|
||||
if len(forms) != 2 {
|
||||
t.Fatalf("len(forms) = %d, want 2", len(forms))
|
||||
}
|
||||
if forms[0]["name"] != "contact" || forms[1]["name"] != "feedback" {
|
||||
t.Errorf("forms = %#v", forms)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArraysAndInlineTables(t *testing.T) {
|
||||
tree, err := Parse([]byte(`
|
||||
ports = [80, 443]
|
||||
mixed = [
|
||||
"a",
|
||||
"b",
|
||||
]
|
||||
point = { x = 1, y = 2 }
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
ports := tree["ports"].([]any)
|
||||
if len(ports) != 2 || ports[0] != int64(80) || ports[1] != int64(443) {
|
||||
t.Errorf("ports = %#v", ports)
|
||||
}
|
||||
mixed := tree["mixed"].([]any)
|
||||
if len(mixed) != 2 || mixed[0] != "a" || mixed[1] != "b" {
|
||||
t.Errorf("mixed = %#v", mixed)
|
||||
}
|
||||
point := tree["point"].(map[string]any)
|
||||
if point["x"] != int64(1) || point["y"] != int64(2) {
|
||||
t.Errorf("point = %#v", point)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDateTime(t *testing.T) {
|
||||
tree, err := Parse([]byte(`
|
||||
offset = 1979-05-27T07:32:00Z
|
||||
local = 1979-05-27T07:32:00
|
||||
day = 1979-05-27
|
||||
clock = 07:32:00
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if off, ok := tree["offset"].(time.Time); !ok || off.Year() != 1979 || off.Hour() != 7 {
|
||||
t.Errorf("offset = %#v (%T)", tree["offset"], tree["offset"])
|
||||
}
|
||||
if ldt, ok := tree["local"].(LocalDateTime); !ok || ldt.Year() != 1979 || ldt.Hour() != 7 {
|
||||
t.Errorf("local = %#v (%T)", tree["local"], tree["local"])
|
||||
}
|
||||
if d, ok := tree["day"].(LocalDate); !ok || d.Month() != time.May || d.Day() != 27 {
|
||||
t.Errorf("day = %#v (%T)", tree["day"], tree["day"])
|
||||
}
|
||||
if clk, ok := tree["clock"].(LocalTime); !ok || clk.Hour() != 7 || clk.Minute() != 32 {
|
||||
t.Errorf("clock = %#v (%T)", tree["clock"], tree["clock"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeFormats(t *testing.T) {
|
||||
tree, err := Parse([]byte("a = 1987-07-05 17:45:00Z\nb = 1987-07-05t17:45:00z\nc = 1977-12-21T10:32:00.555\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if _, ok := tree["a"].(time.Time); !ok {
|
||||
t.Errorf("a is %T, want time.Time", tree["a"])
|
||||
}
|
||||
if _, ok := tree["b"].(time.Time); !ok {
|
||||
t.Errorf("b is %T, want time.Time", tree["b"])
|
||||
}
|
||||
if _, ok := tree["c"].(LocalDateTime); !ok {
|
||||
t.Errorf("c is %T, want LocalDateTime", tree["c"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalDateTime(t *testing.T) {
|
||||
type Doc struct {
|
||||
Created time.Time `toml:"created"`
|
||||
Day LocalDate `toml:"day"`
|
||||
}
|
||||
var d Doc
|
||||
if err := Unmarshal([]byte("created = 2026-06-20T10:00:00Z\nday = 2026-06-20\n"), &d); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if d.Created.Year() != 2026 || d.Created.Hour() != 10 {
|
||||
t.Errorf("Created = %v", d.Created)
|
||||
}
|
||||
if d.Day.Year() != 2026 || d.Day.Month() != time.June || d.Day.Day() != 20 {
|
||||
t.Errorf("Day = %v", d.Day)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalStruct(t *testing.T) {
|
||||
type SMTP struct {
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
}
|
||||
type Form struct {
|
||||
Name string `toml:"name"`
|
||||
SMTP SMTP `toml:"smtp"`
|
||||
Origins []string `toml:"allowed_origins"`
|
||||
}
|
||||
type Config struct {
|
||||
Port int `toml:"port"`
|
||||
Forms []Form `toml:"forms"`
|
||||
}
|
||||
|
||||
data := []byte(`
|
||||
port = 8080
|
||||
|
||||
[[forms]]
|
||||
name = "contact"
|
||||
allowed_origins = ["https://example.com"]
|
||||
|
||||
[forms.smtp]
|
||||
host = "smtp.example.com"
|
||||
port = 587
|
||||
`)
|
||||
|
||||
var cfg Config
|
||||
if err := Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if cfg.Port != 8080 {
|
||||
t.Errorf("Port = %d", cfg.Port)
|
||||
}
|
||||
if len(cfg.Forms) != 1 {
|
||||
t.Fatalf("len(Forms) = %d", len(cfg.Forms))
|
||||
}
|
||||
f := cfg.Forms[0]
|
||||
if f.Name != "contact" || f.SMTP.Host != "smtp.example.com" || f.SMTP.Port != 587 {
|
||||
t.Errorf("form = %#v", f)
|
||||
}
|
||||
if len(f.Origins) != 1 || f.Origins[0] != "https://example.com" {
|
||||
t.Errorf("origins = %#v", f.Origins)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalCaseInsensitiveAndUntagged(t *testing.T) {
|
||||
type Config struct {
|
||||
Title string
|
||||
Count int
|
||||
}
|
||||
var cfg Config
|
||||
if err := Unmarshal([]byte("title = \"x\"\ncount = 3\n"), &cfg); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if cfg.Title != "x" || cfg.Count != 3 {
|
||||
t.Errorf("cfg = %#v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisallowUnknownFields(t *testing.T) {
|
||||
type C struct {
|
||||
Known string `toml:"known"`
|
||||
}
|
||||
data := []byte("known = \"x\"\nbogus = 1\n")
|
||||
|
||||
var lenient C
|
||||
if err := Unmarshal(data, &lenient); err != nil {
|
||||
t.Fatalf("lenient unmarshal: %v", err)
|
||||
}
|
||||
|
||||
var strict C
|
||||
err := NewDecoder().DisallowUnknownFields().Decode(data, &strict)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown field, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkippedFieldTag(t *testing.T) {
|
||||
type C struct {
|
||||
Keep string `toml:"keep"`
|
||||
Skip string `toml:"-"`
|
||||
}
|
||||
var c C
|
||||
if err := Unmarshal([]byte("keep = \"y\"\n"), &c); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if c.Keep != "y" || c.Skip != "" {
|
||||
t.Errorf("c = %#v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyntaxErrorReportsLine(t *testing.T) {
|
||||
_, err := Parse([]byte("a = 1\nb = \nc = 3\n"))
|
||||
if err == nil {
|
||||
t.Fatal("expected a syntax error")
|
||||
}
|
||||
se, ok := err.(*SyntaxError)
|
||||
if !ok {
|
||||
t.Fatalf("error is %T, want *SyntaxError", err)
|
||||
}
|
||||
if se.Line != 2 {
|
||||
t.Errorf("Line = %d, want 2", se.Line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComments(t *testing.T) {
|
||||
tree, err := Parse([]byte(`
|
||||
# a leading comment
|
||||
key = "value" # trailing comment
|
||||
# another
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if tree["key"] != "value" {
|
||||
t.Errorf("key = %q", tree["key"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateKeyRejected(t *testing.T) {
|
||||
_, err := Parse([]byte("a = 1\na = 2\n"))
|
||||
if err == nil {
|
||||
t.Fatal("expected duplicate key error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsInvalidNumbers(t *testing.T) {
|
||||
for _, tok := range []string{
|
||||
"01", "-01", "00",
|
||||
"1__0", "_1", "1_", "0x_1", "1_.0",
|
||||
"1.", ".5", "1.2.3", "1.e2",
|
||||
"0x", "0o", "0b", "0b2", "0o8", "0xG",
|
||||
"+0x1",
|
||||
} {
|
||||
if _, err := Parse([]byte("v = " + tok + "\n")); err == nil {
|
||||
t.Errorf("%q: expected an error, got none", tok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptsNumberEdgeCases(t *testing.T) {
|
||||
cases := map[string]any{
|
||||
"0": int64(0),
|
||||
"-0": int64(0),
|
||||
"+99": int64(99),
|
||||
"1_000": int64(1000),
|
||||
"0xDEAD_BEEF": int64(0xDEADBEEF),
|
||||
"0o755": int64(493),
|
||||
"0b1010": int64(10),
|
||||
"0.0": 0.0,
|
||||
"3.14": 3.14,
|
||||
"6.022e23": 6.022e23,
|
||||
"1e10": 1e10,
|
||||
"-2.5E-3": -2.5e-3,
|
||||
}
|
||||
for tok, want := range cases {
|
||||
tree, err := Parse([]byte("v = " + tok + "\n"))
|
||||
if err != nil {
|
||||
t.Errorf("%q: %v", tok, err)
|
||||
continue
|
||||
}
|
||||
if got := tree["v"]; got != want {
|
||||
t.Errorf("%q = %#v (%T), want %#v", tok, got, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsTableRedefinition(t *testing.T) {
|
||||
_, err := Parse([]byte("[a]\nx = 1\n\n[a]\ny = 2\n"))
|
||||
if err == nil {
|
||||
t.Fatal("expected a table-redefinition error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowsImplicitThenExplicitTable(t *testing.T) {
|
||||
tree, err := Parse([]byte("[a.b]\nx = 1\n\n[a]\ny = 2\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
a := tree["a"].(map[string]any)
|
||||
if a["y"] != int64(2) {
|
||||
t.Errorf("a.y = %#v", a["y"])
|
||||
}
|
||||
if b := a["b"].(map[string]any); b["x"] != int64(1) {
|
||||
t.Errorf("a.b.x = %#v", b["x"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsControlCharInString(t *testing.T) {
|
||||
if _, err := Parse([]byte("v = \"a\x01b\"\n")); err == nil {
|
||||
t.Fatal("expected a control-character error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowsEscapedControlChar(t *testing.T) {
|
||||
tree, err := Parse([]byte(`v = "\u0000"`))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if tree["v"] != "\x00" {
|
||||
t.Errorf("v = %q", tree["v"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultilineQuotesAtDelimiter(t *testing.T) {
|
||||
tree, err := Parse([]byte("a = '''''two quotes'''''\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if tree["a"] != "''two quotes''" {
|
||||
t.Errorf("a = %q", tree["a"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsInlineTableExtension(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"by header": "a = { b = 1 }\n[a.c]\nx = 2\n",
|
||||
"by dotted key": "a = { b = 1 }\na.c = 2\n",
|
||||
"header over it": "a = { b = 1 }\n[a]\nx = 2\n",
|
||||
}
|
||||
for name, doc := range cases {
|
||||
if _, err := Parse([]byte(doc)); err == nil {
|
||||
t.Errorf("%s: expected an inline-table extension error", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsSpecInvalid(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"single-digit hour": "a = 2023-10-01T1:32:00Z\n",
|
||||
"inline duplicate key": "a = { b = 1, b = 2 }\n",
|
||||
"inline dotted overwrite": "a = { b = 1, b.c = 2 }\n",
|
||||
"dotted over header": "[a.b]\nx = 1\n[a]\nb.y = 2\n",
|
||||
"table over array": "[[t]]\n[t]\n",
|
||||
}
|
||||
for name, doc := range cases {
|
||||
if _, err := Parse([]byte(doc)); err == nil {
|
||||
t.Errorf("%s: expected an error", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArrayOfTablesPerElementSubtable(t *testing.T) {
|
||||
tree, err := Parse([]byte(`
|
||||
[[forms]]
|
||||
name = "a"
|
||||
|
||||
[forms.smtp]
|
||||
host = "h1"
|
||||
|
||||
[[forms]]
|
||||
name = "b"
|
||||
|
||||
[forms.smtp]
|
||||
host = "h2"
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
forms := tree["forms"].([]map[string]any)
|
||||
if len(forms) != 2 {
|
||||
t.Fatalf("len(forms) = %d", len(forms))
|
||||
}
|
||||
if h := forms[0]["smtp"].(map[string]any)["host"]; h != "h1" {
|
||||
t.Errorf("forms[0].smtp.host = %v", h)
|
||||
}
|
||||
if h := forms[1]["smtp"].(map[string]any)["host"]; h != "h2" {
|
||||
t.Errorf("forms[1].smtp.host = %v", h)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user