diff --git a/example/gen/main.go b/example/gen/main.go index 4b9fd8c..adfaca7 100644 --- a/example/gen/main.go +++ b/example/gen/main.go @@ -1,8 +1,12 @@ package main import ( + "os" + "path/filepath" + "github.com/starius/api2" "github.com/starius/api2/example" + "github.com/starius/api2/typegen" ) func main() { @@ -15,6 +19,11 @@ func main() { &example.CustomType{}, &example.CustomType2{}, }, + OnDone: func(options *api2.TypesGenConfig, parser *typegen.Parser, routes []api2.Route) { + _ = os.RemoveAll(filepath.Join(options.OutDir, "schema.ts")) + schemaFile, _ := os.OpenFile(filepath.Join(options.OutDir, "schema.ts"), os.O_WRONLY|os.O_CREATE, 0755) + _ = typegen.PrintJDT(parser, schemaFile) + }, }) api2.GenerateOpenApiSpec(&api2.TypesGenConfig{ OutDir: "./openapi", diff --git a/example/ts-types/schema.ts b/example/ts-types/schema.ts new file mode 100755 index 0000000..0a021ff --- /dev/null +++ b/example/ts-types/schema.ts @@ -0,0 +1,75 @@ +// prettier-ignore +import * as t from './gen' +import type {JTDSchema} from 'libs/validator' + +export const schema = { +ExampleCustomType: { + "metadata": { + "allOf": [ + "ExampleUserSettings" + ] + } +} as JTDSchema, +ExampleCustomType2: { + "metadata": { + "allOf": [ + "ExampleUserSettings" + ] + } +} as JTDSchema, +ExampleDirection: { + "metadata": { + "enumType": "int", + "enumValues": [ + 1, + 0, + 2, + 3 + ] + }, + "type": "int32" +} as JTDSchema, +ExampleEchoRequest: { + "properties": { + "dir": { + "ref": "ExampleDirection" + }, + "items": { + "elements": {} + }, + "maps": { + "values": { + "ref": "ExampleDirection" + } + }, + "session": { + "type": "string" + }, + "text": { + "type": "string" + } + } +} as JTDSchema, +ExampleEchoResponse: { + "properties": { + "text": { + "type": "string" + } + } +} as JTDSchema, +ExampleHelloRequest: { + "properties": { + "key": { + "type": "string" + } + } +} as JTDSchema, +ExampleHelloResponse: { + "properties": { + "session": { + "type": "string" + } + } +} as JTDSchema, +ExampleUserSettings: {} as JTDSchema, +} diff --git a/go.mod b/go.mod index 0233a91..4dab070 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.14 require ( github.com/getkin/kin-openapi v0.49.0 github.com/grpc-ecosystem/grpc-gateway v1.9.5 + github.com/jsontypedef/json-typedef-go v0.0.0-20200503043955-4280071bd745 + github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508 google.golang.org/grpc v1.29.1 ) diff --git a/go.sum b/go.sum index 9e72aa9..e86efe7 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/jsontypedef/json-typedef-go v0.0.0-20200503043955-4280071bd745 h1:g5e59oKweCDh4NCkwPVTCSvkA/xI4rEJ9gHpGwtc7gQ= +github.com/jsontypedef/json-typedef-go v0.0.0-20200503043955-4280071bd745/go.mod h1:vt/4bvsIJyrXdW084b66lVgihz1sOcKAC9l4tqJuty8= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -44,6 +46,12 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/ts_client.go b/ts_client.go index bbaf107..4646b59 100644 --- a/ts_client.go +++ b/ts_client.go @@ -94,6 +94,7 @@ type TypesGenConfig struct { Routes []interface{} Types []interface{} Blacklist []BlacklistItem + OnDone func(cfg *TypesGenConfig, p *typegen.Parser, routes []Route) } func panicIf(err error) { @@ -117,6 +118,12 @@ func SerializeCustom(t reflect.Type) string { } return "" } +func SerializeCustomSchema(t reflect.Type) string { + if t == jsonRawMessageType { + return "unknown" + } + return "" +} func GenerateTSClient(options *TypesGenConfig) { if options.ClientTemplate == nil { @@ -127,6 +134,7 @@ func GenerateTSClient(options *TypesGenConfig) { panicIf(err) typesFile, err := os.OpenFile(filepath.Join(options.OutDir, "gen.ts"), os.O_WRONLY|os.O_CREATE, 0755) panicIf(err) + panicIf(err) parser := typegen.NewParser() parser.CustomParse = CustomParse allRoutes := []Route{} @@ -146,8 +154,9 @@ func GenerateTSClient(options *TypesGenConfig) { } parser.ParseRaw(options.Types...) typegen.PrintTsTypes(parser, typesFile, SerializeCustom) - panicIf(err) - + if options.OnDone != nil { + options.OnDone(options, parser, allRoutes) + } } func serializeTypeInfo(t *preparedType) ([]byte, error) { diff --git a/typegen/enum.go b/typegen/enum.go index ce5d82d..d8cd43d 100644 --- a/typegen/enum.go +++ b/typegen/enum.go @@ -60,6 +60,21 @@ func (this *EnumValue) Stringify() string { } return "" } +func (this *EnumValue) RawValue() (interface{}, bool) { + k := this.value.Kind() + switch k { + case reflect.String: + return this.value.String(), false + case reflect.Int: + _, hasToString := this.value.Type().MethodByName("String") + if hasToString { + return fmt.Sprintf("%v", this.value), false + } else { + return this.value.Int(), true + } + } + return "", false +} func getTypedEnumValues(t reflect.Type) []EnumValue { values, err := getEnumsFromAst(t.PkgPath(), t.String()) diff --git a/typegen/record.go b/typegen/record.go index 4a8c296..60310b5 100644 --- a/typegen/record.go +++ b/typegen/record.go @@ -3,6 +3,7 @@ package typegen import ( "path" "reflect" + "strings" ) type RawType struct { @@ -15,6 +16,7 @@ type IType interface { SetName(name, pkg string) GetPackage() string RefName() string + IdName() string GetType() reflect.Type } @@ -75,6 +77,11 @@ func (this *BaseType) RefName() string { return path.Base(pkg) + "." + this.Name } +func (this *BaseType) IdName() string { + pkg := this.GetPackage() + return strings.Title(path.Base(pkg)) + this.Name +} + func (this *BaseType) GetType() reflect.Type { return this.T } diff --git a/typegen/schema.printer.go b/typegen/schema.printer.go new file mode 100644 index 0000000..0b8fcb3 --- /dev/null +++ b/typegen/schema.printer.go @@ -0,0 +1,180 @@ +package typegen + +import ( + "encoding/json" + "io" + "reflect" + "text/template" + + jdt "github.com/jsontypedef/json-typedef-go" +) + +type JSONTypeDefSchema struct { + Definitions map[string]JSONTypeDefSchema `json:"definitions,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Nullable bool `json:"nullable,omitempty"` + Ref *string `json:"ref,omitempty"` + Type jdt.Type `json:"type,omitempty"` + Enum []string `json:"enum,omitempty"` + Elements *JSONTypeDefSchema `json:"elements,omitempty"` + Properties map[string]JSONTypeDefSchema `json:"properties,omitempty"` + OptionalProperties map[string]JSONTypeDefSchema `json:"optionalProperties,omitempty"` + AdditionalProperties bool `json:"additionalProperties,omitempty"` + Values *JSONTypeDefSchema `json:"values,omitempty"` + Discriminator string `json:"discriminator,omitempty"` + Mapping map[string]JSONTypeDefSchema `json:"mapping,omitempty"` +} + +const GlobalSchemaTemplate = `// prettier-ignore +import * as t from './gen' +import type {JTDSchema} from 'libs/validator' + +export const schema = { +{{range $name, $json := .Content}}{{$name}}: {{$json|Marshal}} as JTDSchema,{{"\n"}}{{end}}} +` + +func PrintJDT(p *Parser, writer io.Writer) error { + def := &JSONTypeDefSchema{} + def.Metadata = make(map[string]interface{}) + def.Definitions = make(map[string]JSONTypeDefSchema) + for _, st := range p.seen { + schema := genereateJDT(p, st) + def.Definitions[st.IdName()] = *schema + } + + tmpl, err := template.New("schema template").Funcs(template.FuncMap{ + "Marshal": func(v interface{}) string { + a, _ := json.MarshalIndent(v, "", " ") + return string(a) + }, + "TypeName": func(v string) string { + for _, st := range p.seen { + if st.IdName() == v { + return st.RefName() + } + } + return v + }, + }).Parse(GlobalSchemaTemplate) + panicIf(err) + tmpl.Execute(writer, map[string]interface{}{ + "Content": def.Definitions, + }) + return nil +} + +func ToSchema(schema *JSONTypeDefSchema) (*jdt.Schema, error) { + res, err := json.Marshal(schema) + if err != nil { + return nil, err + } + var jtdschema jdt.Schema + err = json.Unmarshal(res, &jtdschema) + if err != nil { + return nil, err + } + return &jtdschema, nil +} + +func typeToSchemaString(t reflect.Type, fieldSchema *JSONTypeDefSchema, getTypeName TypeToString) *JSONTypeDefSchema { + k := t.Kind() + + if fieldSchema.Metadata == nil { + fieldSchema.Metadata = make(map[string]interface{}) + } + switch { + case k == reflect.Ptr: + t = indirect(t) + fieldSchema.Nullable = true + return fieldSchema + case k == reflect.Struct: + if isDate(t) { + fieldSchema.Type = jdt.TypeTimestamp + return fieldSchema + } + ref := getTypeName(t) + fieldSchema.Ref = &ref + return fieldSchema + case isNumber(k) && isEnum(t): + ref := getTypeName(t) + fieldSchema.Ref = &ref + return fieldSchema + case isNumber(k): + fieldSchema.Type = jdt.TypeFloat64 + return fieldSchema + case k == reflect.String && isEnum(t): + ref := getTypeName(t) + fieldSchema.Ref = &ref + return fieldSchema + case k == reflect.String: + fieldSchema.Type = jdt.TypeString + return fieldSchema + case k == reflect.Bool: + fieldSchema.Type = jdt.TypeBoolean + return fieldSchema + case k == reflect.Slice || k == reflect.Array: + fieldSchema.Elements = typeToSchemaString(t.Elem(), &JSONTypeDefSchema{}, getTypeName) + return fieldSchema + case k == reflect.Interface: + return fieldSchema + case k == reflect.Map: + fieldSchema.Values = typeToSchemaString(t.Elem(), &JSONTypeDefSchema{}, getTypeName) + return fieldSchema + } + return fieldSchema +} + +func genereateJDT(p *Parser, s IType) *JSONTypeDefSchema { + t := &JSONTypeDefSchema{} + propertiesTypes := t + t.Metadata = make(map[string]interface{}) + switch v := s.(type) { + case *EnumDef: + enumType := v.T.Kind().String() + enumValues := []interface{}{} + isInt := false + for _, v := range v.Values { + value, isIntValue := v.RawValue() + if !isIntValue { + val := value.(string) + t.Enum = append(t.Enum, val) + } else { + isInt = true + enumValues = append(enumValues, value) + } + } + if isInt { + t.Type = jdt.TypeInt32 + } + t.Metadata["enumType"] = enumType + if len(enumValues) > 0 { + t.Metadata["enumValues"] = enumValues + } + return t + case *RecordDef: + if len(v.Embedded) != 0 { + types := []string{} + for _, v := range v.Embedded { + types = append(types, p.GetVisited(v).IdName()) + } + t.Metadata["allOf"] = types + t.Properties = make(map[string]JSONTypeDefSchema) + } + + propertiesTypes.Properties = make(map[string]JSONTypeDefSchema) + for _, field := range v.Fields { + scm := &JSONTypeDefSchema{} + if field.Type == nil { + continue + } + keyName := field.Key + if field.Tag.FieldName != "" { + keyName = field.Tag.FieldName + } + propertiesTypes.Properties[keyName] = *typeToSchemaString(field.Type, scm, func(t reflect.Type) string { + return p.GetVisited(t).IdName() + }) + } + } + return t +}