├── README.md ├── cmd └── json2struct │ └── main.go ├── docs ├── app.css ├── google0b1ca8f8ab67b7d2.html ├── index.html ├── playground.js └── playground.js.map ├── go.mod ├── go.sum ├── json2struct.go ├── json2struct_test.go └── playground ├── Makefile └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # json2struct 2 | 3 | Translate to Golang struct from json 4 | 5 | ## Playground 6 | 7 | Try playground [https://yudppp.github.io/json2struct](https://yudppp.github.io/json2struct) 8 | 9 | ## CLI 10 | 11 | ### Install 12 | 13 | ``` 14 | $ go get github.com/yudppp/json2struct/cmd/json2struct 15 | ``` 16 | 17 | ### How to use 18 | 19 | ``` 20 | $ echo '{"url": "http://blog.yudppp.com", "text": "Hello:)", "status": 1, "categories": [{"name": "k8s"}]}' | json2struct -name=blog 21 | type Blog struct { 22 | Categories []BlogCategory `json:"categories"` 23 | Status int `json:"status"` 24 | Text string `json:"text"` 25 | URL string `json:"url"` 26 | } 27 | 28 | type BlogCategory struct { 29 | Name string `json:"name"` 30 | } 31 | ``` 32 | 33 | #### options 34 | 35 | | option | description | 36 | |:-----------|:-----------| 37 | | name | Set struct name (default "data") | 38 | | prefix | Set struct name prefix | 39 | | suffix | Set struct name suffix | 40 | | short | Set short struct name mode | 41 | | local | Use local struct mode | 42 | | omitempty | Set omitempty mode | 43 | | example | Use example tag (https://github.com/yudppp/structs)| 44 | -------------------------------------------------------------------------------- /cmd/json2struct/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/yudppp/json2struct" 10 | ) 11 | 12 | var ( 13 | debug = flag.Bool("debug", false, "Set debug mode") 14 | omitempty = flag.Bool("omitempty", false, "Set omitempty mode") 15 | short = flag.Bool("short", false, "Set short struct name mode") 16 | local = flag.Bool("local", false, "Use local struct mode") 17 | example = flag.Bool("example", false, "Use example tag mode") 18 | prefix = flag.String("prefix", "", "Set struct name prefix") 19 | suffix = flag.String("suffix", "", "Set struct name suffix") 20 | name = flag.String("name", json2struct.DefaultStructName, "Set struct name") 21 | ) 22 | 23 | func main() { 24 | flag.Parse() 25 | json2struct.SetDebug(*debug) 26 | opt := json2struct.Options{ 27 | UseOmitempty: *omitempty, 28 | UseShortStruct: *short, 29 | UseLocal: *local, 30 | UseExample: *example, 31 | Prefix: *prefix, 32 | Suffix: *suffix, 33 | Name: strings.ToLower(*name), 34 | } 35 | parsed, err := json2struct.Parse(os.Stdin, opt) 36 | if err != nil { 37 | panic(err) 38 | } 39 | fmt.Println(parsed) 40 | } 41 | -------------------------------------------------------------------------------- /docs/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: Consolas, 'Courier New', Courier, Monaco, monospace; 4 | } 5 | 6 | label { 7 | font-weight: 200; 8 | } 9 | 10 | pre { 11 | margin: 0; 12 | } 13 | 14 | code { 15 | overflow-x: scroll; 16 | } 17 | 18 | pre, code { 19 | font-family: Consolas, 'Courier New', Courier, Monaco, monospace; 20 | line-height: 1.2; 21 | } 22 | 23 | textarea { 24 | height: 100px; 25 | } 26 | 27 | .header { 28 | padding: 16px 32px; 29 | background-color: #0ead9e; 30 | color: #fff; 31 | } 32 | 33 | .footer { 34 | height: 32px; 35 | padding: 0 32px; 36 | border-top: 1px solid #ccc; 37 | } 38 | 39 | .wrapper { 40 | padding: 0 16px; 41 | } 42 | 43 | .col { 44 | width: calc(100vw - 64px); 45 | min-width: 400px; 46 | padding: 16px; 47 | } 48 | 49 | .u-full-width { 50 | max-width: 100%; 51 | } 52 | 53 | @media all and (min-width: 900px) { 54 | .wrapper { 55 | display: flex; 56 | max-width: 1280px; 57 | margin: 0 auto; 58 | } 59 | .col { 60 | min-height: calc(100vh - 56px - 64px); 61 | } 62 | } -------------------------------------------------------------------------------- /docs/google0b1ca8f8ab67b7d2.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google0b1ca8f8ab67b7d2.html -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yudppp/json2struct 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/c9s/inflect v0.0.0-20130402162822-006c50878f3f 7 | github.com/go-openapi/swag v0.19.15 8 | github.com/hexops/vecty v0.6.0 9 | github.com/stretchr/testify v1.7.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/c9s/inflect v0.0.0-20130402162822-006c50878f3f h1:sfSR+DQofxa5poubse9++Zv6wE70rAQ+IZQThxhVHxU= 2 | github.com/c9s/inflect v0.0.0-20130402162822-006c50878f3f/go.mod h1:IyAzlY1qfFweht0xujIYk1aO4Ele06nfXJ+E94dmGIw= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= 8 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 9 | github.com/hexops/vecty v0.6.0 h1:iiHfDOLEJufGy/hfPGzOTPkZe6rCszElYmUSzRQqK1w= 10 | github.com/hexops/vecty v0.6.0/go.mod h1:hVOPHAhrkXTf/9fl31Bpn2QvkW2ZOUZ0I3b3cohwCpI= 11 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 12 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 13 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 14 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 15 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 16 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 17 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 18 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 19 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 20 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 24 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 25 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 26 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 29 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 30 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 31 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 32 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 35 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /json2struct.go: -------------------------------------------------------------------------------- 1 | package json2struct 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "go/format" 7 | "io" 8 | "log" 9 | "reflect" 10 | "sort" 11 | "strings" 12 | 13 | "github.com/c9s/inflect" 14 | "github.com/go-openapi/swag" 15 | ) 16 | 17 | type ObjectType uint 18 | 19 | const ( 20 | Invalid ObjectType = iota 21 | Value 22 | Hash 23 | Array 24 | ) 25 | 26 | const ( 27 | rootID = "$" 28 | DefaultStructName = "data" 29 | ) 30 | 31 | var debugMode bool 32 | var option Options 33 | 34 | func SetDebug(v bool) { 35 | debugMode = v 36 | } 37 | 38 | type Options struct { 39 | UseOmitempty bool 40 | UseShortStruct bool 41 | UseLocal bool 42 | UseExample bool 43 | Name string 44 | Prefix string 45 | Suffix string 46 | } 47 | 48 | func Parse(reader io.Reader, opt Options) (string, error) { 49 | var input interface{} 50 | if err := json.NewDecoder(reader).Decode(&input); err != nil { 51 | return "", err 52 | } 53 | if opt.Name == "" { 54 | opt.Name = DefaultStructName 55 | } 56 | opt.Prefix = swag.ToGoName(opt.Prefix) 57 | opt.Suffix = swag.ToGoName(opt.Suffix) 58 | option = opt 59 | walker := NewWalker(input) 60 | walker.start() 61 | if debugMode { 62 | b, _ := json.MarshalIndent(walker.structure, "", " ") 63 | walker.logln(string(b)) 64 | } 65 | return walker.output(), nil 66 | } 67 | 68 | type Walker struct { 69 | root interface{} 70 | nest int 71 | structure *Structure 72 | } 73 | 74 | func NewWalker(root interface{}) *Walker { 75 | return &Walker{ 76 | root: root, 77 | } 78 | } 79 | 80 | func (w *Walker) logln(a ...interface{}) { 81 | if debugMode { 82 | prefix := "" 83 | if w.nest > 1 { 84 | prefix = strings.Repeat("\t", w.nest-1) 85 | } 86 | a = append([]interface{}{prefix}, a...) 87 | log.Println(a...) 88 | } 89 | } 90 | 91 | func (w *Walker) start() { 92 | w.walk(rootID, option.Name, w.root, nil) 93 | } 94 | 95 | func (w *Walker) output() string { 96 | return strings.Join(w.structure.Output(), "\n\n") 97 | } 98 | 99 | func (w *Walker) walk(spath, name string, data interface{}, parent *Structure) { 100 | if debugMode { 101 | w.nest++ 102 | defer func() { 103 | w.nest-- 104 | }() 105 | } 106 | 107 | if name != "" { 108 | spath = fmt.Sprintf("%s.%s", spath, name) 109 | } 110 | 111 | switch getType(data) { 112 | case Value: 113 | v := reflect.ValueOf(data) 114 | kind := v.Kind() 115 | if kind == reflect.Float64 { 116 | kind = getNumberKind(v.Float()) 117 | } 118 | w.logln(name, kind) 119 | parent.AddPropety(name, kind, v.Interface(), nil) 120 | case Array: 121 | spath = fmt.Sprintf("%s[]", spath) 122 | w.logln(name) 123 | w.logln("[") 124 | list, _ := data.([]interface{}) 125 | item := inflect.Singularize(name) 126 | current := NewStructure(spath, item) 127 | for _, val := range list { 128 | w.walk(spath, item, val, current) 129 | } 130 | if parent == nil { 131 | w.structure = current 132 | } else { 133 | parent.AddPropety(name, reflect.Array, list, current) 134 | } 135 | w.logln("]") 136 | case Hash: 137 | current := NewStructure(spath, name) 138 | w.logln(name) 139 | w.logln("{") 140 | h, _ := data.(map[string]interface{}) 141 | for key, val := range h { 142 | w.walk(spath, key, val, current) 143 | } 144 | w.logln("}") 145 | if parent == nil { 146 | w.structure = current 147 | } else { 148 | parent.AddPropety(name, reflect.Map, nil, current) 149 | } 150 | case Invalid: 151 | parent.AddPropety(name, reflect.Interface, nil, nil) 152 | } 153 | return 154 | } 155 | 156 | type Structure struct { 157 | ID string 158 | Name string 159 | Props Props 160 | } 161 | 162 | type Props []Propety 163 | 164 | type Propety struct { 165 | Name string 166 | Kind reflect.Kind 167 | Value interface{} 168 | Refs *Structure `json:",omitempty"` 169 | } 170 | 171 | func NewStructure(spath, name string) *Structure { 172 | if !option.UseShortStruct { 173 | name = SpathToName(spath, name) 174 | } 175 | name = fmt.Sprintf("%s%s%s", option.Prefix, swag.ToGoName(name), option.Suffix) 176 | if option.UseLocal { 177 | name = swag.ToVarName(name) 178 | } 179 | return &Structure{ 180 | ID: spath, 181 | Name: name, 182 | Props: make([]Propety, 0, 8), 183 | } 184 | } 185 | 186 | func SpathToName(spath, name string) string { 187 | args := strings.Split(spath, ".") 188 | result := make([]string, 0, len(args)) 189 | for _, v := range args { 190 | if v == rootID { 191 | continue 192 | } 193 | if strings.HasSuffix(v, "[]") { 194 | continue 195 | } 196 | result = append(result, swag.ToGoName(v)) 197 | } 198 | if strings.HasSuffix(spath, "[]") { 199 | result = append(result, swag.ToGoName(name)) 200 | } 201 | return strings.Join(result, "") 202 | } 203 | 204 | func (s *Structure) AddPropety(name string, kind reflect.Kind, val interface{}, refs *Structure) { 205 | for i, prop := range s.Props { 206 | if prop.Name != name { 207 | continue 208 | } 209 | // float64 > int 210 | if prop.Kind == reflect.Float64 && kind == reflect.Int || 211 | prop.Kind == reflect.Int && kind == reflect.Float64 { 212 | s.Props[i] = Propety{Name: name, Kind: reflect.Float64} 213 | return 214 | } 215 | // other kinds -> interface 216 | if prop.Kind != kind { 217 | s.Props[i] = Propety{Name: name, Kind: reflect.Interface} 218 | return 219 | } 220 | // merge map pops 221 | if kind == reflect.Map { 222 | if refs == nil || prop.Refs == nil { 223 | return 224 | } 225 | for _, p := range refs.Props { 226 | prop.Refs.AddPropety(p.Name, p.Kind, val, p.Refs) 227 | } 228 | } 229 | return 230 | } 231 | prop := Propety{Name: name, Kind: kind, Value: val} 232 | if refs != nil { 233 | prop.Refs = refs 234 | } 235 | s.Props = append(s.Props, prop) 236 | sort.Sort(s.Props) 237 | } 238 | 239 | func (s *Structure) Output() []string { 240 | refs := s.Refs() 241 | result := make([]string, 0, 8) 242 | if !strings.HasSuffix(s.ID, "[]") { 243 | result = append(result, s.String()) 244 | } 245 | for _, ref := range refs { 246 | result = append(result, ref.Output()...) 247 | } 248 | return result 249 | } 250 | 251 | func (s *Structure) String() string { 252 | props := make([]string, len(s.Props)) 253 | for i, prop := range s.Props { 254 | props[i] = prop.String() 255 | } 256 | str := fmt.Sprintf("type %s struct{\n%v\n}", s.Name, strings.Join(props, "\n")) 257 | 258 | formated, err := format.Source([]byte(str)) 259 | if err != nil { 260 | return str 261 | } 262 | return string(formated) 263 | } 264 | 265 | func (s *Structure) Refs() []*Structure { 266 | refs := make([]*Structure, 0, len(s.Props)) 267 | for _, v := range s.Props { 268 | if v.Refs != nil { 269 | switch v.Kind { 270 | case reflect.Map: 271 | refs = append(refs, v.Refs) 272 | case reflect.Array: 273 | refs = append(refs, v.Refs.Refs()...) 274 | } 275 | } 276 | } 277 | return refs 278 | } 279 | 280 | func (p *Propety) String() string { 281 | kind := "interface{}" 282 | isStruct := false 283 | switch p.Kind { 284 | case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: 285 | kind = p.Kind.String() 286 | case reflect.Slice, reflect.Array: 287 | kind = "[]interface{}" 288 | if p.Refs.Name != "" && len(p.Refs.Props) != 0 { 289 | if len(p.Refs.Refs()) == 0 && len(p.Refs.Props) == 1 { 290 | if p.Refs.Props[0].Kind != reflect.Interface { 291 | kind = fmt.Sprintf("[]%s", p.Refs.Props[0].Kind) 292 | } 293 | } else { 294 | kind = fmt.Sprintf("[]%s", p.Refs.Name) 295 | isStruct = true 296 | } 297 | } 298 | 299 | case reflect.Map: 300 | if p.Refs.Name != "" { 301 | kind = p.Refs.Name 302 | isStruct = true 303 | if option.UseOmitempty { 304 | kind = fmt.Sprintf("*%s", kind) 305 | } 306 | } 307 | } 308 | jsonOption := "" 309 | if option.UseOmitempty { 310 | jsonOption = ",omitempty" 311 | } 312 | exampleOption := "" 313 | if option.UseExample && p.Value != nil && !isStruct { 314 | list, ok := p.Value.([]interface{}) 315 | if ok { 316 | strs := make([]string, len(list)) 317 | for i, v := range list { 318 | strs[i] = fmt.Sprint(v) 319 | } 320 | p.Value = strings.Join(strs, ",") 321 | } 322 | if p.Value != "" { 323 | exampleOption = fmt.Sprintf(" example:\"%v\"", p.Value) 324 | } 325 | } 326 | propName := swag.ToGoName(p.Name) 327 | if option.UseLocal { 328 | propName = swag.ToVarName(propName) 329 | } 330 | return fmt.Sprintf("\t%s %s `json:\"%s%s\"%s`", propName, kind, p.Name, jsonOption, exampleOption) 331 | } 332 | 333 | func (p Props) Len() int { 334 | return len(p) 335 | } 336 | 337 | func (p Props) Swap(i, j int) { 338 | p[i], p[j] = p[j], p[i] 339 | } 340 | 341 | func (p Props) Less(i, j int) bool { 342 | return p[i].Name < p[j].Name 343 | } 344 | 345 | func getType(data interface{}) ObjectType { 346 | v := reflect.ValueOf(data) 347 | switch v.Kind() { 348 | case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: 349 | return Value 350 | case reflect.Slice, reflect.Array: 351 | return Array 352 | case reflect.Map: 353 | return Hash 354 | default: 355 | return Invalid 356 | } 357 | } 358 | 359 | func getNumberKind(f float64) reflect.Kind { 360 | decimals := 10000 361 | shift := float64(decimals) * f 362 | num := int(shift) 363 | if num%decimals == 0 { 364 | return reflect.Int 365 | } 366 | return reflect.Float64 367 | } 368 | -------------------------------------------------------------------------------- /json2struct_test.go: -------------------------------------------------------------------------------- 1 | package json2struct 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | type TestCase struct { 9 | Input string 10 | InputOption Options 11 | // MEMO: replace backquote to singlequote 12 | Expected string 13 | } 14 | 15 | var testCases = []TestCase{ 16 | { 17 | Input: `{"text": "hello"}`, 18 | Expected: `type Data struct { 19 | Text string 'json:"text"' 20 | }`, 21 | }, 22 | { 23 | Input: `[{"id": 123}]`, 24 | InputOption: Options{Name: "Categories"}, 25 | Expected: `type Category struct { 26 | ID int 'json:"id"' 27 | }`, 28 | }, 29 | { 30 | Input: `[{"name": 123},{"name": 3.14}]`, 31 | InputOption: Options{Name: "Tags"}, 32 | Expected: `type Tag struct { 33 | Name float64 'json:"name"' 34 | }`, 35 | }, 36 | { 37 | Input: `[{"name": 123},{"name": "stringer"}]`, 38 | InputOption: Options{Name: "Tags"}, 39 | Expected: `type Tag struct { 40 | Name interface{} 'json:"name"' 41 | }`, 42 | }, 43 | { 44 | Input: `{"nest": {"text": "hello"}}`, 45 | Expected: `type Data struct { 46 | Nest DataNest 'json:"nest"' 47 | } 48 | 49 | type DataNest struct { 50 | Text string 'json:"text"' 51 | }`, 52 | }, 53 | { 54 | Input: `{"nest": {"text": "hello"}}`, 55 | InputOption: Options{UseShortStruct: true}, 56 | Expected: `type Data struct { 57 | Nest Nest 'json:"nest"' 58 | } 59 | 60 | type Nest struct { 61 | Text string 'json:"text"' 62 | }`, 63 | }, 64 | { 65 | Input: `{"nest": {"text": "hello"}}`, 66 | InputOption: Options{UseLocal: true}, 67 | Expected: `type data struct { 68 | nest dataNest 'json:"nest"' 69 | } 70 | 71 | type dataNest struct { 72 | text string 'json:"text"' 73 | }`, 74 | }, 75 | { 76 | Input: `{"nest": {"text": "hello"}}`, 77 | InputOption: Options{UseOmitempty: true}, 78 | Expected: `type Data struct { 79 | Nest *DataNest 'json:"nest,omitempty"' 80 | } 81 | 82 | type DataNest struct { 83 | Text string 'json:"text,omitempty"' 84 | }`, 85 | }, 86 | { 87 | Input: `{"nest": {"text": "hello"}}`, 88 | InputOption: Options{Prefix: "input"}, 89 | Expected: `type InputData struct { 90 | Nest InputDataNest 'json:"nest"' 91 | } 92 | 93 | type InputDataNest struct { 94 | Text string 'json:"text"' 95 | }`, 96 | }, 97 | { 98 | Input: `{"nest": {"text": "hello"}}`, 99 | InputOption: Options{Suffix: "result"}, 100 | Expected: `type DataResult struct { 101 | Nest DataNestResult 'json:"nest"' 102 | } 103 | 104 | type DataNestResult struct { 105 | Text string 'json:"text"' 106 | }`, 107 | }, 108 | { 109 | Input: `{"categories": [{}]}`, 110 | Expected: `type Data struct { 111 | Categories []DataCategory 'json:"categories"' 112 | } 113 | 114 | type DataCategory struct { 115 | }`, 116 | }, 117 | { 118 | Input: `{"categories": []}`, 119 | Expected: `type Data struct { 120 | Categories []interface{} 'json:"categories"' 121 | }`, 122 | }, 123 | { 124 | Input: `{"categories": [1]}`, 125 | Expected: `type Data struct { 126 | Categories []int 'json:"categories"' 127 | }`, 128 | }, 129 | { 130 | Input: `{"categories": [1,"abc"]}`, 131 | Expected: `type Data struct { 132 | Categories []interface{} 'json:"categories"' 133 | }`, 134 | }, 135 | { 136 | Input: `{"categories": null}`, 137 | Expected: `type Data struct { 138 | Categories interface{} 'json:"categories"' 139 | }`, 140 | }, 141 | { 142 | Input: `{"post": {"status": 1, "accept_comment": true, "title": "hello world", "tags": [1,2,4]}, "categories": [{"name": "aws", "num": 123}, {"name": 123, "num": 3.14}], "url": "http://blog.yudppp.com", "profile_image_url": "http://blog.yudppp.com/img/profile.gif", "comments": []}`, 143 | Expected: `type Data struct { 144 | Categories []DataCategory 'json:"categories"' 145 | Comments []interface{} 'json:"comments"' 146 | Post DataPost 'json:"post"' 147 | ProfileImageURL string 'json:"profile_image_url"' 148 | URL string 'json:"url"' 149 | } 150 | 151 | type DataCategory struct { 152 | Name interface{} 'json:"name"' 153 | Num float64 'json:"num"' 154 | } 155 | 156 | type DataPost struct { 157 | AcceptComment bool 'json:"accept_comment"' 158 | Status int 'json:"status"' 159 | Tags []int 'json:"tags"' 160 | Title string 'json:"title"' 161 | }`, 162 | }, 163 | { 164 | Input: `[{"post": {"status": 1, "accept_comment": true, "title": "hello world", "tags": [1,2,4]}, "categories": [{"name": "aws", "num": 123}, {"name": 123, "num": 3.14}], "url": "http://blog.yudppp.com", "profile_image_url": "http://blog.yudppp.com/img/profile.gif", "comments": []}]`, 165 | InputOption: Options{UseShortStruct: true, Name: "json"}, 166 | Expected: `type JSON struct { 167 | Categories []Category 'json:"categories"' 168 | Comments []interface{} 'json:"comments"' 169 | Post Post 'json:"post"' 170 | ProfileImageURL string 'json:"profile_image_url"' 171 | URL string 'json:"url"' 172 | } 173 | 174 | type Category struct { 175 | Name interface{} 'json:"name"' 176 | Num float64 'json:"num"' 177 | } 178 | 179 | type Post struct { 180 | AcceptComment bool 'json:"accept_comment"' 181 | Status int 'json:"status"' 182 | Tags []int 'json:"tags"' 183 | Title string 'json:"title"' 184 | }`, 185 | }, 186 | { 187 | Input: `[{"post": {"status": 1, "accept_comment": true, "title": "hello world", "tags": [1,2,4]}, "categories": [{"name": "aws", "num": 123}, {"name": 123, "num": 3.14}], "url": "http://blog.yudppp.com", "profile_image_url": "http://blog.yudppp.com/img/profile.gif", "comments": []}]`, 188 | InputOption: Options{UseExample: true, Name: "json"}, 189 | Expected: `type JSON struct { 190 | Categories []JSONCategory 'json:"categories"' 191 | Comments []interface{} 'json:"comments"' 192 | Post JSONPost 'json:"post"' 193 | ProfileImageURL string 'json:"profile_image_url" example:"http://blog.yudppp.com/img/profile.gif"' 194 | URL string 'json:"url" example:"http://blog.yudppp.com"' 195 | } 196 | 197 | type JSONCategory struct { 198 | Name interface{} 'json:"name"' 199 | Num float64 'json:"num"' 200 | } 201 | 202 | type JSONPost struct { 203 | AcceptComment bool 'json:"accept_comment" example:"true"' 204 | Status int 'json:"status" example:"1"' 205 | Tags []int 'json:"tags" example:"1,2,4"' 206 | Title string 'json:"title" example:"hello world"' 207 | }`, 208 | }, 209 | } 210 | 211 | func TestParse(t *testing.T) { 212 | for _, v := range testCases { 213 | expected := strings.Replace(v.Expected, "'", "`", -1) 214 | actual, _ := Parse(strings.NewReader(v.Input), v.InputOption) 215 | if actual != expected { 216 | t.Errorf("\ninput:\n%v\ngot:\n%v\nwant:\n%v", v.Input, actual, expected) 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /playground/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | gopherjs build . --minify --output playground.js 3 | mv ./playground.js ../docs 4 | mv ./playground.js.map ../docs -------------------------------------------------------------------------------- /playground/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/hexops/vecty" 8 | "github.com/hexops/vecty/elem" 9 | "github.com/hexops/vecty/event" 10 | "github.com/hexops/vecty/prop" 11 | "github.com/yudppp/json2struct" 12 | ) 13 | 14 | func main() { 15 | vecty.SetTitle("json2struct playground") 16 | vecty.AddStylesheet("https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css") 17 | vecty.AddStylesheet("app.css") 18 | vecty.RenderBody(&PageView{ 19 | Input: `{ 20 | "text": "Hello", 21 | "categories": [ 22 | {"id": 1,"name": "Golang"} 23 | ] 24 | }`, 25 | StructName: "blog", 26 | }) 27 | 28 | } 29 | 30 | // PageView is our main page component. 31 | type PageView struct { 32 | vecty.Core 33 | Input string 34 | StructName string 35 | Prefix string 36 | Suffix string 37 | UseShortStruct bool 38 | UseLocal bool 39 | UseOmitempty bool 40 | UseExampleTag bool 41 | lastTimeKey int64 42 | } 43 | 44 | // Render implements the vecty.Component interface. 45 | func (p *PageView) Render() vecty.ComponentOrHTML { 46 | return elem.Body( 47 | elem.Header( 48 | vecty.Property("class", "header"), 49 | vecty.Text("json2struct playground"), 50 | ), 51 | elem.Div( 52 | vecty.Property("class", "wrapper"), 53 | elem.Div( 54 | vecty.Property("class", "col input"), 55 | elem.Div( 56 | vecty.Tag("label", 57 | vecty.Text("input json"), 58 | ), 59 | elem.TextArea( 60 | vecty.Property("class", "u-full-width"), 61 | vecty.Text(p.Input), 62 | event.Input(func(e *vecty.Event) { 63 | p.Input = e.Target.Get("value").String() 64 | p.Rerender() 65 | }), 66 | ), 67 | ), 68 | elem.Div( 69 | vecty.Tag("label", 70 | vecty.Text("struct name"), 71 | ), 72 | elem.Input( 73 | prop.Value(p.StructName), 74 | prop.Type(prop.TypeText), 75 | event.Input(func(e *vecty.Event) { 76 | p.StructName = e.Target.Get("value").String() 77 | p.Rerender() 78 | }), 79 | ), 80 | ), 81 | elem.Div( 82 | vecty.Tag("label", 83 | vecty.Text("struct prefix name"), 84 | ), 85 | elem.Input( 86 | prop.Value(p.Prefix), 87 | prop.Type(prop.TypeText), 88 | event.Input(func(e *vecty.Event) { 89 | p.Prefix = e.Target.Get("value").String() 90 | p.Rerender() 91 | }), 92 | ), 93 | ), 94 | elem.Div( 95 | vecty.Tag("label", 96 | vecty.Text("struct suffix name"), 97 | ), 98 | elem.Input( 99 | prop.Value(p.Suffix), 100 | prop.Type(prop.TypeText), 101 | event.Input(func(e *vecty.Event) { 102 | p.Suffix = e.Target.Get("value").String() 103 | p.Rerender() 104 | }), 105 | ), 106 | ), 107 | elem.Div( 108 | vecty.Tag("label", 109 | vecty.Text("omitempty mode"), 110 | ), 111 | elem.Input( 112 | vecty.Property("class", "toggle"), 113 | prop.Type(prop.TypeCheckbox), 114 | prop.Checked(p.UseOmitempty), 115 | event.Change(func(e *vecty.Event) { 116 | p.UseOmitempty = e.Target.Get("checked").Bool() 117 | p.Rerender() 118 | }), 119 | ), 120 | ), 121 | elem.Div( 122 | vecty.Tag("label", 123 | vecty.Text("short mode"), 124 | ), 125 | elem.Input( 126 | vecty.Property("class", "toggle"), 127 | prop.Type(prop.TypeCheckbox), 128 | prop.Checked(p.UseShortStruct), 129 | event.Change(func(e *vecty.Event) { 130 | p.UseShortStruct = e.Target.Get("checked").Bool() 131 | p.Rerender() 132 | }), 133 | ), 134 | ), 135 | elem.Div( 136 | vecty.Tag("label", 137 | vecty.Text("local mode"), 138 | ), 139 | elem.Input( 140 | vecty.Property("class", "toggle"), 141 | prop.Type(prop.TypeCheckbox), 142 | prop.Checked(p.UseLocal), 143 | event.Change(func(e *vecty.Event) { 144 | p.UseLocal = e.Target.Get("checked").Bool() 145 | p.Rerender() 146 | }), 147 | ), 148 | ), 149 | elem.Div( 150 | vecty.Tag("label", 151 | vecty.Text("example tag mode"), 152 | ), 153 | elem.Input( 154 | vecty.Property("class", "toggle"), 155 | prop.Type(prop.TypeCheckbox), 156 | prop.Checked(p.UseExampleTag), 157 | event.Change(func(e *vecty.Event) { 158 | p.UseExampleTag = e.Target.Get("checked").Bool() 159 | p.Rerender() 160 | }), 161 | ), 162 | ), 163 | ), 164 | elem.Div( 165 | vecty.Property("class", "col output"), 166 | vecty.Tag("label", 167 | vecty.Text("output struct"), 168 | ), 169 | 170 | vecty.Tag("pre", 171 | elem.Code( 172 | &StructObject{ 173 | Input: p.Input, 174 | Option: json2struct.Options{ 175 | Name: p.StructName, 176 | Prefix: p.Prefix, 177 | Suffix: p.Suffix, 178 | UseShortStruct: p.UseShortStruct, 179 | UseLocal: p.UseLocal, 180 | UseOmitempty: p.UseOmitempty, 181 | UseExample: p.UseExampleTag, 182 | }, 183 | }, 184 | ), 185 | ), 186 | ), 187 | ), 188 | elem.Footer( 189 | vecty.Property("class", "footer"), 190 | vecty.Text("Used by "), 191 | elem.Anchor( 192 | prop.Href("https://github.com/yudppp/json2struct"), 193 | vecty.Text("yudppp/json2struct"), 194 | ), 195 | ), 196 | ) 197 | 198 | } 199 | 200 | // Rerender is rerender and debounce 201 | func (p *PageView) Rerender() { 202 | timeKey := time.Now().UnixNano() 203 | p.lastTimeKey = timeKey 204 | go func() { 205 | time.Sleep(800 * time.Millisecond) 206 | if timeKey == p.lastTimeKey { 207 | vecty.Rerender(p) 208 | } 209 | }() 210 | } 211 | 212 | // StructObject is output values. 213 | type StructObject struct { 214 | vecty.Core 215 | Input string 216 | Option json2struct.Options 217 | } 218 | 219 | // Render implements the vecty.Component interface. 220 | func (m *StructObject) Render() *vecty.HTML { 221 | 222 | output, err := json2struct.Parse(strings.NewReader(m.Input), m.Option) 223 | if err != nil { 224 | return elem.Div( 225 | vecty.Text("invalid"), 226 | ) 227 | } 228 | return elem.Div( 229 | vecty.Text(output), 230 | ) 231 | } 232 | --------------------------------------------------------------------------------