- Add merge structures

- Fixes
This commit is contained in:
Alexander Garin 2023-02-13 15:49:37 +03:00
commit 94612c4c47
30 changed files with 1698 additions and 0 deletions

46
.gitignore vendored Normal file
View File

@ -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
!*/

21
LICENSE Normal file
View File

@ -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
README.md Normal file
View File

14
cmd/json2go/main.go Normal file
View File

@ -0,0 +1,14 @@
package main
import (
"githouse.ru/macrox/json2go/cmd"
"log"
)
func init() {
log.SetFlags(0)
}
func main() {
cmd.Execute()
}

114
cmd/root.go Normal file
View File

@ -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)
}
}

22
cmd/version.go Normal file
View File

@ -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)
}

71
pkg/conv/conv.go Normal file
View File

@ -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)
}

27
pkg/conv/conv_test.go Normal file
View File

@ -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"))
}

47
pkg/document/declared.go Normal file
View File

@ -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,
}
}

46
pkg/document/document.go Normal file
View File

@ -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),
}
}

28
pkg/document/field.go Normal file
View File

@ -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, ",")
}

View File

@ -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
}

36
pkg/document/merge.go Normal file
View File

@ -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
}

234
pkg/document/primitives.go Normal file
View File

@ -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}
}

70
pkg/document/struct.go Normal file
View File

@ -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),
}
}

13
pkg/filler/filler.go Normal file
View File

@ -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
}

251
pkg/filler/json.go Normal file
View File

@ -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,
}
}

35
pkg/filler/validation.go Normal file
View File

@ -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
}

188
pkg/scanner/default.go Normal file
View File

@ -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))
}

View File

@ -0,0 +1,7 @@
package scanner
import "testing"
func TestDefaultScanner(t *testing.T) {
testScanner(t, New)
}

10
pkg/scanner/scanner.go Normal file
View File

@ -0,0 +1,10 @@
package scanner
import (
"githouse.ru/macrox/json2go/pkg/token"
)
type Scanner interface {
More() bool
Scan() (token.Token, string, error)
}

View File

@ -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(),
)
}

81
pkg/scanner/standart.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,7 @@
package scanner
import "testing"
func TestStandardScanner(t *testing.T) {
testScanner(t, NewStandard)
}

39
pkg/stack/stack.go Normal file
View File

@ -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
}

48
pkg/stack/stack_test.go Normal file
View File

@ -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("{[( )]}>"))
}

40
pkg/token/token.go Normal file
View File

@ -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)) + ")"
}

11
pkg/utils/path.go Normal file
View File

@ -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))
}

27
pkg/utils/slices.go Normal file
View File

@ -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
}

28
pkg/utils/utils.go Normal file
View File

@ -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]
}