From b349f61a5aa5c9c377a6927bd4e6963fcdb7360a Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Tue, 28 Oct 2025 16:25:54 +0530 Subject: [PATCH 1/8] Add TOML parser and schema validator --- go.mod | 7 + go.sum | 6 + tomlparser/schema.go | 110 ++++++ tomlparser/testdata/ballerina-package.toml | 9 + tomlparser/testdata/invalid-sample.toml | 10 + tomlparser/testdata/missing-required.toml | 2 + tomlparser/testdata/required-schema.json | 19 + tomlparser/testdata/sample-schema.json | 51 +++ tomlparser/testdata/sample.toml | 28 ++ tomlparser/toml.go | 286 ++++++++++++++ tomlparser/toml_test.go | 394 ++++++++++++++++++++ tomlparser/validator.go | 56 +++ tomlparser/validator_test.go | 413 +++++++++++++++++++++ 13 files changed, 1391 insertions(+) create mode 100644 go.sum create mode 100644 tomlparser/schema.go create mode 100644 tomlparser/testdata/ballerina-package.toml create mode 100644 tomlparser/testdata/invalid-sample.toml create mode 100644 tomlparser/testdata/missing-required.toml create mode 100644 tomlparser/testdata/required-schema.json create mode 100644 tomlparser/testdata/sample-schema.json create mode 100644 tomlparser/testdata/sample.toml create mode 100644 tomlparser/toml.go create mode 100644 tomlparser/toml_test.go create mode 100644 tomlparser/validator.go create mode 100644 tomlparser/validator_test.go diff --git a/go.mod b/go.mod index 2ef809e9c..5906470e4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module ballerina-lang-go go 1.24.4 + +require github.com/BurntSushi/toml v1.5.0 + +require ( + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..240bcfa0a --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/tomlparser/schema.go b/tomlparser/schema.go new file mode 100644 index 000000000..c6963ecef --- /dev/null +++ b/tomlparser/schema.go @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tomlparser + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/santhosh-tekuri/jsonschema/v6" +) + +type Schema interface { + Validate(data any) error + FromPath(path string) (Schema, error) + FromString(content string) (Schema, error) +} + +type schemaImpl struct { + compiled *jsonschema.Schema +} + +func NewSchemaFromPath(path string) (Schema, error) { + compiler := jsonschema.NewCompiler() + + schema, err := compiler.Compile(path) + if err != nil { + return nil, fmt.Errorf("failed to compile schema from path %s: %w", path, err) + } + + return &schemaImpl{ + compiled: schema, + }, nil +} + +func NewSchemaFromString(content string) (Schema, error) { + compiler := jsonschema.NewCompiler() + + var schemaDoc any + if err := json.Unmarshal([]byte(content), &schemaDoc); err != nil { + return nil, fmt.Errorf("failed to parse schema JSON: %w", err) + } + + if err := compiler.AddResource("schema.json", schemaDoc); err != nil { + return nil, fmt.Errorf("failed to add schema resource: %w", err) + } + + schema, err := compiler.Compile("schema.json") + if err != nil { + return nil, fmt.Errorf("failed to compile schema: %w", err) + } + + return &schemaImpl{ + compiled: schema, + }, nil +} + +func NewSchemaFromFile(file *os.File) (Schema, error) { + compiler := jsonschema.NewCompiler() + + var schemaDoc any + decoder := json.NewDecoder(file) + if err := decoder.Decode(&schemaDoc); err != nil { + return nil, fmt.Errorf("failed to decode schema JSON: %w", err) + } + + if err := compiler.AddResource("schema.json", schemaDoc); err != nil { + return nil, fmt.Errorf("failed to add schema resource: %w", err) + } + + schema, err := compiler.Compile("schema.json") + if err != nil { + return nil, fmt.Errorf("failed to compile schema: %w", err) + } + + return &schemaImpl{ + compiled: schema, + }, nil +} + +func (s *schemaImpl) Validate(data any) error { + if err := s.compiled.Validate(data); err != nil { + return fmt.Errorf("schema validation failed: %w", err) + } + return nil +} + +func (s *schemaImpl) FromPath(path string) (Schema, error) { + return NewSchemaFromPath(path) +} + +func (s *schemaImpl) FromString(content string) (Schema, error) { + return NewSchemaFromString(content) +} diff --git a/tomlparser/testdata/ballerina-package.toml b/tomlparser/testdata/ballerina-package.toml new file mode 100644 index 000000000..ace39bab1 --- /dev/null +++ b/tomlparser/testdata/ballerina-package.toml @@ -0,0 +1,9 @@ +[package] +org = "foo" +name = "winery" +version = "0.1.0" +license = ["MIT", "Apache-2.0"] +authors = ["jo@wso2.com", "pramodya@wso2.com"] +repository = "https://github.com/ballerinalang/ballerina" +keywords = ["ballerina", "security", "crypto"] +exported = ["winery", "service"] diff --git a/tomlparser/testdata/invalid-sample.toml b/tomlparser/testdata/invalid-sample.toml new file mode 100644 index 000000000..be451cf98 --- /dev/null +++ b/tomlparser/testdata/invalid-sample.toml @@ -0,0 +1,10 @@ +[package] +org = "foo" +name = "winery" +version = "0.1.0" +license = ["MIT", "Apache-2.0"] +authors = ["jo@wso2.com", "pramodya@wso2.com"] +repository = "https://github.com/ballerinalang/ballerina" +keywords = ["ballerina", "security", "crypto"] +exported = ["winery", "service"] +unexpected = "this field is not in schema" diff --git a/tomlparser/testdata/missing-required.toml b/tomlparser/testdata/missing-required.toml new file mode 100644 index 000000000..68e453f51 --- /dev/null +++ b/tomlparser/testdata/missing-required.toml @@ -0,0 +1,2 @@ +name = "test" +description = "missing version field" diff --git a/tomlparser/testdata/required-schema.json b/tomlparser/testdata/required-schema.json new file mode 100644 index 000000000..7dc586014 --- /dev/null +++ b/tomlparser/testdata/required-schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config Schema", + "description": "Schema with required fields", + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + } + } +} diff --git a/tomlparser/testdata/sample-schema.json b/tomlparser/testdata/sample-schema.json new file mode 100644 index 000000000..2797cfa64 --- /dev/null +++ b/tomlparser/testdata/sample-schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ballerina Manifest Spec", + "description": "Schema for Ballerina Manifest", + "type": "object", + "additionalProperties": false, + "properties": { + "package": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "org": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "type": "array", + "items": { + "type": "string" + } + }, + "authors": { + "type": "array", + "items": { + "type": "string" + } + }, + "repository": { + "type": "string" + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "exported": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +} diff --git a/tomlparser/testdata/sample.toml b/tomlparser/testdata/sample.toml new file mode 100644 index 000000000..b56e07894 --- /dev/null +++ b/tomlparser/testdata/sample.toml @@ -0,0 +1,28 @@ +title = "Server Configuration" + +[owner] +name = "WSO2" + +[database] +server = "192.168.1.1" +ports = [ 8001, 8001, 8002 ] +connection_max = 5000 +enabled = true + +[servers] + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +[[routes]] +name = "health" +path = "/health" + +[[routes]] +name = "metrics" +path = "/metrics" +method = "GET" diff --git a/tomlparser/toml.go b/tomlparser/toml.go new file mode 100644 index 000000000..c83ada44b --- /dev/null +++ b/tomlparser/toml.go @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tomlparser + +import ( + "io" + "os" + "strings" + + "github.com/BurntSushi/toml" +) + +type Toml struct { + rootNode map[string]any + metadata toml.MetaData + diagnostics []Diagnostic + content string +} + +type Diagnostic struct { + Message string + Severity string + Location *Location +} + +type Location struct { + StartLine int + StartColumn int + EndLine int + EndColumn int +} + +func Read(path string) (*Toml, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return ReadString(string(content)) +} + +func ReadWithSchema(path string, schema Schema) (*Toml, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return ReadStringWithSchema(string(content), schema) +} + +func ReadStream(reader io.Reader) (*Toml, error) { + content, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + return ReadString(string(content)) +} + +func ReadStreamWithSchema(reader io.Reader, schema Schema) (*Toml, error) { + content, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + return ReadStringWithSchema(string(content), schema) +} + +func ReadString(content string) (*Toml, error) { + var data map[string]any + metadata, err := toml.Decode(content, &data) + + t := &Toml{ + rootNode: data, + metadata: metadata, + diagnostics: make([]Diagnostic, 0), + content: content, + } + + if err != nil { + t.diagnostics = append(t.diagnostics, Diagnostic{ + Message: err.Error(), + Severity: "ERROR", + }) + } + + return t, err +} + +func ReadStringWithSchema(content string, schema Schema) (*Toml, error) { + t, err := ReadString(content) + if err != nil { + return t, err + } + + validator := NewValidator(schema) + validationErr := validator.Validate(t) + if validationErr != nil { + return t, validationErr + } + + return t, nil +} + +func (t *Toml) Validate(schema Schema) error { + validator := NewValidator(schema) + return validator.Validate(t) +} + +func (t *Toml) Get(dottedKey string) (any, bool) { + keys := splitDottedKey(dottedKey) + return t.getValueByPath(keys) +} + +func (t *Toml) GetString(dottedKey string) (string, bool) { + val, ok := t.Get(dottedKey) + if !ok { + return "", false + } + str, ok := val.(string) + return str, ok +} + +func (t *Toml) GetInt(dottedKey string) (int64, bool) { + val, ok := t.Get(dottedKey) + if !ok { + return 0, false + } + + switch v := val.(type) { + case int64: + return v, true + case int: + return int64(v), true + default: + return 0, false + } +} + +func (t *Toml) GetFloat(dottedKey string) (float64, bool) { + val, ok := t.Get(dottedKey) + if !ok { + return 0, false + } + f, ok := val.(float64) + return f, ok +} + +func (t *Toml) GetBool(dottedKey string) (bool, bool) { + val, ok := t.Get(dottedKey) + if !ok { + return false, false + } + b, ok := val.(bool) + return b, ok +} + +func (t *Toml) GetArray(dottedKey string) ([]any, bool) { + val, ok := t.Get(dottedKey) + if !ok { + return nil, false + } + arr, ok := val.([]any) + return arr, ok +} + +func (t *Toml) GetTable(dottedKey string) (*Toml, bool) { + val, ok := t.Get(dottedKey) + if !ok { + return nil, false + } + + table, ok := val.(map[string]any) + if !ok { + return nil, false + } + + return &Toml{ + rootNode: table, + metadata: t.metadata, + diagnostics: make([]Diagnostic, 0), + content: "", + }, true +} + +func (t *Toml) GetTables(dottedKey string) ([]*Toml, bool) { + val, ok := t.Get(dottedKey) + if !ok { + return nil, false + } + + arr, ok := val.([]any) + if !ok { + if tableArr, ok := val.([]map[string]any); ok { + result := make([]*Toml, len(tableArr)) + for i, table := range tableArr { + result[i] = &Toml{ + rootNode: table, + metadata: t.metadata, + diagnostics: make([]Diagnostic, 0), + content: "", + } + } + return result, true + } + return nil, false + } + + result := make([]*Toml, 0) + for _, item := range arr { + if table, ok := item.(map[string]any); ok { + result = append(result, &Toml{ + rootNode: table, + metadata: t.metadata, + diagnostics: make([]Diagnostic, 0), + content: "", + }) + } + } + + if len(result) == 0 { + return nil, false + } + + return result, true +} + +func (t *Toml) Diagnostics() []Diagnostic { + return t.diagnostics +} + +func (t *Toml) RootNode() map[string]any { + return t.rootNode +} + +func (t *Toml) ToMap() map[string]any { + return t.rootNode +} + +func (t *Toml) To(target any) { + _, err := toml.Decode(t.content, target) + if err != nil { + t.diagnostics = append(t.diagnostics, Diagnostic{ + Message: err.Error(), + Severity: "ERROR", + }) + } +} + +func splitDottedKey(dottedKey string) []string { + return strings.Split(dottedKey, ".") +} + +func (t *Toml) getValueByPath(keys []string) (any, bool) { + current := any(t.rootNode) + + for _, key := range keys { + key = strings.Trim(key, "\"") + + currentMap, ok := current.(map[string]any) + if !ok { + return nil, false + } + + val, exists := currentMap[key] + if !exists { + return nil, false + } + + current = val + } + + return current, true +} diff --git a/tomlparser/toml_test.go b/tomlparser/toml_test.go new file mode 100644 index 000000000..12e26df62 --- /dev/null +++ b/tomlparser/toml_test.go @@ -0,0 +1,394 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tomlparser + +import ( + "fmt" + "strings" + "testing" +) + +const sampleToml = ` +title = "Server Configuration" + +[owner] +name = "WSO2" + +[database] +server = "192.168.1.1" +ports = [ 8001, 8001, 8002 ] +connection_max = 5000 +enabled = true + +[servers] + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +[[routes]] +name = "health" +path = "/health" + +[[routes]] +name = "metrics" +path = "/metrics" +method = "GET" +` + +func TestReadString(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + if toml == nil { + t.Fatal("Expected non-nil TOML object") + } +} + +func TestGet(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + tests := []struct { + name string + key string + expected any + }{ + {"root string", "title", "Server Configuration"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := toml.Get(tt.key) + if !ok { + t.Fatalf("Get(%q) returned ok=false; want true", tt.key) + } + if got != tt.expected { + t.Errorf("Get(%q) = %v, want %v", tt.key, got, tt.expected) + } + }) + } +} + +func TestGetString(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + tests := []struct { + key string + expected string + }{ + {"owner.name", "WSO2"}, + {"servers.alpha.ip", "10.0.0.1"}, + } + + for _, tt := range tests { + t.Run(tt.key, func(t *testing.T) { + got, ok := toml.GetString(tt.key) + if !ok { + t.Fatalf("GetString(%q) returned ok=false; want true", tt.key) + } + if got != tt.expected { + t.Errorf("GetString(%q) = %q, want %q", tt.key, got, tt.expected) + } + }) + } +} + +func TestGetInt(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + tests := []struct { + key string + expected int64 + }{ + {"database.connection_max", 5000}, + } + + for _, tt := range tests { + t.Run(tt.key, func(t *testing.T) { + got, ok := toml.GetInt(tt.key) + if !ok { + t.Fatalf("GetInt(%q) returned ok=false; want true", tt.key) + } + if got != tt.expected { + t.Errorf("GetInt(%q) = %d, want %d", tt.key, got, tt.expected) + } + }) + } +} + +func TestGetBool(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + tests := []struct { + key string + expected bool + }{ + {"database.enabled", true}, + } + + for _, tt := range tests { + t.Run(tt.key, func(t *testing.T) { + got, ok := toml.GetBool(tt.key) + if !ok { + t.Fatalf("GetBool(%q) returned ok=false; want true", tt.key) + } + if got != tt.expected { + t.Errorf("GetBool(%q) = %v, want %v", tt.key, got, tt.expected) + } + }) + } +} + +func TestGetArray(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + t.Run("database.ports length", func(t *testing.T) { + ports, ok := toml.GetArray("database.ports") + if !ok { + t.Fatalf("GetArray(%q) returned ok=false; want true", "database.ports") + } + if len(ports) != 3 { + t.Errorf("len(GetArray(%q)) = %d, want %d", "database.ports", len(ports), 3) + } + }) +} + +func TestGetTable(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + t.Run("servers.alpha ip", func(t *testing.T) { + serverTable, ok := toml.GetTable("servers.alpha") + if !ok { + t.Fatalf("GetTable(%q) returned ok=false; want true", "servers.alpha") + } + ip, ok := serverTable.GetString("ip") + if !ok { + t.Fatalf("GetString(%q) on table returned ok=false; want true", "ip") + } + if ip != "10.0.0.1" { + t.Errorf("servers.alpha.ip = %q, want %q", ip, "10.0.0.1") + } + }) +} + +func TestGetTables(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + routes, ok := toml.GetTables("routes") + if !ok { + t.Fatalf("GetTables(%q) returned ok=false; want true", "routes") + } + if len(routes) != 2 { + t.Fatalf("len(GetTables(%q)) = %d, want %d", "routes", len(routes), 2) + } + + cases := []struct { + idx int + key string + wantStr string + wantInt int64 + isString bool + }{ + {0, "name", "health", 0, true}, + {1, "path", "/metrics", 0, true}, + } + + for _, c := range cases { + t.Run("route-idx-"+c.key, func(t *testing.T) { + if c.isString { + got, ok := routes[c.idx].GetString(c.key) + if !ok { + t.Fatalf("GetString(%q) on routes[%d] returned ok=false; want true", c.key, c.idx) + } + if got != c.wantStr { + t.Errorf("routes[%d].%s = %q, want %q", c.idx, c.key, got, c.wantStr) + } + return + } + got, ok := routes[c.idx].GetInt(c.key) + if !ok { + t.Fatalf("GetInt(%q) on routes[%d] returned ok=false; want true", c.key, c.idx) + } + if got != c.wantInt { + t.Errorf("routes[%d].%s = %d, want %d", c.idx, c.key, got, c.wantInt) + } + }) + } +} + +func TestToMap(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + m := toml.ToMap() + if m == nil { + t.Fatal("ToMap() returned nil; want non-nil map") + } + if got := m["title"]; got != "Server Configuration" { + t.Errorf("ToMap()[%q] = %v, want %v", "title", got, "Server Configuration") + } +} + +func TestTo(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + t.Run("successful unmarshal", func(t *testing.T) { + type Config struct { + Title string + Owner struct { + Name string + } + Database struct { + Server string + Ports []int + ConnectionMax int `toml:"connection_max"` + Enabled bool + } + } + + var config Config + toml.To(&config) + + if len(toml.Diagnostics()) > 0 { + t.Errorf("To() added unexpected diagnostics: %v", toml.Diagnostics()) + } + + if config.Title != "Server Configuration" { + t.Errorf("config.Title = %q, want %q", config.Title, "Server Configuration") + } + if config.Owner.Name != "WSO2" { + t.Errorf("config.Owner.Name = %q, want %q", config.Owner.Name, "WSO2") + } + if config.Database.ConnectionMax != 5000 { + t.Errorf("config.Database.ConnectionMax = %d, want %d", config.Database.ConnectionMax, 5000) + } + }) + + t.Run("unmarshal type mismatch", func(t *testing.T) { + freshToml, _ := ReadString(sampleToml) + + type BadConfig struct { + Title int + } + + var badConfig BadConfig + freshToml.To(&badConfig) + + diagnostics := freshToml.Diagnostics() + fmt.Println(diagnostics) + if len(diagnostics) == 0 { + t.Error("To() should add diagnostics for type mismatch, but got none") + } + }) +} + +func TestReadFile(t *testing.T) { + toml, err := Read("testdata/sample.toml") + if err != nil { + t.Fatalf("Failed to read TOML file: %v", err) + } + + title, ok := toml.GetString("title") + if !ok { + t.Fatalf("GetString(%q) returned ok=false; want true", "title") + } + if title != "Server Configuration" { + t.Errorf("GetString(%q) = %q, want %q", "title", title, "Server Configuration") + } +} + +func TestReadStream(t *testing.T) { + reader := strings.NewReader(sampleToml) + toml, err := ReadStream(reader) + if err != nil { + t.Fatalf("Failed to read TOML from stream: %v", err) + } + + title, ok := toml.GetString("title") + if !ok { + t.Fatalf("GetString(%q) returned ok=false; want true", "title") + } + if title != "Server Configuration" { + t.Errorf("GetString(%q) = %q, want %q", "title", title, "Server Configuration") + } +} + +func TestDiagnostics(t *testing.T) { + invalidToml := ` + invalid toml syntax here + missing = sign + ` + + toml, err := ReadString(invalidToml) + if err == nil { + t.Error("Expected error for invalid TOML") + } + + if toml == nil { + t.Fatal("Expected non-nil TOML object even with errors") + } + + diagnostics := toml.Diagnostics() + if len(diagnostics) == 0 { + t.Error("Expected diagnostics for invalid TOML") + } +} + +func TestNonExistentKey(t *testing.T) { + toml, err := ReadString(sampleToml) + if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + _, ok := toml.Get("nonexistent.key") + if ok { + t.Error("Expected false for non-existent key") + } +} diff --git a/tomlparser/validator.go b/tomlparser/validator.go new file mode 100644 index 000000000..93de6b610 --- /dev/null +++ b/tomlparser/validator.go @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tomlparser + +import ( + "fmt" +) + +type Validator interface { + Validate(toml *Toml) error +} + +type validatorImpl struct { + schema Schema +} + +func NewValidator(schema Schema) Validator { + return &validatorImpl{ + schema: schema, + } +} + +func (v *validatorImpl) Validate(toml *Toml) error { + if toml == nil { + return fmt.Errorf("toml document is nil") + } + + data := toml.ToMap() + + if err := v.schema.Validate(data); err != nil { + diagnostic := Diagnostic{ + Message: err.Error(), + Severity: "ERROR", + } + toml.diagnostics = append(toml.diagnostics, diagnostic) + return err + } + + return nil +} diff --git a/tomlparser/validator_test.go b/tomlparser/validator_test.go new file mode 100644 index 000000000..339899aa8 --- /dev/null +++ b/tomlparser/validator_test.go @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tomlparser + +import ( + "os" + "strings" + "testing" +) + +func TestSchemaFromPath(t *testing.T) { + schema, err := NewSchemaFromPath("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to load schema from path: %v", err) + } + + if schema == nil { + t.Fatal("Schema should not be nil") + } +} + +func TestSchemaFromString(t *testing.T) { + schemaJSON := `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": {"type": "string"} + } + }` + + schema, err := NewSchemaFromString(schemaJSON) + if err != nil { + t.Fatalf("Failed to create schema from string: %v", err) + } + + if schema == nil { + t.Fatal("Schema should not be nil") + } +} + +func TestSchemaFromFile(t *testing.T) { + file, err := os.Open("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to open schema file: %v", err) + } + defer file.Close() + + schema, err := NewSchemaFromFile(file) + if err != nil { + t.Fatalf("Failed to create schema from file: %v", err) + } + + if schema == nil { + t.Fatal("Schema should not be nil") + } +} + +func TestValidatorWithValidToml(t *testing.T) { + schema, err := NewSchemaFromPath("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to load schema: %v", err) + } + + tomlDoc, err := Read("testdata/ballerina-package.toml") + if err != nil { + t.Fatalf("Failed to read TOML: %v", err) + } + + validator := NewValidator(schema) + err = validator.Validate(tomlDoc) + if err != nil { + t.Errorf("Validation should pass for valid TOML: %v", err) + } + + if len(tomlDoc.Diagnostics()) > 0 { + t.Errorf("Expected no diagnostics, but got %d", len(tomlDoc.Diagnostics())) + } +} + +func TestValidatorWithInvalidToml(t *testing.T) { + schema, err := NewSchemaFromPath("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to load schema: %v", err) + } + + tomlDoc, err := Read("testdata/invalid-sample.toml") + if err != nil { + t.Fatalf("Failed to read TOML: %v", err) + } + + validator := NewValidator(schema) + err = validator.Validate(tomlDoc) + if err == nil { + t.Error("Validation should fail for invalid TOML") + } + + if len(tomlDoc.Diagnostics()) == 0 { + t.Error("Expected diagnostics for validation errors") + } + + diagnostic := tomlDoc.Diagnostics()[0] + if diagnostic.Severity != "ERROR" { + t.Errorf("Expected ERROR severity, got %s", diagnostic.Severity) + } + + if !strings.Contains(diagnostic.Message, "additionalProperties") && + !strings.Contains(diagnostic.Message, "unexpected") { + t.Logf("Diagnostic message: %s", diagnostic.Message) + } +} + +func TestReadWithSchema(t *testing.T) { + schema, err := NewSchemaFromPath("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to load schema: %v", err) + } + + tomlDoc, err := ReadWithSchema("testdata/ballerina-package.toml", schema) + if err != nil { + t.Errorf("ReadWithSchema should succeed for valid TOML: %v", err) + } + + if tomlDoc == nil { + t.Fatal("TOML document should not be nil") + } + + org, ok := tomlDoc.GetString("package.org") + if !ok || org != "foo" { + t.Errorf("Expected org to be 'foo', got %s", org) + } +} + +func TestReadWithSchemaInvalid(t *testing.T) { + schema, err := NewSchemaFromPath("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to load schema: %v", err) + } + + tomlDoc, err := ReadWithSchema("testdata/invalid-sample.toml", schema) + if err == nil { + t.Error("ReadWithSchema should fail for invalid TOML") + } + + if tomlDoc == nil { + t.Fatal("TOML document should not be nil even on validation error") + } + + if len(tomlDoc.Diagnostics()) == 0 { + t.Error("Expected diagnostics for validation errors") + } +} + +func TestReadStringWithSchema(t *testing.T) { + schemaJSON := `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "name": {"type": "string"}, + "version": {"type": "string"} + } + }` + + schema, err := NewSchemaFromString(schemaJSON) + if err != nil { + t.Fatalf("Failed to create schema: %v", err) + } + + tomlContent := ` +name = "test" +version = "1.0.0" +` + + tomlDoc, err := ReadStringWithSchema(tomlContent, schema) + if err != nil { + t.Errorf("ReadStringWithSchema should succeed: %v", err) + } + + if tomlDoc == nil { + t.Fatal("TOML document should not be nil") + } +} + +func TestReadStringWithSchemaInvalid(t *testing.T) { + schemaJSON := `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "name": {"type": "string"} + } + }` + + schema, err := NewSchemaFromString(schemaJSON) + if err != nil { + t.Fatalf("Failed to create schema: %v", err) + } + + tomlContent := ` +name = "test" +unexpected = "field" +` + + tomlDoc, err := ReadStringWithSchema(tomlContent, schema) + if err == nil { + t.Error("ReadStringWithSchema should fail for invalid TOML") + } + + if len(tomlDoc.Diagnostics()) == 0 { + t.Error("Expected diagnostics for validation errors") + } +} + +func TestReadStreamWithSchema(t *testing.T) { + schema, err := NewSchemaFromPath("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to load schema: %v", err) + } + + file, err := os.Open("testdata/ballerina-package.toml") + if err != nil { + t.Fatalf("Failed to open TOML file: %v", err) + } + defer file.Close() + + tomlDoc, err := ReadStreamWithSchema(file, schema) + if err != nil { + t.Errorf("ReadStreamWithSchema should succeed: %v", err) + } + + if tomlDoc == nil { + t.Fatal("TOML document should not be nil") + } +} + +func TestValidateMethod(t *testing.T) { + tomlDoc, err := Read("testdata/ballerina-package.toml") + if err != nil { + t.Fatalf("Failed to read TOML: %v", err) + } + + schema, err := NewSchemaFromPath("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to load schema: %v", err) + } + + err = tomlDoc.Validate(schema) + if err != nil { + t.Errorf("Validate should succeed: %v", err) + } +} + +func TestValidateMethodWithInvalidData(t *testing.T) { + tomlDoc, err := Read("testdata/invalid-sample.toml") + if err != nil { + t.Fatalf("Failed to read TOML: %v", err) + } + + schema, err := NewSchemaFromPath("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to load schema: %v", err) + } + + err = tomlDoc.Validate(schema) + if err == nil { + t.Error("Validate should fail for invalid TOML") + } + + if len(tomlDoc.Diagnostics()) == 0 { + t.Error("Expected diagnostics for validation errors") + } +} + +func TestSchemaWithRequiredFields(t *testing.T) { + schema, err := NewSchemaFromPath("testdata/required-schema.json") + if err != nil { + t.Fatalf("Failed to load schema: %v", err) + } + + tomlDoc, err := Read("testdata/missing-required.toml") + if err != nil { + t.Fatalf("Failed to read TOML: %v", err) + } + + err = tomlDoc.Validate(schema) + if err == nil { + t.Error("Validation should fail when required fields are missing") + } + + if !strings.Contains(err.Error(), "required") && + !strings.Contains(err.Error(), "missing") && + !strings.Contains(err.Error(), "version") { + t.Logf("Error message: %s", err.Error()) + } +} + +func TestSchemaValidationWithTypeError(t *testing.T) { + schemaJSON := `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "count": {"type": "number"} + } + }` + + schema, err := NewSchemaFromString(schemaJSON) + if err != nil { + t.Fatalf("Failed to create schema: %v", err) + } + + tomlContent := `count = "not a number"` + + _, err = ReadStringWithSchema(tomlContent, schema) + if err == nil { + t.Error("Validation should fail for type mismatch") + } +} + +func TestSchemaValidationWithArrays(t *testing.T) { + schemaJSON := `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": {"type": "string"} + } + } + }` + + schema, err := NewSchemaFromString(schemaJSON) + if err != nil { + t.Fatalf("Failed to create schema: %v", err) + } + + tomlContent := `tags = ["tag1", "tag2", "tag3"]` + + tomlDoc, err := ReadStringWithSchema(tomlContent, schema) + if err != nil { + t.Errorf("Validation should succeed for valid array: %v", err) + } + + tags, ok := tomlDoc.GetArray("tags") + if !ok || len(tags) != 3 { + t.Errorf("Expected 3 tags, got %d", len(tags)) + } +} + +func TestSchemaValidationWithNestedObjects(t *testing.T) { + schema, err := NewSchemaFromPath("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to load schema: %v", err) + } + + tomlContent := ` +[package] +org = "myorg" +name = "mypackage" +version = "1.0.0" +` + + tomlDoc, err := ReadStringWithSchema(tomlContent, schema) + if err != nil { + t.Errorf("Validation should succeed for nested object: %v", err) + } + + name, ok := tomlDoc.GetString("package.name") + if !ok || name != "mypackage" { + t.Errorf("Expected package.name to be 'mypackage', got %s", name) + } +} + +func TestValidatorWithNilToml(t *testing.T) { + schema, err := NewSchemaFromPath("testdata/sample-schema.json") + if err != nil { + t.Fatalf("Failed to load schema: %v", err) + } + + validator := NewValidator(schema) + err = validator.Validate(nil) + if err == nil { + t.Error("Validator should return error for nil TOML") + } + + if !strings.Contains(err.Error(), "nil") { + t.Errorf("Error should mention nil, got: %s", err.Error()) + } +} + +func TestSchemaInvalidJSON(t *testing.T) { + invalidJSON := `{"invalid json` + + _, err := NewSchemaFromString(invalidJSON) + if err == nil { + t.Error("Should fail for invalid JSON schema") + } +} From a92bac74326393d324f702b2990a7d35537a72c0 Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Thu, 30 Oct 2025 10:31:19 +0530 Subject: [PATCH 2/8] Add error locations to diagnostics --- tomlparser/toml.go | 31 ++++++++++++++++------ tomlparser/toml_test.go | 57 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/tomlparser/toml.go b/tomlparser/toml.go index c83ada44b..4ec67e778 100644 --- a/tomlparser/toml.go +++ b/tomlparser/toml.go @@ -18,7 +18,10 @@ package tomlparser +// TODO: Currently toml parser gets a single diagnostic at a time. After migrating the parser, should use the same for toml as well. + import ( + "errors" "io" "os" "strings" @@ -90,10 +93,7 @@ func ReadString(content string) (*Toml, error) { } if err != nil { - t.diagnostics = append(t.diagnostics, Diagnostic{ - Message: err.Error(), - Severity: "ERROR", - }) + t.diagnostics = append(t.diagnostics, parseErrorDiagnostic(err)) } return t, err @@ -252,10 +252,7 @@ func (t *Toml) ToMap() map[string]any { func (t *Toml) To(target any) { _, err := toml.Decode(t.content, target) if err != nil { - t.diagnostics = append(t.diagnostics, Diagnostic{ - Message: err.Error(), - Severity: "ERROR", - }) + t.diagnostics = append(t.diagnostics, parseErrorDiagnostic(err)) } } @@ -284,3 +281,21 @@ func (t *Toml) getValueByPath(keys []string) (any, bool) { return current, true } + +func parseErrorDiagnostic(err error) Diagnostic { + diagnostic := Diagnostic{ + Message: err.Error(), + Severity: "ERROR", + } + var parseErr toml.ParseError + if errors.As(err, &parseErr) { + diagnostic.Message = parseErr.Message + diagnostic.Location = &Location{ + StartLine: parseErr.Position.Line, + StartColumn: parseErr.Position.Col, + EndLine: parseErr.Position.Line, + EndColumn: parseErr.Position.Col + parseErr.Position.Len, + } + } + return diagnostic +} diff --git a/tomlparser/toml_test.go b/tomlparser/toml_test.go index 12e26df62..18c57ca4b 100644 --- a/tomlparser/toml_test.go +++ b/tomlparser/toml_test.go @@ -392,3 +392,60 @@ func TestNonExistentKey(t *testing.T) { t.Error("Expected false for non-existent key") } } + +// Consolidated essential location/diagnostics tests +func TestErrorLocation_DuplicateKeys(t *testing.T) { + invalidToml := ` +title = "Test" +title = "Duplicate" +` + + tomlDoc, err := ReadString(invalidToml) + if err == nil { + t.Fatal("Expected error for duplicate keys, got nil") + } + + diags := tomlDoc.Diagnostics() + if len(diags) == 0 { + t.Fatal("Expected diagnostics, got none") + } + + diag := diags[0] + if diag.Location == nil { + t.Fatal("Expected location information, got nil") + } + + // The error should be on line 3 (duplicate title) + if diag.Location.StartLine != 3 { + t.Errorf("Expected StartLine 3, got %d", diag.Location.StartLine) + } + if diag.Location.StartColumn <= 0 { + t.Errorf("Expected positive StartColumn, got %d", diag.Location.StartColumn) + } +} + +func TestErrorLocation_SyntaxError(t *testing.T) { + invalidToml := ` +[section +key = "value" +` + + tomlDoc, err := ReadString(invalidToml) + if err == nil { + t.Fatal("Expected error for invalid syntax, got nil") + } + + diags := tomlDoc.Diagnostics() + if len(diags) == 0 { + t.Fatal("Expected diagnostics, got none") + } + + diag := diags[0] + if diag.Location == nil { + t.Fatal("Expected location information, got nil") + } + // The error should be on line 3 where "key" starts + if diag.Location.StartLine != 3 { + t.Errorf("Expected StartLine 3, got %d", diag.Location.StartLine) + } +} From 1b9d1ae6e9c1b81b5d78619742686e32b039a0fc Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Thu, 30 Oct 2025 10:54:58 +0530 Subject: [PATCH 3/8] Update license header format --- tomlparser/schema.go | 32 +++++++++++++++----------------- tomlparser/toml.go | 32 +++++++++++++++----------------- tomlparser/toml_test.go | 32 +++++++++++++++----------------- tomlparser/validator.go | 32 +++++++++++++++----------------- tomlparser/validator_test.go | 32 +++++++++++++++----------------- 5 files changed, 75 insertions(+), 85 deletions(-) diff --git a/tomlparser/schema.go b/tomlparser/schema.go index c6963ecef..6efd08730 100644 --- a/tomlparser/schema.go +++ b/tomlparser/schema.go @@ -1,20 +1,18 @@ -/* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. package tomlparser diff --git a/tomlparser/toml.go b/tomlparser/toml.go index 4ec67e778..74d3a97c5 100644 --- a/tomlparser/toml.go +++ b/tomlparser/toml.go @@ -1,20 +1,18 @@ -/* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. package tomlparser diff --git a/tomlparser/toml_test.go b/tomlparser/toml_test.go index 18c57ca4b..5dbc5ba9c 100644 --- a/tomlparser/toml_test.go +++ b/tomlparser/toml_test.go @@ -1,20 +1,18 @@ -/* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. package tomlparser diff --git a/tomlparser/validator.go b/tomlparser/validator.go index 93de6b610..51eb069da 100644 --- a/tomlparser/validator.go +++ b/tomlparser/validator.go @@ -1,20 +1,18 @@ -/* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. package tomlparser diff --git a/tomlparser/validator_test.go b/tomlparser/validator_test.go index 339899aa8..600cc0484 100644 --- a/tomlparser/validator_test.go +++ b/tomlparser/validator_test.go @@ -1,20 +1,18 @@ -/* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. package tomlparser From 9ff60532c8fd40da822783db30ad16cb837ebad6 Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Thu, 30 Oct 2025 15:52:58 +0530 Subject: [PATCH 4/8] Extract `readFile` helper function --- tomlparser/toml.go | 16 ++++++++++++---- tomlparser/toml_test.go | 2 -- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tomlparser/toml.go b/tomlparser/toml.go index 74d3a97c5..980daa74d 100644 --- a/tomlparser/toml.go +++ b/tomlparser/toml.go @@ -47,20 +47,28 @@ type Location struct { EndColumn int } -func Read(path string) (*Toml, error) { +func readFile(path string) (string, error) { content, err := os.ReadFile(path) + if err != nil { + return "", err + } + return string(content), nil +} + +func Read(path string) (*Toml, error) { + content, err := readFile(path) if err != nil { return nil, err } - return ReadString(string(content)) + return ReadString(content) } func ReadWithSchema(path string, schema Schema) (*Toml, error) { - content, err := os.ReadFile(path) + content, err := readFile(path) if err != nil { return nil, err } - return ReadStringWithSchema(string(content), schema) + return ReadStringWithSchema(content, schema) } func ReadStream(reader io.Reader) (*Toml, error) { diff --git a/tomlparser/toml_test.go b/tomlparser/toml_test.go index 5dbc5ba9c..7e2c577ef 100644 --- a/tomlparser/toml_test.go +++ b/tomlparser/toml_test.go @@ -17,7 +17,6 @@ package tomlparser import ( - "fmt" "strings" "testing" ) @@ -320,7 +319,6 @@ func TestTo(t *testing.T) { freshToml.To(&badConfig) diagnostics := freshToml.Diagnostics() - fmt.Println(diagnostics) if len(diagnostics) == 0 { t.Error("To() should add diagnostics for type mismatch, but got none") } From 2c5747ca3bcb3ebf0d49fe720b2e8be25846ae10 Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Fri, 31 Oct 2025 10:28:04 +0530 Subject: [PATCH 5/8] Refactor schema loading from path and reader --- tomlparser/schema.go | 37 +++++++++++-------------------------- tomlparser/toml.go | 16 ++++++++++++---- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/tomlparser/schema.go b/tomlparser/schema.go index 6efd08730..1b5512ac9 100644 --- a/tomlparser/schema.go +++ b/tomlparser/schema.go @@ -19,6 +19,7 @@ package tomlparser import ( "encoding/json" "fmt" + "io" "os" "github.com/santhosh-tekuri/jsonschema/v6" @@ -35,16 +36,11 @@ type schemaImpl struct { } func NewSchemaFromPath(path string) (Schema, error) { - compiler := jsonschema.NewCompiler() - - schema, err := compiler.Compile(path) + content, err := readFile(path) if err != nil { - return nil, fmt.Errorf("failed to compile schema from path %s: %w", path, err) + return nil, fmt.Errorf("failed to read schema file %s: %w", path, err) } - - return &schemaImpl{ - compiled: schema, - }, nil + return NewSchemaFromString(content) } func NewSchemaFromString(content string) (Schema, error) { @@ -69,27 +65,16 @@ func NewSchemaFromString(content string) (Schema, error) { }, nil } -func NewSchemaFromFile(file *os.File) (Schema, error) { - compiler := jsonschema.NewCompiler() - - var schemaDoc any - decoder := json.NewDecoder(file) - if err := decoder.Decode(&schemaDoc); err != nil { - return nil, fmt.Errorf("failed to decode schema JSON: %w", err) - } - - if err := compiler.AddResource("schema.json", schemaDoc); err != nil { - return nil, fmt.Errorf("failed to add schema resource: %w", err) - } - - schema, err := compiler.Compile("schema.json") +func NewSchemaFromReader(reader io.Reader) (Schema, error) { + content, err := readFromReader(reader) if err != nil { - return nil, fmt.Errorf("failed to compile schema: %w", err) + return nil, fmt.Errorf("failed to read schema: %w", err) } + return NewSchemaFromString(content) +} - return &schemaImpl{ - compiled: schema, - }, nil +func NewSchemaFromFile(file *os.File) (Schema, error) { + return NewSchemaFromReader(file) } func (s *schemaImpl) Validate(data any) error { diff --git a/tomlparser/toml.go b/tomlparser/toml.go index 980daa74d..c13f54a77 100644 --- a/tomlparser/toml.go +++ b/tomlparser/toml.go @@ -55,6 +55,14 @@ func readFile(path string) (string, error) { return string(content), nil } +func readFromReader(reader io.Reader) (string, error) { + content, err := io.ReadAll(reader) + if err != nil { + return "", err + } + return string(content), nil +} + func Read(path string) (*Toml, error) { content, err := readFile(path) if err != nil { @@ -72,19 +80,19 @@ func ReadWithSchema(path string, schema Schema) (*Toml, error) { } func ReadStream(reader io.Reader) (*Toml, error) { - content, err := io.ReadAll(reader) + content, err := readFromReader(reader) if err != nil { return nil, err } - return ReadString(string(content)) + return ReadString(content) } func ReadStreamWithSchema(reader io.Reader, schema Schema) (*Toml, error) { - content, err := io.ReadAll(reader) + content, err := readFromReader(reader) if err != nil { return nil, err } - return ReadStringWithSchema(string(content), schema) + return ReadStringWithSchema(content, schema) } func ReadString(content string) (*Toml, error) { From 85177268a73853811f0f50c26814b927f77fd925 Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Thu, 13 Nov 2025 11:35:23 +0530 Subject: [PATCH 6/8] Use `fs.FS` interface for file system access --- tomlparser/schema.go | 11 ++++++----- tomlparser/toml.go | 14 ++++++------- tomlparser/toml_test.go | 3 ++- tomlparser/validator_test.go | 38 +++++++++++++++++++----------------- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/tomlparser/schema.go b/tomlparser/schema.go index 1b5512ac9..28a902e64 100644 --- a/tomlparser/schema.go +++ b/tomlparser/schema.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "io/fs" "os" "github.com/santhosh-tekuri/jsonschema/v6" @@ -27,7 +28,7 @@ import ( type Schema interface { Validate(data any) error - FromPath(path string) (Schema, error) + FromPath(fsys fs.FS, path string) (Schema, error) FromString(content string) (Schema, error) } @@ -35,8 +36,8 @@ type schemaImpl struct { compiled *jsonschema.Schema } -func NewSchemaFromPath(path string) (Schema, error) { - content, err := readFile(path) +func NewSchemaFromPath(fsys fs.FS, path string) (Schema, error) { + content, err := readFile(fsys, path) if err != nil { return nil, fmt.Errorf("failed to read schema file %s: %w", path, err) } @@ -84,8 +85,8 @@ func (s *schemaImpl) Validate(data any) error { return nil } -func (s *schemaImpl) FromPath(path string) (Schema, error) { - return NewSchemaFromPath(path) +func (s *schemaImpl) FromPath(fsys fs.FS, path string) (Schema, error) { + return NewSchemaFromPath(fsys, path) } func (s *schemaImpl) FromString(content string) (Schema, error) { diff --git a/tomlparser/toml.go b/tomlparser/toml.go index c13f54a77..b9273b600 100644 --- a/tomlparser/toml.go +++ b/tomlparser/toml.go @@ -21,7 +21,7 @@ package tomlparser import ( "errors" "io" - "os" + "io/fs" "strings" "github.com/BurntSushi/toml" @@ -47,8 +47,8 @@ type Location struct { EndColumn int } -func readFile(path string) (string, error) { - content, err := os.ReadFile(path) +func readFile(fsys fs.FS, path string) (string, error) { + content, err := fs.ReadFile(fsys, path) if err != nil { return "", err } @@ -63,16 +63,16 @@ func readFromReader(reader io.Reader) (string, error) { return string(content), nil } -func Read(path string) (*Toml, error) { - content, err := readFile(path) +func Read(fsys fs.FS, path string) (*Toml, error) { + content, err := readFile(fsys, path) if err != nil { return nil, err } return ReadString(content) } -func ReadWithSchema(path string, schema Schema) (*Toml, error) { - content, err := readFile(path) +func ReadWithSchema(fsys fs.FS, path string, schema Schema) (*Toml, error) { + content, err := readFile(fsys, path) if err != nil { return nil, err } diff --git a/tomlparser/toml_test.go b/tomlparser/toml_test.go index 7e2c577ef..4e5a80bd6 100644 --- a/tomlparser/toml_test.go +++ b/tomlparser/toml_test.go @@ -17,6 +17,7 @@ package tomlparser import ( + "os" "strings" "testing" ) @@ -326,7 +327,7 @@ func TestTo(t *testing.T) { } func TestReadFile(t *testing.T) { - toml, err := Read("testdata/sample.toml") + toml, err := Read(os.DirFS("."), "testdata/sample.toml") if err != nil { t.Fatalf("Failed to read TOML file: %v", err) } diff --git a/tomlparser/validator_test.go b/tomlparser/validator_test.go index 600cc0484..4df7b8e78 100644 --- a/tomlparser/validator_test.go +++ b/tomlparser/validator_test.go @@ -22,8 +22,10 @@ import ( "testing" ) +var fsys = os.DirFS(".") + func TestSchemaFromPath(t *testing.T) { - schema, err := NewSchemaFromPath("testdata/sample-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/sample-schema.json") if err != nil { t.Fatalf("Failed to load schema from path: %v", err) } @@ -70,12 +72,12 @@ func TestSchemaFromFile(t *testing.T) { } func TestValidatorWithValidToml(t *testing.T) { - schema, err := NewSchemaFromPath("testdata/sample-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/sample-schema.json") if err != nil { t.Fatalf("Failed to load schema: %v", err) } - tomlDoc, err := Read("testdata/ballerina-package.toml") + tomlDoc, err := Read(fsys, "testdata/ballerina-package.toml") if err != nil { t.Fatalf("Failed to read TOML: %v", err) } @@ -92,12 +94,12 @@ func TestValidatorWithValidToml(t *testing.T) { } func TestValidatorWithInvalidToml(t *testing.T) { - schema, err := NewSchemaFromPath("testdata/sample-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/sample-schema.json") if err != nil { t.Fatalf("Failed to load schema: %v", err) } - tomlDoc, err := Read("testdata/invalid-sample.toml") + tomlDoc, err := Read(fsys, "testdata/invalid-sample.toml") if err != nil { t.Fatalf("Failed to read TOML: %v", err) } @@ -124,12 +126,12 @@ func TestValidatorWithInvalidToml(t *testing.T) { } func TestReadWithSchema(t *testing.T) { - schema, err := NewSchemaFromPath("testdata/sample-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/sample-schema.json") if err != nil { t.Fatalf("Failed to load schema: %v", err) } - tomlDoc, err := ReadWithSchema("testdata/ballerina-package.toml", schema) + tomlDoc, err := ReadWithSchema(fsys, "testdata/ballerina-package.toml", schema) if err != nil { t.Errorf("ReadWithSchema should succeed for valid TOML: %v", err) } @@ -145,12 +147,12 @@ func TestReadWithSchema(t *testing.T) { } func TestReadWithSchemaInvalid(t *testing.T) { - schema, err := NewSchemaFromPath("testdata/sample-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/sample-schema.json") if err != nil { t.Fatalf("Failed to load schema: %v", err) } - tomlDoc, err := ReadWithSchema("testdata/invalid-sample.toml", schema) + tomlDoc, err := ReadWithSchema(fsys, "testdata/invalid-sample.toml", schema) if err == nil { t.Error("ReadWithSchema should fail for invalid TOML") } @@ -226,7 +228,7 @@ unexpected = "field" } func TestReadStreamWithSchema(t *testing.T) { - schema, err := NewSchemaFromPath("testdata/sample-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/sample-schema.json") if err != nil { t.Fatalf("Failed to load schema: %v", err) } @@ -248,12 +250,12 @@ func TestReadStreamWithSchema(t *testing.T) { } func TestValidateMethod(t *testing.T) { - tomlDoc, err := Read("testdata/ballerina-package.toml") + tomlDoc, err := Read(fsys, "testdata/ballerina-package.toml") if err != nil { t.Fatalf("Failed to read TOML: %v", err) } - schema, err := NewSchemaFromPath("testdata/sample-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/sample-schema.json") if err != nil { t.Fatalf("Failed to load schema: %v", err) } @@ -265,12 +267,12 @@ func TestValidateMethod(t *testing.T) { } func TestValidateMethodWithInvalidData(t *testing.T) { - tomlDoc, err := Read("testdata/invalid-sample.toml") + tomlDoc, err := Read(fsys, "testdata/invalid-sample.toml") if err != nil { t.Fatalf("Failed to read TOML: %v", err) } - schema, err := NewSchemaFromPath("testdata/sample-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/sample-schema.json") if err != nil { t.Fatalf("Failed to load schema: %v", err) } @@ -286,12 +288,12 @@ func TestValidateMethodWithInvalidData(t *testing.T) { } func TestSchemaWithRequiredFields(t *testing.T) { - schema, err := NewSchemaFromPath("testdata/required-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/required-schema.json") if err != nil { t.Fatalf("Failed to load schema: %v", err) } - tomlDoc, err := Read("testdata/missing-required.toml") + tomlDoc, err := Read(fsys, "testdata/missing-required.toml") if err != nil { t.Fatalf("Failed to read TOML: %v", err) } @@ -361,7 +363,7 @@ func TestSchemaValidationWithArrays(t *testing.T) { } func TestSchemaValidationWithNestedObjects(t *testing.T) { - schema, err := NewSchemaFromPath("testdata/sample-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/sample-schema.json") if err != nil { t.Fatalf("Failed to load schema: %v", err) } @@ -385,7 +387,7 @@ version = "1.0.0" } func TestValidatorWithNilToml(t *testing.T) { - schema, err := NewSchemaFromPath("testdata/sample-schema.json") + schema, err := NewSchemaFromPath(fsys, "testdata/sample-schema.json") if err != nil { t.Fatalf("Failed to load schema: %v", err) } From a1adbb93b3221de0e63a8b8d9a09071118a4f573 Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Mon, 17 Nov 2025 14:58:08 +0530 Subject: [PATCH 7/8] Refactor diagnostic severity to use the enum --- tomlparser/schema.go | 7 +++---- tomlparser/toml.go | 12 +++++++----- tomlparser/validator.go | 4 +++- tomlparser/validator_test.go | 4 +++- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/tomlparser/schema.go b/tomlparser/schema.go index 28a902e64..c74d34354 100644 --- a/tomlparser/schema.go +++ b/tomlparser/schema.go @@ -21,7 +21,6 @@ import ( "fmt" "io" "io/fs" - "os" "github.com/santhosh-tekuri/jsonschema/v6" ) @@ -45,13 +44,13 @@ func NewSchemaFromPath(fsys fs.FS, path string) (Schema, error) { } func NewSchemaFromString(content string) (Schema, error) { - compiler := jsonschema.NewCompiler() - var schemaDoc any if err := json.Unmarshal([]byte(content), &schemaDoc); err != nil { return nil, fmt.Errorf("failed to parse schema JSON: %w", err) } + compiler := jsonschema.NewCompiler() + if err := compiler.AddResource("schema.json", schemaDoc); err != nil { return nil, fmt.Errorf("failed to add schema resource: %w", err) } @@ -74,7 +73,7 @@ func NewSchemaFromReader(reader io.Reader) (Schema, error) { return NewSchemaFromString(content) } -func NewSchemaFromFile(file *os.File) (Schema, error) { +func NewSchemaFromFile(file fs.File) (Schema, error) { return NewSchemaFromReader(file) } diff --git a/tomlparser/toml.go b/tomlparser/toml.go index b9273b600..487a33ed1 100644 --- a/tomlparser/toml.go +++ b/tomlparser/toml.go @@ -24,6 +24,8 @@ import ( "io/fs" "strings" + "ballerina-lang-go/tools/diagnostics" + "github.com/BurntSushi/toml" ) @@ -36,7 +38,7 @@ type Toml struct { type Diagnostic struct { Message string - Severity string + Severity diagnostics.DiagnosticSeverity Location *Location } @@ -204,7 +206,7 @@ func (t *Toml) GetTable(dottedKey string) (*Toml, bool) { return &Toml{ rootNode: table, metadata: t.metadata, - diagnostics: make([]Diagnostic, 0), + diagnostics: nil, content: "", }, true } @@ -223,7 +225,7 @@ func (t *Toml) GetTables(dottedKey string) ([]*Toml, bool) { result[i] = &Toml{ rootNode: table, metadata: t.metadata, - diagnostics: make([]Diagnostic, 0), + diagnostics: nil, content: "", } } @@ -238,7 +240,7 @@ func (t *Toml) GetTables(dottedKey string) ([]*Toml, bool) { result = append(result, &Toml{ rootNode: table, metadata: t.metadata, - diagnostics: make([]Diagnostic, 0), + diagnostics: nil, content: "", }) } @@ -299,7 +301,7 @@ func (t *Toml) getValueByPath(keys []string) (any, bool) { func parseErrorDiagnostic(err error) Diagnostic { diagnostic := Diagnostic{ Message: err.Error(), - Severity: "ERROR", + Severity: diagnostics.Error, } var parseErr toml.ParseError if errors.As(err, &parseErr) { diff --git a/tomlparser/validator.go b/tomlparser/validator.go index 51eb069da..277d58257 100644 --- a/tomlparser/validator.go +++ b/tomlparser/validator.go @@ -18,6 +18,8 @@ package tomlparser import ( "fmt" + + "ballerina-lang-go/tools/diagnostics" ) type Validator interface { @@ -44,7 +46,7 @@ func (v *validatorImpl) Validate(toml *Toml) error { if err := v.schema.Validate(data); err != nil { diagnostic := Diagnostic{ Message: err.Error(), - Severity: "ERROR", + Severity: diagnostics.Error, } toml.diagnostics = append(toml.diagnostics, diagnostic) return err diff --git a/tomlparser/validator_test.go b/tomlparser/validator_test.go index 4df7b8e78..c7b2fcef9 100644 --- a/tomlparser/validator_test.go +++ b/tomlparser/validator_test.go @@ -20,6 +20,8 @@ import ( "os" "strings" "testing" + + "ballerina-lang-go/tools/diagnostics" ) var fsys = os.DirFS(".") @@ -115,7 +117,7 @@ func TestValidatorWithInvalidToml(t *testing.T) { } diagnostic := tomlDoc.Diagnostics()[0] - if diagnostic.Severity != "ERROR" { + if diagnostic.Severity != diagnostics.Error { t.Errorf("Expected ERROR severity, got %s", diagnostic.Severity) } From 25e22346c0df60d143610fec8582d6f94dbd4746 Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Mon, 17 Nov 2025 15:01:34 +0530 Subject: [PATCH 8/8] Remove unused `RootNode()` method --- tomlparser/toml.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tomlparser/toml.go b/tomlparser/toml.go index 487a33ed1..38c656a20 100644 --- a/tomlparser/toml.go +++ b/tomlparser/toml.go @@ -257,10 +257,6 @@ func (t *Toml) Diagnostics() []Diagnostic { return t.diagnostics } -func (t *Toml) RootNode() map[string]any { - return t.rootNode -} - func (t *Toml) ToMap() map[string]any { return t.rootNode }