Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: pacedotdev/oto
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.6.0
Choose a base ref
...
head repository: pacedotdev/oto
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.7.0
Choose a head ref
  • 17 commits
  • 13 files changed
  • 1 contributor

Commits on Jul 22, 2020

  1. Copy the full SHA
    a7f49d7 View commit details
  2. Copy the full SHA
    783d17c View commit details
  3. Copy the full SHA
    253f8c0 View commit details
  4. simpler template

    matryer committed Jul 22, 2020
    Copy the full SHA
    63a183f View commit details
  5. TypeScript experiment

    matryer committed Jul 22, 2020
    Copy the full SHA
    c8d466f View commit details
  6. Copy the full SHA
    5bb841b View commit details
  7. updated TypeScript template

    matryer committed Jul 22, 2020
    Copy the full SHA
    51d19db View commit details
  8. added caution note

    matryer committed Jul 22, 2020
    Copy the full SHA
    47a034e View commit details
  9. Copy the full SHA
    1242f79 View commit details
  10. Copy the full SHA
    55c9e1a View commit details
  11. better camelize down function

    matryer committed Jul 22, 2020
    Copy the full SHA
    0b5b07c View commit details
  12. Copy the full SHA
    f483294 View commit details
  13. improved TypeScript client

    matryer committed Jul 22, 2020
    Copy the full SHA
    ab947d8 View commit details
  14. Copy the full SHA
    3ee4cb1 View commit details
  15. Copy the full SHA
    9ca65a3 View commit details

Commits on Jul 23, 2020

  1. fixed camel case bug

    matryer committed Jul 23, 2020
    Copy the full SHA
    df41351 View commit details
  2. Copy the full SHA
    1b83946 View commit details
Showing with 417 additions and 22 deletions.
  1. +2 −1 .gitignore
  2. +24 −2 README.md
  3. +2 −0 main.go
  4. +5 −6 otohttp/templates/client.js.plush
  5. +73 −0 otohttp/templates/client.ts.plush
  6. +79 −5 parser.go
  7. +42 −1 parser_test.go
  8. +28 −7 render.go
  9. +3 −0 render_test.go
  10. +94 −0 split.go
  11. +53 −0 split_test.go
  12. +2 −0 testdata/services/pleasantries/greeter.go
  13. +10 −0 testdata/services/pleasantries/welcomer.go
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -12,4 +12,5 @@
*.out

# binary
oto
oto
.DS_Store
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
Go driven rpc code generation tool for right now.

- 100% Go
- Describe services with Go interfaces
- Describe services with Go interfaces (with structs, methods, comments, etc.)
- Generate server and client code
- Modify the templates to solve your particular needs

