Skip to content

Commit

Permalink
Merge pull request #30 from 0x-buidl/feat/interface_inheritance
Browse files Browse the repository at this point in the history
feat: integrate interface inheritance
  • Loading branch information
gzuidhof committed Aug 16, 2023
2 parents d409f24 + 86b5105 commit 6cb2668
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

examples/time
examples/tygo
examples/http
examples/http
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Tygo is a tool for generating Typescript typings from Golang source files that j

It preserves comments, understands constants and also supports non-struct `type` expressions. It's perfect for generating equivalent types for a Golang REST API to be used in your front-end codebase.

**🚀 Now supports Golang 1.18 generic types**
**🚀 Now supports Golang 1.18 generic types, struct inheritance**

## Installation

Expand Down Expand Up @@ -223,7 +223,7 @@ export interface Nicknames {

### Readonly fields

Sometimes a field should be immutable, you can `,readonly` to the `tstype` tag to mark a field as `readonly`.
Sometimes a field should be immutable, you can add `,readonly` to the `tstype` tag to mark a field as `readonly`.

```golang
// Golang input
Expand All @@ -241,6 +241,42 @@ export interface Cat {
}
```

## Inheritance

Tygo supports interface inheritance. To extend an `inlined` struct, use the tag `tstype:,extends` on struct fields you wish to extend. Only `struct` types can be extended.

Example usage [here](examples/inheritance)

```go
// Golang input
import "example.com/external"

type Base struct {
Name string `json:"name"`
}

type Base2[T string | int] struct {
ID T `json:"id"`
}

type Other[T int] struct {
Base ` tstype:",extends"`
Base2[T] ` tstype:",extends"`
external.AnotherStruct ` tstype:",extends"`
OtherValue string ` json:"other_value"`
}
```

```typescript
// Typescript output
export interface Other<T extends number /* int */>
extends Base,
Base2<T>,
external.AnotherStruct {
other_value: string;
}
```

## Generics

Tygo supports generic types (Go version >= 1.18) out of the box.
Expand Down Expand Up @@ -294,7 +330,7 @@ packages:
// Golang input
type Foo struct {
TaggedField string `yaml:"custom_field_name_in_yaml"`
UntaggedField string
UntaggedField string
}
```

Expand Down
5 changes: 3 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import (
)

func Execute() {
var rootCmd = &cobra.Command{
rootCmd := &cobra.Command{
Use: "tygo",
Short: "Tool for generating Typescript from Go types",
Long: `Tygo generates Typescript interfaces and constants from Go files by parsing them.`,
}

rootCmd.PersistentFlags().String("config", "tygo.yaml", "config file to load (default is tygo.yaml in the current folder)")
rootCmd.PersistentFlags().
String("config", "tygo.yaml", "config file to load (default is tygo.yaml in the current folder)")
rootCmd.Version = Version() + " " + Target() + " (" + CommitDate() + ") " + Commit()
rootCmd.PersistentFlags().BoolP("debug", "D", false, "Debug mode (prints debug messages)")

Expand Down
4 changes: 4 additions & 0 deletions examples/bookstore/author.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ type AuthorBookListing struct {
AuthorName string `json:"author_name"`
WrittenBooks []Book `json:"written_books"`
}

type AuthorWithInheritance[T int] struct {
ID T `json:"id"`
}
7 changes: 6 additions & 1 deletion examples/bookstore/book.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ type Book struct {
// ISBN identifier of the book, null if not known.
ISBN ISBN `json:"isbn"`

Genre string `json:"genre" tstype:"'novel' | 'crime' | 'fantasy'"`
Genre string `json:"genre" tstype:"'novel' | 'crime' | 'fantasy'"`
Chapters []Chapter `json:"chapters"`

PublishedAt *time.Time `json:"published_at"`
}

type TextBook[T int] struct {
Book ` tstype:",inline"`
Pages T ` json:"pages"`
}
6 changes: 6 additions & 0 deletions examples/bookstore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export interface AuthorBookListing {
author_name: string;
written_books: Book[];
}
export interface AuthorWithInheritance<T extends number /* int */> {
id: T;
}

//////////
// source: book.go
Expand All @@ -33,3 +36,6 @@ export interface Book {
chapters: Chapter[];
published_at?: string /* RFC 3339 formatted */;
}
export interface TextBook<T extends number /* int */> {
pages: T;
}
22 changes: 22 additions & 0 deletions examples/inheritance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Code generated by tygo. DO NOT EDIT.
import * as bookapp from "../bookstore"

//////////
// source: inheritance.go

export interface Base {
name: string;
}
export interface Base2<T extends string | number /* int */> {
id: T;
}
export interface Base3<T extends string, X extends number /* int */> {
class: T;
level: X;
}
export interface Other<T extends number /* int */, X extends string> extends Base, Base2<T>, Base3<X, T>, bookapp.Book, bookapp.TextBook<T> {
otherWithBase: Base;
otherWithBase2: Base2<X>;
otherValue: string;
author: bookapp.AuthorWithInheritance<T>;
}
28 changes: 28 additions & 0 deletions examples/inheritance/inheritance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package inheritance

import bookapp "github.com/gzuidhof/tygo/examples/bookstore"

type Base struct {
Name string `json:"name"`
}

type Base2[T string | int] struct {
ID T `json:"id"`
}

type Base3[T string, X int] struct {
Class T `json:"class"`
Level X `json:"level"`
}

type Other[T int, X string] struct {
Base `tstype:",extends"`
Base2[T] `tstype:",extends"`
Base3[X, T] `tstype:",extends"`
OtherWithBase Base ` json:"otherWithBase"`
OtherWithBase2 Base2[X] ` json:"otherWithBase2"`
OtherValue string ` json:"otherValue"`
Author bookapp.AuthorWithInheritance[T] `tstype:"bookapp.AuthorWithInheritance<T>" json:"author"`
bookapp.Book `tstype:",extends"`
bookapp.TextBook[T] `tstype:",extends"`
}
8 changes: 7 additions & 1 deletion tygo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ packages:
indent: " "
exclude_files:
- "excluded.go"
frontmatter: | # We can define some additional text to put at the start of the file.
frontmatter:
| # We can define some additional text to put at the start of the file.
export type Something = string | number;
- path: "github.com/gzuidhof/tygo/examples/simple"
fallback_type: unknown
- path: "github.com/gzuidhof/tygo/examples/inheritance"
fallback_type: unknown
frontmatter:
| # We can define some additional text to put at the start of the file.
import * as bookapp from "../bookstore"
- path: "github.com/gzuidhof/tygo/examples/generic"
fallback_type: unknown
- path: "github.com/gzuidhof/tygo/examples/preserveTypeComments"
Expand Down
24 changes: 17 additions & 7 deletions tygo/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package tygo

import (
"fmt"
"regexp"
"strings"

"go/ast"
"go/token"
"regexp"
"strings"

"github.com/fatih/structtag"
)
Expand Down Expand Up @@ -36,7 +35,12 @@ func (g *PackageGenerator) writeIndent(s *strings.Builder, depth int) {
}
}

func (g *PackageGenerator) writeType(s *strings.Builder, t ast.Expr, depth int, optionalParens bool) {
func (g *PackageGenerator) writeType(
s *strings.Builder,
t ast.Expr,
depth int,
optionalParens bool,
) {
switch t := t.(type) {
case *ast.StarExpr:
if optionalParens {
Expand Down Expand Up @@ -147,7 +151,11 @@ func (g *PackageGenerator) writeTypeParamsFields(s *strings.Builder, fields []*a
s.WriteByte('>')
}

func (g *PackageGenerator) writeInterfaceFields(s *strings.Builder, fields []*ast.Field, depth int) {
func (g *PackageGenerator) writeInterfaceFields(
s *strings.Builder,
fields []*ast.Field,
depth int,
) {
// Usually interfaces in Golang don't have fields, but generic (union) interfaces we can map to Typescript.

if len(fields) == 0 { // Type without any fields (probably only has methods)
Expand All @@ -161,7 +169,9 @@ func (g *PackageGenerator) writeInterfaceFields(s *strings.Builder, fields []*as
continue
}
if !didContainNonFuncFields {
s.WriteByte('\n') // We need to write a newline so comments of generic components render nicely.
s.WriteByte(
'\n',
) // We need to write a newline so comments of generic components render nicely.
didContainNonFuncFields = true
}

Expand Down Expand Up @@ -227,7 +237,7 @@ func (g *PackageGenerator) writeStructFields(s *strings.Builder, fields []*ast.F
tstypeTag, err := tags.Get("tstype")
if err == nil {
tstype = tstypeTag.Name
if tstype == "-" {
if tstype == "-" || tstypeTag.HasOption("extends") {
continue
}
required = tstypeTag.HasOption("required")
Expand Down
88 changes: 84 additions & 4 deletions tygo/write_toplevel.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"go/ast"
"strings"

"github.com/fatih/structtag"
)

type groupContext struct {
Expand Down Expand Up @@ -67,8 +69,13 @@ func (g *PackageGenerator) writeSpec(s *strings.Builder, spec ast.Spec, group *g
// `type X struct { ... }`
// or
// `type Bar = string`
func (g *PackageGenerator) writeTypeSpec(s *strings.Builder, ts *ast.TypeSpec, group *groupContext) {
if ts.Doc != nil && g.PreserveTypeComments() { // The spec has its own comment, which overrules the grouped comment.
func (g *PackageGenerator) writeTypeSpec(
s *strings.Builder,
ts *ast.TypeSpec,
group *groupContext,
) {
if ts.Doc != nil &&
g.PreserveTypeComments() { // The spec has its own comment, which overrules the grouped comment.
g.writeCommentGroup(s, ts.Doc, 0)
} else if group.isGroupedDeclaration && g.PreserveTypeComments() {
g.writeCommentGroupIfNotNil(s, group.doc, 0)
Expand All @@ -83,6 +90,7 @@ func (g *PackageGenerator) writeTypeSpec(s *strings.Builder, ts *ast.TypeSpec, g
g.writeTypeParamsFields(s, ts.TypeParams.List)
}

g.writeTypeInheritanceSpec(s, st.Fields.List)
s.WriteString(" {\n")
g.writeStructFields(s, st.Fields.List, 0)
s.WriteString("}")
Expand Down Expand Up @@ -113,9 +121,43 @@ func (g *PackageGenerator) writeTypeSpec(s *strings.Builder, ts *ast.TypeSpec, g
}
}

// Writing of type inheritance specs, which are expressions like
// `type X struct { }`
// `type Y struct { X `tstype:",extends"` }`
// `export interface Y extends X { }`
func (g *PackageGenerator) writeTypeInheritanceSpec(s *strings.Builder, fields []*ast.Field) {
inheritances := make([]string, 0)
for _, f := range fields {
if f.Type != nil && f.Tag != nil {
tags, err := structtag.Parse(f.Tag.Value[1 : len(f.Tag.Value)-1])
if err != nil {
panic(err)
}

tstypeTag, err := tags.Get("tstype")
if err != nil || !tstypeTag.HasOption("extends") {
continue
}

name, valid := getInheritedType(f.Type)
if valid {
inheritances = append(inheritances, name)
}
}
}
if len(inheritances) > 0 {
s.WriteString(" extends ")
s.WriteString(strings.Join(inheritances, ", "))
}
}

// Writing of value specs, which are exported const expressions like
// const SomeValue = 3
func (g *PackageGenerator) writeValueSpec(s *strings.Builder, vs *ast.ValueSpec, group *groupContext) {
func (g *PackageGenerator) writeValueSpec(
s *strings.Builder,
vs *ast.ValueSpec,
group *groupContext,
) {
for i, name := range vs.Names {
group.iotaValue = group.iotaValue + 1
if name.Name == "_" {
Expand All @@ -125,7 +167,8 @@ func (g *PackageGenerator) writeValueSpec(s *strings.Builder, vs *ast.ValueSpec,
continue
}

if vs.Doc != nil && g.PreserveTypeComments() { // The spec has its own comment, which overrules the grouped comment.
if vs.Doc != nil &&
g.PreserveTypeComments() { // The spec has its own comment, which overrules the grouped comment.
g.writeCommentGroup(s, vs.Doc, 0)
} else if group.isGroupedDeclaration && g.PreserveTypeComments() {
g.writeCommentGroupIfNotNil(s, group.doc, 0)
Expand Down Expand Up @@ -187,3 +230,40 @@ func (g *PackageGenerator) writeValueSpec(s *strings.Builder, vs *ast.ValueSpec,

}
}

func getInheritedType(f ast.Expr) (name string, valid bool) {
switch ft := f.(type) {
case *ast.Ident:
if ft.Obj != nil && ft.Obj.Decl != nil {
dcl, ok := ft.Obj.Decl.(*ast.TypeSpec)
if ok {
_, isStruct := dcl.Type.(*ast.StructType)
valid = isStruct && dcl.Name.IsExported()
name = dcl.Name.Name
break
}
}
case *ast.IndexExpr:
name, valid = getInheritedType(ft.X)
if valid {
generic := getIdent(ft.Index.(*ast.Ident).Name)
name += fmt.Sprintf("<%s>", generic)
break
}
case *ast.IndexListExpr:
name, valid = getInheritedType(ft.X)
if valid {
generic := ""
for _, index := range ft.Indices {
generic += fmt.Sprintf("%s, ", getIdent(index.(*ast.Ident).Name))
}
name += fmt.Sprintf("<%s>", generic[:len(generic)-2])
break
}
case *ast.SelectorExpr:
valid = ft.Sel.IsExported()
name = fmt.Sprintf("%s.%s", ft.X, ft.Sel)

}
return
}

0 comments on commit 6cb2668

Please sign in to comment.