diff --git a/challenge-02/cmd/main.go b/challenge-02/cmd/main.go new file mode 100644 index 0000000..817e784 --- /dev/null +++ b/challenge-02/cmd/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "io" + "os" + + "github.com/priyanshsao/coding-challenges-go/challenge-02/json" + "github.com/sirupsen/logrus" +) + +func main() { + + setLogger() + + path := "./tests/step2/invalid2.json" + file, err := os.Open(path) + if err != nil { + logrus.Error(err) + return + } + defer file.Close() + + fileData, _ := io.ReadAll(file) + + parser := json.NewParser(fileData) + + res, err := parser.Parse() + if err != nil { + logrus.Errorf("unable to parse: %v", err) + return + } + + fmt.Println(res) +} + +func setLogger() { + + // remove unwanted things and enforce colors + logrus.SetFormatter(&logrus.TextFormatter{ + ForceColors: true, + DisableLevelTruncation: true, + }) + + // set log level to debug. + logrus.SetLevel(logrus.DebugLevel) +} diff --git a/challenge-02/diagrams/lexer.mmd b/challenge-02/diagrams/lexer.mmd index 85527a9..dc93b12 100644 --- a/challenge-02/diagrams/lexer.mmd +++ b/challenge-02/diagrams/lexer.mmd @@ -1,51 +1,51 @@ -flowchart TD - - %% base structure - root["NextToken()"] --> sws["SkipWhiteSpace()"] --> case{case} - - %% Characters - bo["{"] - eo["}"] - ba["["] - ea["]"] - ns[":"] - vs[","] - str["string"] - eof["EOF (0)"] - df["default"] - dgom["digit or -"] - lit["literal"] - - %% Functions - rn["ReadNum()"] - rl["ReadLiteral()"] - rs["ReadString()"] - - %% Return values - t_bo["BeginObject Token"] - t_eo["EndObject Token"] - t_ba["BeginArray Token"] - t_ea["EndArray Token"] - t_ns["NameSeparator Token"] - t_vs["ValueSeparator Token"] - t_str["String Token"] - t_num["Number Token"] - t_lit["True/False/Null Token"] - t_eof["EOF"] - t_illegal["Illegal Token"] - - %% Flow - case --> bo --> t_bo - case --> eo --> t_eo - case --> ba --> t_ba - case --> ea --> t_ea - case --> str --> rs --> t_str - case --> ns --> t_ns - case --> vs --> t_vs - case --> eof --> t_eof - - case --> df --> cond{if} - - cond --> dgom --> rn --> t_num - cond --> lit --> rl --> t_lit +flowchart TD + + %% base structure + root["NextToken()"] --> sws["skipWhiteSpace()"] --> case{case} + + %% Characters + bo["{"] + eo["}"] + ba["["] + ea["]"] + ns[":"] + vs[","] + str["string"] + eof["EOF (0)"] + df["default"] + dgom["digit or -"] + lit["literal"] + + %% Functions + rn["readNum()"] + rl["readLiteral()"] + rs["readString()"] + + %% Return values + t_bo["BeginObject Token"] + t_eo["EndObject Token"] + t_ba["BeginArray Token"] + t_ea["EndArray Token"] + t_ns["NameSeparator Token"] + t_vs["ValueSeparator Token"] + t_str["String Token"] + t_num["Number Token"] + t_lit["True/False/Null Token"] + t_eof["EOF"] + t_illegal["Illegal Token"] + + %% Flow + case --> bo --> t_bo + case --> eo --> t_eo + case --> ba --> t_ba + case --> ea --> t_ea + case --> str --> rs --> t_str + case --> ns --> t_ns + case --> vs --> t_vs + case --> eof --> t_eof + + case --> df --> cond{if} + + cond --> dgom --> rn --> t_num + cond --> lit --> rl --> t_lit cond -->|"else"| t_illegal \ No newline at end of file diff --git a/challenge-02/diagrams/parser.mmd b/challenge-02/diagrams/parser.mmd index 3ad10bb..758530d 100644 --- a/challenge-02/diagrams/parser.mmd +++ b/challenge-02/diagrams/parser.mmd @@ -1,24 +1,24 @@ -flowchart TD - - %% Base Structure - root["Parse()"] --> pt["ParseToken()"] --> |"Move()"| cond{if} --> |equals to EOF| R1[Return parsed object] - - %% Functions - f_po["ParseObject()"] - f_pa["ParseArray()"] - - pt --> case{"case(current token)"} - - case --> |LITERAL| R2["return true/false/nil"] - case --> |NUMBER| R3["return float(token.literal)"] - case --> |STRING| R4["return token.literal"] - case --> |BEGIN_OBJECT| f_po - case --> |BEGIN_ARRAY| f_pa - case --> |EOF| R5["return error"] - case --> |ILLEGAL| R8["return error"] - case --> |DEFAULT| R9["return error"] - - f_po --> |"find value of token"| pt - f_po --> |"found EOF"| R6["return object"] - f_pa --> |"find value of token"| pt +flowchart TD + + %% Base Structure + root["Parse()"] --> pt["parseToken()"] --> |"move()"| cond{if} --> |equals to EOF| R1[Return parsed object] + + %% Functions + f_po["parseObject()"] + f_pa["parseArray()"] + + pt --> case{"case(current token)"} + + case --> |LITERAL| R2["return true/false/nil"] + case --> |NUMBER| R3["return float(token.literal)"] + case --> |STRING| R4["return token.literal"] + case --> |BEGIN_OBJECT| f_po + case --> |BEGIN_ARRAY| f_pa + case --> |EOF| R5["return error"] + case --> |ILLEGAL| R8["return error"] + case --> |DEFAULT| R9["return error"] + + f_po --> |"find value of token"| pt + f_po --> |"found EOF"| R6["return object"] + f_pa --> |"find value of token"| pt f_pa --> |"found EOF"| R7["return array"] \ No newline at end of file diff --git a/challenge-02/internal/define/tokens.go b/challenge-02/internal/define/tokens.go new file mode 100644 index 0000000..68aa68d --- /dev/null +++ b/challenge-02/internal/define/tokens.go @@ -0,0 +1,78 @@ +package define + +// TokenType denotes tokens, can be one of this : +// structural, value, special. +type TokenType string + +// Structural tokens +const ( + // BeginArray denotes starting of array. + BeginArray TokenType = "[" + // BeginObject denotes starting of json-object. + BeginObject TokenType = "{" + // EndArray denotes end of an array. + EndArray TokenType = "]" + // EndObject denotes end of json-object. + EndObject TokenType = "}" + // NameSeperator denotes separation between + // json key-value pairs. + NameSeparator TokenType = ":" + // ValueSeperator denotes separation between + // elements in json-object. + ValueSeparator TokenType = "," +) + +// Value tokens +const ( + // String denotes a normal string value. + String TokenType = "string" + // Number denotes numbers : int, float etc. + Number TokenType = "number" + // True denotes true literal. + True TokenType = "true" + // False denotes false literal. + False TokenType = "false" + // Null denotes null value (acc to json-spec). + Null TokenType = "null" +) + +// Special +const ( + // EOF denotes the end of file. + EOF TokenType = "EOF" + // Illegal denotes unkown charcters, + // that are not defined in language grammer. + Illegal TokenType = "ILLEGAL" +) + +// Token represents a token with it's type and value. +type Token struct { + Type TokenType + Literal string +} + +func (tt TokenType) String() string { + + switch tt { + case BeginObject: + return "BeginObject" + case EndObject: + return "EndObject" + case BeginArray: + return "BeginArray" + case EndArray: + return "EndArray" + case NameSeparator: + return "NameSeperator" + case ValueSeparator: + return "ValueSeperator" + case Number: + return "Number" + default: + if tt == True || tt == False || tt == Null { + return "Literal" + } + + return "Unknown" + } +} diff --git a/challenge-02/internal/lexer/define.go b/challenge-02/internal/lexer/define.go new file mode 100644 index 0000000..dc623f1 --- /dev/null +++ b/challenge-02/internal/lexer/define.go @@ -0,0 +1,17 @@ +package lexer + +// Lexer converts input characters into predefined language tokens. +// It stores data such as the actual input, current character, current position +// of lexer etc. +type Lexer struct { + // input stores the input to be processed. + input []byte + // char stores the current character that needs to be processed. + char byte + // position stores the position of character we are at. + position int + // next stores the postion of character next to the current character. + next int + // line stores the line number of current character. + line int +} diff --git a/challenge-02/internal/lexer/lexer.go b/challenge-02/internal/lexer/lexer.go new file mode 100644 index 0000000..388b6de --- /dev/null +++ b/challenge-02/internal/lexer/lexer.go @@ -0,0 +1,154 @@ +package lexer + +import "github.com/priyanshsao/coding-challenges-go/challenge-02/internal/define" + +// New returns a new lexer. +func New(input []byte) *Lexer { + + l := new(Lexer) + l.input = input + l.line = 1 + l.read() + + return l +} + +// NextToken returns token, by processing the current character. +func (l *Lexer) NextToken() define.Token { + + var token define.Token + + l.skipWhiteSpace() + + switch l.char { + case '[': + token = define.Token{Type: define.BeginArray, Literal: string(l.char)} + case ']': + token = define.Token{Type: define.EndArray, Literal: string(l.char)} + case '{': + token = define.Token{Type: define.BeginObject, Literal: string(l.char)} + case '}': + token = define.Token{Type: define.EndObject, Literal: string(l.char)} + case ':': + token = define.Token{Type: define.NameSeparator, Literal: string(l.char)} + case ',': + token = define.Token{Type: define.ValueSeparator, Literal: string(l.char)} + case '"': + token = l.readString() + case 0: + token = define.Token{Type: define.EOF, Literal: ""} + default: + if isDigit(l.char) || l.char == '-' { + token = l.readNum() + } else if isLiteral(l.char) { + token = l.readLiteral() + } else { + token = define.Token{Type: define.Illegal, Literal: string(l.char)} + } + } + + l.read() + + return token +} + +// CurrentLine returns the line number which +// is being processed by the lexer. +func (l *Lexer) CurrentLine() int { + + return l.line +} + +func (l *Lexer) read() { + + if l.next >= len(l.input) { + l.char = 0 + } else { + l.char = l.input[l.next] + } + + l.position = l.next + l.next++ +} + +func (l *Lexer) lookNext() byte { + + if l.next >= len(l.input) { + return 0 + } + + return l.input[l.next] +} + +func (l *Lexer) skipWhiteSpace() { + + for l.char == ' ' || l.char == '\t' || l.char == '\n' || l.char == '\r' { + if l.char == '\n' { + l.line++ + } + + l.read() + } +} + +func (l *Lexer) readString() define.Token { + + start := l.position + 1 + + for { + l.read() + + if l.char == '"' { + return define.Token{Type: define.String, Literal: string(l.input[start:l.position])} + } + if l.char == 0 { + return define.Token{Type: define.Illegal, Literal: string(l.input[start:l.position])} + } + } +} + +func (l *Lexer) readNum() define.Token { + + start := l.position + + for isDigit(l.lookNext()) { + l.read() + } + + if l.lookNext() == '.' { + l.read() + if !isDigit(l.lookNext()) { + return define.Token{Type: define.Illegal, Literal: string(l.input[start : l.position+1])} + } + for isDigit(l.lookNext()) { + l.read() + } + } + + return define.Token{ + Type: define.Number, + Literal: string(l.input[start : l.position+1]), + } +} + +func (l *Lexer) readLiteral() define.Token { + + start := l.position + + for isAlphabet(l.lookNext()) { + l.read() + } + + literal := define.TokenType(l.input[start : l.position+1]) + + switch literal { + case define.True: + return define.Token{Type: define.True, Literal: "true"} + case define.False: + return define.Token{Type: define.False, Literal: "false"} + case define.Null: + return define.Token{Type: define.Null, Literal: "nil"} + default: + return define.Token{Type: define.Illegal, Literal: string(literal)} + } +} diff --git a/challenge-02/lexer_test.go b/challenge-02/internal/lexer/lexer_test.go similarity index 76% rename from challenge-02/lexer_test.go rename to challenge-02/internal/lexer/lexer_test.go index b7aa87e..c267d99 100644 --- a/challenge-02/lexer_test.go +++ b/challenge-02/internal/lexer/lexer_test.go @@ -1,13 +1,15 @@ -package main +package lexer import ( "reflect" "testing" + + "github.com/priyanshsao/coding-challenges-go/challenge-02/internal/define" ) func Test_NewLexer(t *testing.T) { input := []byte(`{"Key":"value"}`) - l := NewLexer(input) + l := New(input) if l.char != '{' { t.Errorf("expected '%c' but got '%c'", input[0], l.char) @@ -68,7 +70,7 @@ func Test_Read(t *testing.T) { next: tt.next, line: tt.line, } - l.Read() + l.read() if l.char != tt.expected.char { t.Errorf("expected '%v' but got '%v'", tt.expected.char, l.char) @@ -106,8 +108,8 @@ func Test_LookNext(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := NewLexer([]byte(tt.input)) - got := l.LookNext() + l := New([]byte(tt.input)) + got := l.lookNext() if got != tt.expected { t.Errorf("expected '%c' but got '%c'", tt.expected, got) @@ -120,31 +122,31 @@ func Test_NextToken(t *testing.T) { type test struct { name string input string - expected []Token + expected []define.Token } tests := []test{ { name: "Should return exact tokens for all base case", input: `[]{}:,"HelLo"12trueF`, - expected: []Token{ - {BeginArray, "["}, - {EndArray, "]"}, - {BeginObject, "{"}, - {EndObject, "}"}, - {NameSeparator, ":"}, - {ValueSeparator, ","}, - {String, "HelLo"}, - {Number, "12"}, - {True, "true"}, - {Illegal, "F"}, + expected: []define.Token{ + {define.BeginArray, "["}, + {define.EndArray, "]"}, + {define.BeginObject, "{"}, + {define.EndObject, "}"}, + {define.NameSeparator, ":"}, + {define.ValueSeparator, ","}, + {define.String, "HelLo"}, + {define.Number, "12"}, + {define.True, "true"}, + {define.Illegal, "F"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := NewLexer([]byte(tt.input)) + l := New([]byte(tt.input)) for i := 0; i < len(tt.expected); i++ { token := l.NextToken() @@ -161,31 +163,32 @@ func Test_ReadString(t *testing.T) { type test struct { name string input string - expected Token + expected define.Token } tests := []test{ { name: "returns token for simple string", input: `"key"`, - expected: Token{String, "key"}, + expected: define.Token{define.String, "key"}, }, { name: "returns token for spaced string with space in literal", input: `" key"`, - expected: Token{String, " key"}, + expected: define.Token{define.String, " key"}, }, { name: "returns Illegal token for unclosed string", input: `"key`, - expected: Token{Illegal, "key"}, + expected: define.Token{define.Illegal, "key"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := NewLexer([]byte(tt.input)) - token := l.ReadString() + l := New([]byte(tt.input)) + + token := l.readString() if token.Type != tt.expected.Type { t.Fatalf("expected `%s` token type got `%s`", tt.expected.Type, token.Type) @@ -202,79 +205,79 @@ func Test_ReadNum(t *testing.T) { type test struct { name string input string - expected Token + expected define.Token } tests := []test{ { name: "Should return token for positive int", input: "9004", - expected: Token{ - Number, - "9004", + expected: define.Token{ + Type: define.Number, + Literal: "9004", }, }, { name: "Should return token for negative int", input: "-236", - expected: Token{ - Number, - "-236", + expected: define.Token{ + Type: define.Number, + Literal: "-236", }, }, { name: "Should return token for negative decimal number", input: "+23", - expected: Token{ - Illegal, - "+", + expected: define.Token{ + Type: define.Illegal, + Literal: "+", }, }, { name: "Should return illegal token for non-number", input: "a", - expected: Token{ - Illegal, - "a", + expected: define.Token{ + Type: define.Illegal, + Literal: "a", }, }, { name: "Should return token upto right position", input: "4-23.6", - expected: Token{ - Number, - "4", + expected: define.Token{ + Type: define.Number, + Literal: "4", }, }, { name: "Should return illegal token for no digit after '.'", input: "423.", - expected: Token{ - Illegal, - "423.", + expected: define.Token{ + Type: define.Illegal, + Literal: "423.", }, }, { name: "Should return illegal token for repititive '.'", input: "423..8", - expected: Token{ - Illegal, - "423.", + expected: define.Token{ + Type: define.Illegal, + Literal: "423.", }, }, { name: "Should return token upto valid digits", input: "4.23.8", - expected: Token{ - Number, - "4.23", + expected: define.Token{ + Type: define.Number, + Literal: "4.23", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := NewLexer([]byte(tt.input)) + l := New([]byte(tt.input)) prevPos := l.position numToken := l.NextToken() @@ -295,39 +298,39 @@ func Test_ReadLiteral(t *testing.T) { type test struct { name string input string - expected Token + expected define.Token } tests := []test{ { name: "Should return token for simple literal", input: "true", - expected: Token{ - True, + expected: define.Token{ + define.True, "true", }, }, { name: "Should return illegal token for capitalized literal", input: "TRUE", - expected: Token{ - Illegal, + expected: define.Token{ + define.Illegal, "T", }, }, { name: "Should return illegal token for mix cased literal", input: "tRUE", - expected: Token{ - Illegal, + expected: define.Token{ + define.Illegal, "t", }, }, { name: "Should return illegal token for wrong literal", input: "tru", - expected: Token{ - Illegal, + expected: define.Token{ + define.Illegal, "tru", }, }, @@ -335,7 +338,7 @@ func Test_ReadLiteral(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := NewLexer([]byte(tt.input)) + l := New([]byte(tt.input)) prevPos := l.position token := l.NextToken() diff --git a/challenge-02/internal/lexer/utils.go b/challenge-02/internal/lexer/utils.go new file mode 100644 index 0000000..2760996 --- /dev/null +++ b/challenge-02/internal/lexer/utils.go @@ -0,0 +1,13 @@ +package lexer + +func isAlphabet(char byte) bool { + return char >= 'a' && char <= 'z' +} + +func isLiteral(char byte) bool { + return char == 't' || char == 'f' || char == 'n' +} + +func isDigit(char byte) bool { + return char >= '0' && char <= '9' +} diff --git a/challenge-02/internal/parser/define.go b/challenge-02/internal/parser/define.go new file mode 100644 index 0000000..6b63018 --- /dev/null +++ b/challenge-02/internal/parser/define.go @@ -0,0 +1,17 @@ +package parser + +import ( + "github.com/priyanshsao/coding-challenges-go/challenge-02/internal/define" + "github.com/priyanshsao/coding-challenges-go/challenge-02/internal/lexer" +) + +// parser converts language tokens into go map. +// It stores a lexer, the current token and next token. +type parser struct { + // lexer stores the lexer. + lexer *lexer.Lexer + // currentToken stores the token being parsed. + currentToken define.Token + // nextToken stores the token that is going to be parsed next. + nextToken define.Token +} diff --git a/challenge-02/errors.go b/challenge-02/internal/parser/errors.go similarity index 96% rename from challenge-02/errors.go rename to challenge-02/internal/parser/errors.go index bc9b3fe..183815a 100644 --- a/challenge-02/errors.go +++ b/challenge-02/internal/parser/errors.go @@ -1,4 +1,4 @@ -package main +package parser import "errors" diff --git a/challenge-02/internal/parser/parser.go b/challenge-02/internal/parser/parser.go new file mode 100644 index 0000000..b475af5 --- /dev/null +++ b/challenge-02/internal/parser/parser.go @@ -0,0 +1,162 @@ +package parser + +import ( + "fmt" + "strconv" + + "github.com/priyanshsao/coding-challenges-go/challenge-02/internal/define" + "github.com/priyanshsao/coding-challenges-go/challenge-02/internal/lexer" +) + +// New returns a new parser that implements the json.Parser interface. +func New(data []byte) *parser { + + p := new(parser) + + p.lexer = lexer.New(data) + p.nextToken = p.lexer.NextToken() + p.move() + + return p +} + +// Parse parses all the tokens, returns parsed values in go map, +// if error returns error. +func (p *parser) Parse() (any, error) { + + parsedObject, err := p.parseToken() + if err != nil { + return nil, err + } + + p.move() + if p.currentToken.Type != define.EOF { + return nil, fmt.Errorf("%w '%s' at line %d, expected %v", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine(), define.EOF) + } + + return parsedObject, nil +} + +func (p *parser) parseToken() (any, error) { + var parsedValue any + var err error + + token := p.currentToken + tokenType := p.currentToken.Type + + switch tokenType { + case define.True: + parsedValue, _ = strconv.ParseBool(token.Literal) + case define.False: + parsedValue, _ = strconv.ParseBool(token.Literal) + case define.Null: + parsedValue = nil + case define.Number: + parsedValue, err = strconv.ParseFloat(token.Literal, 64) + if err != nil { + return nil, err + } + case define.String: + parsedValue = token.Literal + case define.BeginObject: + parsedValue, err = p.parseObject() + if err != nil { + return nil, err + } + case define.BeginArray: + parsedValue, err = p.parseArray() + if err != nil { + return nil, err + } + case define.EOF: + return nil, fmt.Errorf("%w at line %d", ErrUnexpectedEOF, p.lexer.CurrentLine()) + case define.Illegal: + return nil, fmt.Errorf("%w '%s' at line %d", ErrIllegalToken, token.Literal, p.lexer.CurrentLine()) + default: + return nil, fmt.Errorf("%w '%s' at line %d", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine()) + } + + return parsedValue, nil +} + +func (p *parser) parseObject() (any, error) { + + object := make(map[string]any) + + p.move() + if p.currentToken.Type == define.EndObject { + return object, nil + } + + for { + if p.currentToken.Type != define.String { + return nil, fmt.Errorf("%w '%s' at line %d, expected key", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine()) + } + + key := p.currentToken.Literal + + p.move() + if p.currentToken.Type != define.NameSeparator { + return nil, fmt.Errorf("%w %s at line %d, expected %v", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine(), define.NameSeparator) + } + + p.move() + value, err := p.parseToken() + if err != nil { + return nil, err + } + + object[key] = value + + p.move() + if p.currentToken.Type != define.ValueSeparator { + break + } + + p.move() + } + + if p.currentToken.Type != define.EndObject { + return nil, fmt.Errorf("%w '%s' at line %d, expected %v", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine(), define.EndObject) + } + + return object, nil +} + +func (p *parser) parseArray() (any, error) { + + var array []any + + p.move() + if p.currentToken.Type == define.EndArray { + return array, nil + } + + for { + parsedValue, err := p.parseToken() + if err != nil { + return nil, err + } + + array = append(array, parsedValue) + + p.move() + if p.currentToken.Type != define.ValueSeparator { + break + } + + p.move() + } + + if p.currentToken.Type != define.EndArray { + return nil, fmt.Errorf("%w %s at line %d, expected %v", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine(), define.EndArray) + } + + return array, nil +} + +func (p *parser) move() { + + p.currentToken = p.nextToken + p.nextToken = p.lexer.NextToken() +} diff --git a/challenge-02/parser_test.go b/challenge-02/internal/parser/parser_test.go similarity index 84% rename from challenge-02/parser_test.go rename to challenge-02/internal/parser/parser_test.go index bdbf837..0245021 100644 --- a/challenge-02/parser_test.go +++ b/challenge-02/internal/parser/parser_test.go @@ -1,9 +1,11 @@ -package main +package parser import ( "errors" "reflect" "testing" + + "github.com/priyanshsao/coding-challenges-go/challenge-02/internal/define" ) func Test_NewParser(t *testing.T) { @@ -11,39 +13,39 @@ func Test_NewParser(t *testing.T) { type test struct { name string input string - expected Parser + expected parser } tests := []test{ { name: "Returns parser with current and next token", input: "{}", - expected: Parser{ - currentToken: Token{BeginObject, "{"}, - nextToken: Token{EndObject, "}"}, + expected: parser{ + currentToken: define.Token{define.BeginObject, "{"}, + nextToken: define.Token{define.EndObject, "}"}, }, }, { name: "Returns Parser with EOF tokens for empty input", input: "", - expected: Parser{ - currentToken: Token{EOF, ""}, - nextToken: Token{EOF, ""}, + expected: parser{ + currentToken: define.Token{define.EOF, ""}, + nextToken: define.Token{define.EOF, ""}, }, }, { name: "Returns Parser with EOF tokens for input with empty lines", input: "\n\r\n\t ", - expected: Parser{ - currentToken: Token{EOF, ""}, - nextToken: Token{EOF, ""}, + expected: parser{ + currentToken: define.Token{define.EOF, ""}, + nextToken: define.Token{define.EOF, ""}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := NewParser([]byte(tt.input)) + p := New([]byte(tt.input)) pExpected := tt.expected if p.currentToken != pExpected.currentToken { @@ -61,15 +63,15 @@ func Test_Move(t *testing.T) { type test struct { name string input string - expected Parser + expected parser } tests := []test{ { name: "Moves the current Token to next Token", input: "{true}", - expected: Parser{ - currentToken: Token{True, "true"}, + expected: parser{ + currentToken: define.Token{define.True, "true"}, }, }, } @@ -78,8 +80,8 @@ func Test_Move(t *testing.T) { t.Run(tt.name, func(t *testing.T) { - p := NewParser([]byte(tt.input)) - p.Move() + p := New([]byte(tt.input)) + p.move() if p.currentToken != tt.expected.currentToken { t.Errorf("expected '%s'(%s) but got '%s'(%s)", tt.expected.currentToken.Literal, tt.expected.currentToken.Type.String(), p.currentToken.Literal, p.currentToken.Type.String()) @@ -116,8 +118,8 @@ func Test_ParseObject(t *testing.T) { t.Run(tt.name, func(t *testing.T) { - p := NewParser([]byte(tt.input)) - actualObject, err := p.ParseToken() + p := New([]byte(tt.input)) + actualObject, err := p.parseToken() if err != nil { if tt.expectedErr != nil { if !errors.Is(err, tt.expectedErr) { @@ -157,8 +159,8 @@ func Test_ParseArray(t *testing.T) { t.Run(tt.name, func(t *testing.T) { - p := NewParser([]byte(tt.input)) - actualArr, err := p.ParseToken() + p := New([]byte(tt.input)) + actualArr, err := p.parseToken() if err != nil { if tt.expectedErr != nil && !errors.Is(err, tt.expectedErr) { t.Fatalf("expected %v but got %v", tt.expectedErr, err) @@ -202,10 +204,10 @@ func Test_ParseToken(t *testing.T) { t.Run(tt.name, func(t *testing.T) { - p := NewParser([]byte(tt.input)) + p := New([]byte(tt.input)) for { - parsedObject, err := p.ParseToken() + parsedObject, err := p.parseToken() if err != nil { if errors.Is(err, ErrUnexpectedEOF) { break @@ -247,7 +249,7 @@ func Test_Parse(t *testing.T) { t.Run(tt.name, func(t *testing.T) { - p := NewParser([]byte(tt.input)) + p := New([]byte(tt.input)) parsedObject, err := p.Parse() if err != nil { if tt.expectedErrType != nil && !errors.Is(err, tt.expectedErrType) { diff --git a/challenge-02/json/factory.go b/challenge-02/json/factory.go new file mode 100644 index 0000000..6030902 --- /dev/null +++ b/challenge-02/json/factory.go @@ -0,0 +1,11 @@ +package json + +import ( + "github.com/priyanshsao/coding-challenges-go/challenge-02/internal/parser" +) + +// NewParser returns new parser interface. +func NewParser(data []byte) Parser { + + return parser.New(data) +} diff --git a/challenge-02/json/interface.go b/challenge-02/json/interface.go new file mode 100644 index 0000000..5b3c1c7 --- /dev/null +++ b/challenge-02/json/interface.go @@ -0,0 +1,9 @@ +package json + +// Parser represents a parser, which parses the tokens into go map. +type Parser interface { + // Parse parses the tokens from lexer and + // returns go map with json key-value pairs, + // if error returns the error. + Parse() (any, error) +} diff --git a/challenge-02/lexer.go b/challenge-02/lexer.go deleted file mode 100644 index 342f120..0000000 --- a/challenge-02/lexer.go +++ /dev/null @@ -1,151 +0,0 @@ -package main - -type Lexer struct { - input []byte - char byte - position int - next int - line int -} - -func NewLexer(input []byte) *Lexer { - l := &Lexer{input: input, line: 1} - l.Read() - return l -} - -func (l *Lexer) Read() { - if l.next >= len(l.input) { - l.char = 0 - } else { - l.char = l.input[l.next] - } - l.position = l.next - l.next++ -} - -func (l *Lexer) LookNext() byte { - if l.next >= len(l.input) { - return 0 - } - - return l.input[l.next] -} - -func (l *Lexer) NextToken() Token { - var token Token - - l.SkipWhiteSpace() - - switch l.char { - case '[': - token = Token{BeginArray, string(l.char)} - case ']': - token = Token{EndArray, string(l.char)} - case '{': - token = Token{BeginObject, string(l.char)} - case '}': - token = Token{EndObject, string(l.char)} - case ':': - token = Token{NameSeparator, string(l.char)} - case ',': - token = Token{ValueSeparator, string(l.char)} - case '"': - token = l.ReadString() - case 0: - token = Token{EOF, ""} - default: - if isDigit(l.char) || l.char == '-' { - token = l.ReadNum() - } else if isLiteral(l.char) { - token = l.ReadLiteral() - } else { - token = Token{Illegal, string(l.char)} - } - } - - l.Read() - - return token -} - -func (l *Lexer) ReadString() Token { - start := l.position + 1 - - for { - l.Read() - - if l.char == '"' { - return Token{String, string(l.input[start:l.position])} - } - if l.char == 0 { - return Token{Illegal, string(l.input[start:l.position])} - } - } -} - -func (l *Lexer) ReadNum() Token { - start := l.position - - for isDigit(l.LookNext()) { - l.Read() - } - - if l.LookNext() == '.' { - l.Read() - if !isDigit(l.LookNext()) { - return Token{Illegal, string(l.input[start : l.position+1])} - } - for isDigit(l.LookNext()) { - l.Read() - } - } - - return Token{Number, string(l.input[start : l.position+1])} -} - -func (l *Lexer) ReadLiteral() Token { - start := l.position - - for isAlphabet(l.LookNext()) { - l.Read() - } - - literal := tokenType(l.input[start : l.position+1]) - - switch literal { - case True: - return Token{True, "true"} - case False: - return Token{False, "false"} - case Null: - return Token{Null, "nil"} - default: - return Token{Illegal, string(literal)} - } -} - -func isAlphabet(char byte) bool { - return char >= 'a' && char <= 'z' -} - -func isLiteral(char byte) bool { - return char == 't' || char == 'f' || char == 'n' -} - -func isDigit(char byte) bool { - return char >= '0' && char <= '9' -} - -func (l *Lexer) SkipWhiteSpace() { - for l.char == ' ' || l.char == '\t' || l.char == '\n' || l.char == '\r' { - if l.char == '\n' { - l.line++ - } - l.Read() - } -} - -func (l *Lexer) CurrentLine() int { - return l.line -} diff --git a/challenge-02/main.go b/challenge-02/main.go deleted file mode 100644 index e10261e..0000000 --- a/challenge-02/main.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "io" - "os" - "strconv" -) - -type testFile struct { - name string - step int - isValid bool -} - -var test = []testFile{ - {name: "invalid", step: 1, isValid: false}, - {name: "valid", step: 1, isValid: true}, - {name: "invalid", step: 2, isValid: false}, - {name: "invalid2", step: 2, isValid: false}, - {name: "valid", step: 2, isValid: true}, - {name: "valid2", step: 2, isValid: true}, - {name: "invalid", step: 3, isValid: false}, - {name: "valid", step: 3, isValid: true}, - {name: "invalid", step: 4, isValid: false}, - {name: "valid", step: 4, isValid: true}, - {name: "valid2", step: 4, isValid: true}, -} - -func main() { - - for _, t := range test { - fmt.Printf("\n[step-%d](%s)(%s.json):\n", t.step, getCase(t.isValid), t.name) - - fileData, err := getData("./tests/step" + strconv.Itoa(t.step) + "/" + t.name + ".json") - if err != nil { - fmt.Println("[Error]: ", err) - continue - } - - p := NewParser(fileData) - - out, err := p.Parse() - if err != nil { - fmt.Println("[Fail]: ", err) - } else { - fmt.Println("[Pass]: ", out) - } - } -} - -func getData(path string) ([]byte, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - - fileData, err := io.ReadAll(file) - if err != nil { - return nil, err - } - - return fileData, nil -} - -func getCase(b bool) string { - if b { - return "valid case" - } - return "invalid case" -} diff --git a/challenge-02/parser.go b/challenge-02/parser.go deleted file mode 100644 index 0715191..0000000 --- a/challenge-02/parser.go +++ /dev/null @@ -1,165 +0,0 @@ -package main - -import ( - "fmt" - "strconv" -) - -type Parser struct { - lexer *Lexer - currentToken Token - nextToken Token -} - -func NewParser(input []byte) *Parser { - parser := new(Parser) - lexer := NewLexer(input) - - parser.lexer = lexer - parser.nextToken = lexer.NextToken() - parser.Move() - - return parser -} - -func (p *Parser) Move() { - p.currentToken = p.nextToken - p.nextToken = p.lexer.NextToken() -} - -func (p *Parser) Parse() (any, error) { - parsedObject, err := p.ParseToken() - if err != nil { - return nil, err - } - - p.Move() - - if p.currentToken.Type != EOF { - return nil, fmt.Errorf("%w '%s' at line %d, expected %v", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine(), EOF) - } - - return parsedObject, nil -} - -func (p *Parser) ParseToken() (any, error) { - var parsedValue any - var err error - - token := p.currentToken - tokenType := p.currentToken.Type - - switch tokenType { - case True: - parsedValue, _ = strconv.ParseBool(token.Literal) - case False: - parsedValue, _ = strconv.ParseBool(token.Literal) - case Null: - parsedValue = nil - case Number: - parsedValue, err = strconv.ParseFloat(token.Literal, 64) - if err != nil { - return nil, err - } - case String: - parsedValue = token.Literal - case BeginObject: - parsedValue, err = p.ParseObject() - if err != nil { - return nil, err - } - case BeginArray: - parsedValue, err = p.ParseArray() - if err != nil { - return nil, err - } - case EOF: - return nil, fmt.Errorf("%w at line %d", ErrUnexpectedEOF, p.lexer.CurrentLine()) - case Illegal: - return nil, fmt.Errorf("%w '%s' at line %d", ErrIllegalToken, token.Literal, p.lexer.CurrentLine()) - default: - return nil, fmt.Errorf("%w '%s' at line %d", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine()) - } - - return parsedValue, nil -} - -func (p *Parser) ParseObject() (any, error) { - object := make(map[string]any) - - p.Move() - - if p.currentToken.Type == EndObject { - return object, nil - } - - for { - if p.currentToken.Type != String { - return nil, fmt.Errorf("%w '%s' at line %d, expected key", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine()) - } - - key := p.currentToken.Literal - - p.Move() - - if p.currentToken.Type != NameSeparator { - return nil, fmt.Errorf("%w %s at line %d, expected %v", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine(), NameSeparator) - } - - p.Move() - - value, err := p.ParseToken() - if err != nil { - return nil, err - } - - object[key] = value - - p.Move() - - if p.currentToken.Type != ValueSeparator { - break - } - - p.Move() - } - - if p.currentToken.Type != EndObject { - return nil, fmt.Errorf("%w '%s' at line %d, expected %v", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine(), EndObject) - } - - return object, nil -} - -func (p *Parser) ParseArray() (any, error) { - var array []any - - p.Move() - - if p.currentToken.Type == EndArray { - return array, nil - } - - for { - parsedValue, err := p.ParseToken() - if err != nil { - return nil, err - } - - array = append(array, parsedValue) - - p.Move() - - if p.currentToken.Type != ValueSeparator { - break - } - - p.Move() - } - - if p.currentToken.Type != EndArray { - return nil, fmt.Errorf("%w %s at line %d, expected %v", ErrUnknownToken, p.currentToken.Literal, p.lexer.CurrentLine(), EndArray) - } - - return array, nil -} diff --git a/challenge-02/tokens.go b/challenge-02/tokens.go deleted file mode 100644 index 0f5d346..0000000 --- a/challenge-02/tokens.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -type tokenType string - -// Structural tokens -const ( - BeginArray tokenType = "[" - BeginObject tokenType = "{" - EndArray tokenType = "]" - EndObject tokenType = "}" - NameSeparator tokenType = ":" - ValueSeparator tokenType = "," -) - -// Value tokens -const ( - String tokenType = "string" - Number tokenType = "number" - True tokenType = "true" - False tokenType = "false" - Null tokenType = "null" -) - -// Special -const ( - EOF tokenType = "EOF" - Illegal tokenType = "ILLEGAL" -) - -type Token struct { - Type tokenType - Literal string -} - -// For tests to get the string value of token's type -func (tt tokenType) String() string { - - switch tt { - case BeginObject: - return "BeginObject" - case EndObject: - return "EndObject" - case BeginArray: - return "BeginArray" - case EndArray: - return "EndArray" - case NameSeparator: - return "NameSeperator" - case ValueSeparator: - return "ValueSeperator" - case Number: - return "Number" - default: - if tt == True || tt == False || tt == Null { - return "Literal" - } - - return "Unknown" - } -}