@@ -30,12 +30,14 @@ type GreeterService interface {
// GreetRequest is the request object for GreeterService.Greet.
type GreetRequest struct {
// Name is the person to greet.
// example: "Mat Ryer"
Name string
}

// GreetResponse is the response object for GreeterService.Greet.
type GreetResponse struct {
// Greeting is the greeting that was generated.
// example: "Hello Mat Ryer"
Greeting string
}
```
@@ -72,8 +74,10 @@ Implement the service in Go:
// greeter_service.go
package main

// GreeterService makes nice greetings.
type GreeterService struct{}

// Greet makes a greeting.
func (GreeterService) Greet(ctx context.Context, r GreetRequest) (*GreetResponse, error) {
resp := &GreetResponse{
Greeting: "Hello " + r.Name,
@@ -112,7 +116,7 @@ greeterService
.catch(e => alert(e));
```

### Specifying additional template data
## Specifying additional template data

You can provide strings to your templates via the `-params` flag:

@@ -126,6 +130,24 @@ oto \

Within your templates, you may access these strings with `<%= params["key1"] %>`.

## Examples

To provide an example value for a field, you may use the `example:` prefix line
in a comment.

```go
// GreetRequest is the request object for GreeterService.Greet.
type GreetRequest struct {
// Name is the person to greet.
// example: "Mat Ryer"
Name string
}
```

* The example must be valid JSON

The example is extracted and made available via the `Field.Example` field.

## Contributions

Special thank you to:
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -40,10 +40,12 @@ flags:`)
return err
}
if *template == "" {
flags.PrintDefaults()
return errors.New("missing template")
}
params, err := parseParams(*paramsStr)
if err != nil {
flags.PrintDefaults()
return errors.Wrap(err, "params")
}
parser := newParser(flags.Args()...)
11 changes: 5 additions & 6 deletions otohttp/templates/client.js.plush
Original file line number Diff line number Diff line change
@@ -3,21 +3,20 @@
'use strict';

<%= for (service) in def.Services { %>
export class <%= service.Name %> {
<%= format_comment_text(service.Comment) %>export class <%= service.Name %> {
<%= for (method) in service.Methods { %>
async <%= camelize_down(method.Name) %>(<%= camelize_down(method.InputObject.TypeName) %>) {
<%= format_comment_text(method.Comment) %> async <%= camelize_down(method.Name) %>(<%= camelize_down(method.InputObject.TypeName) %>) {
const headers = {
'Accept': 'application/json',
'Accept-Encoding': 'gzip',
'Content-Type': 'application/json',
'Accept': 'application/json',
'Content-Type': 'application/json',
}
<%= camelize_down(method.InputObject.TypeName) %> = <%= camelize_down(method.InputObject.TypeName) %> || {}
const response = await fetch('/oto/<%= service.Name %>.<%= method.Name %>', {
method: 'POST',
headers: headers,
body: JSON.stringify(<%= camelize_down(method.InputObject.TypeName) %>)
})
return response.json().then((json) => {
return response.json().then(json => {
if (json.error) {
throw new Error(json.error)
}
73 changes: 73 additions & 0 deletions otohttp/templates/client.ts.plush
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Code generated by oto; DO NOT EDIT.

// HeadersFunc allows you to mutate headers for each request.
// Useful for adding authorization into the client.
interface HeadersFunc {
(headers: HeadersInit);
}

// Client provides access to remote services.
export class Client {
// basepath is the path prefix for the requests.
// This may be a path, or an absolute URL.
public basepath: String = '/oto/'
// headers allows calling code to mutate the HTTP
// headers of the underlying HTTP requests.
public headers: HeadersFunc
}

<%= for (service) in def.Services { %>
<%= format_comment_text(service.Comment) %>export class <%= service.Name %> {
constructor(readonly client: Client) {}
<%= for (method) in service.Methods { %>
<%= format_comment_text(method.Comment) %> async <%= camelize_down(method.Name) %>(<%= camelize_down(method.InputObject.TypeName) %>: <%= method.InputObject.TypeName %> = null) {
if (<%= camelize_down(method.InputObject.TypeName) %> == null) {
<%= camelize_down(method.InputObject.TypeName) %> = new <%= method.InputObject.TypeName %>();
}
const headers: HeadersInit = new Headers();
headers.set('Accept', 'application/json');
headers.set('Content-Type', 'application/json');
await this.client.headers(headers);
const response = await fetch(this.client.basepath + '<%= service.Name %>.<%= method.Name %>', {
method: 'POST',
headers: headers,
body: JSON.stringify(<%= camelize_down(method.InputObject.TypeName) %>),
})
return response.json().then((json) => {
if (json.error) {
throw new Error(json.error);
}
return new <%= method.OutputObject.TypeName %>(json);
})
}
<% } %>
}
<% } %>

<%= for (object) in def.Objects { %>
<%= format_comment_text(object.Comment) %>export class <%= object.Name %> {
constructor(data?: any) {
if (data) {
<%= for (field) in object.Fields { %>
<%= if (field.Type.IsObject) { %>
<%= if (field.Type.Multiple) { %>
if (data.<%= camelize_down(field.Name) %>) {
this.<%= camelize_down(field.Name) %> = new Array<<%= field.Type.ObjectName() %>>()
for (let i = 0; i < data.<%= camelize_down(field.Name) %>.length; i++) {
this.<%= camelize_down(field.Name) %>.push(new <%= field.Type.ObjectName() %>(data.<%= camelize_down(field.Name) %>[i]));
}
}
<% } else { %>
this.<%= camelize_down(field.Name) %> = new <%= field.Type.ObjectName() %>(data.<%= camelize_down(field.Name) %>);
<% } %>
<% } else { %>
this.<%= camelize_down(field.Name) %> = data.<%= camelize_down(field.Name) %>;
<% } %>
<% } %>
}
}
<%= for (field) in object.Fields { %>
<%= format_comment_text(field.Comment) %> <%= camelize_down(field.Name) %>: <%= if (field.Type.IsObject) { %><%= field.Type.TypeName %><% } else { %><%= field.Type.JSType() %><% } %><%= if (field.Type.Multiple) { %>[]<% } %>;
<% } %>
}
<% } %>
84 changes: 79 additions & 5 deletions parser.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"bufio"
"encoding/json"
"fmt"
"go/ast"
"go/doc"
@@ -9,6 +11,7 @@ import (
"sort"
"strings"

"github.com/fatih/structtag"
"github.com/pkg/errors"
"golang.org/x/tools/go/packages"
)
@@ -66,11 +69,22 @@ type Object struct {

// Field describes the field inside an Object.
type Field struct {
Name string `json:"name"`
Type FieldType `json:"type"`
OmitEmpty bool `json:"omitEmpty"`
Comment string `json:"comment"`
Tag string `json:"tag"`
Name string `json:"name"`
Type FieldType `json:"type"`
OmitEmpty bool `json:"omitEmpty"`
Comment string `json:"comment"`
Tag string `json:"tag"`
ParsedTags map[string]FieldTag `json:"parsedTags"`
Example interface{} `json:"example"`
}

// FieldTag is a parsed tag.
// For more information, see Struct Tags in Go.
type FieldTag struct {
// Value is the value of the tag.
Value string `json:"value"`
// Options are the options for the tag.
Options []string `json:"options"`
}

// FieldType holds information about the type of data that this
@@ -105,6 +119,13 @@ func (f FieldType) JSType() (string, error) {
return "", errors.Errorf("oto: type not supported: %s", f.TypeName)
}

// ObjectName gets the namespace-free object name of the type.
// For imported packages, the package name is stripped.
func (f FieldType) ObjectName() string {
segs := strings.Split(f.TypeName, ".")
return segs[len(segs)-1]
}

type parser struct {
Verbose bool

@@ -266,13 +287,32 @@ func (p *parser) parseObject(pkg *packages.Package, o types.Object, v *types.Str
return err
}
field.Tag = v.Tag(i)
field.ParsedTags, err = p.parseTags(field.Tag)
if err != nil {
return errors.Wrap(err, "parse field tag")
}
obj.Fields = append(obj.Fields, field)
}
p.def.Objects = append(p.def.Objects, obj)
p.objects[obj.Name] = struct{}{}
return nil
}

func (p *parser) parseTags(tag string) (map[string]FieldTag, error) {
tags, err := structtag.Parse(tag)
if err != nil {
return nil, err
}
fieldTags := make(map[string]FieldTag)
for _, tag := range tags.Tags() {
fieldTags[tag.Key] = FieldTag{
Value: tag.Name,
Options: tag.Options,
}
}
return fieldTags, nil
}

func (p *parser) parseField(pkg *packages.Package, objectName string, v *types.Var) (Field, error) {
var f Field
f.Name = v.Name()
@@ -281,6 +321,10 @@ func (p *parser) parseField(pkg *packages.Package, objectName string, v *types.V
return f, p.wrapErr(errors.New(f.Name+" must be exported"), pkg, v.Pos())
}
var err error
f.Example, f.Comment, err = extractExample(f.Comment)
if err != nil {
return f, p.wrapErr(errors.New("extract comment example"), pkg, v.Pos())
}
f.Type, err = p.parseFieldType(pkg, v)
if err != nil {
return f, errors.Wrap(err, "parse type")
@@ -436,3 +480,33 @@ outer:
func cleanComment(s string) string {
return strings.TrimSpace(s)
}

// extractExample extracts the example from the comment.
// It returns a typed example, and the remaining
// comment string.
// The example should be on the last line.
func extractExample(comment string) (interface{}, string, error) {
var lines []string
const exampleCommentPrefix = "example:"
s := bufio.NewScanner(strings.NewReader(comment))
for s.Scan() {
line := strings.TrimSpace(s.Text())
if strings.HasPrefix(line, exampleCommentPrefix) {
line = strings.TrimSpace(strings.TrimPrefix(line, exampleCommentPrefix))
if line == "" {
return nil, strings.Join(lines, "\n"), nil
}
var val interface{}
if err := json.Unmarshal([]byte(line), &val); err != nil {
return nil, "", err
}
return val, strings.Join(lines, "\n"), nil
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
lines = append(lines, line)
}
return nil, strings.Join(lines, "\n"), nil
}
43 changes: 42 additions & 1 deletion parser_test.go
Original file line number Diff line number Diff line change
@@ -52,6 +52,11 @@ You will love it.`)
is.Equal(greetInputObject.Fields[0].Type.Multiple, false)
is.Equal(greetInputObject.Fields[0].Type.Package, "github.com/pacedotdev/oto/testdata/services")
is.Equal(greetInputObject.Fields[0].Tag, `tagtest:"value,option1,option2"`)
is.True(greetInputObject.Fields[0].ParsedTags != nil)
is.Equal(greetInputObject.Fields[0].ParsedTags["tagtest"].Value, "value")
is.Equal(len(greetInputObject.Fields[0].ParsedTags["tagtest"].Options), 2)
is.Equal(greetInputObject.Fields[0].ParsedTags["tagtest"].Options[0], "option1")
is.Equal(greetInputObject.Fields[0].ParsedTags["tagtest"].Options[1], "option2")

