From 903337e18405c73033e8da91308807948e238adb Mon Sep 17 00:00:00 2001 From: oabrivard Date: Fri, 1 Dec 2023 22:36:02 +0100 Subject: [PATCH] Added basic linting of parsed JSON --- README.md | 4 +- linter/linter.go | 94 +++++++++++++++++++++++++++++++++++++++++++ linter/linter_test.go | 64 +++++++++++++++++++++++++++++ parser/parser.go | 4 ++ 4 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 linter/linter.go create mode 100644 linter/linter_test.go diff --git a/README.md b/README.md index 519604a..bcfa500 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # gojson -JSON Parser built with the help of ChatGPT 4.0. -Grammar used is https://fullstack.wiki/syntax/rfc8259/index. +JSON Parser built with the help of ChatGPT 4.0. +Grammar used is https://fullstack.wiki/syntax/rfc8259/index. diff --git a/linter/linter.go b/linter/linter.go new file mode 100644 index 0000000..d9379a3 --- /dev/null +++ b/linter/linter.go @@ -0,0 +1,94 @@ +// Package linter provides functionality for linting JSON strings. +package linter + +import ( + "fmt" + "strings" + + "gitea.paas.celticinfo.fr/oabrivard/gojson/lexer" + "gitea.paas.celticinfo.fr/oabrivard/gojson/parser" +) + +// JsonLinter struct holds references to a lexer and a parser for JSON linting. +type JsonLinter struct { + lexer *lexer.Lexer // The lexer to tokenize the input + parser *parser.Parser // The parser to parse the tokenized input +} + +// NewJsonLinter creates and initializes a new JsonLinter with the given input string. +func NewJsonLinter(input string) *JsonLinter { + l := lexer.NewLexer(input) + p := parser.NewParser(l) + return &JsonLinter{lexer: l, parser: p} +} + +// Lint performs the linting process on the input JSON. +// It parses the input and then formats it into a nicely structured JSON string. +func (jl *JsonLinter) Lint() (string, error) { + parsedObject := jl.parser.Parse() + + // If parsing errors are present, return an aggregated error message. + if len(jl.parser.Errors()) > 0 { + return "", fmt.Errorf("parsing errors: %v", jl.parser.Errors()) + } + + // Use the custom formatJSON function to format the parsed JSON object. + formattedJson := formatJSON(parsedObject, "") + return string(formattedJson), nil +} + +// formatJSON formats any JSON value into a nicely indented string. +func formatJSON(obj interface{}, indent string) string { + // Type switch to handle different types of JSON values. + switch v := obj.(type) { + case parser.JsonObject: + return formatObject(v, indent) // Format a JSON object + case parser.JsonArray: + return formatArray(v, indent) // Format a JSON array + case string: + return fmt.Sprintf("\"%s\"", v) // Format a JSON string + case nil: + return "null" // Format a JSON null + case bool: + if v { + return "true" + } + return "false" + default: // For numbers and other types, use default formatting + return fmt.Sprintf("%v", v) + } +} + +// formatObject formats a JSON object into a string with proper indentation. +func formatObject(obj map[string]interface{}, indent string) string { + var result strings.Builder + result.WriteString("{\n") + i := 0 + for k, v := range obj { + // Format each key-value pair in the object. + result.WriteString(indent + " \"" + k + "\": " + formatJSON(v, indent+" ")) + if i < len(obj)-1 { + result.WriteString(",") + } + result.WriteString("\n") + i++ + } + result.WriteString(indent + "}") + return result.String() +} + +// formatArray formats a JSON array into a string with proper indentation. +func formatArray(array []interface{}, indent string) string { + var result strings.Builder + result.WriteString("[\n") + for i, v := range array { + // Format each value in the array. + result.WriteString(indent + " " + formatJSON(v, indent+" ")) + if i < len(array)-1 { + result.WriteString(",") + } + result.WriteString("\n") + } + result.WriteString(indent + "]") + return result.String() +} diff --git a/linter/linter_test.go b/linter/linter_test.go new file mode 100644 index 0000000..265fc7b --- /dev/null +++ b/linter/linter_test.go @@ -0,0 +1,64 @@ +package linter + +import ( + "testing" +) + +func TestLintSimpleObject(t *testing.T) { + input := `{"name": "John", "age": 30, "isStudent": false}` + + jl := NewJsonLinter(input) + linted, err := jl.Lint() + + if err != nil { + t.Fatalf(err.Error()) + } + + expected := "{\n \"name\": \"John\",\n \"age\": 30,\n \"isStudent\": false\n}" + + if linted != expected { + t.Errorf("linted object is not as expected. Got %+v, want %+v", linted, expected) + } +} + +func TestLintComplexObject(t *testing.T) { + input := `{ + "key": "value", + "key-n": 101, + "key-o": { + "inner key": "inner value" + }, + "key-l": ["list value"] + }` + + jl := NewJsonLinter(input) + linted, err := jl.Lint() + + if err != nil { + t.Fatalf(err.Error()) + } + + expected := "{\n \"key\": \"value\",\n \"key-n\": 101,\n \"key-o\": {\n \"inner key\": \"inner value\"\n },\n \"key-l\": [\n \"list value\"\n ]\n}" + + if linted != expected { + t.Errorf("linted object is not as expected. Got %+v, want %+v", linted, expected) + } +} + +func TestLintInvalidJson(t *testing.T) { + input := `{ + "key": "value", + "key-n": 101, + "key-o": { + "inner key": "inner value" + }, + "key-l": ['list value'] + }` + + jl := NewJsonLinter(input) + _, err := jl.Lint() + + if err == nil { + t.Errorf("Expected error(s) during linting") + } +} diff --git a/parser/parser.go b/parser/parser.go index 72f63ca..602fdaa 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -220,6 +220,10 @@ func (p *Parser) expectPeek(t token.TokenType) bool { } } +func (p *Parser) Errors() []string { + return p.errors +} + // curTokenIs checks if the current token is of a specific type. func (p *Parser) curTokenIs(t token.TokenType) bool { return p.curToken.Type == t