├── README.md ├── bind.go ├── bind_test.go ├── circle.yml └── example └── example.go /README.md: -------------------------------------------------------------------------------- 1 | # bindgraphql [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://godoc.org/github.com/ariefrahmansyah/bindgraphql) [![CircleCI](https://circleci.com/gh/ariefrahmansyah/bindgraphql/tree/master.png?style=shield)](https://circleci.com/gh/ariefrahmansyah/bindgraphql/tree/master) [![Coverage Status](https://coveralls.io/repos/github/ariefrahmansyah/bindgraphql/badge.svg?branch=master)](https://coveralls.io/github/ariefrahmansyah/bindgraphql?branch=master) [![GoReportCard](https://goreportcard.com/badge/github.com/ariefrahmansyah/bindgraphql)](https://goreportcard.com/report/github.com/ariefrahmansyah/bindgraphql) 2 | 3 | You have RESTful API. You have your struct, it use JSON tag. Then, one of your developer friend introduce you to GraphQL. 4 | 5 | If you want to migrate your API to GraphQL without so much pain, this library is for you. 6 | 7 | ```go 8 | import "github.com/ariefrahmansyah/bindgraphql" 9 | 10 | graphObj, err := bind.NewObject("YourObj", yourObj) 11 | ``` 12 | 13 | # Example 14 | See [example.go](https://github.com/ariefrahmansyah/bindgraphql/blob/master/example/example.go). 15 | 16 | # Contributor 17 | * [Ahmad Muzakki](https://gist.github.com/ahmadmuzakki29/73fc1e21bf7ae087a9ac53299032f09c) 18 | -------------------------------------------------------------------------------- /bind.go: -------------------------------------------------------------------------------- 1 | package bindgraphql 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | 7 | "github.com/graphql-go/graphql" 8 | ) 9 | 10 | // NewObject returns new *graphql.Object instance. 11 | // graphObj, err := bind.NewObject("YourObj", yourObj) 12 | func NewObject(name string, obj interface{}) (*graphql.Object, error) { 13 | fields, err := NewFields(obj) 14 | if err != nil { 15 | return &graphql.Object{}, err 16 | } 17 | 18 | graphObj := graphql.NewObject(graphql.ObjectConfig{ 19 | Name: name, 20 | Fields: fields, 21 | }) 22 | 23 | return graphObj, nil 24 | } 25 | 26 | // NewFields returns new graphql.Fields that ready 27 | // to be used by your graphql.Object. 28 | // graphFields, err := bind.NewFields(yourObj) 29 | // graphObj := graphql.NewObject(graphql.ObjectConfig{ 30 | // Name: "YourObj", 31 | // Fields: graphFields, 32 | // }) 33 | func NewFields(obj interface{}) (graphql.Fields, error) { 34 | graphFields := graphql.Fields{} 35 | 36 | val := reflect.ValueOf(obj) 37 | 38 | for i := 0; i < val.NumField(); i++ { 39 | field := val.Type().Field(i) 40 | tag := getTag(field) 41 | 42 | if skip(tag) { 43 | continue 44 | } 45 | 46 | if _, ok := graphFields[tag]; ok && tag != "ID" { 47 | return graphql.Fields{}, errors.New("duplicate tag of " + tag) 48 | } 49 | 50 | if tag == "" { 51 | if field.Type.Kind() == reflect.Struct { 52 | structFields, err := NewFields(val.Field(i).Interface()) 53 | if err != nil { 54 | return graphql.Fields{}, err 55 | } 56 | 57 | appendFields(graphFields, structFields) 58 | } 59 | 60 | continue 61 | } 62 | 63 | graphFields[tag] = &graphql.Field{ 64 | Type: getGraphType(tag, field.Type.Kind()), 65 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 66 | return getResolve(tag, p.Source), nil 67 | }, 68 | } 69 | 70 | } 71 | 72 | return graphFields, nil 73 | } 74 | 75 | func skip(tag string) bool { 76 | return tag == "-" 77 | } 78 | 79 | func getTag(sf reflect.StructField) string { 80 | tag := sf.Tag.Get("graph") 81 | 82 | if tag == "" { 83 | tag = sf.Tag.Get("json") 84 | } 85 | 86 | return tag 87 | } 88 | 89 | func appendFields(dest, source graphql.Fields) error { 90 | for k, v := range source { 91 | dest[k] = v 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func getGraphType(tag string, fieldKind reflect.Kind) *graphql.Scalar { 98 | if tag == "ID" { 99 | return graphql.ID 100 | } 101 | 102 | switch fieldKind { 103 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 104 | return graphql.Int 105 | case reflect.Float32, reflect.Float64: 106 | return graphql.Float 107 | case reflect.Bool: 108 | return graphql.Boolean 109 | } 110 | 111 | return graphql.String 112 | } 113 | 114 | func getResolve(fieldTag string, obj interface{}) interface{} { 115 | val := reflect.ValueOf(obj) 116 | 117 | for i := 0; i < val.NumField(); i++ { 118 | field := val.Type().Field(i) 119 | tag := getTag(field) 120 | 121 | if skip(tag) { 122 | continue 123 | } 124 | 125 | if tag == fieldTag { 126 | return val.Field(i).Interface() 127 | } 128 | 129 | if field.Type.Kind() == reflect.Struct { 130 | if res := getResolve(fieldTag, val.Field(i).Interface()); res != nil { 131 | return res 132 | } 133 | } 134 | } 135 | 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /bind_test.go: -------------------------------------------------------------------------------- 1 | package bindgraphql 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/graphql-go/graphql" 8 | ) 9 | 10 | type child struct { 11 | StringType string `graph:"string_type"` 12 | } 13 | 14 | type child2 struct { 15 | IntType int `graph:"int_type"` 16 | Int32Type int32 `graph:"int_type"` 17 | } 18 | 19 | type dummy struct { 20 | Skip string `graph:"-"` 21 | ID int64 `graph:"ID"` 22 | IntType int `graph:"int_type"` 23 | Child child 24 | } 25 | 26 | type dummy2 struct { 27 | IntType int `graph:"int_type"` 28 | Child child 29 | Child2 child2 30 | } 31 | 32 | func mockResolve(v interface{}) (interface{}, error) { 33 | return v, nil 34 | } 35 | 36 | func TestNewObject(t *testing.T) { 37 | type args struct { 38 | name string 39 | obj interface{} 40 | } 41 | tests := []struct { 42 | name string 43 | args args 44 | want *graphql.Object 45 | wantErr bool 46 | }{ 47 | { 48 | "NewObject1", 49 | args{ 50 | name: "obj1", 51 | obj: dummy{ 52 | ID: int64(1), 53 | IntType: int(100), 54 | Child: child{ 55 | StringType: "child", 56 | }, 57 | }, 58 | }, 59 | graphql.NewObject(graphql.ObjectConfig{ 60 | Name: "obj1", 61 | Fields: graphql.Fields{ 62 | "ID": &graphql.Field{ 63 | Type: graphql.ID, 64 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 65 | return int64(1), nil 66 | }, 67 | }, 68 | "int_type": &graphql.Field{ 69 | Type: graphql.Int, 70 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 71 | return int(100), nil 72 | }, 73 | }, 74 | "string_type": &graphql.Field{ 75 | Type: graphql.String, 76 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 77 | return "child", nil 78 | }, 79 | }, 80 | }, 81 | }), 82 | false, 83 | }, 84 | { 85 | "NewObject2", 86 | args{ 87 | name: "obj2", 88 | obj: dummy2{ 89 | IntType: int(100), 90 | Child: child{ 91 | StringType: "child", 92 | }, 93 | Child2: child2{ 94 | IntType: int(200), 95 | Int32Type: int32(300), 96 | }, 97 | }, 98 | }, 99 | graphql.NewObject(graphql.ObjectConfig{ 100 | Name: "obj1", 101 | Fields: graphql.Fields{}, 102 | }), 103 | true, 104 | }, 105 | } 106 | for _, tt := range tests { 107 | t.Run(tt.name, func(t *testing.T) { 108 | _, err := NewObject(tt.args.name, tt.args.obj) 109 | if (err != nil) != tt.wantErr { 110 | t.Errorf("NewObject() error = %v, wantErr %v", err, tt.wantErr) 111 | return 112 | } 113 | }) 114 | } 115 | } 116 | 117 | func TestNewFields(t *testing.T) { 118 | type args struct { 119 | obj interface{} 120 | } 121 | tests := []struct { 122 | name string 123 | args args 124 | want graphql.Fields 125 | wantErr bool 126 | }{ 127 | { 128 | "NewFields1", 129 | args{ 130 | obj: dummy{ 131 | ID: int64(1), 132 | IntType: int(100), 133 | Child: child{ 134 | StringType: "child", 135 | }, 136 | }, 137 | }, 138 | graphql.Fields{ 139 | "ID": &graphql.Field{ 140 | Type: graphql.ID, 141 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 142 | return int64(1), nil 143 | }, 144 | }, 145 | "int_type": &graphql.Field{ 146 | Type: graphql.Int, 147 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 148 | return int(100), nil 149 | }, 150 | }, 151 | "string_type": &graphql.Field{ 152 | Type: graphql.String, 153 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 154 | return "child", nil 155 | }, 156 | }, 157 | }, 158 | false, 159 | }, 160 | { 161 | "NewFields2", 162 | args{ 163 | obj: dummy2{ 164 | IntType: int(100), 165 | Child: child{ 166 | StringType: "child", 167 | }, 168 | Child2: child2{ 169 | IntType: int(200), 170 | Int32Type: int32(300), 171 | }, 172 | }, 173 | }, 174 | graphql.Fields{}, 175 | true, 176 | }, 177 | } 178 | for _, tt := range tests { 179 | t.Run(tt.name, func(t *testing.T) { 180 | _, err := NewFields(tt.args.obj) 181 | if (err != nil) != tt.wantErr { 182 | t.Errorf("NewFields() error = %v, wantErr %v", err, tt.wantErr) 183 | return 184 | } 185 | }) 186 | } 187 | } 188 | 189 | func TestSkip(t *testing.T) { 190 | type args struct { 191 | tag string 192 | } 193 | tests := []struct { 194 | name string 195 | args args 196 | want bool 197 | }{ 198 | {"skip1", args{"-"}, true}, 199 | {"skip2", args{"id"}, false}, 200 | {"skip3", args{"payment_id"}, false}, 201 | } 202 | for _, tt := range tests { 203 | t.Run(tt.name, func(t *testing.T) { 204 | if got := skip(tt.args.tag); got != tt.want { 205 | t.Errorf("skip() = %v, want %v", got, tt.want) 206 | } 207 | }) 208 | } 209 | } 210 | 211 | func TestGetTag(t *testing.T) { 212 | type args struct { 213 | sf reflect.StructField 214 | } 215 | tests := []struct { 216 | name string 217 | args args 218 | want string 219 | }{ 220 | {"getTagGraph1", args{reflect.StructField{Tag: `graph:"id"`}}, "id"}, 221 | {"getTagGraph2", args{reflect.StructField{Tag: `graph:"payment_id"`}}, "payment_id"}, 222 | {"getTagGraph3", args{reflect.StructField{Tag: `graph:"user_id"`}}, "user_id"}, 223 | 224 | {"getTagJSON1", args{reflect.StructField{Tag: `json:"id"`}}, "id"}, 225 | {"getTagJSON2", args{reflect.StructField{Tag: `json:"payment_id"`}}, "payment_id"}, 226 | {"getTagJSON3", args{reflect.StructField{Tag: `json:"user_id"`}}, "user_id"}, 227 | } 228 | for _, tt := range tests { 229 | t.Run(tt.name, func(t *testing.T) { 230 | if got := getTag(tt.args.sf); got != tt.want { 231 | t.Errorf("getTag() = %v, want %v", got, tt.want) 232 | } 233 | }) 234 | } 235 | } 236 | 237 | func TestAppendFields(t *testing.T) { 238 | type args struct { 239 | dest graphql.Fields 240 | source graphql.Fields 241 | } 242 | tests := []struct { 243 | name string 244 | args args 245 | wantErr bool 246 | }{ 247 | {"AppendFields1", args{graphql.Fields{}, graphql.Fields{}}, false}, 248 | {"AppendFields2", args{graphql.Fields{}, graphql.Fields{"id": &graphql.Field{Type: graphql.ID}}}, false}, 249 | } 250 | for _, tt := range tests { 251 | t.Run(tt.name, func(t *testing.T) { 252 | if err := appendFields(tt.args.dest, tt.args.source); (err != nil) != tt.wantErr { 253 | t.Errorf("appendFields() error = %v, wantErr %v", err, tt.wantErr) 254 | } 255 | }) 256 | } 257 | } 258 | 259 | func TestGetGraphType(t *testing.T) { 260 | type args struct { 261 | tag string 262 | fieldType reflect.Kind 263 | } 264 | tests := []struct { 265 | name string 266 | args args 267 | want *graphql.Scalar 268 | }{ 269 | {"GetGraphTypeID", args{"ID", reflect.Int}, graphql.ID}, 270 | 271 | {"GetGraphTypeInt", args{"int", reflect.Int}, graphql.Int}, 272 | {"GetGraphTypeInt8", args{"int8", reflect.Int8}, graphql.Int}, 273 | {"GetGraphTypeInt16", args{"int16", reflect.Int16}, graphql.Int}, 274 | {"GetGraphTypeInt32", args{"int32", reflect.Int32}, graphql.Int}, 275 | {"GetGraphTypeInt64", args{"int64", reflect.Int64}, graphql.Int}, 276 | 277 | {"GetGraphTypeFloat32", args{"float32", reflect.Float32}, graphql.Float}, 278 | {"GetGraphTypeFloat64", args{"float64", reflect.Float64}, graphql.Float}, 279 | 280 | {"GetGraphTypeBool", args{"bool", reflect.Bool}, graphql.Boolean}, 281 | 282 | {"GetGraphTypeString", args{"string", reflect.String}, graphql.String}, 283 | } 284 | for _, tt := range tests { 285 | t.Run(tt.name, func(t *testing.T) { 286 | if got := getGraphType(tt.args.tag, tt.args.fieldType); !reflect.DeepEqual(got, tt.want) { 287 | t.Errorf("getGraphType() = %v, want %v", got, tt.want) 288 | } 289 | }) 290 | } 291 | } 292 | 293 | func TestGetResolve(t *testing.T) { 294 | type args struct { 295 | fieldTag string 296 | obj interface{} 297 | } 298 | tests := []struct { 299 | name string 300 | args args 301 | want interface{} 302 | }{ 303 | {"GetResolveSkip", args{fieldTag: "-", obj: dummy{Skip: "skip"}}, nil}, 304 | 305 | {"GetResolveID", args{fieldTag: "ID", obj: dummy{ID: int64(1)}}, int64(1)}, 306 | {"GetResolveInt", args{fieldTag: "int_type", obj: dummy{IntType: int(1)}}, int(1)}, 307 | 308 | {"GetResolveChildString", args{fieldTag: "string_type", obj: dummy{Child: child{StringType: "child"}}}, "child"}, 309 | } 310 | for _, tt := range tests { 311 | t.Run(tt.name, func(t *testing.T) { 312 | if got := getResolve(tt.args.fieldTag, tt.args.obj); !reflect.DeepEqual(got, tt.want) { 313 | t.Errorf("getResolve() = %v, want %v", got, tt.want) 314 | } 315 | }) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | pre: 3 | - go get github.com/mattn/goveralls 4 | override: 5 | - go test -v -cover -race -coverprofile=/home/ubuntu/coverage.out 6 | post: 7 | - /home/ubuntu/.go_workspace/bin/goveralls -coverprofile=/home/ubuntu/coverage.out -service=circle-ci -repotoken=$COVERALLS_REPO_TOKEN -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | 7 | bind "github.com/ariefrahmansyah/bindgraphql" 8 | "github.com/graphql-go/graphql" 9 | ) 10 | 11 | type exampleStruct struct { 12 | ID int64 `graph:"ID"` 13 | IntType int `graph:"int_type"` 14 | Int8Type int8 `graph:"int8_type"` 15 | Int16Type int16 `graph:"int16_type"` 16 | Int32Type int32 `graph:"int32_type"` 17 | Int64Type int64 `graph:"int64_type"` 18 | Float32Type float32 `graph:"float32_type"` 19 | Float64Type float64 `graph:"float64_type"` 20 | BoolType bool `graph:"bool_type"` 21 | StringType string `graph:"string_type"` 22 | } 23 | 24 | var example = exampleStruct{ 25 | ID: 1, 26 | IntType: 1, 27 | Int8Type: 8, 28 | Int16Type: 16, 29 | Int32Type: 32, 30 | Int64Type: 64, 31 | Float32Type: 32, 32 | Float64Type: 64, 33 | BoolType: true, 34 | StringType: "string", 35 | } 36 | 37 | func main() { 38 | log.SetFlags(log.LstdFlags | log.Lshortfile) 39 | 40 | exampleGraphFields, err := bind.NewFields(example) 41 | if err != nil { 42 | log.Println(err) 43 | } 44 | log.Printf("%+v\n", exampleGraphFields) 45 | 46 | exampleObject := graphql.NewObject(graphql.ObjectConfig{ 47 | Name: "Example", 48 | Fields: exampleGraphFields, 49 | }) 50 | log.Printf("%+v\n", exampleObject) 51 | 52 | // You also can create new graphql.Object by using bind.NewObject() 53 | // exampleObject2, err := bind.NewObject("Example", example) 54 | // if err != nil { 55 | // log.Println(err) 56 | // } 57 | // log.Printf("%+v\n", exampleObject2) 58 | 59 | query := graphql.NewObject(graphql.ObjectConfig{ 60 | Name: "Query", 61 | Fields: graphql.Fields{ 62 | "example": &graphql.Field{ 63 | Type: exampleObject, 64 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 65 | return example, nil 66 | }, 67 | }, 68 | }, 69 | }) 70 | log.Printf("%+v\n", query) 71 | 72 | schema, err := graphql.NewSchema(graphql.SchemaConfig{ 73 | Query: query, 74 | }) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | log.Printf("%+v\n", schema) 79 | 80 | request := ` 81 | { 82 | example { 83 | ID, 84 | int_type, 85 | int8_type, 86 | int16_type, 87 | int32_type, 88 | int64_type, 89 | float32_type, 90 | float64_type, 91 | bool_type, 92 | string_type 93 | } 94 | } 95 | ` 96 | 97 | params := graphql.Params{Schema: schema, RequestString: request} 98 | resp := graphql.Do(params) 99 | if len(resp.Errors) > 0 { 100 | log.Fatalf("failed to execute graphql operation, errors: %+v\n", resp.Errors) 101 | } 102 | 103 | respJSON, _ := json.Marshal(resp) 104 | log.Println(string(respJSON)) 105 | } 106 | --------------------------------------------------------------------------------