greetOutputObject, err := def.Object(def.Services[0].Methods[0].OutputObject.TypeName)
is.NoErr(err)
@@ -82,17 +87,24 @@ You will love it.`)
welcomeInputObject, err := def.Object(def.Services[1].Methods[0].InputObject.TypeName)
is.NoErr(err)
is.Equal(welcomeInputObject.Name, "WelcomeRequest")
is.Equal(len(welcomeInputObject.Fields), 2)
is.Equal(len(welcomeInputObject.Fields), 4)

is.Equal(welcomeInputObject.Fields[0].Name, "To")
is.Equal(welcomeInputObject.Fields[0].OmitEmpty, false)
is.Equal(welcomeInputObject.Fields[0].Type.TypeName, "string")
is.Equal(welcomeInputObject.Fields[0].Type.Multiple, false)
is.Equal(welcomeInputObject.Fields[0].Type.Package, "")
is.Equal(welcomeInputObject.Fields[0].Example, "your@email.com")

is.Equal(welcomeInputObject.Fields[1].Name, "Name")
is.Equal(welcomeInputObject.Fields[1].OmitEmpty, false)
is.Equal(welcomeInputObject.Fields[1].Type.TypeName, "string")
is.Equal(welcomeInputObject.Fields[1].Type.Multiple, false)
is.Equal(welcomeInputObject.Fields[1].Type.Package, "")
is.Equal(welcomeInputObject.Fields[1].Example, "John Smith")

is.Equal(welcomeInputObject.Fields[2].Example, float64(3))
is.Equal(welcomeInputObject.Fields[3].Example, true)

welcomeOutputObject, err := def.Object(def.Services[1].Methods[0].OutputObject.TypeName)
is.NoErr(err)
@@ -150,3 +162,32 @@ func TestFieldJSType(t *testing.T) {
}
}
}

func TestExtractExample(t *testing.T) {
is := is.New(t)

example, comment, err := extractExample(`
This is a comment
example: "With an example"
`)
is.NoErr(err)
is.Equal(comment, "This is a comment")
is.Equal(example, "With an example")

example, comment, err = extractExample(`
This is a comment
example: true
`)
is.NoErr(err)
is.Equal(comment, "This is a comment")
is.Equal(example, true)

example, comment, err = extractExample(`
This is a comment
example: 123
`)
is.NoErr(err)
is.Equal(comment, "This is a comment")
is.Equal(example, float64(123))

}
Loading