Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
# [bel](https://en.wikipedia.org/wiki/Bel_(mythology))
Generate TypeScript interfaces from Go structs/interfaces - useful for JSON RPC

[![Go Report Card](https://goreportcard.com/badge/github.com/32leaves/bel)](https://goreportcard.com/report/github.com/32leaves/bel)
[![GoDoc](https://godoc.org/github.com/32leaves/bel?status.svg)](https://godoc.org/github.com/32leaves/bel)
[![gocover.run](https://gocover.run/github.com/32leaves/bel.svg?style=flat&tag=1.10)](https://gocover.run?tag=1.10&repo=github.com%2F32leaves%2Fbel)
[![Stability: Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html)
**This is a fork of https://github.com/csweichel/bel. Thanks go to him for most of the work.**

[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io#github.com/32leaves/bel)

`bel` is used in production in https://gitpod.io.
`bel` is used in production in https://conclude.io.

## Getting started
`bel` is easy to use. There are two steps involved: extract the Typescript information, and generate the Typescript code.
```Go
package main

import (
"github.com/32leaves/bel"
"github.com/laknoll/bel"
)

type Demo struct {
Expand Down Expand Up @@ -61,7 +56,7 @@ package main

import (
"os"
"github.com/32leaves/bel"
"github.com/laknoll/bel"
)

type DemoService interface {
Expand Down Expand Up @@ -90,7 +85,7 @@ export interface DemoService {
```

## Advanced Usage
You can try all the examples mentioned below in [Gitpod](https://gitpod.io#github.com/32leaves/bel).
You can try all the examples mentioned below in [Gitpod](https://gitpod.io#github.com/laknoll/bel).

### FollowStructs
Follow structs enable the transitive generation of types. See [examples/embed-structs.go](examples/follow-structs.go).
Expand Down Expand Up @@ -137,4 +132,4 @@ You can configure the `io.Writer` that _bel_ uses using `bel.GenerateOutputTo`.
# Contributing
All contributions/PR/issue/beer are welcome ❤️.

It's easiest to work with _bel_ using Gitpod: [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io#github.com/32leaves/bel)
It's easiest to work with _bel_ using Gitpod: [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io#github.com/laknoll/bel)
4 changes: 3 additions & 1 deletion doc_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package bel

import (
"sort"
"testing"

"github.com/go-test/deep"
Expand All @@ -19,7 +20,7 @@ type DoSomethingReq struct {
}

func TestParsedSourceDocHandler(t *testing.T) {
handler, err := NewParsedSourceDocHandler(".", "github.com/32leaves/")
handler, err := NewParsedSourceDocHandler(".", "github.com/laknoll/")
if err != nil {
t.Error(err)
return
Expand All @@ -30,6 +31,7 @@ func TestParsedSourceDocHandler(t *testing.T) {
t.Error(err)
return
}
sort.Slice(extract, func(ia, ib int) bool { return extract[ia].Name < extract[ib].Name })

expectation := []TypescriptType{
{
Expand Down
83 changes: 79 additions & 4 deletions enum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ type StructWithEnum struct {
Baz string
}

type StructWithEnumMap struct {
EnumMap map[MyEnum]string
}

func TestParseStringEnum(t *testing.T) {
handler, err := NewParsedSourceEnumHandler(".")
if err != nil {
Expand Down Expand Up @@ -171,8 +175,9 @@ func TestExtractIntEnum(t *testing.T) {
TypedElement: TypedElement{
Name: "Bar",
Type: TypescriptType{
Name: "MyOtherEnum",
Kind: TypescriptKind("simple"),
Name: "MyOtherEnum",
Kind: TypescriptKind("simple"),
IsEnum: true,
},
},
},
Expand All @@ -189,14 +194,84 @@ func TestExtractIntEnum(t *testing.T) {
TypedElement: TypedElement{
Name: "Foo",
Type: TypescriptType{
Name: "MyEnum",
Kind: TypescriptKind("simple"),
Name: "MyEnum",
Kind: TypescriptKind("simple"),
IsEnum: true,
},
},
},
},
},
}
diff := deep.Equal(expectation, extract)
for _, d := range diff {
t.Error(d)
}
}

func TestEnumInMap(t *testing.T) {
handler, err := NewParsedSourceEnumHandler(".")
if err != nil {
t.Error(err)
return
}

extract, err := Extract(StructWithEnumMap{}, WithEnumerations(handler))
if err != nil {
t.Error(err)
return
}
sort.Slice(extract, func(ia, ib int) bool { return extract[ia].Name < extract[ib].Name })
for i := range extract {
sort.Slice(extract[i].Members, func(ia, ib int) bool { return extract[i].Members[ia].Name < extract[i].Members[ib].Name })
sort.Slice(extract[i].EnumMembers, func(ia, ib int) bool { return extract[i].EnumMembers[ia].Value < extract[i].EnumMembers[ib].Value })
}

expectation := []TypescriptType{
{
Name: "MyEnum",
Kind: TypescriptKind("enum"),
EnumMembers: []TypescriptEnumMember{
{
Name: "MemberOne",
Value: "\"member-one\"",
},
{
Name: "MemberThree",
Value: "\"member-three\"",
},
{
Name: "MemberTwo",
Value: "\"member-two\"",
},
},
},
{
Name: "StructWithEnumMap",
Kind: TypescriptKind("iface"),
Members: []TypescriptMember{
{
TypedElement: TypedElement{
Name: "EnumMap",
Type: TypescriptType{
Kind: TypescriptKind("map"),
Params: []TypescriptType{
{
Name: "MyEnum",
Kind: TypescriptKind("simple"),
IsEnum: true,
}, {
Name: "string",
Kind: "simple",
},
},
},
},
},
},
},
}

diff := deep.Equal(expectation, extract)
for _, d := range diff {
t.Error(d)
Expand Down
2 changes: 1 addition & 1 deletion examples/code-generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package main
import (
"os"

"github.com/32leaves/bel"
"github.com/laknoll/bel"
)

// HelloWorld is a simple example struct
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-namer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package main
import (
"reflect"

"github.com/32leaves/bel"
"github.com/laknoll/bel"
)

// ThisStructsName is an empty struct
Expand Down
2 changes: 1 addition & 1 deletion examples/embed-structs.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package main

import (
"github.com/32leaves/bel"
"github.com/laknoll/bel"
)

// ReferencedStruct is a struct referenced by HasAReference
Expand Down
2 changes: 1 addition & 1 deletion examples/enums.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package main

import (
"github.com/32leaves/bel"
"github.com/laknoll/bel"
)

// StringEnum is an enumeration with string values
Expand Down
2 changes: 1 addition & 1 deletion examples/follow-structs.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package main

import (
"github.com/32leaves/bel"
"github.com/laknoll/bel"
)

// User is a struct describing users
Expand Down
2 changes: 1 addition & 1 deletion examples/name-anon-structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"reflect"

"github.com/32leaves/bel"
"github.com/laknoll/bel"
)

// NestedStuff contains a nested anonymous enum
Expand Down
2 changes: 1 addition & 1 deletion examples/sort-alphabetically.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package main

import (
"github.com/32leaves/bel"
"github.com/laknoll/bel"
)

// StructB is a structure with non-alphabetically sorted fields
Expand Down
4 changes: 2 additions & 2 deletions examples/with-documentation.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package main

import (
"github.com/32leaves/bel"
"github.com/laknoll/bel"
)

// WithDocumentation demonstrates how to extract documentation
func WithDocumentation() {
handler, err := bel.NewParsedSourceDocHandler(".", "github.com/32leaves")
handler, err := bel.NewParsedSourceDocHandler(".", "github.com/laknoll")
if err != nil {
panic(err)
}
Expand Down
52 changes: 47 additions & 5 deletions extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"reflect"
"sort"
"strings"

"github.com/iancoleman/strcase"
"unicode"
"unicode/utf8"
)

// ExtractOption is an option used with the Extract function
Expand All @@ -27,6 +27,7 @@ type extractor struct {
sorter func(a, b interface{}) bool
anonStructNamer AnonStructNamer
typeNamer TypeNamer
primitiveNamer TypeNamer
enumHandler EnumHandler
docHandler DocHandler

Expand Down Expand Up @@ -66,6 +67,16 @@ func CustomNamer(namer TypeNamer) ExtractOption {
}
}

// PrimitiveNamer sets a custom function for translating Golang naming convention
// to Typescript naming convention for primitive types. This function does not have to translate
// the type names, just the way they are written.
// Returning an empty string will cause bel to use the underlying primitive type
func PrimitiveNamer(namer TypeNamer) ExtractOption {
return func(e *extractor) {
e.primitiveNamer = namer
}
}

// WithEnumerations configures an enum handler which detects and extracts enums from
// types and constants.
func WithEnumerations(handler EnumHandler) ExtractOption {
Expand Down Expand Up @@ -113,13 +124,36 @@ func (e *extractor) addResult(t *TypescriptType) {
e.result[t.Name] = *t
}

// typeScriptTypeName turns a Go type name into a valid TypeScript identifier: it
// drops runes TypeScript does not allow in an identifier and upper-cases the first
// one. Dropping matters for instantiated generics, whose reflect name carries the
// type arguments, e.g. Pair[string,int] becomes Pairstringint.
//
// Unlike a general camel-casing this leaves the rest of the name alone, so acronyms
// survive: HTTPServer stays HTTPServer rather than becoming Httpserver.
func typeScriptTypeName(s string) string {
var name strings.Builder
for _, r := range s {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '$' {
name.WriteRune(r)
}
}

res := name.String()
if res == "" {
return res
}
r, size := utf8.DecodeRuneInString(res)
return string(unicode.ToUpper(r)) + res[size:]
}

// Extract uses reflection to extract the information required to generate Typescript code
func Extract(s interface{}, opts ...ExtractOption) ([]TypescriptType, error) {
e := &extractor{
embedStructs: false,
followStructs: false,
typeNamer: func(t reflect.Type) string {
return strcase.ToCamel(t.Name())
return typeScriptTypeName(t.Name())
},
docHandler: (*nullDocHandler)(nil),
}
Expand Down Expand Up @@ -285,7 +319,9 @@ func (e *extractor) extractStructField(t reflect.StructField) (*TypescriptMember
segments = segments[1:]
}
for _, seg := range segments {
if seg == "omitempty" {
// omitzero is the Go 1.24+/encoding/json/v2 equivalent of omitempty:
// both make the field absent from the JSON output.
if seg == "omitempty" || seg == "omitzero" {
optional = true
}
}
Expand Down Expand Up @@ -353,7 +389,7 @@ func (e *extractor) getType(ttype reflect.Type, t *reflect.StructField) (*Typesc
EnumMembers: em,
}
e.addResult(enum)
tstype = &TypescriptType{Name: e.typeNamer(ttype), Kind: TypescriptSimpleKind}
tstype = &TypescriptType{Name: e.typeNamer(ttype), Kind: TypescriptSimpleKind, IsEnum: true}
} else {
res, err := e.getPrimitiveType(ttype)
if err != nil {
Expand All @@ -373,6 +409,12 @@ func (e *extractor) getPrimitiveType(t reflect.Type) (*TypescriptType, error) {
}
}

if e.primitiveNamer != nil {
if name := e.primitiveNamer(t); name != "" {
return mktype(name), nil
}
}

kind := t.Kind()
switch kind {
case reflect.Bool:
Expand Down
Loading