commit
94612c4c47
|
@ -0,0 +1,46 @@
|
|||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
.idea/**/aws.xml
|
||||
.idea/**/contentModel.xml
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
cmake-build-*/
|
||||
.idea/**/mongoSettings.xml
|
||||
*.iws
|
||||
out/
|
||||
.idea_modules/
|
||||
atlassian-ide-plugin.xml
|
||||
.idea/replstate.xml
|
||||
.idea/sonarlint/
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
.idea/httpRequests
|
||||
.idea/caches/build_file_checksums.ser
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
go.work
|
||||
*
|
||||
!/.gitignore
|
||||
!*.go
|
||||
!go.sum
|
||||
!go.mod
|
||||
!README.md
|
||||
!LICENSE
|
||||
!*/
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Alexander Garin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"githouse.ru/macrox/json2go/cmd"
|
||||
"log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/filler"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/document"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/conv"
|
||||
"githouse.ru/macrox/json2go/pkg/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
var (
|
||||
inputs []string
|
||||
output string
|
||||
typeNames []string
|
||||
acronyms []string
|
||||
packageName string
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "json2go",
|
||||
Short: "Generate Go type definitions from JSON",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
doc = document.NewDocument(packageName)
|
||||
err error
|
||||
)
|
||||
// iterate input
|
||||
for index, input := range inputs {
|
||||
var (
|
||||
typeName string
|
||||
data []byte
|
||||
)
|
||||
if index < len(typeNames) {
|
||||
typeName = typeNames[index]
|
||||
} else if len(typeNames) > 0 {
|
||||
typeName = typeNames[len(typeNames)-1]
|
||||
} else {
|
||||
typeName = conv.ToCamelCase(utils.TrimmedFilenameFromPath(input))
|
||||
}
|
||||
if len(input) < 1 {
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
data, err = os.ReadFile(input)
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("failed to read input file", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = filler.NewJsonFiller(data, typeName, acronyms...).Fill(doc)
|
||||
if err != nil {
|
||||
slog.Error("failed add to collection", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
|
||||
doc.Comments = append(
|
||||
doc.Comments,
|
||||
fmt.Sprint("Generated with json2go v", version, " // DON'T CHANGE IT!"),
|
||||
time.Now().Format(time.RFC3339),
|
||||
)
|
||||
|
||||
buffer.WriteString(doc.String())
|
||||
|
||||
// write output
|
||||
if output == "" {
|
||||
_, _ = io.Copy(os.Stdout, buffer)
|
||||
} else {
|
||||
// make sure output directory exists
|
||||
_ = os.MkdirAll(filepath.Dir(output), os.ModePerm)
|
||||
err = os.WriteFile(output, buffer.Bytes(), os.ModePerm)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
slog.Error("failed to write output file", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().StringArrayVarP(&inputs, "input", "i", []string{""}, "input files (default: stdin)")
|
||||
rootCmd.Flags().StringVarP(&output, "output", "o", "", "output file (default: stdout)")
|
||||
rootCmd.Flags().StringVarP(&packageName, "package", "p", "main", "package name")
|
||||
rootCmd.Flags().StringArrayVarP(&typeNames, "type", "t", []string{}, "type name")
|
||||
rootCmd.Flags().StringSliceVarP(&acronyms, "acronyms", "a", nil, "specify acronyms")
|
||||
|
||||
if err := rootCmd.MarkFlagFilename("input", "json"); err != nil {
|
||||
slog.Error("failed to mark input flag filename", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := rootCmd.MarkFlagFilename("output", "go"); err != nil {
|
||||
slog.Error("failed to mark input flag filename", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
)
|
||||
|
||||
const version = "0.0.1"
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number of json2go",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Println("json2go v", version)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionCmd.SetOut(os.Stdout)
|
||||
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package conv
|
||||
|
||||
import (
|
||||
"githouse.ru/macrox/json2go/pkg/utils"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CamelCaseConverter interface {
|
||||
ToCamelCase(s string) string
|
||||
}
|
||||
|
||||
type DefaultCamelCaseConverter struct {
|
||||
allCaps map[string]bool
|
||||
converted map[string]string
|
||||
}
|
||||
|
||||
func NewDefaultCamelCaseConverter(allCaps []string) DefaultCamelCaseConverter {
|
||||
return DefaultCamelCaseConverter{
|
||||
allCaps: utils.SliceToMap(allCaps, func(s string) (string, bool) {
|
||||
return s, true
|
||||
}),
|
||||
converted: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
splitRE = regexp.MustCompile(`_+|-+|\s+`)
|
||||
allCapsRE = regexp.MustCompile(`^[A-Z]{2,}\d*$`)
|
||||
capitalizedRE = regexp.MustCompile(`[A-Z][a-z]+\d*`)
|
||||
ToCamelCase = NewDefaultCamelCaseConverter(nil).ToCamelCase
|
||||
)
|
||||
|
||||
func (c DefaultCamelCaseConverter) ToCamelCase(s string) string {
|
||||
if r, ok := c.converted[s]; ok {
|
||||
return r
|
||||
}
|
||||
|
||||
words := splitRE.Split(s, -1)
|
||||
|
||||
for i, word := range words {
|
||||
if len(word) != 0 {
|
||||
if allCapsRE.MatchString(word) {
|
||||
words[i] = word[:1] + strings.ToLower(word[1:])
|
||||
} else {
|
||||
words[i] = strings.ToUpper(word[:1]) + word[1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r := capitalizedRE.ReplaceAllStringFunc(strings.Join(words, ""), func(s string) string {
|
||||
if word := strings.ToUpper(s); c.allCaps[word] {
|
||||
return word
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
c.converted[s] = r
|
||||
return r
|
||||
}
|
||||
|
||||
var (
|
||||
followNonCapRE = regexp.MustCompile(`([a-z\d])([A-Z])`)
|
||||
followCapRE = regexp.MustCompile(`([A-Z])([A-Z][a-z]+)`)
|
||||
)
|
||||
|
||||
func ToSnakeCase(s string) string {
|
||||
s = followNonCapRE.ReplaceAllString(s, "${1}_$2")
|
||||
s = followCapRE.ReplaceAllString(s, "${1}_$2")
|
||||
return strings.ToLower(s)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package conv
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToCamelCase(t *testing.T) {
|
||||
assert.Equal(t, "", ToCamelCase(""))
|
||||
assert.Equal(t, "T", ToCamelCase("t"))
|
||||
assert.Equal(t, "To", ToCamelCase("to"))
|
||||
assert.Equal(t, "ToCamelCase", ToCamelCase("to camel case "))
|
||||
assert.Equal(t, "ToCamelCase", ToCamelCase("to_camel__case"))
|
||||
assert.Equal(t, "ToCamelCase", ToCamelCase("to-camel--case"))
|
||||
assert.Equal(t, "ToCamelCase", ToCamelCase("to_camel -case"))
|
||||
assert.Equal(t, "JsonToGo", ToCamelCase("JSON to Go"))
|
||||
|
||||
converter := NewDefaultCamelCaseConverter([]string{"ID", "JSON"})
|
||||
assert.Equal(t, "ID", converter.ToCamelCase("id"))
|
||||
assert.Equal(t, "JSON", converter.ToCamelCase("json"))
|
||||
assert.Equal(t, "JSONToGo", converter.ToCamelCase("JSON to Go"))
|
||||
}
|
||||
|
||||
func TestToSnakeCase(t *testing.T) {
|
||||
assert.Equal(t, "", ToSnakeCase(""))
|
||||
assert.Equal(t, "json_to_go", ToSnakeCase("JSONToGo"))
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package document
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/format"
|
||||
)
|
||||
|
||||
type declaredType struct {
|
||||
id string
|
||||
t Type
|
||||
}
|
||||
|
||||
func (d *declaredType) Nullable(v ...bool) Type {
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *declaredType) IsNullable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *declaredType) Id() string {
|
||||
return d.id
|
||||
}
|
||||
|
||||
func (d *declaredType) Type() Type {
|
||||
return d.t
|
||||
}
|
||||
|
||||
func (d *declaredType) String() string {
|
||||
s := fmt.Sprintf("type %s %s", d.id, d.t)
|
||||
r, err := format.Source([]byte(s))
|
||||
if err != nil {
|
||||
return s
|
||||
} else {
|
||||
return string(r)
|
||||
}
|
||||
}
|
||||
|
||||
func NewDeclaredType(id string, t Type) DeclaredType {
|
||||
if s, ok := t.(Struct); ok {
|
||||
t = s.Nullable(false)
|
||||
}
|
||||
return &declaredType{
|
||||
id: id,
|
||||
t: t,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package document
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Document struct {
|
||||
Comments []string
|
||||
Package string
|
||||
Types map[string]DeclaredType
|
||||
}
|
||||
|
||||
func (d *Document) String() string {
|
||||
parts := make([]string, 0, len(d.Types)+2)
|
||||
if len(d.Comments) > 0 {
|
||||
parts = append(parts, strings.TrimSpace(fmt.Sprint("// ", strings.Join(d.Comments, "\n// "))))
|
||||
}
|
||||
parts = append(parts, fmt.Sprint("package ", d.Package))
|
||||
for _, t := range d.Types {
|
||||
parts = append(parts, t.String())
|
||||
}
|
||||
return strings.Join(parts, "\n\n")
|
||||
}
|
||||
|
||||
func (d *Document) PutDeclaredType(t DeclaredType, merge bool) {
|
||||
id := t.Id()
|
||||
if v, ok := d.Types[id]; ok && v != nil && merge {
|
||||
d.Types[id] = mergeDeclaredTypes(v, t)
|
||||
} else {
|
||||
d.Types[id] = t
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Document) GetDeclaredType(id string) (t DeclaredType, ok bool) {
|
||||
t, ok = d.Types[id]
|
||||
return
|
||||
}
|
||||
|
||||
func NewDocument(packageName string, comments ...string) *Document {
|
||||
return &Document{
|
||||
Comments: comments,
|
||||
Package: packageName,
|
||||
Types: make(map[string]DeclaredType),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package document
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Field struct {
|
||||
Name string
|
||||
Type Type
|
||||
|
||||
// Json tag params
|
||||
Key string
|
||||
OmitEmpty bool
|
||||
}
|
||||
|
||||
func (f *Field) String() string {
|
||||
return fmt.Sprintf("%s %s `json:\"%s\"`", f.Name, f.Type, f.Options())
|
||||
}
|
||||
|
||||
func (f *Field) Options() string {
|
||||
options := make([]string, 0, 3)
|
||||
options = append(options, f.Key)
|
||||
if f.OmitEmpty {
|
||||
options = append(options, "omitempty")
|
||||
}
|
||||
return strings.Join(options, ",")
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package document
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Type interface {
|
||||
fmt.Stringer
|
||||
Nullable(v ...bool) Type
|
||||
IsNullable() bool
|
||||
}
|
||||
|
||||
type Any interface {
|
||||
Type
|
||||
}
|
||||
|
||||
type Ref interface {
|
||||
Type
|
||||
Id() string
|
||||
}
|
||||
|
||||
type Int interface {
|
||||
Type
|
||||
}
|
||||
|
||||
type Float interface {
|
||||
Type
|
||||
}
|
||||
|
||||
type Bool interface {
|
||||
Type
|
||||
}
|
||||
|
||||
type String interface {
|
||||
Type
|
||||
}
|
||||
|
||||
type Array interface {
|
||||
Type
|
||||
Element(e ...Type) Type
|
||||
}
|
||||
|
||||
type Map interface {
|
||||
Type
|
||||
Key(k ...Type) Type
|
||||
Value(v ...Type) Type
|
||||
}
|
||||
|
||||
type Struct interface {
|
||||
Type
|
||||
|
||||
Set(field *Field) Struct
|
||||
Get(key string) (*Field, bool)
|
||||
Fields() []*Field
|
||||
Keys() []string
|
||||
Len() int
|
||||
}
|
||||
|
||||
type DeclaredType interface {
|
||||
Type
|
||||
Id() string
|
||||
Type() Type
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package document
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/utils"
|
||||
)
|
||||
|
||||
func mergeStructs(a, b Struct) Struct {
|
||||
for _, field := range b.Fields() {
|
||||
field.OmitEmpty = true
|
||||
if utils.IsType[Struct](field.Type) || utils.IsType[Ref](field.Type) {
|
||||
field.Type.Nullable()
|
||||
}
|
||||
if exists, ok := a.Get(field.Key); ok {
|
||||
if reflect.TypeOf(exists.Type) == reflect.TypeOf(field.Type) {
|
||||
switch field.Type.(type) {
|
||||
case Struct:
|
||||
exists.Type = mergeStructs(exists.Type.(Struct), field.Type.(Struct))
|
||||
}
|
||||
continue
|
||||
}
|
||||
exists.Type = NewAny()
|
||||
continue
|
||||
}
|
||||
a.Set(field)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func mergeDeclaredTypes(a, b DeclaredType) DeclaredType {
|
||||
if utils.AllIsType[Struct](a.Type(), b.Type()) {
|
||||
mergeStructs(a.Type().(Struct), b.Type().(Struct))
|
||||
}
|
||||
return a
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
package document
|
||||
|
||||
import "fmt"
|
||||
|
||||
type anyType struct {
|
||||
}
|
||||
|
||||
func (i *anyType) String() string {
|
||||
return "any"
|
||||
}
|
||||
|
||||
func (i *anyType) Nullable(v ...bool) Type {
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *anyType) IsNullable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type refType struct {
|
||||
id string
|
||||
pointer bool
|
||||
}
|
||||
|
||||
func (r *refType) String() string {
|
||||
if r.pointer {
|
||||
return "*" + r.id
|
||||
} else {
|
||||
return r.id
|
||||
}
|
||||
}
|
||||
|
||||
func (r *refType) Id() string {
|
||||
return r.id
|
||||
}
|
||||
|
||||
func (r *refType) IsNullable() bool {
|
||||
return r.pointer
|
||||
}
|
||||
|
||||
func (r *refType) Nullable(v ...bool) Type {
|
||||
if len(v) > 0 {
|
||||
r.pointer = v[0]
|
||||
} else {
|
||||
r.pointer = true
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type intType struct {
|
||||
pointer bool
|
||||
}
|
||||
|
||||
func (i *intType) String() string {
|
||||
if i.pointer {
|
||||
return "*int"
|
||||
} else {
|
||||
return "int"
|
||||
}
|
||||
}
|
||||
|
||||
func (i *intType) Nullable(v ...bool) Type {
|
||||
if len(v) > 0 {
|
||||
i.pointer = v[0]
|
||||
} else {
|
||||
i.pointer = true
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *intType) IsNullable() bool {
|
||||
return i.pointer
|
||||
}
|
||||
|
||||
type floatType struct {
|
||||
pointer bool
|
||||
}
|
||||
|
||||
func (f *floatType) String() string {
|
||||
if f.pointer {
|
||||
return "*float64"
|
||||
} else {
|
||||
return "float64"
|
||||
}
|
||||
}
|
||||
|
||||
func (f *floatType) Nullable(v ...bool) Type {
|
||||
if len(v) > 0 {
|
||||
f.pointer = v[0]
|
||||
} else {
|
||||
f.pointer = true
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *floatType) IsNullable() bool {
|
||||
return f.pointer
|
||||
}
|
||||
|
||||
type boolType struct {
|
||||
pointer bool
|
||||
}
|
||||
|
||||
func (b *boolType) String() string {
|
||||
if b.pointer {
|
||||
return "*bool"
|
||||
} else {
|
||||
return "bool"
|
||||
}
|
||||
}
|
||||
|
||||
func (b *boolType) Nullable(v ...bool) Type {
|
||||
if len(v) > 0 {
|
||||
b.pointer = v[0]
|
||||
} else {
|
||||
b.pointer = true
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *boolType) IsNullable() bool {
|
||||
return b.pointer
|
||||
}
|
||||
|
||||
type stringType struct {
|
||||
pointer bool
|
||||
}
|
||||
|
||||
func (s *stringType) String() string {
|
||||
if s.pointer {
|
||||
return "*string"
|
||||
} else {
|
||||
return "string"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringType) Nullable(v ...bool) Type {
|
||||
if len(v) > 0 {
|
||||
s.pointer = v[0]
|
||||
} else {
|
||||
s.pointer = true
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *stringType) IsNullable() bool {
|
||||
return s.pointer
|
||||
}
|
||||
|
||||
type arrayType struct {
|
||||
element Type
|
||||
}
|
||||
|
||||
func (a *arrayType) String() string {
|
||||
return fmt.Sprintf("[]%s", a.element)
|
||||
}
|
||||
|
||||
func (a *arrayType) IsNullable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *arrayType) Nullable(v ...bool) Type {
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *arrayType) Element(e ...Type) Type {
|
||||
if len(e) > 0 && e[0] != nil {
|
||||
a.element = e[0]
|
||||
}
|
||||
return a.element
|
||||
}
|
||||
|
||||
type mapType struct {
|
||||
key Type
|
||||
value Type
|
||||
}
|
||||
|
||||
func (m *mapType) String() string {
|
||||
return fmt.Sprintf("map[%s]%s", m.key, m.value)
|
||||
}
|
||||
|
||||
func (m *mapType) IsNullable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *mapType) Nullable(v ...bool) Type {
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *mapType) Key(k ...Type) Type {
|
||||
if len(k) > 0 && k[0] != nil {
|
||||
m.key = k[0]
|
||||
}
|
||||
return m.key
|
||||
}
|
||||
|
||||
func (m *mapType) Value(v ...Type) Type {
|
||||
if len(v) > 0 && v[0] != nil {
|
||||
m.value = v[0]
|
||||
}
|
||||
return m.value
|
||||
}
|
||||
|
||||
func NewAny() Any {
|
||||
return new(anyType)
|
||||
}
|
||||
|
||||
func NewRef(id string) Any {
|
||||
return &refType{id: id}
|
||||
}
|
||||
|
||||
func NewInt() Int {
|
||||
return new(intType)
|
||||
}
|
||||
|
||||
func NewFloat() Float {
|
||||
return new(floatType)
|
||||
}
|
||||
|
||||
func NewBool() Bool {
|
||||
return new(boolType)
|
||||
}
|
||||
|
||||
func NewString() String {
|
||||
return new(stringType)
|
||||
}
|
||||
|
||||
func NewArray(element Type) Array {
|
||||
return &arrayType{element: element}
|
||||
}
|
||||
|
||||
func NewMap(key, value Type) Map {
|
||||
return &mapType{key: key, value: value}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package document
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/utils"
|
||||
)
|
||||
|
||||
type structType struct {
|
||||
fields map[string]*Field
|
||||
pointer bool
|
||||
}
|
||||
|
||||
func (s *structType) String() string {
|
||||
return fmt.Sprintf(
|
||||
`struct{
|
||||
%s
|
||||
}`,
|
||||
strings.Join(
|
||||
utils.SliceTransform(maps.Values(s.fields), func(field *Field, _ int) string {
|
||||
return field.String()
|
||||
}),
|
||||
"\n\t",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *structType) IsNullable() bool {
|
||||
return s.pointer
|
||||
}
|
||||
|
||||
func (s *structType) Nullable(v ...bool) Type {
|
||||
if len(v) > 0 {
|
||||
s.pointer = v[0]
|
||||
} else {
|
||||
s.pointer = true
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *structType) Len() int {
|
||||
return len(s.fields)
|
||||
}
|
||||
|
||||
func (s *structType) Keys() []string {
|
||||
return maps.Keys(s.fields)
|
||||
}
|
||||
|
||||
func (s *structType) Fields() []*Field {
|
||||
return maps.Values(s.fields)
|
||||
}
|
||||
|
||||
func (s *structType) Get(key string) (field *Field, ok bool) {
|
||||
field, ok = s.fields[key]
|
||||
return
|
||||
}
|
||||
|
||||
func (s *structType) Set(field *Field) Struct {
|
||||
s.fields[field.Key] = field
|
||||
return s
|
||||
}
|
||||
|
||||
func NewStruct() Struct {
|
||||
return &structType{
|
||||
fields: make(map[string]*Field),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package filler
|
||||
|
||||
import (
|
||||
"githouse.ru/macrox/json2go/pkg/conv"
|
||||
"githouse.ru/macrox/json2go/pkg/document"
|
||||
"githouse.ru/macrox/json2go/pkg/scanner"
|
||||
)
|
||||
|
||||
type Filler interface {
|
||||
scanner.Scanner
|
||||
conv.CamelCaseConverter
|
||||
Fill(out *document.Document) error
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
package filler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/utils"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/token"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/conv"
|
||||
"githouse.ru/macrox/json2go/pkg/document"
|
||||
"githouse.ru/macrox/json2go/pkg/scanner"
|
||||
)
|
||||
|
||||
type jsonFiller struct {
|
||||
scanner.Scanner
|
||||
conv.CamelCaseConverter
|
||||
|
||||
declaredName string
|
||||
}
|
||||
|
||||
func (f *jsonFiller) deduceStruct(types []document.Type, refOut *document.Document) document.Struct {
|
||||
result, m := document.NewStruct(), make(map[string][]*document.Field)
|
||||
var keys []string
|
||||
for _, t := range types {
|
||||
s := t.(document.Struct)
|
||||
for _, key := range s.Keys() {
|
||||
if _, ok := m[key]; !ok {
|
||||
m[key] = make([]*document.Field, 0, len(types))
|
||||
keys = append(keys, key)
|
||||
}
|
||||
if field, ok := s.Get(key); ok && field != nil {
|
||||
m[key] = append(m[key], field)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, key := range keys {
|
||||
fields := m[key]
|
||||
field := fields[0]
|
||||
field.Type = f.deduce(utils.SliceTransform(fields, func(field *document.Field, _ int) document.Type {
|
||||
return field.Type
|
||||
}), refOut)
|
||||
if fields[0].OmitEmpty = len(fields) != len(types); field.OmitEmpty {
|
||||
field.Type = field.Type.Nullable()
|
||||
}
|
||||
result.Set(field)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (f *jsonFiller) deduceMap(types []document.Type, refOut *document.Document) document.Map {
|
||||
key := f.deduce(utils.SliceTransform(types, func(item document.Type, _ int) document.Type {
|
||||
return item.(document.Map).Key()
|
||||
}), refOut)
|
||||
value := f.deduce(utils.SliceTransform(types, func(item document.Type, _ int) document.Type {
|
||||
return item.(document.Map).Value()
|
||||
}), refOut)
|
||||
return document.NewMap(key, value)
|
||||
}
|
||||
|
||||
func (f *jsonFiller) deduce(types []document.Type, refOut *document.Document) document.Type {
|
||||
n := len(types)
|
||||
types = utils.RemoveType[document.Any](types)
|
||||
nullable := len(types) < n
|
||||
|
||||
// Check types count
|
||||
switch len(types) {
|
||||
case 0:
|
||||
return document.NewAny()
|
||||
case 1:
|
||||
if nullable {
|
||||
return types[0].Nullable()
|
||||
} else {
|
||||
return types[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Check complex
|
||||
switch types[0].(type) {
|
||||
case document.String:
|
||||
if utils.AllIsType[document.String](types...) {
|
||||
return document.NewString().Nullable(nullable)
|
||||
}
|
||||
case document.Int:
|
||||
if utils.AllIsType[document.Int](types...) {
|
||||
return document.NewInt().Nullable(nullable)
|
||||
} else {
|
||||
if everyOk := utils.SliceEveryBy(types, func(item document.Type) bool {
|
||||
return utils.IsType[document.Int](item) || utils.IsType[document.Float](item)
|
||||
}); everyOk {
|
||||
return document.NewFloat().Nullable(nullable)
|
||||
}
|
||||
}
|
||||
case document.Float:
|
||||
if utils.AllIsType[document.Float](types...) {
|
||||
return document.NewFloat().Nullable(nullable)
|
||||
}
|
||||
case document.Bool:
|
||||
if utils.AllIsType[document.Bool](types...) {
|
||||
return document.NewBool().Nullable(nullable)
|
||||
}
|
||||
case document.Array:
|
||||
if utils.AllIsType[document.Array](types...) {
|
||||
for i := range types {
|
||||
types[i] = types[i].(document.Array).Element()
|
||||
}
|
||||
return document.NewArray(f.deduce(types, refOut))
|
||||
}
|
||||
case document.Struct:
|
||||
if utils.AllIsType[document.Struct](types...) {
|
||||
return f.deduceStruct(types, refOut).Nullable(nullable)
|
||||
} else {
|
||||
everyOk := utils.SliceEveryBy(types, func(item document.Type) bool {
|
||||
return utils.IsType[document.Struct](item) || utils.IsType[document.Map](item)
|
||||
})
|
||||
if everyOk {
|
||||
for i := range types {
|
||||
if s, ok := types[i].(document.Struct); ok {
|
||||
types[i] = document.NewMap(document.NewString(), f.deduce(utils.SliceTransform(s.Fields(), func(field *document.Field, _ int) document.Type {
|
||||
return field.Type
|
||||
}), refOut))
|
||||
}
|
||||
}
|
||||
return f.deduceMap(types, refOut)
|
||||
}
|
||||
}
|
||||
case document.Map:
|
||||
if utils.AllIsType[document.Map](types...) {
|
||||
return f.deduceMap(types, refOut)
|
||||
}
|
||||
}
|
||||
|
||||
return document.NewAny()
|
||||
}
|
||||
|
||||
func (f *jsonFiller) scanObjectType(refOut *document.Document) (document.Type, error) {
|
||||
keys, types := make([]string, 0), make([]document.Type, 0)
|
||||
for f.More() {
|
||||
if _, key, err := f.Scan(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
if t, err := f.scanType(refOut); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
types = append(types, t)
|
||||
}
|
||||
}
|
||||
// Scan end token "}"
|
||||
if _, _, err := f.Scan(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return document.NewMap(document.NewString(), document.NewAny()), nil
|
||||
}
|
||||
if !validNames(keys) {
|
||||
var keyType document.Type
|
||||
if validIntegers(keys) {
|
||||
keyType = document.NewInt()
|
||||
} else {
|
||||
keyType = document.NewString()
|
||||
}
|
||||
return document.NewMap(keyType, f.deduce(types, refOut)), nil
|
||||
}
|
||||
s := document.NewStruct()
|
||||
for i, key := range keys {
|
||||
name, omitEmpty := f.ToCamelCase(key), false
|
||||
if refOut != nil {
|
||||
if arr, ok := types[i].(document.Array); ok {
|
||||
element := arr.Element()
|
||||
if _, ok = element.(document.Struct); ok {
|
||||
refOut.PutDeclaredType(document.NewDeclaredType(name, element), true)
|
||||
arr.Element(document.NewRef(name))
|
||||
}
|
||||
omitEmpty = true
|
||||
} else if _, ok = types[i].(document.Struct); ok {
|
||||
refOut.PutDeclaredType(document.NewDeclaredType(name, types[i]), true)
|
||||
types[i] = document.NewRef(name)
|
||||
}
|
||||
}
|
||||
s.Set(&document.Field{
|
||||
Key: key,
|
||||
Name: name,
|
||||
Type: types[i],
|
||||
OmitEmpty: omitEmpty,
|
||||
})
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (f *jsonFiller) scanArrayType(refOut *document.Document) (document.Type, error) {
|
||||
types := make([]document.Type, 0)
|
||||
for f.More() {
|
||||
if t, err := f.scanType(refOut); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
types = append(types, t)
|
||||
}
|
||||
}
|
||||
// Scan end token "]"
|
||||
if _, _, err := f.Scan(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return document.NewArray(f.deduce(types, refOut)), nil
|
||||
}
|
||||
|
||||
func (f *jsonFiller) scanType(refOut *document.Document) (document.Type, error) {
|
||||
t, _, err := f.Scan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch t {
|
||||
case token.LeftBrace:
|
||||
return f.scanObjectType(refOut)
|
||||
case token.LeftBracket:
|
||||
return f.scanArrayType(refOut)
|
||||
case token.Bool:
|
||||
return document.NewBool(), nil
|
||||
case token.Int:
|
||||
return document.NewInt(), nil
|
||||
case token.Float:
|
||||
return document.NewFloat(), nil
|
||||
case token.String:
|
||||
return document.NewString(), nil
|
||||
case token.Null:
|
||||
return document.NewAny(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected token %s", t)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *jsonFiller) Fill(out *document.Document) error {
|
||||
if out == nil {
|
||||
return fmt.Errorf("document is nill")
|
||||
}
|
||||
t, err := f.scanType(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out.PutDeclaredType(document.NewDeclaredType(f.declaredName, t), true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewJsonFiller(data []byte, declaredName string, allCaps ...string) Filler {
|
||||
return &jsonFiller{
|
||||
CamelCaseConverter: conv.NewDefaultCamelCaseConverter(allCaps),
|
||||
Scanner: scanner.NewFromBytes(data),
|
||||
declaredName: declaredName,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package filler
|
||||
|
||||
func validNames(items []string) bool {
|
||||
for _, item := range items {
|
||||
if item == "" {
|
||||
return false
|
||||
}
|
||||
if !(('a' <= item[0] && item[0] <= 'z') || ('A' <= item[0] && item[0] <= 'Z') || item[0] == '_') {
|
||||
return false
|
||||
}
|
||||
for i := 1; i < len(item); i++ {
|
||||
if !(('a' <= item[i] && item[i] <= 'z') || ('A' <= item[i] && item[i] <= 'Z') || ('0' <= item[i] && item[i] <= '9') || item[i] == '_') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validIntegers(items []string) bool {
|
||||
for _, s := range items {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
if !(s[0] == '+' || s[0] == '-' || ('0' <= s[0] && s[0] <= '9')) {
|
||||
return false
|
||||
}
|
||||
for i := 1; i < len(s); i++ {
|
||||
if !('0' <= s[i] && s[i] <= '9') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/stack"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/token"
|
||||
)
|
||||
|
||||
const eof = -1
|
||||
|
||||
type Default struct {
|
||||
source string
|
||||
|
||||
character rune
|
||||
offset int
|
||||
readingOffset int
|
||||
|
||||
stack *stack.Stack[token.Token]
|
||||
}
|
||||
|
||||
func (s *Default) next() {
|
||||
if s.readingOffset >= len(s.source) {
|
||||
s.character = eof
|
||||
return
|
||||
}
|
||||
|
||||
r, size := rune(s.source[s.readingOffset]), 1
|
||||
if r >= utf8.RuneSelf {
|
||||
r, size = utf8.DecodeRuneInString(s.source[s.readingOffset:])
|
||||
}
|
||||
|
||||
s.character = r
|
||||
s.offset = s.readingOffset
|
||||
s.readingOffset += size
|
||||
}
|
||||
|
||||
func (s *Default) peek() byte {
|
||||
if s.readingOffset >= len(s.source) {
|
||||
return 0
|
||||
}
|
||||
return s.source[s.readingOffset]
|
||||
}
|
||||
|
||||
func (s *Default) skipWhitespace() {
|
||||
for s.character == ' ' || s.character == '\n' || s.character == '\r' || s.character == '\t' {
|
||||
s.next()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Default) scanString() (string, error) {
|
||||
s.next()
|
||||
start := s.offset
|
||||
|
||||
for s.character != '"' {
|
||||
switch s.character {
|
||||
case eof:
|
||||
return "", fmt.Errorf("")
|
||||
case '\\':
|
||||
s.next()
|
||||
s.next()
|
||||
default:
|
||||
s.next()
|
||||
}
|
||||
}
|
||||
|
||||
end := s.offset
|
||||
s.next()
|
||||
|
||||
return s.source[start:end], nil
|
||||
}
|
||||
|
||||
func (s *Default) scan(target string) error {
|
||||
if s.source[s.offset:s.offset+len(target)] != target {
|
||||
return fmt.Errorf("fail to scan %s", target)
|
||||
}
|
||||
|
||||
s.readingOffset += len(target)
|
||||
s.next()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Default) More() bool {
|
||||
s.skipWhitespace()
|
||||
if s.character == ',' {
|
||||
s.next()
|
||||
}
|
||||
s.skipWhitespace()
|
||||
|
||||
return s.character != eof && s.character != '}' && s.character != ']'
|
||||
}
|
||||
|
||||
func (s *Default) Scan() (token.Token, string, error) {
|
||||
scanAgain:
|
||||
s.skipWhitespace()
|
||||
|
||||
switch s.character {
|
||||
case '{':
|
||||
s.stack.Push(token.LeftBrace)
|
||||
s.next()
|
||||
return token.LeftBrace, "", nil
|
||||
case ':':
|
||||
s.next()
|
||||
goto scanAgain
|
||||
case '}':
|
||||
if s.stack.IsEmpty() {
|
||||
return token.Null, "", fmt.Errorf("brackets do not match")
|
||||
} else {
|
||||
if s.stack.Top() == token.LeftBrace {
|
||||
s.stack.Pop()
|
||||
} else {
|
||||
return token.Illegal, "", fmt.Errorf("expecting '%s', got '%s", token.LeftBrace, s.stack.Top())
|
||||
}
|
||||
}
|
||||
s.next()
|
||||
return token.RightBrace, "", nil
|
||||
case '[':
|
||||
s.stack.Push(token.LeftBracket)
|
||||
s.next()
|
||||
return token.LeftBracket, "", nil
|
||||
case ']':
|
||||
if s.stack.IsEmpty() {
|
||||
return token.Illegal, "", fmt.Errorf("brackets do not match")
|
||||
} else {
|
||||
if s.stack.Top() == token.LeftBracket {
|
||||
s.stack.Pop()
|
||||
} else {
|
||||
return token.Illegal, "", fmt.Errorf("expecting '%s', got '%s", token.LeftBracket, s.stack.Top())
|
||||
}
|
||||
}
|
||||
s.next()
|
||||
return token.RightBracket, "", nil
|
||||
case '"':
|
||||
literal, err := s.scanString()
|
||||
return token.String, literal, err
|
||||
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
t := token.Int
|
||||
s.next()
|
||||
for '0' <= s.character && s.character <= '9' {
|
||||
s.next()
|
||||
}
|
||||
if s.character == '.' {
|
||||
t = token.Float
|
||||
s.next()
|
||||
for '0' <= s.character && s.character <= '9' {
|
||||
s.next()
|
||||
}
|
||||
}
|
||||
if s.character == 'e' || s.character == 'E' {
|
||||
t = token.Float
|
||||
s.next()
|
||||
if s.character == '+' || s.character == '-' {
|
||||
s.next()
|
||||
}
|
||||
for '0' <= s.character && s.character <= '9' {
|
||||
s.next()
|
||||
}
|
||||
}
|
||||
return t, "", nil
|
||||
case 't':
|
||||
return token.Bool, "", s.scan("true")
|
||||
case 'f':
|
||||
return token.Bool, "", s.scan("false")
|
||||
case 'n':
|
||||
return token.Null, "", s.scan("null")
|
||||
case ',':
|
||||
s.next()
|
||||
goto scanAgain
|
||||
case eof:
|
||||
return token.EOF, "", nil
|
||||
default:
|
||||
return token.Illegal, "", fmt.Errorf("illegal character %c", s.character)
|
||||
}
|
||||
}
|
||||
|
||||
func New(s string) Scanner {
|
||||
scanner := new(Default)
|
||||
scanner.source = s
|
||||
scanner.stack = stack.New[token.Token]()
|
||||
scanner.next()
|
||||
return scanner
|
||||
}
|
||||
|
||||
func NewFromBytes(data []byte) Scanner {
|
||||
return New(string(data))
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package scanner
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDefaultScanner(t *testing.T) {
|
||||
testScanner(t, New)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package scanner
|
||||
|
||||
import (
|
||||
"githouse.ru/macrox/json2go/pkg/token"
|
||||
)
|
||||
|
||||
type Scanner interface {
|
||||
More() bool
|
||||
Scan() (token.Token, string, error)
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package scanner
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/token"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Tokens []token.Token
|
||||
|
||||
func (tokens Tokens) String() string {
|
||||
var b strings.Builder
|
||||
|
||||
if len(tokens) != 0 {
|
||||
b.WriteString(tokens[0].String())
|
||||
}
|
||||
for i := 1; i < len(tokens); i++ {
|
||||
b.WriteByte(',')
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(tokens[i].String())
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func getTokens(scanner Scanner) (tokens []token.Token) {
|
||||
t, _, err := scanner.Scan()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for t != token.EOF {
|
||||
tokens = append(tokens, t)
|
||||
t, _, err = scanner.Scan()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func testScanner(t *testing.T, newScanner func(s string) Scanner) {
|
||||
json := `[
|
||||
{
|
||||
"string": "中文",
|
||||
"int": 123,
|
||||
"float": 1.0,
|
||||
"bool": false,
|
||||
"null": null,
|
||||
"array": [
|
||||
"\\\"", -1, 0, 1, 1.0, 1e3, 1e-3, true, false, null
|
||||
]
|
||||
}
|
||||
]`
|
||||
assert.Equal(
|
||||
t,
|
||||
Tokens([]token.Token{
|
||||
token.LeftBracket,
|
||||
token.LeftBrace,
|
||||
token.String, token.String,
|
||||
token.String, token.Int,
|
||||
token.String, token.Float,
|
||||
token.String, token.Bool,
|
||||
token.String, token.Null,
|
||||
token.String, token.LeftBracket,
|
||||
token.String, token.Int, token.Int, token.Int, token.Float, token.Float, token.Float, token.Bool, token.Bool, token.Null,
|
||||
token.RightBracket,
|
||||
token.RightBrace,
|
||||
token.RightBracket,
|
||||
}).String(),
|
||||
Tokens(getTokens(newScanner(json))).String(),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package scanner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"githouse.ru/macrox/json2go/pkg/token"
|
||||
)
|
||||
|
||||
type Standard struct {
|
||||
*json.Decoder
|
||||
}
|
||||
|
||||
func (s Standard) Scan() (token.Token, string, error) {
|
||||
t, err := s.Decoder.Token()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return token.EOF, "", nil
|
||||
}
|
||||
return token.Illegal, "", err
|
||||
}
|
||||
|
||||
switch x := t.(type) {
|
||||
case json.Delim:
|
||||
switch x {
|
||||
case '{':
|
||||
return token.LeftBrace, "", nil
|
||||
case '}':
|
||||
return token.RightBrace, "", nil
|
||||
case '[':
|
||||
return token.LeftBracket, "", nil
|
||||
case ']':
|
||||
return token.RightBracket, "", nil
|
||||
default:
|
||||
return token.Illegal, "", fmt.Errorf("invalid delim %s", x)
|
||||
}
|
||||
case bool:
|
||||
return token.Bool, "", nil
|
||||
case json.Number:
|
||||
n := x.String()
|
||||
|
||||
if n == "" {
|
||||
return token.Float, "", nil
|
||||
}
|
||||
if !(n[0] == '+' || n[0] == '-' || ('0' <= n[0] && n[0] <= '9')) {
|
||||
return token.Float, "", nil
|
||||
}
|
||||
for i := 1; i < len(n); i++ {
|
||||
if !('0' <= n[i] && n[i] <= '9') {
|
||||
return token.Float, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
return token.Int, "", nil
|
||||
case string:
|
||||
return token.String, "", nil
|
||||
case nil:
|
||||
return token.Null, "", nil
|
||||
default:
|
||||
return token.Illegal, "", fmt.Errorf("unexpected type")
|
||||
}
|
||||
}
|
||||
|
||||
func NewStandard(s string) Scanner {
|
||||
scanner := Standard{
|
||||
Decoder: json.NewDecoder(bytes.NewBufferString(s)),
|
||||
}
|
||||
scanner.UseNumber()
|
||||
return scanner
|
||||
}
|
||||
|
||||
func NewStandardFromBytes(data []byte) Scanner {
|
||||
scanner := Standard{
|
||||
Decoder: json.NewDecoder(bytes.NewBuffer(data)),
|
||||
}
|
||||
scanner.UseNumber()
|
||||
return scanner
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package scanner
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStandardScanner(t *testing.T) {
|
||||
testScanner(t, NewStandard)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Stack[T any] struct {
|
||||
items []T
|
||||
}
|
||||
|
||||
func New[T any]() *Stack[T] {
|
||||
return new(Stack[T])
|
||||
}
|
||||
|
||||
func (s *Stack[T]) String() string {
|
||||
return fmt.Sprint(s.items)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) IsEmpty() bool {
|
||||
return len(s.items) == 0
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Size() int {
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Top() T {
|
||||
return s.items[len(s.items)-1]
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Push(item T) {
|
||||
s.items = append(s.items, item)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Pop() T {
|
||||
item := s.items[len(s.items)-1]
|
||||
s.items = s.items[:len(s.items)-1]
|
||||
return item
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package stack
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func valid(s string) bool {
|
||||
stack := New[rune]()
|
||||
|
||||
for _, c := range s {
|
||||
switch c {
|
||||
case '(', '[', '{', '<':
|
||||
stack.Push(c)
|
||||
case ')':
|
||||
if stack.IsEmpty() || stack.Pop() != '(' {
|
||||
return false
|
||||
}
|
||||
case ']':
|
||||
if stack.IsEmpty() || stack.Pop() != '[' {
|
||||
return false
|
||||
}
|
||||
case '}':
|
||||
if stack.IsEmpty() || stack.Pop() != '{' {
|
||||
return false
|
||||
}
|
||||
case '>':
|
||||
if stack.IsEmpty() || stack.Pop() != '<' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stack.IsEmpty()
|
||||
}
|
||||
|
||||
func TestStack(t *testing.T) {
|
||||
assert.True(t, valid(""))
|
||||
assert.True(t, valid("( )"))
|
||||
assert.True(t, valid("[( )]"))
|
||||
assert.True(t, valid("[( )]{}"))
|
||||
assert.True(t, valid("<[( )]{}>"))
|
||||
|
||||
assert.False(t, valid("("))
|
||||
assert.False(t, valid("([)]"))
|
||||
assert.False(t, valid("[{( )]"))
|
||||
assert.False(t, valid("{[( )]}>"))
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package token
|
||||
|
||||
import "strconv"
|
||||
|
||||
type Token int8
|
||||
|
||||
const (
|
||||
Illegal Token = iota
|
||||
LeftBrace // '{'
|
||||
RightBrace // '}'
|
||||
LeftBracket // '['
|
||||
RightBracket // ']'
|
||||
String
|
||||
Int
|
||||
Float
|
||||
Bool
|
||||
Null
|
||||
EOF
|
||||
)
|
||||
|
||||
var tokens = [...]string{
|
||||
Illegal: "illegal",
|
||||
LeftBrace: "{",
|
||||
RightBrace: "}",
|
||||
LeftBracket: "[",
|
||||
RightBracket: "]",
|
||||
String: "string",
|
||||
Int: "int",
|
||||
Float: "float",
|
||||
Bool: "bool",
|
||||
Null: "null",
|
||||
EOF: "EOF",
|
||||
}
|
||||
|
||||
func (token Token) String() string {
|
||||
if 0 <= token && token < Token(len(tokens)) && tokens[token] != "" {
|
||||
return tokens[token]
|
||||
}
|
||||
return "token(" + strconv.Itoa(int(token)) + ")"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TrimmedFilenameFromPath(p string) string {
|
||||
filename := path.Base(p)
|
||||
return strings.TrimSuffix(filename, path.Ext(filename))
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package utils
|
||||
|
||||
func SliceTransform[T any, R any](collection []T, iteratee func(item T, index int) R) []R {
|
||||
result := make([]R, len(collection))
|
||||
for i, item := range collection {
|
||||
result[i] = iteratee(item, i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func SliceEveryBy[T any](collection []T, predicate func(item T) bool) bool {
|
||||
for _, v := range collection {
|
||||
if !predicate(v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func SliceToMap[T any, K comparable, V any](collection []T, transform func(item T) (K, V)) map[K]V {
|
||||
result := make(map[K]V, len(collection))
|
||||
for _, t := range collection {
|
||||
k, v := transform(t)
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package utils
|
||||
|
||||
func IsType[T any](t any) bool {
|
||||
_, ok := t.(T)
|
||||
return ok
|
||||
}
|
||||
|
||||
func AllIsType[T any, I any](input ...I) bool {
|
||||
for _, item := range input {
|
||||
if !IsType[T](item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func RemoveType[T any, I any](input []I) []I {
|
||||
i := 0
|
||||
for j := 0; j < len(input); j++ {
|
||||
if IsType[T](input[j]) {
|
||||
if i != j {
|
||||
input[i] = input[j]
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return input[:i]
|
||||
}
|
Loading…
Reference in New Issue