├── .github └── workflows │ ├── cov.yml │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── apollo_tracing.go ├── assert ├── README.md ├── assert.go ├── assertion_compare.go ├── assertion_format.go └── difflib │ └── difflib.go ├── banner.png ├── bytecode ├── README.md ├── bytecode.go ├── bytecode_benchmark_test.go ├── bytecode_instructions.go ├── bytecode_test.go ├── cache │ └── bytecode_cache.go └── testing_framework.go ├── copy_schema.go ├── directives.go ├── enums.go ├── enums_test.go ├── examples ├── fiber │ ├── .gitignore │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── schema.go ├── gin │ ├── .gitignore │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── schema.go └── relay │ ├── README.md │ ├── backend │ ├── .gitignore │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── schema.go │ └── frontend │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── schema.graphql │ └── src │ ├── App.js │ ├── RelayEnvironment.js │ ├── Todo.js │ ├── __generated__ │ ├── AppCreateTodoMutation.graphql.js │ ├── AppQuery.graphql.js │ ├── TodoDeleteMutation.graphql.js │ ├── TodoFragment.graphql.js │ └── TodoUpdateMutation.graphql.js │ ├── index.css │ └── index.js ├── go.mod ├── go.sum ├── grahql_types.go ├── helpers ├── encodeFloat.go ├── encodeString.go ├── encode_string_benchmark_test.go ├── pointers.go └── time.go ├── implement_helpers.go ├── implement_helpers_test.go ├── inject_schema.go ├── interfaces.go ├── interfaces_test.go ├── parse.go ├── parse_benchmark_test.go ├── parse_test.go ├── readme_test.go ├── resolver.go ├── resolver_benchmark_test.go ├── resolver_test.go ├── tester ├── tester.go └── tester_test.go ├── type_rename.go └── type_rename_test.go /.github/workflows/cov.yml: -------------------------------------------------------------------------------- 1 | name: Test Coveralls 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: 1.17 18 | 19 | - name: Test 20 | run: go test -v -covermode=count -coverprofile=coverage.cov ./... 21 | 22 | - name: Send coverage 23 | uses: shogo82148/actions-goveralls@v1 24 | with: 25 | path-to-profile: coverage.cov 26 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | go: [1.17, 1.15, 1.13] 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go }} 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | debug.test 2 | memprofile 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mark Kopenga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Banner](https://github.com/mjarkk/yarql/blob/main/banner.png?raw=true) 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/mjarkk/yarql.svg)](https://pkg.go.dev/github.com/mjarkk/yarql) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/mjarkk/yarql)](https://goreportcard.com/report/github.com/mjarkk/yarql) 5 | [![Coverage Status](https://coveralls.io/repos/github/mjarkk/go-graphql/badge.svg?branch=main)](https://coveralls.io/github/mjarkk/go-graphql?branch=main) 6 | 7 | # YarQL, A Graphql library for GoLang 8 | 9 | Just a different approach to making graphql servers in Go 10 | 11 | ## Features 12 | 13 | - Easy to use and not much code required 14 | - Schema based on code 15 | - Build on top of the [graphql spec 2021](https://spec.graphql.org/October2021/) 16 | - No code generators 17 | - [Only 1 dependency](go.mod) 18 | - Easy to implement in many web servers, see the 19 | [gin](https://github.com/mjarkk/yarql/blob/main/examples/gin/main.go) and 20 | [fiber](https://github.com/mjarkk/yarql/blob/main/examples/fiber/main.go) 21 | examples 22 | - [File upload support](#file-upload) 23 | - Supports [Apollo tracing](https://github.com/apollographql/apollo-tracing) 24 | - [Fast](#Performance) 25 | 26 | ## Example 27 | 28 | See the [/examples](https://github.com/mjarkk/yarql/tree/main/examples) folder 29 | for more examples 30 | 31 | ```go 32 | package main 33 | 34 | import ( 35 | "log" 36 | "github.com/mjarkk/yarql" 37 | ) 38 | 39 | type Post struct { 40 | Id uint `gq:",ID"` 41 | Title string `gq:"name"` 42 | } 43 | 44 | type QueryRoot struct{} 45 | 46 | func (QueryRoot) ResolvePosts() []Post { 47 | return []Post{ 48 | {1, "post 1"}, 49 | {2, "post 2"}, 50 | {3, "post 3"}, 51 | } 52 | } 53 | 54 | type MethodRoot struct{} 55 | 56 | func main() { 57 | s := yarql.NewSchema() 58 | 59 | err := s.Parse(QueryRoot{}, MethodRoot{}, nil) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | errs := s.Resolve([]byte(` 65 | { 66 | posts { 67 | id 68 | name 69 | } 70 | } 71 | `), yarql.ResolveOptions{}) 72 | for _, err := range errs { 73 | log.Fatal(err) 74 | } 75 | 76 | fmt.Println(string(s.Result)) 77 | // {"data": { 78 | // "posts": [ 79 | // {"id": "1", "name": "post 1"}, 80 | // {"id": "2", "name": "post 2"}, 81 | // {"id": "3", "name": "post 3"} 82 | // ] 83 | // },"errors":[],"extensions":{}} 84 | } 85 | ``` 86 | 87 | ## Docs 88 | 89 | ### Defining a field 90 | 91 | All fields names are by default changed to graphql names, for example `VeryNice` 92 | changes to `veryNice`. There is one exception to the rule when the second letter 93 | is also upper case like `FOO` will stay `FOO` 94 | 95 | In a struct: 96 | 97 | ```go 98 | struct { 99 | A string 100 | } 101 | ``` 102 | 103 | A resolver function inside the a struct: 104 | 105 | ```go 106 | struct { 107 | A func() string 108 | } 109 | ``` 110 | 111 | A resolver attached to the struct. 112 | 113 | Name Must start with `Resolver` followed by one uppercase letter 114 | 115 | _The resolve identifier is trimmed away in the graphql name_ 116 | 117 | ```go 118 | type A struct {} 119 | func (A) ResolveA() string {return "Ahh yea"} 120 | ``` 121 | 122 | ### Supported input and output value types 123 | 124 | These go data kinds should be globally accepted: 125 | 126 | - `bool` 127 | - `int` _all bit sizes_ 128 | - `uint` _all bit sizes_ 129 | - `float` _all bit sizes_ 130 | - `array` 131 | - `ptr` 132 | - `string` 133 | - `struct` 134 | 135 | There are also special values: 136 | 137 | - `time.Time` _converted from/to ISO 8601_ 138 | - `*multipart.FileHeader` _get file from multipart form_ 139 | 140 | ### Ignore fields 141 | 142 | ```go 143 | struct { 144 | // internal fields are ignored 145 | bar string 146 | 147 | // ignore public fields 148 | Bar string `gq:"-"` 149 | } 150 | ``` 151 | 152 | ### Rename field 153 | 154 | ```go 155 | struct { 156 | // Change the graphql field name to "bar" 157 | Foo string `gq:"bar"` 158 | } 159 | ``` 160 | 161 | ### Label as ID field 162 | 163 | ```go 164 | struct Foo { 165 | // Notice the "," before the id 166 | Id string `gq:",id"` 167 | 168 | // Pointers and numbers are also supported 169 | // NOTE NUMBERS WILL BE CONVERTED TO STRINGS IN OUTPUT 170 | PostId *int `gq:",id"` 171 | } 172 | 173 | // Label method response as ID using AttrIsID 174 | // The value returned for AttrIsID is ignored 175 | // You can also still just fine append an error: (string, AttrIsID, error) 176 | func (Foo) ResolveExampleMethod() (string, AttrIsID) { 177 | return "i'm an ID type", 0 178 | } 179 | ``` 180 | 181 | ### Methods and field arguments 182 | 183 | Add a struct to the arguments of a resolver or func field to define arguments 184 | 185 | ```go 186 | func (A) ResolveUserID(args struct{ Id int }) int { 187 | return args.Id 188 | } 189 | ``` 190 | 191 | ### Resolver error response 192 | 193 | You can add an error response argument to send back potential errors. 194 | 195 | These errors will appear in the errors array of the response. 196 | 197 | ```go 198 | func (A) ResolveMe() (*User, error) { 199 | me, err := fetchMe() 200 | return me, err 201 | } 202 | ``` 203 | 204 | ### Context 205 | 206 | You can add `*yarql.Ctx` to every resolver of func field to get more information 207 | about the request or user set properties 208 | 209 | #### Context values 210 | 211 | The context can store values defined by a key. You can add values by using the 212 | 'SetVelue' method and obtain values using the `GetValue` method 213 | 214 | ```go 215 | func (A) ResolveMe(ctx *yarql.Ctx) User { 216 | ctx.SetValue("resolved_me", true) 217 | return ctx.GetValue("me").(User) 218 | } 219 | ``` 220 | 221 | You can also provide values to the `RequestOptions`: 222 | 223 | ```go 224 | yarql.RequestOptions{ 225 | Values: map[string]interface{}{ 226 | "key": "value", 227 | }, 228 | } 229 | ``` 230 | 231 | #### GoLang context 232 | 233 | You can also have a GoLang context attached to our context (`yarql.Ctx`) by 234 | providing the `RequestOptions` with a context or calling the `SetContext` method 235 | on our context (`yarql.Ctx`) 236 | 237 | ```go 238 | import "context" 239 | 240 | yarql.RequestOptions{ 241 | Context: context.Background(), 242 | } 243 | 244 | func (A) ResolveUser(ctx *yarql.Ctx) User { 245 | c := ctx.GetContext() 246 | c = context.WithValue(c, "resolved_user", true) 247 | ctx.SetContext(c) 248 | 249 | return User{} 250 | } 251 | ``` 252 | 253 | ### Optional fields 254 | 255 | All types that might be `nil` will be optional fields, by default these fields 256 | are: 257 | 258 | - Pointers 259 | - Arrays 260 | 261 | ### Enums 262 | 263 | Enums can be defined like so 264 | 265 | Side note on using enums as argument, It might return a nullish value if the 266 | user didn't provide a value 267 | 268 | ```go 269 | // The enum type, everywhere where this value is used it will be converted to an enum in graphql 270 | // This can also be a: string, int(*) or uint(*) 271 | type Fruit uint8 272 | 273 | const ( 274 | Apple Fruit = iota 275 | Peer 276 | Grapefruit 277 | ) 278 | 279 | func main() { 280 | s := yarql.NewSchema() 281 | 282 | // The map key is the enum it's key in graphql 283 | // The map value is the go value the enum key is mapped to or the other way around 284 | // Also the .RegisterEnum(..) method must be called before .Parse(..) 285 | s.RegisterEnum(map[string]Fruit{ 286 | "APPLE": Apple, 287 | "PEER": Peer, 288 | "GRAPEFRUIT": Grapefruit, 289 | }) 290 | 291 | s.Parse(QueryRoot{}, MethodRoot{}, nil) 292 | } 293 | ``` 294 | 295 | ### Interfaces 296 | 297 | Graphql interfaces can be created using go interfaces 298 | 299 | This library needs to analyze all types before you can make a query and as we 300 | cannot query all types that implement a interface you'll need to help the 301 | library with this by calling `Implements` for every implementation. If 302 | `Implements` is not called for a type the response value for that type when 303 | inside a interface will always be `null` 304 | 305 | ```go 306 | type QuerySchema struct { 307 | Bar BarWImpl 308 | Baz BazWImpl 309 | BarOrBaz InterfaceType 310 | } 311 | 312 | type InterfaceType interface { 313 | // Interface fields 314 | ResolveFoo() string 315 | ResolveBar() string 316 | } 317 | 318 | type BarWImpl struct{} 319 | 320 | // Implements hints this library to register BarWImpl 321 | // THIS MUST BE CALLED FOR EVERY TYPE THAT IMPLEMENTS InterfaceType 322 | var _ = yarql.Implements((*InterfaceType)(nil), BarWImpl{}) 323 | 324 | func (BarWImpl) ResolveFoo() string { return "this is bar" } 325 | func (BarWImpl) ResolveBar() string { return "This is bar" } 326 | 327 | type BazWImpl struct{} 328 | var _ = yarql.Implements((*InterfaceType)(nil), BazWImpl{}) 329 | func (BazWImpl) ResolveFoo() string { return "this is baz" } 330 | func (BazWImpl) ResolveBar() string { return "This is baz" } 331 | ``` 332 | 333 |
334 | Relay Node example 335 |
336 | 337 | For a full relay example see 338 | [examples/relay/backend/](./examples/relay/backend/) 339 | 340 | ```go 341 | type Node interface { 342 | ResolveId() (uint, yarql.AttrIsID) 343 | } 344 | 345 | type User struct { 346 | ID uint `gq:"-"` // ignored because of (User).ResolveId() 347 | Name string 348 | } 349 | 350 | var _ = yarql.Implements((*Node)(nil), User{}) 351 | 352 | // ResolveId implements the Node interface 353 | func (u User) ResolveId() (uint, yarql.AttrIsID) { 354 | return u.ID, 0 355 | } 356 | ``` 357 | 358 |
359 | 360 | ### Directives 361 | 362 | These directives are added by default: 363 | 364 | - `@include(if: Boolean!)` _on Fields and fragments, 365 | [spec](https://spec.graphql.org/October2021/#sec--include)_ 366 | - `@skip(if: Boolean!)` _on Fields and fragments, 367 | [spec](https://spec.graphql.org/October2021/#sec--skip)_ 368 | 369 | To add custom directives: 370 | 371 | ```go 372 | func main() { 373 | s := yarql.NewSchema() 374 | 375 | // Also the .RegisterEnum(..) method must be called before .Parse(..) 376 | s.RegisterDirective(Directive{ 377 | // What is the name of the directive 378 | Name: "skip_2", 379 | 380 | // Where can this directive be used in the query 381 | Where: []DirectiveLocation{ 382 | DirectiveLocationField, 383 | DirectiveLocationFragment, 384 | DirectiveLocationFragmentInline, 385 | }, 386 | 387 | // This methods's input work equal to field arguments 388 | // tough the output is required to return DirectiveModifier 389 | // This method is called always when the directive is used 390 | Method: func(args struct{ If bool }) DirectiveModifier { 391 | return DirectiveModifier{ 392 | Skip: args.If, 393 | } 394 | }, 395 | 396 | // The description of the directive 397 | Description: "Directs the executor to skip this field or fragment when the `if` argument is true.", 398 | }) 399 | 400 | s.Parse(QueryRoot{}, MethodRoot{}, nil) 401 | } 402 | ``` 403 | 404 | ### File upload 405 | 406 | _NOTE: This is NOT 407 | [graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec) 408 | tough this is based on 409 | [graphql-multipart-request-spec #55](https://github.com/jaydenseric/graphql-multipart-request-spec/issues/55)_ 410 | 411 | In your go code add `*multipart.FileHeader` to a methods inputs 412 | 413 | ```go 414 | func (SomeStruct) ResolveUploadFile(args struct{ File *multipart.FileHeader }) string { 415 | // ... 416 | } 417 | ``` 418 | 419 | In your graphql query you can now do: 420 | 421 | ```gql 422 | uploadFile(file: "form_file_field_name") 423 | ``` 424 | 425 | In your request add a form file with the field name: `form_file_field_name` 426 | 427 | ## Testing 428 | 429 | There is a 430 | [pkg.go.dev mjarkk/go-graphql/tester](https://pkg.go.dev/github.com/mjarkk/yarql/tester) 431 | package available with handy tools for testing the schema 432 | 433 | ## Performance 434 | 435 | Below shows a benchmark of fetching the graphql schema (query parsing + data 436 | fetching) 437 | 438 | _Note: This benchmark also profiles the cpu and that effects the score by a bit_ 439 | 440 | ```sh 441 | # go test -benchmem -bench "^(BenchmarkResolve)\$" 442 | # goos: darwin 443 | # cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 444 | BenchmarkResolve-12 13246 83731 ns/op 1344 B/op 47 allocs/op 445 | ``` 446 | 447 |
448 | Compared to other libraries 449 |
450 | 451 | Injecting `resolver_benchmark_test.go > BenchmarkHelloWorldResolve` into 452 | [appleboy/golang-graphql-benchmark](https://github.com/appleboy/golang-graphql-benchmark) 453 | results in the following: 454 | 455 | Take these results with a big grain of salt, i didn't use the last version of 456 | the libraries thus my result might be garbage compared to the others by now! 457 | 458 | ```sh 459 | # go test -v -bench=Master -benchmem 460 | # goos: darwin 461 | # goarch: amd64 462 | # pkg: github.com/appleboy/golang-graphql-benchmark 463 | # cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 464 | BenchmarkGoGraphQLMaster 465 | BenchmarkGoGraphQLMaster-12 24992 48180 ns/op 26895 B/op 445 allocs/op 466 | BenchmarkPlaylyfeGraphQLMaster-12 320289 3770 ns/op 2797 B/op 57 allocs/op 467 | BenchmarkGophersGraphQLMaster-12 391269 3114 ns/op 3634 B/op 38 allocs/op 468 | BenchmarkThunderGraphQLMaster-12 708327 1707 ns/op 1288 B/op 30 allocs/op 469 | BenchmarkMjarkkGraphQLGoMaster-12 2560764 466.5 ns/op 80 B/op 1 allocs/op 470 | ``` 471 | 472 |
473 | 474 | ## Alternatives 475 | 476 | - [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go) 477 | :heart: The library that inspired me to make this one 478 | - [ccbrown/api-fu](https://github.com/ccbrown/api-fu) 479 | - [99designs/gqlgen](https://github.com/99designs/gqlgen) 480 | - [graphql-go/graphql](https://github.com/graphql-go/graphql) 481 | -------------------------------------------------------------------------------- /apollo_tracing.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type tracer struct { 9 | Version uint8 `json:"version"` 10 | GoStartTime time.Time `json:"-"` 11 | StartTime string `json:"startTime"` 12 | EndTime string `json:"endTime"` 13 | Duration int64 `json:"duration"` 14 | Parsing tracerStartAndDuration `json:"parsing"` 15 | Validation tracerStartAndDuration `json:"validation"` 16 | Execution tracerExecution `json:"execution"` 17 | } 18 | 19 | type tracerStartAndDuration struct { 20 | StartOffset int64 `json:"startOffset"` 21 | Duration int64 `json:"duration"` 22 | } 23 | 24 | type tracerExecution struct { 25 | Resolvers []tracerResolver `json:"resolvers"` 26 | } 27 | 28 | type tracerResolver struct { 29 | Path json.RawMessage `json:"path"` 30 | ParentType string `json:"parentType"` 31 | FieldName string `json:"fieldName"` 32 | ReturnType string `json:"returnType"` 33 | StartOffset int64 `json:"startOffset"` 34 | Duration int64 `json:"duration"` 35 | } 36 | 37 | func newTracer() *tracer { 38 | return &tracer{ 39 | Version: 1, 40 | GoStartTime: time.Now(), 41 | Execution: tracerExecution{ 42 | Resolvers: []tracerResolver{}, 43 | }, 44 | } 45 | } 46 | 47 | func (t *tracer) reset() { 48 | *t = tracer{ 49 | Version: 1, 50 | GoStartTime: time.Now(), 51 | Execution: tracerExecution{ 52 | Resolvers: t.Execution.Resolvers[:0], 53 | }, 54 | } 55 | } 56 | 57 | func (t *tracer) finish() { 58 | t.StartTime = t.GoStartTime.Format(time.RFC3339Nano) 59 | now := time.Now() 60 | t.EndTime = now.Format(time.RFC3339Nano) 61 | t.Duration = now.Sub(t.GoStartTime).Nanoseconds() 62 | } 63 | -------------------------------------------------------------------------------- /assert/README.md: -------------------------------------------------------------------------------- 1 | A stipped down version of the assert package from github.com/stretchr/testify 2 | -------------------------------------------------------------------------------- /assert/assertion_compare.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | type compareType int 9 | 10 | const ( 11 | compareLess compareType = iota - 1 12 | compareEqual 13 | compareGreater 14 | ) 15 | 16 | var ( 17 | intType = reflect.TypeOf(int(1)) 18 | int8Type = reflect.TypeOf(int8(1)) 19 | int16Type = reflect.TypeOf(int16(1)) 20 | int32Type = reflect.TypeOf(int32(1)) 21 | int64Type = reflect.TypeOf(int64(1)) 22 | 23 | uintType = reflect.TypeOf(uint(1)) 24 | uint8Type = reflect.TypeOf(uint8(1)) 25 | uint16Type = reflect.TypeOf(uint16(1)) 26 | uint32Type = reflect.TypeOf(uint32(1)) 27 | uint64Type = reflect.TypeOf(uint64(1)) 28 | 29 | float32Type = reflect.TypeOf(float32(1)) 30 | float64Type = reflect.TypeOf(float64(1)) 31 | 32 | stringType = reflect.TypeOf("") 33 | ) 34 | 35 | func compare(obj1, obj2 interface{}, kind reflect.Kind) (compareType, bool) { 36 | obj1Value := reflect.ValueOf(obj1) 37 | obj2Value := reflect.ValueOf(obj2) 38 | 39 | // throughout this switch we try and avoid calling .Convert() if possible, 40 | // as this has a pretty big performance impact 41 | switch kind { 42 | case reflect.Int: 43 | { 44 | intobj1, ok := obj1.(int) 45 | if !ok { 46 | intobj1 = obj1Value.Convert(intType).Interface().(int) 47 | } 48 | intobj2, ok := obj2.(int) 49 | if !ok { 50 | intobj2 = obj2Value.Convert(intType).Interface().(int) 51 | } 52 | if intobj1 > intobj2 { 53 | return compareGreater, true 54 | } 55 | if intobj1 == intobj2 { 56 | return compareEqual, true 57 | } 58 | if intobj1 < intobj2 { 59 | return compareLess, true 60 | } 61 | } 62 | case reflect.Int8: 63 | { 64 | int8obj1, ok := obj1.(int8) 65 | if !ok { 66 | int8obj1 = obj1Value.Convert(int8Type).Interface().(int8) 67 | } 68 | int8obj2, ok := obj2.(int8) 69 | if !ok { 70 | int8obj2 = obj2Value.Convert(int8Type).Interface().(int8) 71 | } 72 | if int8obj1 > int8obj2 { 73 | return compareGreater, true 74 | } 75 | if int8obj1 == int8obj2 { 76 | return compareEqual, true 77 | } 78 | if int8obj1 < int8obj2 { 79 | return compareLess, true 80 | } 81 | } 82 | case reflect.Int16: 83 | { 84 | int16obj1, ok := obj1.(int16) 85 | if !ok { 86 | int16obj1 = obj1Value.Convert(int16Type).Interface().(int16) 87 | } 88 | int16obj2, ok := obj2.(int16) 89 | if !ok { 90 | int16obj2 = obj2Value.Convert(int16Type).Interface().(int16) 91 | } 92 | if int16obj1 > int16obj2 { 93 | return compareGreater, true 94 | } 95 | if int16obj1 == int16obj2 { 96 | return compareEqual, true 97 | } 98 | if int16obj1 < int16obj2 { 99 | return compareLess, true 100 | } 101 | } 102 | case reflect.Int32: 103 | { 104 | int32obj1, ok := obj1.(int32) 105 | if !ok { 106 | int32obj1 = obj1Value.Convert(int32Type).Interface().(int32) 107 | } 108 | int32obj2, ok := obj2.(int32) 109 | if !ok { 110 | int32obj2 = obj2Value.Convert(int32Type).Interface().(int32) 111 | } 112 | if int32obj1 > int32obj2 { 113 | return compareGreater, true 114 | } 115 | if int32obj1 == int32obj2 { 116 | return compareEqual, true 117 | } 118 | if int32obj1 < int32obj2 { 119 | return compareLess, true 120 | } 121 | } 122 | case reflect.Int64: 123 | { 124 | int64obj1, ok := obj1.(int64) 125 | if !ok { 126 | int64obj1 = obj1Value.Convert(int64Type).Interface().(int64) 127 | } 128 | int64obj2, ok := obj2.(int64) 129 | if !ok { 130 | int64obj2 = obj2Value.Convert(int64Type).Interface().(int64) 131 | } 132 | if int64obj1 > int64obj2 { 133 | return compareGreater, true 134 | } 135 | if int64obj1 == int64obj2 { 136 | return compareEqual, true 137 | } 138 | if int64obj1 < int64obj2 { 139 | return compareLess, true 140 | } 141 | } 142 | case reflect.Uint: 143 | { 144 | uintobj1, ok := obj1.(uint) 145 | if !ok { 146 | uintobj1 = obj1Value.Convert(uintType).Interface().(uint) 147 | } 148 | uintobj2, ok := obj2.(uint) 149 | if !ok { 150 | uintobj2 = obj2Value.Convert(uintType).Interface().(uint) 151 | } 152 | if uintobj1 > uintobj2 { 153 | return compareGreater, true 154 | } 155 | if uintobj1 == uintobj2 { 156 | return compareEqual, true 157 | } 158 | if uintobj1 < uintobj2 { 159 | return compareLess, true 160 | } 161 | } 162 | case reflect.Uint8: 163 | { 164 | uint8obj1, ok := obj1.(uint8) 165 | if !ok { 166 | uint8obj1 = obj1Value.Convert(uint8Type).Interface().(uint8) 167 | } 168 | uint8obj2, ok := obj2.(uint8) 169 | if !ok { 170 | uint8obj2 = obj2Value.Convert(uint8Type).Interface().(uint8) 171 | } 172 | if uint8obj1 > uint8obj2 { 173 | return compareGreater, true 174 | } 175 | if uint8obj1 == uint8obj2 { 176 | return compareEqual, true 177 | } 178 | if uint8obj1 < uint8obj2 { 179 | return compareLess, true 180 | } 181 | } 182 | case reflect.Uint16: 183 | { 184 | uint16obj1, ok := obj1.(uint16) 185 | if !ok { 186 | uint16obj1 = obj1Value.Convert(uint16Type).Interface().(uint16) 187 | } 188 | uint16obj2, ok := obj2.(uint16) 189 | if !ok { 190 | uint16obj2 = obj2Value.Convert(uint16Type).Interface().(uint16) 191 | } 192 | if uint16obj1 > uint16obj2 { 193 | return compareGreater, true 194 | } 195 | if uint16obj1 == uint16obj2 { 196 | return compareEqual, true 197 | } 198 | if uint16obj1 < uint16obj2 { 199 | return compareLess, true 200 | } 201 | } 202 | case reflect.Uint32: 203 | { 204 | uint32obj1, ok := obj1.(uint32) 205 | if !ok { 206 | uint32obj1 = obj1Value.Convert(uint32Type).Interface().(uint32) 207 | } 208 | uint32obj2, ok := obj2.(uint32) 209 | if !ok { 210 | uint32obj2 = obj2Value.Convert(uint32Type).Interface().(uint32) 211 | } 212 | if uint32obj1 > uint32obj2 { 213 | return compareGreater, true 214 | } 215 | if uint32obj1 == uint32obj2 { 216 | return compareEqual, true 217 | } 218 | if uint32obj1 < uint32obj2 { 219 | return compareLess, true 220 | } 221 | } 222 | case reflect.Uint64: 223 | { 224 | uint64obj1, ok := obj1.(uint64) 225 | if !ok { 226 | uint64obj1 = obj1Value.Convert(uint64Type).Interface().(uint64) 227 | } 228 | uint64obj2, ok := obj2.(uint64) 229 | if !ok { 230 | uint64obj2 = obj2Value.Convert(uint64Type).Interface().(uint64) 231 | } 232 | if uint64obj1 > uint64obj2 { 233 | return compareGreater, true 234 | } 235 | if uint64obj1 == uint64obj2 { 236 | return compareEqual, true 237 | } 238 | if uint64obj1 < uint64obj2 { 239 | return compareLess, true 240 | } 241 | } 242 | case reflect.Float32: 243 | { 244 | float32obj1, ok := obj1.(float32) 245 | if !ok { 246 | float32obj1 = obj1Value.Convert(float32Type).Interface().(float32) 247 | } 248 | float32obj2, ok := obj2.(float32) 249 | if !ok { 250 | float32obj2 = obj2Value.Convert(float32Type).Interface().(float32) 251 | } 252 | if float32obj1 > float32obj2 { 253 | return compareGreater, true 254 | } 255 | if float32obj1 == float32obj2 { 256 | return compareEqual, true 257 | } 258 | if float32obj1 < float32obj2 { 259 | return compareLess, true 260 | } 261 | } 262 | case reflect.Float64: 263 | { 264 | float64obj1, ok := obj1.(float64) 265 | if !ok { 266 | float64obj1 = obj1Value.Convert(float64Type).Interface().(float64) 267 | } 268 | float64obj2, ok := obj2.(float64) 269 | if !ok { 270 | float64obj2 = obj2Value.Convert(float64Type).Interface().(float64) 271 | } 272 | if float64obj1 > float64obj2 { 273 | return compareGreater, true 274 | } 275 | if float64obj1 == float64obj2 { 276 | return compareEqual, true 277 | } 278 | if float64obj1 < float64obj2 { 279 | return compareLess, true 280 | } 281 | } 282 | case reflect.String: 283 | { 284 | stringobj1, ok := obj1.(string) 285 | if !ok { 286 | stringobj1 = obj1Value.Convert(stringType).Interface().(string) 287 | } 288 | stringobj2, ok := obj2.(string) 289 | if !ok { 290 | stringobj2 = obj2Value.Convert(stringType).Interface().(string) 291 | } 292 | if stringobj1 > stringobj2 { 293 | return compareGreater, true 294 | } 295 | if stringobj1 == stringobj2 { 296 | return compareEqual, true 297 | } 298 | if stringobj1 < stringobj2 { 299 | return compareLess, true 300 | } 301 | } 302 | } 303 | 304 | return compareEqual, false 305 | } 306 | 307 | // Greater asserts that the first element is greater than the second 308 | // 309 | // assert.Greater(t, 2, 1) 310 | // assert.Greater(t, float64(2), float64(1)) 311 | // assert.Greater(t, "b", "a") 312 | func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { 313 | return compareTwoValues(t, e1, e2, []compareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) 314 | } 315 | 316 | // GreaterOrEqual asserts that the first element is greater than or equal to the second 317 | // 318 | // assert.GreaterOrEqual(t, 2, 1) 319 | // assert.GreaterOrEqual(t, 2, 2) 320 | // assert.GreaterOrEqual(t, "b", "a") 321 | // assert.GreaterOrEqual(t, "b", "b") 322 | func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { 323 | return compareTwoValues(t, e1, e2, []compareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) 324 | } 325 | 326 | // Less asserts that the first element is less than the second 327 | // 328 | // assert.Less(t, 1, 2) 329 | // assert.Less(t, float64(1), float64(2)) 330 | // assert.Less(t, "a", "b") 331 | func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { 332 | return compareTwoValues(t, e1, e2, []compareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) 333 | } 334 | 335 | // LessOrEqual asserts that the first element is less than or equal to the second 336 | // 337 | // assert.LessOrEqual(t, 1, 2) 338 | // assert.LessOrEqual(t, 2, 2) 339 | // assert.LessOrEqual(t, "a", "b") 340 | // assert.LessOrEqual(t, "b", "b") 341 | func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { 342 | return compareTwoValues(t, e1, e2, []compareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) 343 | } 344 | 345 | // Positive asserts that the specified element is positive 346 | // 347 | // assert.Positive(t, 1) 348 | // assert.Positive(t, 1.23) 349 | func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { 350 | zero := reflect.Zero(reflect.TypeOf(e)) 351 | return compareTwoValues(t, e, zero.Interface(), []compareType{compareGreater}, "\"%v\" is not positive", msgAndArgs) 352 | } 353 | 354 | // Negative asserts that the specified element is negative 355 | // 356 | // assert.Negative(t, -1) 357 | // assert.Negative(t, -1.23) 358 | func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { 359 | zero := reflect.Zero(reflect.TypeOf(e)) 360 | return compareTwoValues(t, e, zero.Interface(), []compareType{compareLess}, "\"%v\" is not negative", msgAndArgs) 361 | } 362 | 363 | func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []compareType, failMessage string, msgAndArgs ...interface{}) bool { 364 | e1Kind := reflect.ValueOf(e1).Kind() 365 | e2Kind := reflect.ValueOf(e2).Kind() 366 | if e1Kind != e2Kind { 367 | return Fail(t, "Elements should be the same type", msgAndArgs...) 368 | } 369 | 370 | compareResult, isComparable := compare(e1, e2, e1Kind) 371 | if !isComparable { 372 | return Fail(t, fmt.Sprintf("Can not compare type \"%s\"", reflect.TypeOf(e1)), msgAndArgs...) 373 | } 374 | 375 | if !containsValue(allowedComparesResults, compareResult) { 376 | return Fail(t, fmt.Sprintf(failMessage, e1, e2), msgAndArgs...) 377 | } 378 | 379 | return true 380 | } 381 | 382 | func containsValue(values []compareType, value compareType) bool { 383 | for _, v := range values { 384 | if v == value { 385 | return true 386 | } 387 | } 388 | 389 | return false 390 | } 391 | -------------------------------------------------------------------------------- /assert/assertion_format.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | // Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either 4 | // a slice or a channel with len == 0. 5 | // 6 | // assert.Emptyf(t, obj, "error message %s", "formatted") 7 | func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool { 8 | return Empty(t, object, append([]interface{}{msg}, args...)...) 9 | } 10 | 11 | // Equalf asserts that two objects are equal. 12 | // 13 | // assert.Equalf(t, 123, 123, "error message %s", "formatted") 14 | // 15 | // Pointer variable equality is determined based on the equality of the 16 | // referenced values (as opposed to the memory addresses). Function equality 17 | // cannot be determined and will always fail. 18 | func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { 19 | return Equal(t, expected, actual, append([]interface{}{msg}, args...)...) 20 | } 21 | 22 | // EqualErrorf asserts that a function returned an error (i.e. not `nil`) 23 | // and that it is equal to the provided error. 24 | // 25 | // actualObj, err := SomeFunction() 26 | // assert.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted") 27 | func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) bool { 28 | return EqualError(t, theError, errString, append([]interface{}{msg}, args...)...) 29 | } 30 | 31 | // Errorf asserts that a function returned an error (i.e. not `nil`). 32 | // 33 | // actualObj, err := SomeFunction() 34 | // if assert.Errorf(t, err, "error message %s", "formatted") { 35 | // assert.Equal(t, expectedErrorf, err) 36 | // } 37 | func Errorf(t TestingT, err error, msg string, args ...interface{}) bool { 38 | return Error(t, err, append([]interface{}{msg}, args...)...) 39 | } 40 | 41 | // ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. 42 | // This is a wrapper for errors.As. 43 | func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) bool { 44 | return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...) 45 | } 46 | 47 | // ErrorIsf asserts that at least one of the errors in err's chain matches target. 48 | // This is a wrapper for errors.Is. 49 | func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { 50 | return ErrorIs(t, err, target, append([]interface{}{msg}, args...)...) 51 | } 52 | 53 | // Failf reports a failure through 54 | func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) bool { 55 | return Fail(t, failureMessage, append([]interface{}{msg}, args...)...) 56 | } 57 | 58 | // Falsef asserts that the specified value is false. 59 | // 60 | // assert.Falsef(t, myBool, "error message %s", "formatted") 61 | func Falsef(t TestingT, value bool, msg string, args ...interface{}) bool { 62 | return False(t, value, append([]interface{}{msg}, args...)...) 63 | } 64 | 65 | // Greaterf asserts that the first element is greater than the second 66 | // 67 | // assert.Greaterf(t, 2, 1, "error message %s", "formatted") 68 | // assert.Greaterf(t, float64(2), float64(1), "error message %s", "formatted") 69 | // assert.Greaterf(t, "b", "a", "error message %s", "formatted") 70 | func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { 71 | return Greater(t, e1, e2, append([]interface{}{msg}, args...)...) 72 | } 73 | 74 | // GreaterOrEqualf asserts that the first element is greater than or equal to the second 75 | // 76 | // assert.GreaterOrEqualf(t, 2, 1, "error message %s", "formatted") 77 | // assert.GreaterOrEqualf(t, 2, 2, "error message %s", "formatted") 78 | // assert.GreaterOrEqualf(t, "b", "a", "error message %s", "formatted") 79 | // assert.GreaterOrEqualf(t, "b", "b", "error message %s", "formatted") 80 | func GreaterOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { 81 | return GreaterOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...) 82 | } 83 | 84 | // JSONEqf asserts that two JSON strings are equivalent. 85 | // 86 | // assert.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") 87 | func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { 88 | return JSONEq(t, expected, actual, append([]interface{}{msg}, args...)...) 89 | } 90 | 91 | // Lessf asserts that the first element is less than the second 92 | // 93 | // assert.Lessf(t, 1, 2, "error message %s", "formatted") 94 | // assert.Lessf(t, float64(1), float64(2), "error message %s", "formatted") 95 | // assert.Lessf(t, "a", "b", "error message %s", "formatted") 96 | func Lessf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { 97 | return Less(t, e1, e2, append([]interface{}{msg}, args...)...) 98 | } 99 | 100 | // LessOrEqualf asserts that the first element is less than or equal to the second 101 | // 102 | // assert.LessOrEqualf(t, 1, 2, "error message %s", "formatted") 103 | // assert.LessOrEqualf(t, 2, 2, "error message %s", "formatted") 104 | // assert.LessOrEqualf(t, "a", "b", "error message %s", "formatted") 105 | // assert.LessOrEqualf(t, "b", "b", "error message %s", "formatted") 106 | func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { 107 | return LessOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...) 108 | } 109 | 110 | // Negativef asserts that the specified element is negative 111 | // 112 | // assert.Negativef(t, -1, "error message %s", "formatted") 113 | // assert.Negativef(t, -1.23, "error message %s", "formatted") 114 | func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) bool { 115 | return Negative(t, e, append([]interface{}{msg}, args...)...) 116 | } 117 | 118 | // Nilf asserts that the specified object is nil. 119 | // 120 | // assert.Nilf(t, err, "error message %s", "formatted") 121 | func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) bool { 122 | return Nil(t, object, append([]interface{}{msg}, args...)...) 123 | } 124 | 125 | // NoErrorf asserts that a function returned no error (i.e. `nil`). 126 | // 127 | // actualObj, err := SomeFunction() 128 | // if assert.NoErrorf(t, err, "error message %s", "formatted") { 129 | // assert.Equal(t, expectedObj, actualObj) 130 | // } 131 | func NoErrorf(t TestingT, err error, msg string, args ...interface{}) bool { 132 | return NoError(t, err, append([]interface{}{msg}, args...)...) 133 | } 134 | 135 | // NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either 136 | // a slice or a channel with len == 0. 137 | // 138 | // if assert.NotEmptyf(t, obj, "error message %s", "formatted") { 139 | // assert.Equal(t, "two", obj[1]) 140 | // } 141 | func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool { 142 | return NotEmpty(t, object, append([]interface{}{msg}, args...)...) 143 | } 144 | 145 | // NotEqualf asserts that the specified values are NOT equal. 146 | // 147 | // assert.NotEqualf(t, obj1, obj2, "error message %s", "formatted") 148 | // 149 | // Pointer variable equality is determined based on the equality of the 150 | // referenced values (as opposed to the memory addresses). 151 | func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { 152 | return NotEqual(t, expected, actual, append([]interface{}{msg}, args...)...) 153 | } 154 | 155 | // NotErrorIsf asserts that at none of the errors in err's chain matches target. 156 | // This is a wrapper for errors.Is. 157 | func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { 158 | return NotErrorIs(t, err, target, append([]interface{}{msg}, args...)...) 159 | } 160 | 161 | // NotNilf asserts that the specified object is not nil. 162 | // 163 | // assert.NotNilf(t, err, "error message %s", "formatted") 164 | func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) bool { 165 | return NotNil(t, object, append([]interface{}{msg}, args...)...) 166 | } 167 | 168 | // Panicsf asserts that the code inside the specified f panics. 169 | // 170 | // assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") 171 | func Panicsf(t TestingT, f func(), msg string, args ...interface{}) bool { 172 | return Panics(t, f, append([]interface{}{msg}, args...)...) 173 | } 174 | 175 | // Truef asserts that the specified value is true. 176 | // 177 | // assert.Truef(t, myBool, "error message %s", "formatted") 178 | func Truef(t TestingT, value bool, msg string, args ...interface{}) bool { 179 | return True(t, value, append([]interface{}{msg}, args...)...) 180 | } 181 | -------------------------------------------------------------------------------- /assert/difflib/difflib.go: -------------------------------------------------------------------------------- 1 | // Package difflib is a partial port of Python difflib module. 2 | // 3 | // Copyright (c) 2013, Patrick Mezard 4 | // 5 | // Redistributions of source code must retain the above copyright 6 | // notice, this list of conditions and the following disclaimer. 7 | // Redistributions in binary form must reproduce the above copyright 8 | // notice, this list of conditions and the following disclaimer in the 9 | // documentation and/or other materials provided with the distribution. 10 | // The names of its contributors may not be used to endorse or promote 11 | // products derived from this software without specific prior written 12 | // permission. 13 | // 14 | // GRAPHQL PACKAGE NOTICE 15 | // This is a modified version of https://github.com/pmezard/go-difflib/blob/master/difflib/difflib.go that is simplified for the parrent package 16 | // 17 | // It provides tools to compare sequences of strings and generate textual diffs. 18 | // The following class and functions have been ported: 19 | // - SequenceMatcher 20 | // - unified_diff 21 | // - context_diff 22 | // Getting unified diffs was the main goal of the port. Keep in mind this code 23 | // is mostly suitable to output text differences in a human friendly way, there 24 | // are no guarantees generated diffs are consumable by patch(1). 25 | package difflib 26 | 27 | import ( 28 | "bufio" 29 | "bytes" 30 | "fmt" 31 | "io" 32 | "strings" 33 | ) 34 | 35 | func min(a, b int) int { 36 | if a < b { 37 | return a 38 | } 39 | return b 40 | } 41 | 42 | func max(a, b int) int { 43 | if a > b { 44 | return a 45 | } 46 | return b 47 | } 48 | 49 | type match struct { 50 | A int 51 | B int 52 | Size int 53 | } 54 | 55 | type opCode struct { 56 | Tag byte 57 | I1 int 58 | I2 int 59 | J1 int 60 | J2 int 61 | } 62 | 63 | // sequenceMatcher compares sequence of strings. The basic 64 | // algorithm predates, and is a little fancier than, an algorithm 65 | // published in the late 1980's by Ratcliff and Obershelp under the 66 | // hyperbolic name "gestalt pattern matching". The basic idea is to find 67 | // the longest contiguous matching subsequence that contains no "junk" 68 | // elements (R-O doesn't address junk). The same idea is then applied 69 | // recursively to the pieces of the sequences to the left and to the right 70 | // of the matching subsequence. This does not yield minimal edit 71 | // sequences, but does tend to yield matches that "look right" to people. 72 | // 73 | // sequenceMatcher tries to compute a "human-friendly diff" between two 74 | // sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the 75 | // longest *contiguous* & junk-free matching subsequence. That's what 76 | // catches peoples' eyes. The Windows(tm) windiff has another interesting 77 | // notion, pairing up elements that appear uniquely in each sequence. 78 | // That, and the method here, appear to yield more intuitive difference 79 | // reports than does diff. This method appears to be the least vulnerable 80 | // to synching up on blocks of "junk lines", though (like blank lines in 81 | // ordinary text files, or maybe "

" lines in HTML files). That may be 82 | // because this is the only method of the 3 that has a *concept* of 83 | // "junk" . 84 | // 85 | // Timing: Basic R-O is cubic time worst case and quadratic time expected 86 | // case. sequenceMatcher is quadratic time for the worst case and has 87 | // expected-case behavior dependent in a complicated way on how many 88 | // elements the sequences have in common; best case time is linear. 89 | type sequenceMatcher struct { 90 | a []string 91 | b []string 92 | b2j map[string][]int 93 | IsJunk func(string) bool 94 | autoJunk bool 95 | bJunk map[string]struct{} 96 | matchingBlocks []match 97 | fullBCount map[string]int 98 | bPopular map[string]struct{} 99 | opCodes []opCode 100 | } 101 | 102 | func newMatcher(a, b []string) *sequenceMatcher { 103 | m := sequenceMatcher{autoJunk: true} 104 | m.SetSeqs(a, b) 105 | return &m 106 | } 107 | 108 | // SetSeqs Sets two sequences to be compared. 109 | func (m *sequenceMatcher) SetSeqs(a, b []string) { 110 | m.SetSeq1(a) 111 | m.SetSeq2(b) 112 | } 113 | 114 | // Set the first sequence to be compared. The second sequence to be compared is 115 | // not changed. 116 | // 117 | // SequenceMatcher computes and caches detailed information about the second 118 | // sequence, so if you want to compare one sequence S against many sequences, 119 | // use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other 120 | // sequences. 121 | // 122 | // SetSeq1 See also SetSeqs() and SetSeq2(). 123 | func (m *sequenceMatcher) SetSeq1(a []string) { 124 | if &a == &m.a { 125 | return 126 | } 127 | m.a = a 128 | m.matchingBlocks = nil 129 | m.opCodes = nil 130 | } 131 | 132 | // SetSeq2 Sets the second sequence to be compared. The first sequence to be compared is 133 | // not changed. 134 | func (m *sequenceMatcher) SetSeq2(b []string) { 135 | if &b == &m.b { 136 | return 137 | } 138 | m.b = b 139 | m.matchingBlocks = nil 140 | m.opCodes = nil 141 | m.fullBCount = nil 142 | m.chainB() 143 | } 144 | 145 | func (m *sequenceMatcher) chainB() { 146 | // Populate line -> index mapping 147 | b2j := map[string][]int{} 148 | for i, s := range m.b { 149 | indices := b2j[s] 150 | indices = append(indices, i) 151 | b2j[s] = indices 152 | } 153 | 154 | // Purge junk elements 155 | m.bJunk = map[string]struct{}{} 156 | if m.IsJunk != nil { 157 | junk := m.bJunk 158 | for s := range b2j { 159 | if m.IsJunk(s) { 160 | junk[s] = struct{}{} 161 | } 162 | } 163 | for s := range junk { 164 | delete(b2j, s) 165 | } 166 | } 167 | 168 | // Purge remaining popular elements 169 | popular := map[string]struct{}{} 170 | n := len(m.b) 171 | if m.autoJunk && n >= 200 { 172 | ntest := n/100 + 1 173 | for s, indices := range b2j { 174 | if len(indices) > ntest { 175 | popular[s] = struct{}{} 176 | } 177 | } 178 | for s := range popular { 179 | delete(b2j, s) 180 | } 181 | } 182 | m.bPopular = popular 183 | m.b2j = b2j 184 | } 185 | 186 | func (m *sequenceMatcher) isBJunk(s string) bool { 187 | _, ok := m.bJunk[s] 188 | return ok 189 | } 190 | 191 | // Find longest matching block in a[alo:ahi] and b[blo:bhi]. 192 | // 193 | // If IsJunk is not defined: 194 | // 195 | // Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where 196 | // alo <= i <= i+k <= ahi 197 | // blo <= j <= j+k <= bhi 198 | // and for all (i',j',k') meeting those conditions, 199 | // k >= k' 200 | // i <= i' 201 | // and if i == i', j <= j' 202 | // 203 | // In other words, of all maximal matching blocks, return one that 204 | // starts earliest in a, and of all those maximal matching blocks that 205 | // start earliest in a, return the one that starts earliest in b. 206 | // 207 | // If IsJunk is defined, first the longest matching block is 208 | // determined as above, but with the additional restriction that no 209 | // junk element appears in the block. Then that block is extended as 210 | // far as possible by matching (only) junk elements on both sides. So 211 | // the resulting block never matches on junk except as identical junk 212 | // happens to be adjacent to an "interesting" match. 213 | // 214 | // If no blocks match, return (alo, blo, 0). 215 | func (m *sequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) match { 216 | // CAUTION: stripping common prefix or suffix would be incorrect. 217 | // E.g., 218 | // ab 219 | // acab 220 | // Longest matching block is "ab", but if common prefix is 221 | // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so 222 | // strip, so ends up claiming that ab is changed to acab by 223 | // inserting "ca" in the middle. That's minimal but unintuitive: 224 | // "it's obvious" that someone inserted "ac" at the front. 225 | // Windiff ends up at the same place as diff, but by pairing up 226 | // the unique 'b's and then matching the first two 'a's. 227 | besti, bestj, bestsize := alo, blo, 0 228 | 229 | // find longest junk-free match 230 | // during an iteration of the loop, j2len[j] = length of longest 231 | // junk-free match ending with a[i-1] and b[j] 232 | j2len := map[int]int{} 233 | for i := alo; i != ahi; i++ { 234 | // look at all instances of a[i] in b; note that because 235 | // b2j has no junk keys, the loop is skipped if a[i] is junk 236 | newj2len := map[int]int{} 237 | for _, j := range m.b2j[m.a[i]] { 238 | // a[i] matches b[j] 239 | if j < blo { 240 | continue 241 | } 242 | if j >= bhi { 243 | break 244 | } 245 | k := j2len[j-1] + 1 246 | newj2len[j] = k 247 | if k > bestsize { 248 | besti, bestj, bestsize = i-k+1, j-k+1, k 249 | } 250 | } 251 | j2len = newj2len 252 | } 253 | 254 | // Extend the best by non-junk elements on each end. In particular, 255 | // "popular" non-junk elements aren't in b2j, which greatly speeds 256 | // the inner loop above, but also means "the best" match so far 257 | // doesn't contain any junk *or* popular non-junk elements. 258 | for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && 259 | m.a[besti-1] == m.b[bestj-1] { 260 | besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 261 | } 262 | for besti+bestsize < ahi && bestj+bestsize < bhi && 263 | !m.isBJunk(m.b[bestj+bestsize]) && 264 | m.a[besti+bestsize] == m.b[bestj+bestsize] { 265 | bestsize++ 266 | } 267 | 268 | // Now that we have a wholly interesting match (albeit possibly 269 | // empty!), we may as well suck up the matching junk on each 270 | // side of it too. Can't think of a good reason not to, and it 271 | // saves post-processing the (possibly considerable) expense of 272 | // figuring out what to do with it. In the case of an empty 273 | // interesting match, this is clearly the right thing to do, 274 | // because no other kind of match is possible in the regions. 275 | for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && 276 | m.a[besti-1] == m.b[bestj-1] { 277 | besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 278 | } 279 | for besti+bestsize < ahi && bestj+bestsize < bhi && 280 | m.isBJunk(m.b[bestj+bestsize]) && 281 | m.a[besti+bestsize] == m.b[bestj+bestsize] { 282 | bestsize++ 283 | } 284 | 285 | return match{A: besti, B: bestj, Size: bestsize} 286 | } 287 | 288 | // GetMatchingBlocks Returns a list of triples describing matching subsequences. 289 | // 290 | // Each triple is of the form (i, j, n), and means that 291 | // a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in 292 | // i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are 293 | // adjacent triples in the list, and the second is not the last triple in the 294 | // list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe 295 | // adjacent equal blocks. 296 | // 297 | // The last triple is a dummy, (len(a), len(b), 0), and is the only 298 | // triple with n==0. 299 | func (m *sequenceMatcher) GetMatchingBlocks() []match { 300 | if m.matchingBlocks != nil { 301 | return m.matchingBlocks 302 | } 303 | 304 | var matchBlocks func(alo, ahi, blo, bhi int, matched []match) []match 305 | matchBlocks = func(alo, ahi, blo, bhi int, matched []match) []match { 306 | match := m.findLongestMatch(alo, ahi, blo, bhi) 307 | i, j, k := match.A, match.B, match.Size 308 | if match.Size > 0 { 309 | if alo < i && blo < j { 310 | matched = matchBlocks(alo, i, blo, j, matched) 311 | } 312 | matched = append(matched, match) 313 | if i+k < ahi && j+k < bhi { 314 | matched = matchBlocks(i+k, ahi, j+k, bhi, matched) 315 | } 316 | } 317 | return matched 318 | } 319 | matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) 320 | 321 | // It's possible that we have adjacent equal blocks in the 322 | // matching_blocks list now. 323 | nonAdjacent := []match{} 324 | i1, j1, k1 := 0, 0, 0 325 | for _, b := range matched { 326 | // Is this block adjacent to i1, j1, k1? 327 | i2, j2, k2 := b.A, b.B, b.Size 328 | if i1+k1 == i2 && j1+k1 == j2 { 329 | // Yes, so collapse them -- this just increases the length of 330 | // the first block by the length of the second, and the first 331 | // block so lengthened remains the block to compare against. 332 | k1 += k2 333 | } else { 334 | // Not adjacent. Remember the first block (k1==0 means it's 335 | // the dummy we started with), and make the second block the 336 | // new block to compare against. 337 | if k1 > 0 { 338 | nonAdjacent = append(nonAdjacent, match{i1, j1, k1}) 339 | } 340 | i1, j1, k1 = i2, j2, k2 341 | } 342 | } 343 | if k1 > 0 { 344 | nonAdjacent = append(nonAdjacent, match{i1, j1, k1}) 345 | } 346 | 347 | nonAdjacent = append(nonAdjacent, match{len(m.a), len(m.b), 0}) 348 | m.matchingBlocks = nonAdjacent 349 | return m.matchingBlocks 350 | } 351 | 352 | // GetOpCodes Returns a list of 5-tuples describing how to turn a into b. 353 | // 354 | // Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple 355 | // has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the 356 | // tuple preceding it, and likewise for j1 == the previous j2. 357 | // 358 | // The tags are characters, with these meanings: 359 | // 360 | // 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] 361 | // 362 | // 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. 363 | // 364 | // 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. 365 | // 366 | // 'e' (equal): a[i1:i2] == b[j1:j2] 367 | func (m *sequenceMatcher) GetOpCodes() []opCode { 368 | if m.opCodes != nil { 369 | return m.opCodes 370 | } 371 | i, j := 0, 0 372 | matching := m.GetMatchingBlocks() 373 | opCodes := make([]opCode, 0, len(matching)) 374 | for _, m := range matching { 375 | // invariant: we've pumped out correct diffs to change 376 | // a[:i] into b[:j], and the next matching block is 377 | // a[ai:ai+size] == b[bj:bj+size]. So we need to pump 378 | // out a diff to change a[i:ai] into b[j:bj], pump out 379 | // the matching block, and move (i,j) beyond the match 380 | ai, bj, size := m.A, m.B, m.Size 381 | tag := byte(0) 382 | if i < ai && j < bj { 383 | tag = 'r' 384 | } else if i < ai { 385 | tag = 'd' 386 | } else if j < bj { 387 | tag = 'i' 388 | } 389 | if tag > 0 { 390 | opCodes = append(opCodes, opCode{tag, i, ai, j, bj}) 391 | } 392 | i, j = ai+size, bj+size 393 | // the list of matching blocks is terminated by a 394 | // sentinel with size 0 395 | if size > 0 { 396 | opCodes = append(opCodes, opCode{'e', ai, i, bj, j}) 397 | } 398 | } 399 | m.opCodes = opCodes 400 | return m.opCodes 401 | } 402 | 403 | // GetGroupedOpCodes Isolates changes clusters by eliminating ranges with no changes. 404 | // 405 | // Return a generator of groups with up to n lines of context. 406 | // Each group is in the same format as returned by GetOpCodes(). 407 | func (m *sequenceMatcher) GetGroupedOpCodes(n int) [][]opCode { 408 | if n < 0 { 409 | n = 3 410 | } 411 | codes := m.GetOpCodes() 412 | if len(codes) == 0 { 413 | codes = []opCode{{'e', 0, 1, 0, 1}} 414 | } 415 | // Fixup leading and trailing groups if they show no changes. 416 | if codes[0].Tag == 'e' { 417 | c := codes[0] 418 | i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 419 | codes[0] = opCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} 420 | } 421 | if codes[len(codes)-1].Tag == 'e' { 422 | c := codes[len(codes)-1] 423 | i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 424 | codes[len(codes)-1] = opCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} 425 | } 426 | nn := n + n 427 | groups := [][]opCode{} 428 | group := []opCode{} 429 | for _, c := range codes { 430 | i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 431 | // End the current group and start a new one whenever 432 | // there is a large range with no changes. 433 | if c.Tag == 'e' && i2-i1 > nn { 434 | group = append(group, opCode{c.Tag, i1, min(i2, i1+n), 435 | j1, min(j2, j1+n)}) 436 | groups = append(groups, group) 437 | group = []opCode{} 438 | i1, j1 = max(i1, i2-n), max(j1, j2-n) 439 | } 440 | group = append(group, opCode{c.Tag, i1, i2, j1, j2}) 441 | } 442 | if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { 443 | groups = append(groups, group) 444 | } 445 | return groups 446 | } 447 | 448 | // Convert range to the "ed" format 449 | func formatRangeUnified(start, stop int) string { 450 | // Per the diff spec at http://www.unix.org/single_unix_specification/ 451 | beginning := start + 1 // lines start numbering with one 452 | length := stop - start 453 | if length == 1 { 454 | return fmt.Sprintf("%d", beginning) 455 | } 456 | if length == 0 { 457 | beginning-- // empty ranges begin at line just before the range 458 | } 459 | return fmt.Sprintf("%d,%d", beginning, length) 460 | } 461 | 462 | // WriteUnifiedDiff Compares two sequences of lines; generate the delta as a unified diff. 463 | // 464 | // Unified diffs are a compact way of showing line changes and a few 465 | // lines of context. The number of context lines is set by 'n' which 466 | // defaults to three. 467 | // 468 | // By default, the diff control lines (those with ---, +++, or @@) are 469 | // created with a trailing newline. This is helpful so that inputs 470 | // created from file.readlines() result in diffs that are suitable for 471 | // file.writelines() since both the inputs and outputs have trailing 472 | // newlines. 473 | // 474 | // For inputs that do not have trailing newlines, set the lineterm 475 | // argument to "" so that the output will be uniformly newline free. 476 | // 477 | // The unidiff format normally has a header for filenames and modification 478 | // times. Any or all of these may be specified using strings for 479 | // 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. 480 | // The modification times are normally expressed in the ISO 8601 format. 481 | func WriteUnifiedDiff(writer io.Writer, a, b []string) error { 482 | buf := bufio.NewWriter(writer) 483 | defer buf.Flush() 484 | wf := func(format string, args ...interface{}) error { 485 | _, err := buf.WriteString(fmt.Sprintf(format, args...)) 486 | return err 487 | } 488 | ws := func(s string) error { 489 | _, err := buf.WriteString(s) 490 | return err 491 | } 492 | 493 | started := false 494 | m := newMatcher(a, b) 495 | for _, g := range m.GetGroupedOpCodes(1) { 496 | if !started { 497 | started = true 498 | err := wf("--- Expected\n") 499 | if err != nil { 500 | return err 501 | } 502 | err = wf("+++ Actual\n") 503 | if err != nil { 504 | return err 505 | } 506 | } 507 | first, last := g[0], g[len(g)-1] 508 | range1 := formatRangeUnified(first.I1, last.I2) 509 | range2 := formatRangeUnified(first.J1, last.J2) 510 | if err := wf("@@ -%s +%s @@\n", range1, range2); err != nil { 511 | return err 512 | } 513 | for _, c := range g { 514 | i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 515 | if c.Tag == 'e' { 516 | for _, line := range a[i1:i2] { 517 | if err := ws(" " + line); err != nil { 518 | return err 519 | } 520 | } 521 | continue 522 | } 523 | if c.Tag == 'r' || c.Tag == 'd' { 524 | for _, line := range a[i1:i2] { 525 | if err := ws("-" + line); err != nil { 526 | return err 527 | } 528 | } 529 | } 530 | if c.Tag == 'r' || c.Tag == 'i' { 531 | for _, line := range b[j1:j2] { 532 | if err := ws("+" + line); err != nil { 533 | return err 534 | } 535 | } 536 | } 537 | } 538 | } 539 | return nil 540 | } 541 | 542 | // GetUnifiedDiffString is Like WriteUnifiedDiff but returns the diff a string. 543 | func GetUnifiedDiffString(a, b []string) (string, error) { 544 | w := &bytes.Buffer{} 545 | err := WriteUnifiedDiff(w, a, b) 546 | return w.String(), err 547 | } 548 | 549 | // SplitLines Splits a string on "\n" while preserving them. The output can be used 550 | // as input for UnifiedDiff and ContextDiff structures. 551 | func SplitLines(s string) []string { 552 | lines := strings.SplitAfter(s, "\n") 553 | lines[len(lines)-1] += "\n" 554 | return lines 555 | } 556 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjarkk/yarql/688af2cd6fdab0525548c9cff11e34cd367a00a6/banner.png -------------------------------------------------------------------------------- /bytecode/README.md: -------------------------------------------------------------------------------- 1 | # Bytecode query parser 2 | 3 | This is a test to improve the query parsing and resolve speed. 4 | 5 | Currently this is no-where used and still a WIP. 6 | 7 | ## How to understand it? 8 | 9 | Currently the best way to understand the bytecode is to read: 10 | 11 | - `bytecode_test.go` and `testing_framework.go` to see what query results in what bytecode 12 | - `bytecode_instructions.go` 13 | -------------------------------------------------------------------------------- /bytecode/bytecode_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package bytecode 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkQueryParser(b *testing.B) { 8 | ctx := NewParserCtx() 9 | ctx.Query = []byte(schemaQuery) 10 | 11 | for i := 0; i < b.N; i++ { 12 | ctx.ParseQueryToBytecode(nil) 13 | if len(ctx.Errors) > 0 { 14 | panic(ctx.Errors[len(ctx.Errors)-1]) 15 | } 16 | } 17 | } 18 | 19 | var schemaQuery = []byte(` 20 | query IntrospectionQuery { 21 | __schema { 22 | queryType { 23 | name 24 | } 25 | mutationType { 26 | name 27 | } 28 | subscriptionType { 29 | name 30 | } 31 | types { 32 | ...FullType 33 | } 34 | directives { 35 | name 36 | description 37 | locations 38 | args { 39 | ...InputValue 40 | } 41 | } 42 | } 43 | } 44 | 45 | fragment FullType on __Type { 46 | kind 47 | name 48 | description 49 | fields(includeDeprecated: true) { 50 | name 51 | description 52 | args { 53 | ...InputValue 54 | } 55 | type { 56 | ...TypeRef 57 | } 58 | isDeprecated 59 | deprecationReason 60 | } 61 | inputFields { 62 | ...InputValue 63 | } 64 | interfaces { 65 | ...TypeRef 66 | } 67 | enumValues(includeDeprecated: true) { 68 | name 69 | description 70 | isDeprecated 71 | deprecationReason 72 | } 73 | possibleTypes { 74 | ...TypeRef 75 | } 76 | } 77 | 78 | fragment InputValue on __InputValue { 79 | name 80 | description 81 | type { 82 | ...TypeRef 83 | } 84 | defaultValue 85 | } 86 | 87 | fragment TypeRef on __Type { 88 | kind 89 | name 90 | ofType { 91 | kind 92 | name 93 | ofType { 94 | kind 95 | name 96 | ofType { 97 | kind 98 | name 99 | ofType { 100 | kind 101 | name 102 | ofType { 103 | kind 104 | name 105 | ofType { 106 | kind 107 | name 108 | ofType { 109 | kind 110 | name 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | `) 120 | -------------------------------------------------------------------------------- /bytecode/bytecode_instructions.go: -------------------------------------------------------------------------------- 1 | package bytecode 2 | 3 | // Action defines an action that should be taken based when parsing the schema 4 | type Action = byte 5 | 6 | // All possible actions 7 | const ( 8 | ActionEnd Action = 'e' 9 | ActionOperator Action = 'o' 10 | ActionOperatorArgs Action = 'A' 11 | ActionOperatorArg Action = 'a' 12 | ActionField Action = 'f' 13 | ActionSpread Action = 's' 14 | ActionFragment Action = 'F' 15 | ActionValue Action = 'v' 16 | ActionObjectValueField Action = 'u' 17 | ActionDirective Action = 'd' 18 | ) 19 | 20 | // ValueKind defines a input value kind 21 | type ValueKind = byte 22 | 23 | // All possible value kinds 24 | const ( 25 | ValueVariable ValueKind = '$' 26 | ValueInt ValueKind = 'i' 27 | ValueFloat ValueKind = 'f' 28 | ValueString ValueKind = 's' 29 | ValueBoolean ValueKind = 'b' 30 | ValueNull ValueKind = 'n' 31 | ValueEnum ValueKind = 'e' 32 | ValueList ValueKind = 'l' 33 | ValueObject ValueKind = 'o' 34 | ) 35 | 36 | // OperatorKind defines the kind of operation 37 | type OperatorKind = byte 38 | 39 | // All possible operators 40 | const ( 41 | OperatorQuery OperatorKind = 'q' 42 | OperatorMutation OperatorKind = 'm' 43 | OperatorSubscription OperatorKind = 's' 44 | ) 45 | 46 | // represends: 47 | // 48 | // query { 49 | // ^- Kind 50 | // 51 | // writes: 52 | // 0 [actionNewOperator] [kind] [t/f (has arguments)] [nr of directives in uint8] 53 | // 54 | // additional append: 55 | // [name...] 56 | func (ctx *ParserCtx) instructionNewOperation(kind OperatorKind) int { 57 | res := len(ctx.Res) 58 | ctx.Res = append(ctx.Res, 0, ActionOperator, kind, 'f', 0) 59 | return res 60 | } 61 | 62 | func (ctx *ParserCtx) instructionNewOperationArgs() { 63 | ctx.Res = append(ctx.Res, 0, ActionOperatorArgs) 64 | } 65 | 66 | // represends: 67 | // 68 | // query foo(banana: String) { 69 | // ^- ActionOperatorArg 70 | // 71 | // writes: 72 | // 0 [ActionOperatorArg] [0000 (encoded uint32 telling how long this full instruction is)] 73 | // 74 | // additional required append: 75 | // [Name] 0 [Graphql Type] 0 [t/f (has a default value?)] 76 | // 77 | // returns: 78 | // the start location of the 4 bit encoded uint32 79 | func (ctx *ParserCtx) instructionNewOperationArg() int { 80 | ctx.Res = append(ctx.Res, 0, ActionOperatorArg, 0, 0, 0, 0) 81 | return len(ctx.Res) - 4 82 | } 83 | 84 | // represends: 85 | // 86 | // fragment InputValue on __InputValue { 87 | // ^- Name ^- Type Name 88 | // 89 | // writes: 90 | // 0 [ActionFragment] 91 | // 92 | // additional required append: 93 | // [Name] 0 [Type Name] 94 | func (ctx *ParserCtx) instructionNewFragment() int { 95 | res := len(ctx.Res) 96 | ctx.Res = append(ctx.Res, 0, ActionFragment) 97 | return res 98 | } 99 | 100 | // represends: 101 | // 102 | // query { a } 103 | // ^ 104 | // 105 | // writes: 106 | // 0 [actionField] [directives count as uint8] [0000 length remainder of field as uint32] [0000 uint32 key value of name] 107 | // 108 | // additional required append: 109 | // [Fieldname] 0 110 | // OR 111 | // [Alias] 0 [Fieldname] 112 | func (ctx *ParserCtx) instructionNewField() { 113 | ctx.Res = append(ctx.Res, 0, ActionField, 0, 0, 0, 0, 0, 0, 0, 0, 0) 114 | } 115 | 116 | // represends: 117 | // 118 | // { 119 | // ...Foo 120 | // ^- Fragment spread in selector 121 | // ... on Banana {} 122 | // ^- Also a fragment spread 123 | // } 124 | // 125 | // writes: 126 | // 0 [ActionSpread] [t/f (t = inline fragment, f = pointer to fragment)] [directives count in uint8] [0000 uint32 length of remainder of fragment] 127 | // 128 | // additional required append: 129 | // [Typename or Fragment Name] 130 | func (ctx *ParserCtx) instructionNewFragmentSpread(isInline bool) { 131 | if isInline { 132 | ctx.Res = append(ctx.Res, 0, ActionSpread, 't', 0, 0, 0, 0, 0) 133 | } else { 134 | ctx.Res = append(ctx.Res, 0, ActionSpread, 'f', 0, 0, 0, 0, 0) 135 | } 136 | } 137 | 138 | // represends: 139 | // 140 | // @banana(arg: 2) 141 | // 142 | // writes: 143 | // 0 [ActionDirective] [t/f (t = has arguments, f = no arguments)] 144 | // 145 | // additional required append: 146 | // [Directive name] 147 | func (ctx *ParserCtx) instructionNewDirective() { 148 | ctx.Res = append(ctx.Res, 0, ActionDirective, 'f') 149 | } 150 | 151 | // represends: 152 | // 153 | // {a: "a", b: "b", ...} 154 | // ^- This represends the start of a set 155 | // AND 156 | // (a: "a", b: "b", ...) 157 | // ^- This represends the start of a set 158 | // 159 | // writes: 160 | // 0 [ActionValue] [ValueObject] 161 | func (ctx *ParserCtx) instructionNewValueObject() { 162 | ctx.Res = append(ctx.Res, 0, ActionValue, ValueObject, 0, 0, 0, 0) 163 | } 164 | 165 | func (ctx *ParserCtx) instructionNewValueList() { 166 | ctx.Res = append(ctx.Res, 0, ActionValue, ValueList, 0, 0, 0, 0) 167 | } 168 | 169 | func (ctx *ParserCtx) instructionNewValueBoolean(val bool) { 170 | if val { 171 | ctx.Res = append(ctx.Res, 0, ActionValue, ValueBoolean, 1, 0, 0, 0, '1') 172 | } else { 173 | ctx.Res = append(ctx.Res, 0, ActionValue, ValueBoolean, 1, 0, 0, 0, '0') 174 | } 175 | } 176 | 177 | func (ctx *ParserCtx) instructionNewValueNull() { 178 | ctx.Res = append(ctx.Res, 0, ActionValue, ValueNull, 0, 0, 0, 0) 179 | } 180 | 181 | // writes: 182 | // 0 [ActionValue] [ActionValue...] [0000 the number of bytes the value will take up encoded as uint32] 183 | func (ctx *ParserCtx) instructionNewValueEnum() { 184 | ctx.Res = append(ctx.Res, 0, ActionValue, ValueEnum, 0, 0, 0, 0) 185 | } 186 | 187 | // writes: 188 | // 0 [ActionValue] [ValueVariable...] [0000 the number of bytes the value will take up encoded as uint32] 189 | func (ctx *ParserCtx) instructionNewValueVariable() { 190 | ctx.Res = append(ctx.Res, 0, ActionValue, ValueVariable, 0, 0, 0, 0) 191 | } 192 | 193 | // writes: 194 | // 0 [ActionValue] [valueInt] [0000 the number of bytes the value will take up encoded as uint32] 195 | func (ctx *ParserCtx) instructionNewValueInt() { 196 | ctx.Res = append(ctx.Res, 0, ActionValue, ValueInt, 0, 0, 0, 0) 197 | } 198 | 199 | // writes: 200 | // 0 [ActionValue] [valueString...] [0000 the number of bytes the value will take up encoded as uint32] 201 | func (ctx *ParserCtx) instructionNewValueString() { 202 | ctx.Res = append(ctx.Res, 0, ActionValue, ValueString, 0, 0, 0, 0) 203 | } 204 | 205 | // represends: 206 | // 207 | // {a: "a", b: "b", ...} 208 | // ^- This represends a field inside a set 209 | // AND 210 | // (a: "a", b: "b", ...) 211 | // ^- This represends a field inside a set 212 | // 213 | // writes: 214 | // 0 [ActionObjectValueField] 215 | // 216 | // additional required append: 217 | // [fieldname] 218 | func (ctx *ParserCtx) instructionStartNewValueObjectField() { 219 | ctx.Res = append(ctx.Res, 0, ActionObjectValueField) 220 | } 221 | 222 | // represends: 223 | // 224 | // query { } 225 | // ^- End 226 | // 227 | // query { a { } } 228 | // ^- End 229 | // 230 | // query { a } 231 | // ^- End 232 | // 233 | // writes: 234 | // 0 [ActionEndClosure] 235 | func (ctx *ParserCtx) instructionEnd() { 236 | ctx.Res = append(ctx.Res, 0, ActionEnd) 237 | } 238 | -------------------------------------------------------------------------------- /bytecode/cache/bytecode_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/mjarkk/yarql/helpers" 7 | ) 8 | 9 | // BytecodeCache contains the bytecode cache 10 | // The map index is the length of the query 11 | // The value is a slice with a list of queries 12 | type BytecodeCache map[int][]cacheEntry 13 | 14 | type cacheEntry struct { 15 | query []byte 16 | bytecode []byte 17 | target *string 18 | targetIdx int 19 | fragmentLocation []int 20 | } 21 | 22 | // GetEntry might return the bytecode, the fragment locations of the query and targetIdx 23 | func (c BytecodeCache) GetEntry(query []byte, target *string) ([]byte, []int, int) { 24 | entries, ok := c[len(query)] 25 | if !ok { 26 | return nil, nil, -1 27 | } 28 | 29 | for _, entry := range entries { 30 | if bytes.Equal(entry.query, query) && ((target == nil && entry.target == nil) || (target != nil && entry.target != nil && *target == *entry.target)) { 31 | return entry.bytecode, entry.fragmentLocation, entry.targetIdx 32 | } 33 | } 34 | 35 | return nil, nil, -1 36 | } 37 | 38 | // SetEntry sets a new entry in the cache 39 | func (c BytecodeCache) SetEntry(query, bytecode []byte, target *string, targetIdx int, fragmentLocation []int) { 40 | if len(c) == 100 { 41 | // Remove some random entries 42 | // FIXME Dunno if this is a good value to start dropping stuff 43 | var deleted uint8 44 | for key := range c { 45 | delete(c, key) 46 | deleted++ 47 | if deleted == 5 { 48 | break 49 | } 50 | } 51 | } 52 | 53 | queryLen := len(query) 54 | entries, ok := c[queryLen] 55 | if !ok { 56 | entries = []cacheEntry{} 57 | } else if len(entries) == 20 { 58 | // Drop the last cache entry for this length query 59 | // FIXME Dunno if this is a good value to start dropping stuff 60 | entries = entries[:len(entries)-1] 61 | } 62 | 63 | var targetCopy *string 64 | if target != nil { 65 | targetCopy = helpers.StrPtr(*target) 66 | } 67 | 68 | newCacheEntry := cacheEntry{ 69 | query: make([]byte, len(query)), 70 | bytecode: make([]byte, len(bytecode)), 71 | target: targetCopy, 72 | targetIdx: targetIdx, 73 | fragmentLocation: fragmentLocation, 74 | } 75 | copy(newCacheEntry.query, query) 76 | copy(newCacheEntry.bytecode, bytecode) 77 | 78 | c[queryLen] = append([]cacheEntry{newCacheEntry}, entries...) 79 | } 80 | -------------------------------------------------------------------------------- /bytecode/testing_framework.go: -------------------------------------------------------------------------------- 1 | package bytecode 2 | 3 | import ( 4 | "hash/fnv" 5 | "strconv" 6 | ) 7 | 8 | func writeUint32At(res []byte, at int, value uint32) []byte { 9 | res[at] = byte(0xff & value) 10 | res[at+1] = byte(0xff & (value >> 8)) 11 | res[at+2] = byte(0xff & (value >> 16)) 12 | res[at+3] = byte(0xff & (value >> 24)) 13 | return res 14 | } 15 | 16 | type testOperator struct { 17 | kind OperatorKind // default = OperatorQuery 18 | name string 19 | args []testOperatorArg 20 | directives []testDirective 21 | fields []testField 22 | } 23 | 24 | func (o testOperator) toBytes() []byte { 25 | if o.kind == 0 { 26 | o.kind = OperatorQuery 27 | } 28 | res := []byte{0, 'o', o.kind} 29 | if len(o.args) > 0 { // has args 30 | res = append(res, 't') 31 | } else { 32 | res = append(res, 'f') 33 | } 34 | res = append(res, byte(len(o.directives))) // directs count 35 | res = append(res, []byte(o.name)...) 36 | if len(o.args) > 0 { 37 | res = append(res, 0, 0, 0, 0, 0) // length of args 38 | start := len(res) 39 | res = append(res, 0, 'A') // args 40 | for _, arg := range o.args { 41 | res = arg.toBytes(res) 42 | } 43 | res = append(res, 0, 'e') // end of args 44 | end := len(res) 45 | res = writeUint32At(res, start-4, uint32(end-start)) 46 | } 47 | if len(o.directives) > 0 { 48 | for _, directive := range o.directives { 49 | res = directive.toBytes(res) 50 | } 51 | } 52 | if len(o.fields) > 0 { 53 | for _, field := range o.fields { 54 | res = field.toBytes(res) 55 | } 56 | } 57 | res = append(res, 0, 'e') // end of query 58 | return res 59 | } 60 | 61 | type testFragment struct { 62 | name string // REQUIRED 63 | on string // REQUIRED 64 | fields []testField 65 | } 66 | 67 | func (o testFragment) toBytes() []byte { 68 | res := []byte{0, 'F'} 69 | res = append(res, []byte(o.name)...) 70 | res = append(res, 0) 71 | res = append(res, []byte(o.on)...) 72 | if len(o.fields) > 0 { 73 | for _, field := range o.fields { 74 | res = field.toBytes(res) 75 | } 76 | } 77 | res = append(res, 0, 'e') 78 | return res 79 | } 80 | 81 | type testDirective struct { 82 | name string // REQUIRED 83 | arguments []typeObjectValue 84 | } 85 | 86 | func (o testDirective) toBytes(res []byte) []byte { 87 | res = append(res, 0, 'd') 88 | if o.arguments != nil { 89 | res = append(res, 't') 90 | } else { 91 | res = append(res, 'f') 92 | } 93 | res = append(res, []byte(o.name)...) 94 | if o.arguments != nil { 95 | res = testValue{ 96 | kind: ValueObject, 97 | objectValue: o.arguments, 98 | }.toBytes(res) 99 | } 100 | return res 101 | } 102 | 103 | type testOperatorArg struct { 104 | name string // REQUIRED 105 | bytecodeType string // REQUIRED 106 | defaultValue *testValue 107 | } 108 | 109 | func (o testOperatorArg) toBytes(res []byte) []byte { 110 | start := len(res) + 1 111 | res = append(res, 0, 'a') // arg instruction 112 | res = append(res, 0, 0, 0, 0) // arg length 113 | res = append(res, []byte(o.name)...) 114 | res = append(res, 0) 115 | res = append(res, []byte(o.bytecodeType)...) 116 | if o.defaultValue != nil { // has default value 117 | res = append(res, 0, 't') 118 | res = o.defaultValue.toBytes(res) 119 | } else { 120 | res = append(res, 0, 'f') 121 | } 122 | end := len(res) 123 | 124 | res = writeUint32At(res, start+1, uint32(end-start)) 125 | 126 | return res 127 | } 128 | 129 | type testValue struct { 130 | kind ValueKind 131 | 132 | // ValueList: 133 | list []testValue 134 | 135 | // ValueInt 136 | intValue int64 137 | 138 | // ValueFloat 139 | floatValue string 140 | 141 | // stringValue 142 | stringValue string 143 | 144 | // ValueBoolean 145 | boolValue bool 146 | 147 | // ValueObject 148 | objectValue []typeObjectValue 149 | 150 | // ValueVariable 151 | variableValue string 152 | 153 | // ValueEnum 154 | enumValue string 155 | } 156 | 157 | type typeObjectValue struct { 158 | name string 159 | value testValue 160 | } 161 | 162 | func (o testValue) toBytes(res []byte) []byte { 163 | res = append(res, 0, 'v', o.kind) // value instruction 164 | res = append(res, 0, 0, 0, 0) 165 | start := len(res) 166 | 167 | switch o.kind { 168 | case ValueVariable: 169 | res = append(res, []byte(o.variableValue)...) 170 | case ValueInt: 171 | res = strconv.AppendInt(res, o.intValue, 10) 172 | case ValueFloat: 173 | res = append(res, []byte(o.floatValue)...) 174 | case ValueString: 175 | res = append(res, []byte(o.stringValue)...) 176 | case ValueBoolean: 177 | if o.boolValue { 178 | res = append(res, '1') 179 | } else { 180 | res = append(res, '0') 181 | } 182 | case ValueNull: 183 | // Do nothing :) 184 | case ValueEnum: 185 | res = append(res, []byte(o.enumValue)...) 186 | case ValueList: 187 | for _, item := range o.list { 188 | res = item.toBytes(res) 189 | } 190 | res = append(res, 0, 'e') 191 | case ValueObject: 192 | for _, entry := range o.objectValue { 193 | res = append(res, 0, 'u') 194 | res = append(res, []byte(entry.name)...) 195 | res = entry.value.toBytes(res) 196 | } 197 | res = append(res, 0, 'e') 198 | } 199 | 200 | end := len(res) 201 | res = writeUint32At(res, start-4, uint32(end-start)) 202 | 203 | return res 204 | } 205 | 206 | type testField struct { 207 | name string // REQUIRED 208 | fields []testField 209 | isFragment bool 210 | directives []testDirective 211 | 212 | // only if isFragment == false 213 | alias string 214 | arguments []typeObjectValue 215 | } 216 | 217 | func getObjKey(key []byte) uint32 { 218 | hasher := fnv.New32() 219 | hasher.Write(key) 220 | return hasher.Sum32() 221 | } 222 | 223 | func (o testField) toBytes(res []byte) []byte { 224 | if o.isFragment { 225 | res = append(res, 0, 's') // start of fragment 226 | if o.fields != nil { 227 | res = append(res, 't') 228 | } else { 229 | res = append(res, 'f') 230 | } 231 | res = append(res, byte(len(o.directives))) 232 | res = append(res, 0, 0, 0, 0) 233 | fragmentStart := len(res) 234 | 235 | res = append(res, []byte(o.name)...) 236 | for _, directive := range o.directives { 237 | res = directive.toBytes(res) 238 | } 239 | if o.fields != nil { 240 | for _, field := range o.fields { 241 | res = field.toBytes(res) 242 | } 243 | res = append(res, 0, 'e') 244 | } 245 | 246 | fragmentEnd := len(res) 247 | res = writeUint32At(res, fragmentStart-4, uint32(fragmentEnd-fragmentStart)) 248 | return res 249 | } 250 | 251 | res = append(res, 0, 'f') 252 | res = append(res, byte(len(o.directives))) 253 | res = append(res, 0, 0, 0, 0) 254 | res = append(res, 0, 0, 0, 0) 255 | res = writeUint32At(res, len(res)-4, getObjKey([]byte(o.name))) 256 | start := len(res) 257 | 258 | if len(o.alias) > 0 { 259 | res = append(res, byte(len(o.alias))) 260 | res = append(res, []byte(o.alias)...) 261 | res = append(res, byte(len(o.name))) 262 | res = append(res, []byte(o.name)...) 263 | } else { 264 | res = append(res, byte(len(o.name))) 265 | res = append(res, []byte(o.name)...) 266 | res = append(res, 0) 267 | } 268 | for _, directive := range o.directives { 269 | res = directive.toBytes(res) 270 | } 271 | for _, field := range o.fields { 272 | res = field.toBytes(res) 273 | } 274 | if o.arguments != nil { 275 | res = testValue{ 276 | kind: ValueObject, 277 | objectValue: o.arguments, 278 | }.toBytes(res) 279 | } 280 | res = append(res, 0, 'e') 281 | 282 | end := len(res) 283 | res = writeUint32At(res, start-8, uint32(end-start)) 284 | 285 | return res 286 | } 287 | -------------------------------------------------------------------------------- /copy_schema.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/mjarkk/yarql/bytecode" 7 | "github.com/mjarkk/yarql/helpers" 8 | "github.com/valyala/fastjson" 9 | ) 10 | 11 | // Copy is meant to be used to create a pool of schema objects 12 | // The function itself is quiet slow so don't use this function in every request 13 | func (s *Schema) Copy() *Schema { 14 | if !s.parsed { 15 | panic("Schema has not been parsed yet, call Parse before attempting to copy it") 16 | } 17 | 18 | types := s.types.copy() 19 | interfaces := s.interfaces.copy() 20 | 21 | enums := make([]enum, len(s.definedEnums)) 22 | for idx, enum := range s.definedEnums { 23 | enums[idx] = *enum.copy() 24 | } 25 | 26 | directives := map[DirectiveLocation][]*Directive{} 27 | for key, value := range s.definedDirectives { 28 | directivesToAdd := make([]*Directive, len(value)) 29 | for idx, directive := range value { 30 | directivesToAdd[idx] = directive.copy() 31 | } 32 | directives[key] = directivesToAdd 33 | } 34 | 35 | res := &Schema{ 36 | parsed: true, 37 | 38 | types: *types, 39 | inTypes: *s.inTypes.copy(), 40 | interfaces: *interfaces, 41 | 42 | rootQuery: s.rootQuery.copy(), 43 | rootQueryValue: s.rootQueryValue, 44 | rootMethod: s.rootMethod.copy(), 45 | rootMethodValue: s.rootMethodValue, 46 | MaxDepth: s.MaxDepth, 47 | definedEnums: enums, 48 | definedDirectives: directives, 49 | 50 | Result: make([]byte, len(s.Result)), 51 | graphqlTypesMap: nil, 52 | graphqlTypesList: nil, 53 | graphqlObjFields: map[string][]qlField{}, 54 | } 55 | 56 | res.ctx = s.ctx.copy(res) 57 | 58 | return res 59 | } 60 | 61 | func (ctx *Ctx) copy(schema *Schema) *Ctx { 62 | res := &Ctx{ 63 | schema: schema, 64 | query: *bytecode.NewParserCtx(), 65 | charNr: ctx.charNr, 66 | context: nil, 67 | path: []byte{}, 68 | getFormFile: ctx.getFormFile, 69 | operatorHasArguments: ctx.operatorHasArguments, 70 | operatorArgumentsStartAt: ctx.operatorArgumentsStartAt, 71 | tracingEnabled: ctx.tracingEnabled, 72 | tracing: ctx.tracing, 73 | prefRecordingStartTime: ctx.prefRecordingStartTime, 74 | rawVariables: ctx.rawVariables, 75 | variablesParsed: false, 76 | variablesJSONParser: &fastjson.Parser{}, 77 | variables: nil, 78 | reflectValues: [256]reflect.Value{}, 79 | currentReflectValueIdx: 0, 80 | funcInputs: []reflect.Value{}, 81 | values: nil, 82 | } 83 | res.ctxReflection = reflect.ValueOf(res) 84 | return res 85 | } 86 | 87 | func (m *Directive) copy() *Directive { 88 | var parsedMethod *objMethod 89 | if m.parsedMethod != nil { 90 | parsedMethod = m.parsedMethod.copy() 91 | } 92 | return &Directive{ 93 | Name: m.Name, 94 | Where: m.Where, 95 | Method: m.Method, // Maybe TODO 96 | methodReflection: m.methodReflection, // Maybe TODO 97 | parsedMethod: parsedMethod, 98 | Description: m.Description, 99 | } 100 | } 101 | 102 | func (m *enum) copy() *enum { 103 | res := &enum{ 104 | contentType: m.contentType, 105 | contentKind: m.contentKind, 106 | typeName: m.typeName, 107 | entries: []enumEntry{}, 108 | qlType: *m.qlType.copy(), 109 | } 110 | for _, entry := range m.entries { 111 | res.entries = append(res.entries, enumEntry{ 112 | keyBytes: entry.keyBytes[:], 113 | key: entry.key, 114 | value: entry.value, // Maybe TODO 115 | }) 116 | } 117 | 118 | return res 119 | } 120 | 121 | func (m *qlType) copy() *qlType { 122 | res := &qlType{ 123 | Kind: m.Kind, 124 | Fields: m.Fields, 125 | PossibleTypes: m.PossibleTypes, 126 | EnumValues: m.EnumValues, 127 | InputFields: m.InputFields, 128 | 129 | // The json fields are not relevant in the context this method is used 130 | } 131 | if m.Name != nil { 132 | res.Name = helpers.StrPtr("") 133 | *res.Name = *m.Name 134 | } 135 | if m.Description != nil { 136 | res.Name = helpers.StrPtr("") 137 | *res.Name = *m.Name 138 | } 139 | if m.Interfaces != nil { 140 | res.Interfaces = make([]qlType, len(m.Interfaces)) 141 | for idx, interf := range m.Interfaces { 142 | res.Interfaces[idx] = *interf.copy() 143 | } 144 | } 145 | if m.OfType != nil { 146 | res.OfType = m.OfType.copy() 147 | } 148 | return res 149 | } 150 | 151 | func (m *inputMap) copy() *inputMap { 152 | res := inputMap{} 153 | for key, value := range *m { 154 | res[key] = value.copy() 155 | } 156 | return &res 157 | } 158 | 159 | func (t *types) copy() *types { 160 | res := types{} 161 | 162 | for k, v := range *t { 163 | res[k] = v.copy() 164 | } 165 | 166 | return &res 167 | } 168 | 169 | func (o *obj) copy() *obj { 170 | res := obj{ 171 | valueType: o.valueType, 172 | typeName: o.typeName, 173 | typeNameBytes: o.typeNameBytes[:], 174 | goTypeName: o.goTypeName, 175 | goPkgPath: o.goPkgPath, 176 | qlFieldName: o.qlFieldName[:], 177 | customObjValue: o.customObjValue, // maybe TODO 178 | structFieldIdx: o.structFieldIdx, 179 | dataValueType: o.dataValueType, 180 | isID: o.isID, 181 | enumTypeIndex: o.enumTypeIndex, 182 | } 183 | 184 | if o.innerContent != nil { 185 | res.innerContent = o.innerContent.copy() 186 | } 187 | 188 | if o.method != nil { 189 | res.method = o.method.copy() 190 | } 191 | 192 | if o.objContents != nil { 193 | res.objContents = map[uint32]*obj{} 194 | for key, value := range o.objContents { 195 | res.objContents[key] = value.copy() 196 | } 197 | } 198 | 199 | if o.implementations != nil { 200 | for _, impl := range o.implementations { 201 | res.implementations = append(res.implementations, impl.copy()) 202 | } 203 | } 204 | 205 | return &res 206 | } 207 | 208 | func (m *objMethod) copy() *objMethod { 209 | res := objMethod{ 210 | isTypeMethod: m.isTypeMethod, 211 | goFunctionName: m.goFunctionName, 212 | goType: m.goType, 213 | checkedIns: m.checkedIns, 214 | outNr: m.outNr, 215 | outType: *m.outType.copy(), 216 | } 217 | if m.errorOutNr != nil { 218 | errOutNr := 0 219 | res.errorOutNr = &errOutNr 220 | 221 | *res.errorOutNr = *m.errorOutNr 222 | } 223 | 224 | if m.ins != nil { 225 | res.ins = make([]baseInput, len(m.ins)) 226 | for i, in := range m.ins { 227 | res.ins[i] = *in.copy() 228 | } 229 | } 230 | 231 | if m.inFields != nil { 232 | res.inFields = map[string]referToInput{} 233 | for key, value := range m.inFields { 234 | res.inFields[key] = *value.copy() 235 | } 236 | } 237 | 238 | return &res 239 | } 240 | 241 | func (m *referToInput) copy() *referToInput { 242 | return &referToInput{ 243 | inputIdx: m.inputIdx, 244 | input: *m.input.copy(), 245 | } 246 | } 247 | 248 | func (m *input) copy() *input { 249 | var structContent map[string]input 250 | 251 | if m.structContent != nil { 252 | structContent = map[string]input{} 253 | for key, value := range m.structContent { 254 | structContent[key] = *value.copy() 255 | } 256 | } 257 | 258 | var elem *input 259 | if m.elem != nil { 260 | elem = m.elem.copy() 261 | } 262 | 263 | return &input{ 264 | kind: m.kind, 265 | isEnum: m.isEnum, 266 | enumTypeIndex: m.enumTypeIndex, 267 | isID: m.isID, 268 | isFile: m.isFile, 269 | isTime: m.isTime, 270 | goFieldIdx: m.goFieldIdx, 271 | gqFieldName: m.gqFieldName, 272 | elem: elem, 273 | isStructPointers: m.isStructPointers, 274 | structName: m.structName, 275 | structContent: structContent, 276 | } 277 | } 278 | 279 | func (m *baseInput) copy() *baseInput { 280 | res := &baseInput{ 281 | isCtx: m.isCtx, 282 | } 283 | if m.goType != nil { 284 | reflectType := reflect.TypeOf(0) 285 | res.goType = &reflectType 286 | 287 | *res.goType = *m.goType 288 | } 289 | 290 | return res 291 | } 292 | -------------------------------------------------------------------------------- /directives.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | ) 7 | 8 | // DirectiveLocation defines the location a directive can be used in 9 | type DirectiveLocation uint8 10 | 11 | const ( 12 | // DirectiveLocationField can be called from a field 13 | DirectiveLocationField DirectiveLocation = iota 14 | // DirectiveLocationFragment can be called from a fragment 15 | DirectiveLocationFragment 16 | // DirectiveLocationFragmentInline can be called from a inline fragment 17 | DirectiveLocationFragmentInline 18 | ) 19 | 20 | // String returns the DirectiveLocation as a string 21 | func (l DirectiveLocation) String() string { 22 | switch l { 23 | case DirectiveLocationField: 24 | return "" 25 | case DirectiveLocationFragment: 26 | return "" 27 | case DirectiveLocationFragmentInline: 28 | return "" 29 | default: 30 | return "" 31 | } 32 | } 33 | 34 | // ToQlDirectiveLocation returns the matching graphql location 35 | func (l DirectiveLocation) ToQlDirectiveLocation() __DirectiveLocation { 36 | switch l { 37 | case DirectiveLocationField: 38 | return directiveLocationField 39 | case DirectiveLocationFragment: 40 | return directiveLocationFragmentSpread 41 | case DirectiveLocationFragmentInline: 42 | return directiveLocationInlineFragment 43 | default: 44 | return directiveLocationField 45 | } 46 | } 47 | 48 | // Directive is what defines a directive 49 | type Directive struct { 50 | // Required 51 | Name string 52 | Where []DirectiveLocation 53 | // Should be of type: func(args like any other method) DirectiveModifier 54 | Method interface{} 55 | methodReflection reflect.Value 56 | parsedMethod *objMethod 57 | 58 | // Not required 59 | Description string 60 | } 61 | 62 | // TODO 63 | // type ModifyOnWriteContent func(bytes []byte) []byte 64 | 65 | // DirectiveModifier defines modifications to the response 66 | // Nothing is this struct is required and will be ignored if not set 67 | type DirectiveModifier struct { 68 | // Skip field/(inline)fragment 69 | Skip bool 70 | 71 | // TODO make this 72 | // ModifyOnWriteContent allows you to modify field JSON response data before it's written to the result 73 | // Note that there is no checking for validation here it's up to you to return valid json 74 | // ModifyOnWriteContent ModifyOnWriteContent 75 | } 76 | 77 | // RegisterDirective registers a new directive 78 | func (s *Schema) RegisterDirective(directive Directive) error { 79 | if s.parsed { 80 | return errors.New("(*yarql.Schema).RegisterDirective() cannot be ran after (*yarql.Schema).Parse()") 81 | } 82 | 83 | err := checkDirective(&directive) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | ptrToDirective := &directive 89 | for _, location := range directive.Where { 90 | directivesForLocation, ok := s.definedDirectives[location] 91 | if !ok { 92 | directivesForLocation = []*Directive{} 93 | } else { 94 | // Check for already defined directives with the same name 95 | for _, alreadyDefinedDirective := range directivesForLocation { 96 | if directive.Name == alreadyDefinedDirective.Name { 97 | return errors.New("you cannot have duplicated directive names in " + location.String() + " with name " + directive.Name) 98 | } 99 | } 100 | } 101 | directivesForLocation = append(directivesForLocation, ptrToDirective) 102 | s.definedDirectives[location] = directivesForLocation 103 | } 104 | 105 | return nil 106 | } 107 | 108 | func checkDirective(directive *Directive) error { 109 | if len(directive.Name) == 0 { 110 | return errors.New("cannot register directive with empty name") 111 | } 112 | for _, char := range directive.Name { 113 | if char >= '0' && char <= '9' || char >= 'A' && char <= 'Z' || char >= 'a' && char <= 'z' || char == '_' { 114 | continue 115 | } 116 | return errors.New(string(char) + " in " + directive.Name + " is not allowed as directive name") 117 | } 118 | if directive.Where == nil { 119 | return errors.New("where must be defined") 120 | } 121 | if directive.Method == nil { 122 | return errors.New("method must be defined") 123 | } 124 | if directive.Method == nil { 125 | return errors.New("method must be defined") 126 | } 127 | directive.methodReflection = reflect.ValueOf(directive.Method) 128 | if directive.methodReflection.IsNil() { 129 | return errors.New("method must be defined") 130 | } 131 | if directive.methodReflection.Kind() != reflect.Func { 132 | return errors.New("method is not a function") 133 | } 134 | methodType := directive.methodReflection.Type() 135 | switch methodType.NumOut() { 136 | case 0: 137 | return errors.New("method should return DirectiveModifier") 138 | case 1: 139 | // OK 140 | default: 141 | return errors.New("method should only return DirectiveModifier") 142 | } 143 | 144 | outType := methodType.Out(0) 145 | directiveModifierType := reflect.TypeOf(DirectiveModifier{}) 146 | if outType.Name() != directiveModifierType.Name() || outType.PkgPath() != directiveModifierType.PkgPath() { 147 | return errors.New("method should return DirectiveModifier") 148 | } 149 | 150 | directive.parsedMethod = &objMethod{ 151 | isTypeMethod: false, 152 | goType: methodType, 153 | 154 | ins: []baseInput{}, 155 | inFields: map[string]referToInput{}, 156 | checkedIns: false, 157 | } 158 | 159 | // Inputs checked in (s *Schema).Parse(..) 160 | 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /enums.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "sort" 8 | 9 | h "github.com/mjarkk/yarql/helpers" 10 | ) 11 | 12 | type enum struct { 13 | contentType reflect.Type 14 | contentKind reflect.Kind 15 | typeName string 16 | entries []enumEntry 17 | qlType qlType 18 | } 19 | 20 | type enumEntry struct { 21 | keyBytes []byte 22 | key string 23 | value reflect.Value 24 | } 25 | 26 | func (s *Schema) getEnum(t reflect.Type) (int, *enum) { 27 | if len(t.PkgPath()) == 0 || len(t.Name()) == 0 || !validEnumType(t) { 28 | return -1, nil 29 | } 30 | 31 | for i, enum := range s.definedEnums { 32 | if enum.typeName == t.Name() { 33 | return i, &enum 34 | } 35 | } 36 | 37 | return -1, nil 38 | } 39 | 40 | func validEnumType(t reflect.Type) bool { 41 | switch t.Kind() { 42 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 43 | // All int kinds are allowed 44 | return true 45 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 46 | // All uint kinds are allowed 47 | return true 48 | case reflect.String: 49 | // Strings are allowed 50 | return true 51 | default: 52 | return false 53 | } 54 | } 55 | 56 | // RegisterEnum registers a new enum type 57 | func (s *Schema) RegisterEnum(enumMap interface{}) (added bool, err error) { 58 | if s.parsed { 59 | return false, errors.New("(*yarql.Schema).RegisterEnum() cannot be ran after (*yarql.Schema).Parse()") 60 | } 61 | 62 | enum, err := registerEnumCheck(enumMap) 63 | if enum == nil || err != nil { 64 | return false, err 65 | } 66 | 67 | s.definedEnums = append(s.definedEnums, *enum) 68 | return true, nil 69 | } 70 | 71 | func registerEnumCheck(enumMap interface{}) (*enum, error) { 72 | mapReflection := reflect.ValueOf(enumMap) 73 | invalidTypeMsg := fmt.Errorf("RegisterEnum input must be of type map[string]CustomType(int..|uint..|string) as input, %+v given", enumMap) 74 | 75 | if enumMap == nil || mapReflection.IsZero() || mapReflection.IsNil() { 76 | return nil, invalidTypeMsg 77 | } 78 | 79 | mapType := mapReflection.Type() 80 | 81 | if mapType.Kind() != reflect.Map { 82 | // Tye input type must be a map 83 | return nil, invalidTypeMsg 84 | } 85 | if mapType.Key().Kind() != reflect.String { 86 | // The map key must be a string 87 | return nil, invalidTypeMsg 88 | } 89 | contentType := mapType.Elem() 90 | if !validEnumType(contentType) { 91 | return nil, invalidTypeMsg 92 | } 93 | 94 | if contentType.PkgPath() == "" || contentType.Name() == "" { 95 | return nil, errors.New("RegisterEnum input map value must have a global custom type value (type Animals string) or (type Rules uint64)") 96 | } 97 | 98 | inputLen := mapReflection.Len() 99 | if inputLen == 0 { 100 | // No point in registering enums with 0 items 101 | return nil, nil 102 | } 103 | 104 | entries := make([]enumEntry, inputLen) 105 | qlTypeEnumValues := make([]qlEnumValue, inputLen) 106 | 107 | iter := mapReflection.MapRange() 108 | i := 0 109 | for iter.Next() { 110 | k := iter.Key() 111 | keyStr := k.Interface().(string) 112 | if keyStr == "" { 113 | return nil, errors.New("RegisterEnum input map cannot contain empty keys") 114 | } 115 | 116 | err := validGraphQlName([]byte(keyStr)) 117 | if err != nil { 118 | return nil, errors.New(`RegisterEnum map key must start with an alphabetic character (lower or upper) followed by the same or a "_", key given: ` + keyStr) 119 | } 120 | 121 | entries[i] = enumEntry{ 122 | keyBytes: []byte(keyStr), 123 | key: keyStr, 124 | value: iter.Value(), 125 | } 126 | qlTypeEnumValues[i] = qlEnumValue{ 127 | Name: keyStr, 128 | Description: h.PtrToEmptyStr, 129 | IsDeprecated: false, 130 | DeprecationReason: nil, 131 | } 132 | i++ 133 | } 134 | sort.Slice(qlTypeEnumValues, func(a int, b int) bool { return qlTypeEnumValues[a].Name < qlTypeEnumValues[b].Name }) 135 | 136 | name := contentType.Name() 137 | qlType := qlType{ 138 | Kind: typeKindEnum, 139 | Name: &name, 140 | Description: h.PtrToEmptyStr, 141 | EnumValues: func(args isDeprecatedArgs) []qlEnumValue { return qlTypeEnumValues }, 142 | } 143 | 144 | return &enum{ 145 | contentType: contentType, 146 | contentKind: contentType.Kind(), 147 | entries: entries, 148 | typeName: name, 149 | qlType: qlType, 150 | }, nil 151 | } 152 | -------------------------------------------------------------------------------- /enums_test.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "testing" 5 | 6 | a "github.com/mjarkk/yarql/assert" 7 | ) 8 | 9 | func TestRegisterEnum(t *testing.T) { 10 | type TestEnumString string 11 | res, err := registerEnumCheck(map[string]TestEnumString{ 12 | "A": "B", 13 | }) 14 | a.NoError(t, err) 15 | a.NotNil(t, res) 16 | 17 | type TestEnumUint uint 18 | res, err = registerEnumCheck(map[string]TestEnumUint{ 19 | "A": 1, 20 | }) 21 | a.NoError(t, err) 22 | a.NotNil(t, res) 23 | 24 | type TestEnumInt int 25 | res, err = registerEnumCheck(map[string]TestEnumInt{ 26 | "A": 1, 27 | }) 28 | a.NoError(t, err) 29 | a.NotNil(t, res) 30 | } 31 | 32 | func TestEmptyEnumShouldNotBeRegistered(t *testing.T) { 33 | type TestEnum string 34 | res, err := registerEnumCheck(map[string]TestEnum{}) 35 | a.NoError(t, err) 36 | a.Nil(t, res) 37 | } 38 | 39 | func TestRegisterEnumFails(t *testing.T) { 40 | type TestEnum string 41 | 42 | _, err := registerEnumCheck(0) 43 | a.Error(t, err, "Cannot generate an enum of non map types") 44 | 45 | _, err = registerEnumCheck(nil) 46 | a.Error(t, err, "Cannot generate an enum of non map types 2") 47 | 48 | _, err = registerEnumCheck(map[int]TestEnum{1: "a"}) 49 | a.Error(t, err, "Enum must have a string key type") 50 | 51 | _, err = registerEnumCheck(map[string]struct{}{"a": {}}) 52 | a.Error(t, err, "Enum value cannot be complex") 53 | 54 | _, err = registerEnumCheck(map[string]string{"foo": "bar"}) 55 | a.Error(t, err, "Enum value must be a custom type") 56 | 57 | _, err = registerEnumCheck(map[string]TestEnum{"": ""}) 58 | a.Error(t, err, "Enum keys cannot be empty") 59 | 60 | // Maybe fix this?? 61 | // _, err = registerEnumCheck(map[string]TestEnum{ 62 | // "Foo": "Baz", 63 | // "Bar": "Baz", 64 | // }) 65 | // Error(t, err, "Enum cannot have duplicated values") 66 | 67 | _, err = registerEnumCheck(map[string]TestEnum{"1": ""}) 68 | a.Error(t, err, "Enum cannot have an invalid graphql name, where first letter is number") 69 | 70 | _, err = registerEnumCheck(map[string]TestEnum{"_": ""}) 71 | a.Error(t, err, "Enum cannot have an invalid graphql name, where first letter is underscore") 72 | 73 | _, err = registerEnumCheck(map[string]TestEnum{"A!!!!": ""}) 74 | a.Error(t, err, "Enum cannot have an invalid graphql name, where remainder of name is invalid") 75 | } 76 | 77 | type TestEnum2 uint8 78 | 79 | const ( 80 | TestEnum2Foo TestEnum2 = iota 81 | TestEnum2Bar 82 | TestEnum2Baz 83 | ) 84 | 85 | type TestEnumFunctionInput struct{} 86 | 87 | func (TestEnumFunctionInput) ResolveBar(args struct{ E TestEnum2 }) TestEnum2 { 88 | return args.E 89 | } 90 | 91 | func TestEnum(t *testing.T) { 92 | s := NewSchema() 93 | 94 | added, err := s.RegisterEnum(map[string]TestEnum2{ 95 | "FOO": TestEnum2Foo, 96 | "BAR": TestEnum2Bar, 97 | "BAZ": TestEnum2Baz, 98 | }) 99 | a.True(t, added) 100 | a.NoError(t, err) 101 | 102 | res, errs := bytecodeParse(t, s, `{bar(e: BAZ)}`, TestEnumFunctionInput{}, M{}, ResolveOptions{NoMeta: true}) 103 | for _, err := range errs { 104 | panic(err) 105 | } 106 | a.Equal(t, `{"bar":"BAZ"}`, res) 107 | } 108 | -------------------------------------------------------------------------------- /examples/fiber/.gitignore: -------------------------------------------------------------------------------- 1 | go-graphql-fiber-example 2 | -------------------------------------------------------------------------------- /examples/fiber/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mjarkk/yarql-fiber-example 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.11.0 7 | github.com/mjarkk/yarql v0.0.0-20210604095247-1a621d53c91c 8 | ) 9 | 10 | replace github.com/mjarkk/yarql => ../../ 11 | -------------------------------------------------------------------------------- /examples/fiber/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= 2 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gofiber/fiber/v2 v2.11.0 h1:97PoVZI3JLlJyfMHFhKZoEHQEfTwOXvhQs2+YoLr9jk= 6 | github.com/gofiber/fiber/v2 v2.11.0/go.mod h1:oZTLWqYnqpMMuF922SjGbsYZsdpE1MCfh416HNdweIM= 7 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 8 | github.com/klauspost/compress v1.12.2 h1:2KCfW3I9M7nSc5wOqXAlW2v2U6v+w6cbjvbfp+OykW8= 9 | github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 14 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 15 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 16 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 17 | github.com/valyala/fasthttp v1.26.0 h1:k5Tooi31zPG/g8yS6o2RffRO2C9B9Kah9SY8j/S7058= 18 | github.com/valyala/fasthttp v1.26.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= 19 | github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= 20 | github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= 21 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 22 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 23 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 24 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 25 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 26 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= 29 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 31 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 32 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 33 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 36 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /examples/fiber/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "mime/multipart" 6 | 7 | "github.com/gofiber/fiber/v2" 8 | yarql "github.com/mjarkk/yarql" 9 | ) 10 | 11 | func main() { 12 | app := fiber.New() 13 | 14 | schema := yarql.NewSchema() 15 | err := schema.Parse(QueryRoot{}, MethodRoot{}, nil) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | app.All("/graphql", func(c *fiber.Ctx) error { 21 | res, _ := schema.HandleRequest( 22 | c.Method(), 23 | func(key string) string { return c.Query(key) }, 24 | func(key string) (string, error) { return c.FormValue(key), nil }, 25 | func() []byte { return c.Body() }, 26 | string(c.Request().Header.ContentType()), 27 | &yarql.RequestOptions{ 28 | GetFormFile: func(key string) (*multipart.FileHeader, error) { return c.FormFile(key) }, 29 | Tracing: true, 30 | }, 31 | ) 32 | 33 | c.Response().Header.Set("Content-Type", "application/json") 34 | return c.Send(res) 35 | }) 36 | 37 | app.Listen(":3000") 38 | } 39 | -------------------------------------------------------------------------------- /examples/fiber/schema.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // QueryRoot defines the entry point for all graphql queries 4 | type QueryRoot struct{} 5 | 6 | // MethodRoot defines the entry for all method graphql queries 7 | type MethodRoot struct{} 8 | 9 | // User contains the data of a user 10 | type User struct { 11 | ID uint `gq:"id"` 12 | Name string 13 | Email string 14 | } 15 | 16 | // Post contains the data of a post 17 | type Post struct { 18 | Title string 19 | } 20 | 21 | // ResolveUsers resolves a list of users 22 | func (QueryRoot) ResolveUsers() []User { 23 | return []User{ 24 | {ID: 1, Name: "Pieter", Email: "pietpaulesma@gmail.com"}, 25 | {ID: 2, Name: "Peer", Email: "peer@gmail.com"}, 26 | {ID: 3, Name: "Henk", Email: "henk@gmail.com"}, 27 | } 28 | } 29 | 30 | // ResolvePosts resolves all the posts of a user 31 | func (u User) ResolvePosts() []Post { 32 | if u.ID == 1 { 33 | return []Post{ 34 | {Title: "Very nice"}, 35 | {Title: "Very cool"}, 36 | {Title: "Ok"}, 37 | } 38 | } 39 | return []Post{} 40 | } 41 | -------------------------------------------------------------------------------- /examples/gin/.gitignore: -------------------------------------------------------------------------------- 1 | go-graphql-gin-example 2 | -------------------------------------------------------------------------------- /examples/gin/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mjarkk/yarql-gin-example 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.7.2 // indirect 7 | github.com/go-playground/validator/v10 v10.6.1 // indirect 8 | github.com/golang/protobuf v1.5.2 // indirect 9 | github.com/json-iterator/go v1.1.11 // indirect 10 | github.com/leodido/go-urn v1.2.1 // indirect 11 | github.com/mattn/go-isatty v0.0.13 // indirect 12 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 13 | github.com/modern-go/reflect2 v1.0.1 // indirect 14 | github.com/ugorji/go v1.2.6 // indirect 15 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect 16 | golang.org/x/sys v0.0.0-20210603125802-9665404d3644 // indirect 17 | golang.org/x/text v0.3.6 // indirect 18 | gopkg.in/yaml.v2 v2.4.0 // indirect 19 | github.com/mjarkk/yarql v0.0.0-20210604095247-1a621d53c91c // indirect 20 | ) 21 | 22 | replace github.com/mjarkk/yarql => ../../ 23 | -------------------------------------------------------------------------------- /examples/gin/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 5 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 6 | github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA= 7 | github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 8 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 9 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 10 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 11 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 12 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 13 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 14 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 15 | github.com/go-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I= 16 | github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= 17 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 18 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 19 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 20 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 21 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 22 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 23 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 24 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 25 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 26 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= 27 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 28 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 29 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 30 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 31 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 32 | github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= 33 | github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 34 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 36 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 37 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 38 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 39 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 43 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 44 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 45 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 46 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 47 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 48 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 49 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 50 | github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= 51 | github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= 52 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 53 | github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= 54 | github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= 55 | github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= 56 | github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= 57 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 58 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 59 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= 60 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 61 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 62 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= 63 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 65 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I= 69 | golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 71 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 72 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 73 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 74 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 75 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 76 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= 78 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 79 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 80 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 82 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 83 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 84 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 87 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 88 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 89 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 90 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 91 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 92 | -------------------------------------------------------------------------------- /examples/gin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "mime/multipart" 7 | "sync" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/mjarkk/yarql" 11 | ) 12 | 13 | func main() { 14 | r := gin.Default() 15 | 16 | schema := yarql.NewSchema() 17 | err := schema.Parse(QueryRoot{}, MethodRoot{}, nil) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | // The GraphQL is not thread safe so we use this lock to prevent race conditions and other errors 23 | var lock sync.Mutex 24 | 25 | r.Any("/graphql", func(c *gin.Context) { 26 | var form *multipart.Form 27 | 28 | getForm := func() (*multipart.Form, error) { 29 | if form != nil { 30 | return form, nil 31 | } 32 | 33 | var err error 34 | form, err = c.MultipartForm() 35 | return form, err 36 | } 37 | 38 | lock.Lock() 39 | defer lock.Unlock() 40 | 41 | res, _ := schema.HandleRequest( 42 | c.Request.Method, 43 | c.Query, 44 | func(key string) (string, error) { 45 | form, err := getForm() 46 | if err != nil { 47 | return "", err 48 | } 49 | values, ok := form.Value[key] 50 | if !ok || len(values) == 0 { 51 | return "", nil 52 | } 53 | return values[0], nil 54 | }, 55 | func() []byte { 56 | requestBody, _ := ioutil.ReadAll(c.Request.Body) 57 | return requestBody 58 | }, 59 | c.ContentType(), 60 | &yarql.RequestOptions{ 61 | GetFormFile: func(key string) (*multipart.FileHeader, error) { 62 | form, err := getForm() 63 | if err != nil { 64 | return nil, err 65 | } 66 | files, ok := form.File[key] 67 | if !ok || len(files) == 0 { 68 | return nil, nil 69 | } 70 | return files[0], nil 71 | }, 72 | Tracing: true, 73 | }, 74 | ) 75 | 76 | c.Data(200, "application/json", res) 77 | }) 78 | 79 | r.Run() 80 | } 81 | -------------------------------------------------------------------------------- /examples/gin/schema.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // QueryRoot defines the entry point for all graphql queries 4 | type QueryRoot struct{} 5 | 6 | // MethodRoot defines the entry for all method graphql queries 7 | type MethodRoot struct{} 8 | 9 | // User contains information about a user 10 | type User struct { 11 | ID uint `gq:"id"` 12 | Name string 13 | Email string 14 | } 15 | 16 | // Post contains information about a post 17 | type Post struct { 18 | Title string 19 | } 20 | 21 | // ResolveUsers resolves all users 22 | func (QueryRoot) ResolveUsers() []User { 23 | return []User{ 24 | {ID: 1, Name: "Pieter", Email: "pietpaulesma@gmail.com"}, 25 | {ID: 2, Name: "Peer", Email: "peer@gmail.com"}, 26 | {ID: 3, Name: "Henk", Email: "henk@gmail.com"}, 27 | } 28 | } 29 | 30 | // ResolvePosts resolves the posts of a specific user posts 31 | func (u User) ResolvePosts() []Post { 32 | if u.ID == 1 { 33 | return []Post{ 34 | {Title: "Very nice"}, 35 | {Title: "Very cool"}, 36 | {Title: "Ok"}, 37 | } 38 | } 39 | return []Post{} 40 | } 41 | -------------------------------------------------------------------------------- /examples/relay/README.md: -------------------------------------------------------------------------------- 1 | # Relay example 2 | 3 | # Backend 4 | 5 | ```sh 6 | cd backend 7 | go run . 8 | ``` 9 | 10 | # Frontend 11 | 12 | ```sh 13 | cd frontend 14 | npm install 15 | 16 | # Generate the schema.graphql file 17 | # Note backend needs to be running for this step 18 | npm run get-schema 19 | 20 | # Run the relay compiler 21 | npm run relay 22 | 23 | # Start the frontend 24 | # Note to view the website the backend needs to be running 25 | npm run start 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/relay/backend/.gitignore: -------------------------------------------------------------------------------- 1 | go-graphql-relay-example 2 | -------------------------------------------------------------------------------- /examples/relay/backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mjarkk/yarql-relay-example 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.11.0 7 | github.com/mjarkk/yarql v0.0.0-20210604095247-1a621d53c91c 8 | ) 9 | 10 | replace github.com/mjarkk/yarql => ../../../ 11 | -------------------------------------------------------------------------------- /examples/relay/backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= 2 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gofiber/fiber/v2 v2.11.0 h1:97PoVZI3JLlJyfMHFhKZoEHQEfTwOXvhQs2+YoLr9jk= 6 | github.com/gofiber/fiber/v2 v2.11.0/go.mod h1:oZTLWqYnqpMMuF922SjGbsYZsdpE1MCfh416HNdweIM= 7 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 8 | github.com/klauspost/compress v1.12.2 h1:2KCfW3I9M7nSc5wOqXAlW2v2U6v+w6cbjvbfp+OykW8= 9 | github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 14 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 15 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 16 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 17 | github.com/valyala/fasthttp v1.26.0 h1:k5Tooi31zPG/g8yS6o2RffRO2C9B9Kah9SY8j/S7058= 18 | github.com/valyala/fasthttp v1.26.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= 19 | github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= 20 | github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= 21 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 22 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 23 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 24 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 25 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 26 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= 29 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 31 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 32 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 33 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 36 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /examples/relay/backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "mime/multipart" 6 | 7 | "github.com/gofiber/fiber/v2" 8 | "github.com/gofiber/fiber/v2/middleware/cors" 9 | "github.com/mjarkk/yarql" 10 | ) 11 | 12 | func main() { 13 | app := fiber.New() 14 | 15 | app.Use(cors.New()) 16 | 17 | schema := yarql.NewSchema() 18 | err := schema.Parse(QueryRoot{}, MethodRoot{}, nil) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | app.All("/graphql", func(c *fiber.Ctx) error { 24 | res, _ := schema.HandleRequest( 25 | c.Method(), 26 | func(key string) string { return c.Query(key) }, 27 | func(key string) (string, error) { return c.FormValue(key), nil }, 28 | func() []byte { return c.Body() }, 29 | string(c.Request().Header.ContentType()), 30 | &yarql.RequestOptions{ 31 | GetFormFile: func(key string) (*multipart.FileHeader, error) { return c.FormFile(key) }, 32 | Tracing: true, 33 | }, 34 | ) 35 | 36 | c.Response().Header.Set("Content-Type", "application/json") 37 | return c.Send(res) 38 | }) 39 | 40 | log.Fatal(app.Listen(":5500")) 41 | } 42 | -------------------------------------------------------------------------------- /examples/relay/backend/schema.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mjarkk/yarql" 7 | ) 8 | 9 | // QueryRoot defines the entry point for all graphql queries 10 | type QueryRoot struct{} 11 | 12 | // MethodRoot defines the entry for all method graphql queries 13 | type MethodRoot struct{} 14 | 15 | // Node defines the required relay Node interface 16 | // ref: https://relay.dev/docs/guides/graphql-server-specification/ 17 | type Node interface { 18 | ResolveId() (uint, yarql.AttrIsID) 19 | } 20 | 21 | // Todo respresents a todo entry 22 | type Todo struct { 23 | ID uint `gq:"-"` // ignored because of (Todo).ResolveId() 24 | Title string 25 | Done bool 26 | } 27 | 28 | var _ = yarql.Implements((*Node)(nil), Todo{}) 29 | 30 | // ResolveId implements the Node interface 31 | func (u Todo) ResolveId() (uint, yarql.AttrIsID) { 32 | return u.ID, 0 33 | } 34 | 35 | var todoIdx = uint(3) 36 | 37 | var todos = []Todo{ 38 | {ID: 1, Title: "Get groceries", Done: false}, 39 | {ID: 2, Title: "Make TODO app", Done: true}, 40 | } 41 | 42 | // ResolveTodos returns all todos 43 | func (QueryRoot) ResolveTodos() []Todo { 44 | return todos 45 | } 46 | 47 | // GetTodoArgs are the arguments for the ResolveTodo 48 | type GetTodoArgs struct { 49 | ID uint `gq:"id,id"` // rename field to id and label field to have ID type 50 | } 51 | 52 | // ResolveTodo returns a todo by id 53 | func (q QueryRoot) ResolveTodo(args GetTodoArgs) *Todo { 54 | for _, todo := range todos { 55 | if todo.ID == args.ID { 56 | return &todo 57 | } 58 | } 59 | return nil 60 | } 61 | 62 | // CreateTodoArgs are the arguments for the ResolveCreateTodo 63 | type CreateTodoArgs struct { 64 | Title string 65 | } 66 | 67 | // ResolveCreateTodo creates a new todo 68 | func (m MethodRoot) ResolveCreateTodo(args CreateTodoArgs) Todo { 69 | todo := Todo{ 70 | ID: todoIdx, 71 | Title: fmt.Sprint(args.Title), // Copy title 72 | Done: false, 73 | } 74 | todos = append(todos, todo) 75 | todoIdx++ 76 | return todo 77 | } 78 | 79 | // UpdateTodoArgs are the arguments for the ResolveUpdateTodo 80 | type UpdateTodoArgs struct { 81 | ID uint `gq:"id,id"` // rename field to id and label field to have ID type 82 | Title *string 83 | Done *bool 84 | } 85 | 86 | // ResolveUpdateTodo updates a todo 87 | func (m MethodRoot) ResolveUpdateTodo(args UpdateTodoArgs) (Todo, error) { 88 | idx := -1 89 | for i, todo := range todos { 90 | if todo.ID == args.ID { 91 | idx = i 92 | break 93 | } 94 | } 95 | if idx == -1 { 96 | return Todo{}, fmt.Errorf("todo with id %d not found", args.ID) 97 | } 98 | 99 | todo := todos[idx] 100 | if args.Title != nil { 101 | todo.Title = *args.Title 102 | } 103 | if args.Done != nil { 104 | todo.Done = *args.Done 105 | } 106 | todos[idx] = todo 107 | 108 | return todo, nil 109 | } 110 | 111 | // ResolveDeleteTodo deletes a todo 112 | func (m MethodRoot) ResolveDeleteTodo(args GetTodoArgs) ([]Todo, error) { 113 | idx := -1 114 | for i, todo := range todos { 115 | if todo.ID == args.ID { 116 | idx = i 117 | break 118 | } 119 | } 120 | if idx == -1 { 121 | return nil, fmt.Errorf("todo with id %d not found", args.ID) 122 | } 123 | 124 | todos = append(todos[:idx], todos[idx+1:]...) 125 | return todos, nil 126 | } 127 | -------------------------------------------------------------------------------- /examples/relay/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/relay/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /examples/relay/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.14.1", 7 | "@testing-library/react": "^11.2.7", 8 | "@testing-library/user-event": "^12.8.3", 9 | "babel-plugin-relay": "^12.0.0", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-relay": "^12.0.0", 13 | "react-scripts": "4.0.3", 14 | "relay-runtime": "^12.0.0", 15 | "web-vitals": "^1.1.2" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject", 22 | "relay": "relay-compiler --src ./src --schema ./schema.graphql", 23 | "relay:watch": "relay-compiler --src ./src --schema ./schema.graphql --watch", 24 | "get-schema": "get-graphql-schema http://127.0.0.1:5000/graphql > schema.graphql", 25 | "get-schema:json": "get-graphql-schema http://127.0.0.1:5000/graphql --json > schema.json" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "get-graphql-schema": "^2.1.2", 47 | "relay-compiler": "^12.0.0", 48 | "relay-config": "^12.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/relay/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjarkk/yarql/688af2cd6fdab0525548c9cff11e34cd367a00a6/examples/relay/frontend/public/favicon.ico -------------------------------------------------------------------------------- /examples/relay/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |

32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/relay/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjarkk/yarql/688af2cd6fdab0525548c9cff11e34cd367a00a6/examples/relay/frontend/public/logo192.png -------------------------------------------------------------------------------- /examples/relay/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjarkk/yarql/688af2cd6fdab0525548c9cff11e34cd367a00a6/examples/relay/frontend/public/logo512.png -------------------------------------------------------------------------------- /examples/relay/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/relay/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/relay/frontend/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryRoot 3 | mutation: MethodRoot 4 | } 5 | 6 | """ 7 | The File scalar type references to a multipart file, often used to upload files 8 | to the server. Expects a string with the form file field name 9 | """ 10 | scalar File 11 | 12 | type MethodRoot { 13 | createTodo(title: String!): Todo! 14 | deleteTodo(id: ID!): [Todo!] 15 | updateTodo(done: Boolean, id: ID!, title: String): Todo! 16 | } 17 | 18 | interface Node { 19 | id: ID! 20 | } 21 | 22 | type QueryRoot { 23 | todo(id: ID!): Todo 24 | todos: [Todo!] 25 | } 26 | 27 | """ 28 | The Time scalar type references to a ISO 8601 date+time, often used to insert 29 | and/or view dates. Expects a string with the ISO 8601 format 30 | """ 31 | scalar Time 32 | 33 | type Todo implements Node { 34 | done: Boolean! 35 | id: ID! 36 | title: String! 37 | } 38 | 39 | -------------------------------------------------------------------------------- /examples/relay/frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { usePreloadedQuery, commitMutation, useRelayEnvironment } from 'react-relay' 2 | import graphql from 'babel-plugin-relay/macro' 3 | import { Todo } from './Todo' 4 | import { useState } from 'react' 5 | 6 | export const AppQuery = graphql` 7 | query AppQuery { 8 | todos { 9 | ...TodoFragment 10 | } 11 | } 12 | `; 13 | 14 | export const AppCreateTodoMutation = graphql` 15 | mutation AppCreateTodoMutation($title: String!) { 16 | createTodo(title: $title) { 17 | ...TodoFragment 18 | } 19 | } 20 | ` 21 | 22 | export function App({ queryRef, refresh }) { 23 | const environment = useRelayEnvironment() 24 | const data = usePreloadedQuery( 25 | AppQuery, 26 | queryRef, 27 | ); 28 | const [newTodo, setNewTodo] = useState(''); 29 | 30 | const submitCreateNewTodo = e => { 31 | e.preventDefault() 32 | commitMutation(environment, { 33 | mutation: AppCreateTodoMutation, 34 | variables: { title: newTodo }, 35 | }) 36 | setNewTodo('') 37 | refresh() 38 | } 39 | 40 | return ( 41 |
42 |
43 | 44 | setNewTodo(e.target.value)} 47 | /> 48 |
49 | 50 |
51 | {data.todos.map(todo => 52 | 53 | )} 54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /examples/relay/frontend/src/RelayEnvironment.js: -------------------------------------------------------------------------------- 1 | import { Environment, Network, RecordSource, Store } from 'relay-runtime'; 2 | 3 | // Relay passes a "params" object with the query name and text. So we define a helper function 4 | // to call our fetchGraphQL utility with params.text. 5 | async function fetchRelay(params, variables) { 6 | console.log(`fetching query ${params.name} with ${JSON.stringify(variables)}`); 7 | 8 | // Fetch data from GitHub's GraphQL API: 9 | const response = await fetch('http://localhost:5000/graphql', { 10 | method: 'POST', 11 | headers: { 'Content-Type': 'application/json' }, 12 | body: JSON.stringify({ 13 | query: params.text, 14 | variables, 15 | }), 16 | }); 17 | 18 | // Get the response as JSON 19 | return await response.json(); 20 | } 21 | 22 | // Export a singleton instance of Relay Environment configured with our network function: 23 | export default new Environment({ 24 | network: Network.create(fetchRelay), 25 | store: new Store(new RecordSource()), 26 | }); 27 | -------------------------------------------------------------------------------- /examples/relay/frontend/src/Todo.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useFragment, commitMutation, useRelayEnvironment } from 'react-relay' 3 | import graphql from 'babel-plugin-relay/macro' 4 | 5 | export const TodoFragment = graphql` 6 | fragment TodoFragment on Todo { 7 | id 8 | title 9 | done 10 | } 11 | `; 12 | 13 | const TodoUpdateMutation = graphql` 14 | mutation TodoUpdateMutation($id: ID!, $done: Boolean, $title: String) { 15 | updateTodo(id: $id, done: $done, title: $title) { 16 | ...TodoFragment 17 | } 18 | } 19 | `; 20 | 21 | const TodoDeleteMutation = graphql` 22 | mutation TodoDeleteMutation($id: ID!) { 23 | deleteTodo(id: $id) { 24 | id 25 | } 26 | } 27 | `; 28 | 29 | 30 | export function Todo({ todo }) { 31 | const data = useFragment(TodoFragment, todo) 32 | const environment = useRelayEnvironment() 33 | const [updateTitle, setUpdateTitle] = useState(undefined) 34 | 35 | const update = (done, title) => { 36 | commitMutation(environment, { 37 | mutation: TodoUpdateMutation, 38 | variables: { 39 | id: data.id, 40 | done, 41 | title, 42 | }, 43 | }) 44 | } 45 | 46 | const delete_ = () => { 47 | const { id } = data 48 | commitMutation(environment, { 49 | mutation: TodoDeleteMutation, 50 | variables: { id }, 51 | updater: store => { 52 | const storeRoot = store.getRoot() 53 | storeRoot.setLinkedRecords( 54 | storeRoot.getLinkedRecords('todos') 55 | .filter(x => x.getDataID() !== id), 56 | 'todos', 57 | ) 58 | }, 59 | }) 60 | } 61 | 62 | const updateTitleSubmit = e => { 63 | e.preventDefault() 64 | update(data.done, updateTitle) 65 | setUpdateTitle(undefined) 66 | } 67 | 68 | if (!data) return undefined; 69 | 70 | return ( 71 | updateTitle === undefined ? 72 |
73 | update(!data.done, data.title)} 77 | /> 78 | 79 | 80 | {data.title} 81 |
82 | : 83 |
84 | 85 | setUpdateTitle(e.target.value)} 87 | value={updateTitle} 88 | /> 89 |
90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /examples/relay/frontend/src/__generated__/AppCreateTodoMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type { ConcreteRequest } from 'relay-runtime'; 11 | type TodoFragment$ref = any; 12 | export type AppCreateTodoMutationVariables = {| 13 | title: string 14 | |}; 15 | export type AppCreateTodoMutationResponse = {| 16 | +createTodo: {| 17 | +$fragmentRefs: TodoFragment$ref 18 | |} 19 | |}; 20 | export type AppCreateTodoMutation = {| 21 | variables: AppCreateTodoMutationVariables, 22 | response: AppCreateTodoMutationResponse, 23 | |}; 24 | */ 25 | 26 | 27 | /* 28 | mutation AppCreateTodoMutation( 29 | $title: String! 30 | ) { 31 | createTodo(title: $title) { 32 | ...TodoFragment 33 | id 34 | } 35 | } 36 | 37 | fragment TodoFragment on Todo { 38 | id 39 | title 40 | done 41 | } 42 | */ 43 | 44 | const node/*: ConcreteRequest*/ = (function(){ 45 | var v0 = [ 46 | { 47 | "defaultValue": null, 48 | "kind": "LocalArgument", 49 | "name": "title" 50 | } 51 | ], 52 | v1 = [ 53 | { 54 | "kind": "Variable", 55 | "name": "title", 56 | "variableName": "title" 57 | } 58 | ]; 59 | return { 60 | "fragment": { 61 | "argumentDefinitions": (v0/*: any*/), 62 | "kind": "Fragment", 63 | "metadata": null, 64 | "name": "AppCreateTodoMutation", 65 | "selections": [ 66 | { 67 | "alias": null, 68 | "args": (v1/*: any*/), 69 | "concreteType": "Todo", 70 | "kind": "LinkedField", 71 | "name": "createTodo", 72 | "plural": false, 73 | "selections": [ 74 | { 75 | "args": null, 76 | "kind": "FragmentSpread", 77 | "name": "TodoFragment" 78 | } 79 | ], 80 | "storageKey": null 81 | } 82 | ], 83 | "type": "MethodRoot", 84 | "abstractKey": null 85 | }, 86 | "kind": "Request", 87 | "operation": { 88 | "argumentDefinitions": (v0/*: any*/), 89 | "kind": "Operation", 90 | "name": "AppCreateTodoMutation", 91 | "selections": [ 92 | { 93 | "alias": null, 94 | "args": (v1/*: any*/), 95 | "concreteType": "Todo", 96 | "kind": "LinkedField", 97 | "name": "createTodo", 98 | "plural": false, 99 | "selections": [ 100 | { 101 | "alias": null, 102 | "args": null, 103 | "kind": "ScalarField", 104 | "name": "id", 105 | "storageKey": null 106 | }, 107 | { 108 | "alias": null, 109 | "args": null, 110 | "kind": "ScalarField", 111 | "name": "title", 112 | "storageKey": null 113 | }, 114 | { 115 | "alias": null, 116 | "args": null, 117 | "kind": "ScalarField", 118 | "name": "done", 119 | "storageKey": null 120 | } 121 | ], 122 | "storageKey": null 123 | } 124 | ] 125 | }, 126 | "params": { 127 | "cacheID": "98f73e54df3110673f78fbab6f8017f1", 128 | "id": null, 129 | "metadata": {}, 130 | "name": "AppCreateTodoMutation", 131 | "operationKind": "mutation", 132 | "text": "mutation AppCreateTodoMutation(\n $title: String!\n) {\n createTodo(title: $title) {\n ...TodoFragment\n id\n }\n}\n\nfragment TodoFragment on Todo {\n id\n title\n done\n}\n" 133 | } 134 | }; 135 | })(); 136 | // prettier-ignore 137 | (node/*: any*/).hash = '392c4e637c6c2e4a26107d120a738312'; 138 | 139 | module.exports = node; 140 | -------------------------------------------------------------------------------- /examples/relay/frontend/src/__generated__/AppQuery.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type { ConcreteRequest } from 'relay-runtime'; 11 | type TodoFragment$ref = any; 12 | export type AppQueryVariables = {||}; 13 | export type AppQueryResponse = {| 14 | +todos: ?$ReadOnlyArray<{| 15 | +$fragmentRefs: TodoFragment$ref 16 | |}> 17 | |}; 18 | export type AppQuery = {| 19 | variables: AppQueryVariables, 20 | response: AppQueryResponse, 21 | |}; 22 | */ 23 | 24 | 25 | /* 26 | query AppQuery { 27 | todos { 28 | ...TodoFragment 29 | id 30 | } 31 | } 32 | 33 | fragment TodoFragment on Todo { 34 | id 35 | title 36 | done 37 | } 38 | */ 39 | 40 | const node/*: ConcreteRequest*/ = { 41 | "fragment": { 42 | "argumentDefinitions": [], 43 | "kind": "Fragment", 44 | "metadata": null, 45 | "name": "AppQuery", 46 | "selections": [ 47 | { 48 | "alias": null, 49 | "args": null, 50 | "concreteType": "Todo", 51 | "kind": "LinkedField", 52 | "name": "todos", 53 | "plural": true, 54 | "selections": [ 55 | { 56 | "args": null, 57 | "kind": "FragmentSpread", 58 | "name": "TodoFragment" 59 | } 60 | ], 61 | "storageKey": null 62 | } 63 | ], 64 | "type": "QueryRoot", 65 | "abstractKey": null 66 | }, 67 | "kind": "Request", 68 | "operation": { 69 | "argumentDefinitions": [], 70 | "kind": "Operation", 71 | "name": "AppQuery", 72 | "selections": [ 73 | { 74 | "alias": null, 75 | "args": null, 76 | "concreteType": "Todo", 77 | "kind": "LinkedField", 78 | "name": "todos", 79 | "plural": true, 80 | "selections": [ 81 | { 82 | "alias": null, 83 | "args": null, 84 | "kind": "ScalarField", 85 | "name": "id", 86 | "storageKey": null 87 | }, 88 | { 89 | "alias": null, 90 | "args": null, 91 | "kind": "ScalarField", 92 | "name": "title", 93 | "storageKey": null 94 | }, 95 | { 96 | "alias": null, 97 | "args": null, 98 | "kind": "ScalarField", 99 | "name": "done", 100 | "storageKey": null 101 | } 102 | ], 103 | "storageKey": null 104 | } 105 | ] 106 | }, 107 | "params": { 108 | "cacheID": "3d21021aa5aa129a25e7d1a34ce0b73a", 109 | "id": null, 110 | "metadata": {}, 111 | "name": "AppQuery", 112 | "operationKind": "query", 113 | "text": "query AppQuery {\n todos {\n ...TodoFragment\n id\n }\n}\n\nfragment TodoFragment on Todo {\n id\n title\n done\n}\n" 114 | } 115 | }; 116 | // prettier-ignore 117 | (node/*: any*/).hash = 'ed0e6924e32ace481e4559fe8398b805'; 118 | 119 | module.exports = node; 120 | -------------------------------------------------------------------------------- /examples/relay/frontend/src/__generated__/TodoDeleteMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type { ConcreteRequest } from 'relay-runtime'; 11 | export type TodoDeleteMutationVariables = {| 12 | id: string 13 | |}; 14 | export type TodoDeleteMutationResponse = {| 15 | +deleteTodo: ?$ReadOnlyArray<{| 16 | +id: string 17 | |}> 18 | |}; 19 | export type TodoDeleteMutation = {| 20 | variables: TodoDeleteMutationVariables, 21 | response: TodoDeleteMutationResponse, 22 | |}; 23 | */ 24 | 25 | 26 | /* 27 | mutation TodoDeleteMutation( 28 | $id: ID! 29 | ) { 30 | deleteTodo(id: $id) { 31 | id 32 | } 33 | } 34 | */ 35 | 36 | const node/*: ConcreteRequest*/ = (function(){ 37 | var v0 = [ 38 | { 39 | "defaultValue": null, 40 | "kind": "LocalArgument", 41 | "name": "id" 42 | } 43 | ], 44 | v1 = [ 45 | { 46 | "alias": null, 47 | "args": [ 48 | { 49 | "kind": "Variable", 50 | "name": "id", 51 | "variableName": "id" 52 | } 53 | ], 54 | "concreteType": "Todo", 55 | "kind": "LinkedField", 56 | "name": "deleteTodo", 57 | "plural": true, 58 | "selections": [ 59 | { 60 | "alias": null, 61 | "args": null, 62 | "kind": "ScalarField", 63 | "name": "id", 64 | "storageKey": null 65 | } 66 | ], 67 | "storageKey": null 68 | } 69 | ]; 70 | return { 71 | "fragment": { 72 | "argumentDefinitions": (v0/*: any*/), 73 | "kind": "Fragment", 74 | "metadata": null, 75 | "name": "TodoDeleteMutation", 76 | "selections": (v1/*: any*/), 77 | "type": "MethodRoot", 78 | "abstractKey": null 79 | }, 80 | "kind": "Request", 81 | "operation": { 82 | "argumentDefinitions": (v0/*: any*/), 83 | "kind": "Operation", 84 | "name": "TodoDeleteMutation", 85 | "selections": (v1/*: any*/) 86 | }, 87 | "params": { 88 | "cacheID": "ab295834469c2bd92c58ef80860083bd", 89 | "id": null, 90 | "metadata": {}, 91 | "name": "TodoDeleteMutation", 92 | "operationKind": "mutation", 93 | "text": "mutation TodoDeleteMutation(\n $id: ID!\n) {\n deleteTodo(id: $id) {\n id\n }\n}\n" 94 | } 95 | }; 96 | })(); 97 | // prettier-ignore 98 | (node/*: any*/).hash = 'cd5b282bef51c07c308977e5889b00ae'; 99 | 100 | module.exports = node; 101 | -------------------------------------------------------------------------------- /examples/relay/frontend/src/__generated__/TodoFragment.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type { ReaderFragment } from 'relay-runtime'; 11 | import type { FragmentReference } from "relay-runtime"; 12 | declare export opaque type TodoFragment$ref: FragmentReference; 13 | declare export opaque type TodoFragment$fragmentType: TodoFragment$ref; 14 | export type TodoFragment = {| 15 | +id: string, 16 | +title: string, 17 | +done: boolean, 18 | +$refType: TodoFragment$ref, 19 | |}; 20 | export type TodoFragment$data = TodoFragment; 21 | export type TodoFragment$key = { 22 | +$data?: TodoFragment$data, 23 | +$fragmentRefs: TodoFragment$ref, 24 | ... 25 | }; 26 | */ 27 | 28 | 29 | const node/*: ReaderFragment*/ = { 30 | "argumentDefinitions": [], 31 | "kind": "Fragment", 32 | "metadata": null, 33 | "name": "TodoFragment", 34 | "selections": [ 35 | { 36 | "alias": null, 37 | "args": null, 38 | "kind": "ScalarField", 39 | "name": "id", 40 | "storageKey": null 41 | }, 42 | { 43 | "alias": null, 44 | "args": null, 45 | "kind": "ScalarField", 46 | "name": "title", 47 | "storageKey": null 48 | }, 49 | { 50 | "alias": null, 51 | "args": null, 52 | "kind": "ScalarField", 53 | "name": "done", 54 | "storageKey": null 55 | } 56 | ], 57 | "type": "Todo", 58 | "abstractKey": null 59 | }; 60 | // prettier-ignore 61 | (node/*: any*/).hash = 'b80f9cfa2c9113fc4e0fd3802c259047'; 62 | 63 | module.exports = node; 64 | -------------------------------------------------------------------------------- /examples/relay/frontend/src/__generated__/TodoUpdateMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type { ConcreteRequest } from 'relay-runtime'; 11 | type TodoFragment$ref = any; 12 | export type TodoUpdateMutationVariables = {| 13 | id: string, 14 | done?: ?boolean, 15 | title?: ?string, 16 | |}; 17 | export type TodoUpdateMutationResponse = {| 18 | +updateTodo: {| 19 | +$fragmentRefs: TodoFragment$ref 20 | |} 21 | |}; 22 | export type TodoUpdateMutation = {| 23 | variables: TodoUpdateMutationVariables, 24 | response: TodoUpdateMutationResponse, 25 | |}; 26 | */ 27 | 28 | 29 | /* 30 | mutation TodoUpdateMutation( 31 | $id: ID! 32 | $done: Boolean 33 | $title: String 34 | ) { 35 | updateTodo(id: $id, done: $done, title: $title) { 36 | ...TodoFragment 37 | id 38 | } 39 | } 40 | 41 | fragment TodoFragment on Todo { 42 | id 43 | title 44 | done 45 | } 46 | */ 47 | 48 | const node/*: ConcreteRequest*/ = (function(){ 49 | var v0 = { 50 | "defaultValue": null, 51 | "kind": "LocalArgument", 52 | "name": "done" 53 | }, 54 | v1 = { 55 | "defaultValue": null, 56 | "kind": "LocalArgument", 57 | "name": "id" 58 | }, 59 | v2 = { 60 | "defaultValue": null, 61 | "kind": "LocalArgument", 62 | "name": "title" 63 | }, 64 | v3 = [ 65 | { 66 | "kind": "Variable", 67 | "name": "done", 68 | "variableName": "done" 69 | }, 70 | { 71 | "kind": "Variable", 72 | "name": "id", 73 | "variableName": "id" 74 | }, 75 | { 76 | "kind": "Variable", 77 | "name": "title", 78 | "variableName": "title" 79 | } 80 | ]; 81 | return { 82 | "fragment": { 83 | "argumentDefinitions": [ 84 | (v0/*: any*/), 85 | (v1/*: any*/), 86 | (v2/*: any*/) 87 | ], 88 | "kind": "Fragment", 89 | "metadata": null, 90 | "name": "TodoUpdateMutation", 91 | "selections": [ 92 | { 93 | "alias": null, 94 | "args": (v3/*: any*/), 95 | "concreteType": "Todo", 96 | "kind": "LinkedField", 97 | "name": "updateTodo", 98 | "plural": false, 99 | "selections": [ 100 | { 101 | "args": null, 102 | "kind": "FragmentSpread", 103 | "name": "TodoFragment" 104 | } 105 | ], 106 | "storageKey": null 107 | } 108 | ], 109 | "type": "MethodRoot", 110 | "abstractKey": null 111 | }, 112 | "kind": "Request", 113 | "operation": { 114 | "argumentDefinitions": [ 115 | (v1/*: any*/), 116 | (v0/*: any*/), 117 | (v2/*: any*/) 118 | ], 119 | "kind": "Operation", 120 | "name": "TodoUpdateMutation", 121 | "selections": [ 122 | { 123 | "alias": null, 124 | "args": (v3/*: any*/), 125 | "concreteType": "Todo", 126 | "kind": "LinkedField", 127 | "name": "updateTodo", 128 | "plural": false, 129 | "selections": [ 130 | { 131 | "alias": null, 132 | "args": null, 133 | "kind": "ScalarField", 134 | "name": "id", 135 | "storageKey": null 136 | }, 137 | { 138 | "alias": null, 139 | "args": null, 140 | "kind": "ScalarField", 141 | "name": "title", 142 | "storageKey": null 143 | }, 144 | { 145 | "alias": null, 146 | "args": null, 147 | "kind": "ScalarField", 148 | "name": "done", 149 | "storageKey": null 150 | } 151 | ], 152 | "storageKey": null 153 | } 154 | ] 155 | }, 156 | "params": { 157 | "cacheID": "ba868358442cfbc21acefaa4ee106dc0", 158 | "id": null, 159 | "metadata": {}, 160 | "name": "TodoUpdateMutation", 161 | "operationKind": "mutation", 162 | "text": "mutation TodoUpdateMutation(\n $id: ID!\n $done: Boolean\n $title: String\n) {\n updateTodo(id: $id, done: $done, title: $title) {\n ...TodoFragment\n id\n }\n}\n\nfragment TodoFragment on Todo {\n id\n title\n done\n}\n" 163 | } 164 | }; 165 | })(); 166 | // prettier-ignore 167 | (node/*: any*/).hash = '9a5204b230425bc65f5bea936e8b77df'; 168 | 169 | module.exports = node; 170 | -------------------------------------------------------------------------------- /examples/relay/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | font-size: 18px; 9 | } 10 | 11 | .App { 12 | padding: 10px; 13 | } 14 | 15 | .todo { 16 | padding: 5px; 17 | } 18 | -------------------------------------------------------------------------------- /examples/relay/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useCallback } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import { App, AppQuery } from './App'; 5 | import { RelayEnvironmentProvider, useQueryLoader, loadQuery } from 'react-relay/hooks'; 6 | import RelayEnvironment from "./RelayEnvironment" 7 | 8 | const initialQueryRef = loadQuery( 9 | RelayEnvironment, 10 | AppQuery, 11 | null, 12 | ); 13 | 14 | ReactDOM.render( 15 | 16 | 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | 23 | function AppWrapper({ initialQueryRef }) { 24 | const [queryRef, loadQuery] = useQueryLoader( 25 | AppQuery, 26 | initialQueryRef, 27 | ); 28 | 29 | const refresh = useCallback(() => { 30 | loadQuery({}, { fetchPolicy: 'network-only' }); 31 | }, [loadQuery]); 32 | 33 | return ( 34 | 35 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mjarkk/yarql 2 | 3 | go 1.16 4 | 5 | require github.com/valyala/fastjson v1.6.3 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= 2 | github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= 3 | -------------------------------------------------------------------------------- /grahql_types.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | h "github.com/mjarkk/yarql/helpers" 5 | ) 6 | 7 | // 8 | // Types represent: 9 | // https://spec.graphql.org/October2021/#sec-Schema-Introspection 10 | // 11 | 12 | var _ = TypeRename(qlSchema{}, "__Schema", true) 13 | 14 | type qlSchema struct { 15 | Types func() []qlType `json:"-"` 16 | // For testing perposes mainly 17 | JSONTypes []qlType `json:"types" gq:"-"` 18 | 19 | QueryType *qlType `json:"queryType"` 20 | MutationType *qlType `json:"mutationType"` 21 | SubscriptionType *qlType `json:"subscriptionType"` 22 | Directives []qlDirective `json:"directives"` 23 | } 24 | 25 | type isDeprecatedArgs struct { 26 | IncludeDeprecated bool `json:"includeDeprecated"` 27 | } 28 | 29 | type __TypeKind uint8 30 | 31 | const ( 32 | typeKindScalar __TypeKind = iota 33 | typeKindObject 34 | typeKindInterface 35 | typeKindUnion 36 | typeKindEnum 37 | typeKindInputObject 38 | typeKindList 39 | typeKindNonNull 40 | ) 41 | 42 | var typeKindEnumMap = map[string]__TypeKind{ 43 | "SCALAR": typeKindScalar, 44 | "OBJECT": typeKindObject, 45 | "INTERFACE": typeKindInterface, 46 | "UNION": typeKindUnion, 47 | "ENUM": typeKindEnum, 48 | "INPUT_OBJECT": typeKindInputObject, 49 | "LIST": typeKindList, 50 | "NON_NULL": typeKindNonNull, 51 | } 52 | 53 | func (kind __TypeKind) String() string { 54 | switch kind { 55 | case typeKindScalar: 56 | return "SCALAR" 57 | case typeKindObject: 58 | return "OBJECT" 59 | case typeKindInterface: 60 | return "INTERFACE" 61 | case typeKindUnion: 62 | return "UNION" 63 | case typeKindEnum: 64 | return "ENUM" 65 | case typeKindInputObject: 66 | return "INPUT_OBJECT" 67 | case typeKindList: 68 | return "LIST" 69 | case typeKindNonNull: 70 | return "NON_NULL" 71 | } 72 | return "" 73 | } 74 | 75 | var _ = TypeRename(qlType{}, "__Type", true) 76 | 77 | // This type represents the graphql __Type type 78 | // https://spec.graphql.org/October2021/#sec-Schema-Introspection 79 | type qlType struct { 80 | Kind __TypeKind `json:"-"` 81 | Name *string `json:"name"` 82 | Description *string `json:"description"` 83 | 84 | // OBJECT and INTERFACE only 85 | Fields func(isDeprecatedArgs) []qlField `json:"-"` 86 | 87 | // OBJECT only 88 | Interfaces []qlType `json:"interfaces"` 89 | 90 | // INTERFACE and UNION only 91 | PossibleTypes func() []qlType `json:"possibleTypes"` 92 | 93 | // ENUM only 94 | EnumValues func(isDeprecatedArgs) []qlEnumValue `json:"-"` 95 | 96 | // INPUT_OBJECT only 97 | InputFields func() []qlInputValue `json:"-"` 98 | 99 | // NON_NULL and LIST only 100 | OfType *qlType `json:"ofType"` 101 | 102 | // SCALAR only 103 | SpecifiedByURL *string `json:"specifiedByUrl"` 104 | 105 | // For testing perposes 106 | JSONKind string `json:"kind" gq:"-"` 107 | JSONFields []qlField `json:"fields" gq:"-"` 108 | JSONInputFields []qlField `json:"inputFields" gq:"-"` 109 | } 110 | 111 | var _ = TypeRename(qlField{}, "__Field", true) 112 | 113 | type qlField struct { 114 | Name string `json:"name"` 115 | Description *string `json:"description"` 116 | Args []qlInputValue `json:"args"` 117 | Type qlType `json:"type"` 118 | IsDeprecated bool `json:"isDeprecated"` 119 | DeprecationReason *string `json:"deprecationReason"` 120 | } 121 | 122 | var _ = TypeRename(qlEnumValue{}, "__EnumValue", true) 123 | 124 | type qlEnumValue struct { 125 | Name string `json:"name"` 126 | Description *string `json:"description"` 127 | IsDeprecated bool `json:"isDeprecated"` 128 | DeprecationReason *string `json:"deprecationReason"` 129 | } 130 | 131 | var _ = TypeRename(qlInputValue{}, "__InputValue", true) 132 | 133 | type qlInputValue struct { 134 | Name string `json:"name"` 135 | Description *string `json:"description"` 136 | Type qlType `json:"type"` 137 | DefaultValue *string `json:"defaultValue"` 138 | } 139 | 140 | type __DirectiveLocation uint8 141 | 142 | const ( 143 | directiveLocationQuery __DirectiveLocation = iota 144 | directiveLocationMutation 145 | directiveLocationSubscription 146 | directiveLocationField 147 | directiveLocationFragmentDefinition 148 | directiveLocationFragmentSpread 149 | directiveLocationInlineFragment 150 | directiveLocationSchema 151 | directiveLocationScalar 152 | directiveLocationObject 153 | directiveLocationFieldDefinition 154 | directiveLocationArgumentDefinition 155 | directiveLocationInterface 156 | directiveLocationUnion 157 | directiveLocationEnum 158 | directiveLocationEnumValue 159 | directiveLocationInputObject 160 | directiveLocationInputFieldDefinition 161 | ) 162 | 163 | var directiveLocationMap = map[string]__DirectiveLocation{ 164 | "QUERY": directiveLocationQuery, 165 | "MUTATION": directiveLocationMutation, 166 | "SUBSCRIPTION": directiveLocationSubscription, 167 | "FIELD": directiveLocationField, 168 | "FRAGMENT_DEFINITION": directiveLocationFragmentDefinition, 169 | "FRAGMENT_SPREAD": directiveLocationFragmentSpread, 170 | "INLINE_FRAGMENT": directiveLocationInlineFragment, 171 | "SCHEMA": directiveLocationSchema, 172 | "SCALAR": directiveLocationScalar, 173 | "OBJECT": directiveLocationObject, 174 | "FIELD_DEFINITION": directiveLocationFieldDefinition, 175 | "ARGUMENT_DEFINITION": directiveLocationArgumentDefinition, 176 | "INTERFACE": directiveLocationInterface, 177 | "UNION": directiveLocationUnion, 178 | "ENUM": directiveLocationEnum, 179 | "ENUM_VALUE": directiveLocationEnumValue, 180 | "INPUT_OBJECT": directiveLocationInputObject, 181 | "INPUT_FIELD_DEFINITION": directiveLocationInputFieldDefinition, 182 | } 183 | 184 | var _ = TypeRename(qlDirective{}, "__Directive", true) 185 | 186 | type qlDirective struct { 187 | Name string `json:"name"` 188 | Description *string `json:"description"` 189 | Locations []__DirectiveLocation `json:"-"` 190 | JSONLocations []string `json:"locations" gq:"-"` 191 | Args []qlInputValue `json:"args"` 192 | } 193 | 194 | var ( 195 | scalarBoolean = qlType{ 196 | Kind: typeKindScalar, 197 | Name: h.StrPtr("Boolean"), 198 | Description: h.StrPtr("The `Boolean` scalar type represents `true` or `false`."), 199 | } 200 | scalarInt = qlType{ 201 | Kind: typeKindScalar, 202 | Name: h.StrPtr("Int"), 203 | Description: h.StrPtr("The Int scalar type represents a signed 32‐bit numeric non‐fractional value."), 204 | } 205 | scalarFloat = qlType{ 206 | Kind: typeKindScalar, 207 | Name: h.StrPtr("Float"), 208 | Description: h.StrPtr("The Float scalar type represents signed double‐precision fractional values as specified by IEEE 754."), 209 | } 210 | scalarString = qlType{ 211 | Kind: typeKindScalar, 212 | Name: h.StrPtr("String"), 213 | Description: h.StrPtr("The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text."), 214 | } 215 | scalarID = qlType{ 216 | Kind: typeKindScalar, 217 | Name: h.StrPtr("ID"), 218 | Description: h.StrPtr("The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache"), 219 | } 220 | scalarFile = qlType{ 221 | Kind: typeKindScalar, 222 | Name: h.StrPtr("File"), 223 | Description: h.StrPtr("The File scalar type references to a multipart file, often used to upload files to the server. Expects a string with the form file field name"), 224 | SpecifiedByURL: h.StrPtr("https://github.com/mjarkk/yarql#file-upload"), 225 | } 226 | scalarTime = qlType{ 227 | Kind: typeKindScalar, 228 | Name: h.StrPtr("Time"), 229 | Description: h.StrPtr("The Time scalar type references to a ISO 8601 date+time, often used to insert and/or view dates. Expects a string with the ISO 8601 format"), 230 | SpecifiedByURL: h.StrPtr("https://en.wikipedia.org/wiki/ISO_8601"), 231 | } 232 | ) 233 | 234 | var scalars = map[string]qlType{ 235 | "Boolean": scalarBoolean, 236 | "Int": scalarInt, 237 | "Float": scalarFloat, 238 | "String": scalarString, 239 | "ID": scalarID, 240 | "File": scalarFile, 241 | "Time": scalarTime, 242 | } 243 | -------------------------------------------------------------------------------- /helpers/encodeFloat.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | ) 7 | 8 | // FloatToJSON is a Modified copy of https://golang.org/src/encoding/json/encode.go > floatEncoder.encode(..) 9 | // Copyright for function below: 10 | // 11 | // Copyright 2010 The Go Authors. All rights reserved. 12 | // Use of this source code is governed by a BSD-style 13 | // license that can be found in the LICENSE file. 14 | // 15 | // IMPORTANT the full license can be found in this repo: https://github.com/golang/go 16 | func FloatToJSON(bits int, f float64, e *[]byte) { 17 | if math.IsInf(f, 0) || math.IsNaN(f) { 18 | *e = append(*e, []byte("0.0")...) 19 | return 20 | } 21 | 22 | abs := math.Abs(f) 23 | fmt := byte('f') 24 | // Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right. 25 | if abs != 0 { 26 | if bits == 64 && (abs < 1e-6 || abs >= 1e21) || bits == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) { 27 | fmt = 'e' 28 | } 29 | } 30 | 31 | b := strconv.FormatFloat(f, fmt, -1, bits) 32 | if fmt == 'e' { 33 | // clean up e-09 to e-9 34 | n := len(b) 35 | if n >= 4 && b[n-4:n-1] == "e-0" { 36 | *e = append(*e, b[:n-2]...) 37 | *e = append(*e, b[n-1:]...) 38 | return 39 | } 40 | } 41 | *e = append(*e, b...) 42 | } 43 | -------------------------------------------------------------------------------- /helpers/encodeString.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "unicode/utf8" 5 | ) 6 | 7 | // StringToJSON converts a string into json and writes the result into e 8 | // Modified copy of https://golang.org/src/encoding/json/encode.go > encodeState.stringBytes(..) 9 | // Copyright for function below: 10 | // 11 | // Copyright 2010 The Go Authors. All rights reserved. 12 | // Use of this source code is governed by a BSD-style 13 | // license that can be found in the LICENSE file. 14 | // 15 | // IMPORTANT the full license can be found in this repo: https://github.com/golang/go 16 | func StringToJSON(s string, e *[]byte) { 17 | const hex = "0123456789abcdef" 18 | 19 | *e = append(*e, '"') 20 | start := 0 21 | for i := 0; i < len(s); { 22 | if b := s[i]; b < utf8.RuneSelf { 23 | if b >= ' ' && b <= '}' && b != '\\' && b != '"' { 24 | i++ 25 | continue 26 | } 27 | 28 | if b == '\u007f' { 29 | i++ 30 | continue 31 | } 32 | 33 | if start < i { 34 | *e = append(*e, s[start:i]...) 35 | } 36 | *e = append(*e, '\\') 37 | switch b { 38 | case '\\', '"': 39 | *e = append(*e, b) 40 | case '\n': 41 | *e = append(*e, 'n') 42 | case '\r': 43 | *e = append(*e, 'r') 44 | case '\t': 45 | *e = append(*e, 't') 46 | default: 47 | // This encodes bytes < 0x20 except for \t, \n and \r. 48 | // If escapeHTML is set, it also escapes <, >, and & 49 | // because they can lead to security holes when 50 | // user-controlled strings are rendered into JSON 51 | // and served to some browsers. 52 | *e = append(*e, []byte(`u00`)...) 53 | *e = append(*e, hex[b>>4], hex[b&0xF]) 54 | } 55 | i++ 56 | start = i 57 | continue 58 | } 59 | 60 | c, size := utf8.DecodeRuneInString(s[i:]) 61 | if c == utf8.RuneError && size == 1 { 62 | if start < i { 63 | *e = append(*e, s[start:i]...) 64 | } 65 | *e = append(*e, []byte(`\ufffd`)...) 66 | i += size 67 | start = i 68 | continue 69 | } 70 | // U+2028 is LINE SEPARATOR. 71 | // U+2029 is PARAGRAPH SEPARATOR. 72 | // They are both technically valid characters in JSON strings, 73 | // but don't work in JSONP, which has to be evaluated as JavaScript, 74 | // and can lead to security holes there. It is valid JSON to 75 | // escape them, so we do so unconditionally. 76 | // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. 77 | if c == '\u2028' || c == '\u2029' { 78 | if start < i { 79 | *e = append(*e, s[start:i]...) 80 | } 81 | *e = append(*e, []byte(`\u202`)...) 82 | *e = append(*e, hex[c&0xF]) 83 | i += size 84 | start = i 85 | continue 86 | } 87 | i += size 88 | } 89 | if start < len(s) { 90 | *e = append(*e, s[start:]...) 91 | } 92 | *e = append(*e, '"') 93 | } 94 | -------------------------------------------------------------------------------- /helpers/encode_string_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import "testing" 4 | 5 | func BenchmarkEncodeString(b *testing.B) { 6 | inputString1 := "abc" 7 | inputString2 := "Some long string that includes spaces and a ." 8 | inputString3 := `Wow this includes \\ and && and <> and ""` 9 | inputString4 := "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text." 10 | out := []byte{} 11 | 12 | for i := 0; i < b.N; i++ { 13 | StringToJSON(inputString1, &out) 14 | StringToJSON(inputString2, &out) 15 | StringToJSON(inputString3, &out) 16 | StringToJSON(inputString4, &out) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /helpers/pointers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | // CheckStrPtr returns a pointer to a string or returns a nil pointer if v is empty 4 | func CheckStrPtr(v string) *string { 5 | if len(v) == 0 { 6 | return nil 7 | } 8 | return StrPtr(v) 9 | } 10 | 11 | // StrPtr returns a pointer to a string. 12 | func StrPtr(v string) *string { 13 | return &v 14 | } 15 | 16 | // PtrToEmptyStr returns a nil string pointer 17 | var PtrToEmptyStr = new(string) 18 | 19 | // BoolPtr returns a pointer to a bool. 20 | func BoolPtr(v bool) *bool { 21 | return &v 22 | } 23 | 24 | // IntPtr returns a pointer to an int. 25 | func IntPtr(v int) *int { 26 | return &v 27 | } 28 | 29 | // Int64Ptr returns a pointer to a int64. 30 | func Int64Ptr(v int64) *int64 { 31 | return &v 32 | } 33 | 34 | // Int32Ptr returns a pointer to a int32. 35 | func Int32Ptr(v int32) *int32 { 36 | return &v 37 | } 38 | 39 | // Int16Ptr returns a pointer to a int16. 40 | func Int16Ptr(v int16) *int16 { 41 | return &v 42 | } 43 | 44 | // Int8Ptr returns a pointer to a int8. 45 | func Int8Ptr(v int8) *int8 { 46 | return &v 47 | } 48 | 49 | // UintPtr returns a pointer to a uint. 50 | func UintPtr(v uint) *uint { 51 | return &v 52 | } 53 | 54 | // Uint64Ptr returns a pointer to a uint64. 55 | func Uint64Ptr(v uint64) *uint64 { 56 | return &v 57 | } 58 | 59 | // Uint32Ptr returns a pointer to a uint32. 60 | func Uint32Ptr(v uint32) *uint32 { 61 | return &v 62 | } 63 | 64 | // Uint16Ptr returns a pointer to a uint16. 65 | func Uint16Ptr(v uint16) *uint16 { 66 | return &v 67 | } 68 | 69 | // Uint8Ptr returns a pointer to a uint8. 70 | func Uint8Ptr(v uint8) *uint8 { 71 | return &v 72 | } 73 | -------------------------------------------------------------------------------- /helpers/time.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | // The ISO 8601 layout might also be "2006-01-02T15:04:05.999Z" but it's mentioned less than the current so i presume what we're now using is correct 9 | var timeISO8601Layout = "2006-01-02T15:04:05.000Z" 10 | 11 | // ParseIso8601String parses a string in the ISO 8601 format 12 | func ParseIso8601String(val string) (time.Time, error) { 13 | parsedTime, err := time.Parse(timeISO8601Layout, val) 14 | if err != nil { 15 | return time.Time{}, errors.New("time value doesn't match the ISO 8601 layout") 16 | } 17 | return parsedTime, nil 18 | } 19 | 20 | // TimeToIso8601String converts a time.Time to a string in the ISO 8601 format 21 | // The value is appended to the target 22 | func TimeToIso8601String(target *[]byte, t time.Time) { 23 | *target = t.AppendFormat(*target, timeISO8601Layout) 24 | } 25 | -------------------------------------------------------------------------------- /implement_helpers.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "mime/multipart" 8 | "strings" 9 | 10 | "github.com/mjarkk/yarql/helpers" 11 | "github.com/valyala/fastjson" 12 | ) 13 | 14 | // RequestOptions are extra options / arguments for the (*Schema).HandleRequest method 15 | type RequestOptions struct { 16 | Context context.Context // Request context can be used to verify 17 | Values map[string]interface{} // Passed directly to the request context 18 | GetFormFile func(key string) (*multipart.FileHeader, error) // Get form file to support file uploading 19 | Tracing bool // https://github.com/apollographql/apollo-tracing 20 | } 21 | 22 | // HandleRequest handles a http request and returns a response 23 | func (s *Schema) HandleRequest( 24 | method string, // GET, POST, etc.. 25 | getQuery func(key string) string, // URL value (needs to be un-escaped before returning) 26 | getFormField func(key string) (string, error), // get form field, only used if content type == form data 27 | getBody func() []byte, // get the request body 28 | contentType string, // body content type, can be an empty string if method == "GET" 29 | options *RequestOptions, // optional options 30 | ) ([]byte, []error) { 31 | method = strings.ToUpper(method) 32 | 33 | errRes := func(errorMsg string) ([]byte, []error) { 34 | response := []byte(`{"data":{},"errors":[{"message":`) 35 | helpers.StringToJSON(errorMsg, &response) 36 | response = append(response, []byte(`}],"extensions":{}}`)...) 37 | return response, []error{errors.New(errorMsg)} 38 | } 39 | 40 | if contentType == "application/json" || ((contentType == "text/plain" || contentType == "multipart/form-data") && method != "GET") { 41 | var body []byte 42 | if contentType == "multipart/form-data" { 43 | value, err := getFormField("operations") 44 | if err != nil { 45 | return errRes(err.Error()) 46 | } 47 | body = []byte(value) 48 | } else { 49 | body = getBody() 50 | } 51 | if len(body) == 0 { 52 | return errRes("empty body") 53 | } 54 | 55 | var p fastjson.Parser 56 | v, err := p.Parse(string(body)) 57 | if err != nil { 58 | return errRes("invalid json body") 59 | } 60 | if v.Type() == fastjson.TypeArray { 61 | // Handle batch query 62 | responseErrs := []error{} 63 | response := bytes.NewBuffer([]byte("[")) 64 | for _, item := range v.GetArray() { 65 | // TODO potential speed improvement by executing all items at once 66 | if item == nil { 67 | continue 68 | } 69 | 70 | if response.Len() > 1 { 71 | response.WriteByte(',') 72 | } 73 | 74 | query, operationName, variables, err := getBodyData(item) 75 | if err != nil { 76 | responseErrs = append(responseErrs, err) 77 | res, _ := errRes(err.Error()) 78 | response.Write(res) 79 | } else { 80 | errs := s.handleSingleRequest( 81 | query, 82 | variables, 83 | operationName, 84 | options, 85 | ) 86 | responseErrs = append(responseErrs, errs...) 87 | response.Write(s.Result) 88 | } 89 | } 90 | response.WriteByte(']') 91 | return response.Bytes(), responseErrs 92 | } 93 | 94 | query, operationName, variables, err := getBodyData(v) 95 | if err != nil { 96 | return errRes(err.Error()) 97 | } 98 | errs := s.handleSingleRequest( 99 | query, 100 | variables, 101 | operationName, 102 | options, 103 | ) 104 | return s.Result, errs 105 | } 106 | 107 | errs := s.handleSingleRequest( 108 | getQuery("query"), 109 | getQuery("variables"), 110 | getQuery("operationName"), 111 | options, 112 | ) 113 | return s.Result, errs 114 | } 115 | 116 | func (s *Schema) handleSingleRequest( 117 | query, 118 | variables, 119 | operationName string, 120 | options *RequestOptions, 121 | ) []error { 122 | resolveOptions := ResolveOptions{ 123 | OperatorTarget: operationName, 124 | Variables: variables, 125 | } 126 | if options != nil { 127 | if options.Context != nil { 128 | resolveOptions.Context = options.Context 129 | } 130 | if options.Values != nil { 131 | resolveOptions.Values = &options.Values 132 | } 133 | if options.GetFormFile != nil { 134 | resolveOptions.GetFormFile = options.GetFormFile 135 | } 136 | resolveOptions.Tracing = options.Tracing 137 | } 138 | 139 | return s.Resolve(s2b(query), resolveOptions) 140 | } 141 | 142 | func getBodyData(body *fastjson.Value) (query, operationName, variables string, err error) { 143 | if body.Type() != fastjson.TypeObject { 144 | err = errors.New("body should be a object") 145 | return 146 | } 147 | 148 | jsonQuery := body.Get("query") 149 | if jsonQuery == nil { 150 | err = errors.New("query should be defined") 151 | return 152 | } 153 | queryBytes, err := jsonQuery.StringBytes() 154 | if err != nil { 155 | err = errors.New("invalid query param, must be a valid string") 156 | return 157 | } 158 | query = string(queryBytes) 159 | 160 | jsonOperationName := body.Get("operationName") 161 | if jsonOperationName != nil { 162 | t := jsonOperationName.Type() 163 | if t != fastjson.TypeNull { 164 | if t != fastjson.TypeString { 165 | err = errors.New("expected operationName to be a string but got " + t.String()) 166 | return 167 | } 168 | operationNameBytes, errOut := jsonOperationName.StringBytes() 169 | if errOut != nil { 170 | err = errors.New("invalid operationName param, must be a valid string") 171 | return 172 | } 173 | operationName = string(operationNameBytes) 174 | } 175 | } 176 | 177 | jsonVariables := body.Get("variables") 178 | if jsonVariables != nil { 179 | t := jsonVariables.Type() 180 | if t != fastjson.TypeNull { 181 | if t != fastjson.TypeObject { 182 | err = errors.New("expected variables to be a key value object but got: " + t.String()) 183 | return 184 | } 185 | variables = jsonVariables.String() 186 | } 187 | } 188 | 189 | return 190 | } 191 | -------------------------------------------------------------------------------- /implement_helpers_test.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | a "github.com/mjarkk/yarql/assert" 9 | ) 10 | 11 | func TestHandleRequestRequestInURL(t *testing.T) { 12 | s := NewSchema() 13 | err := s.Parse(TestResolveSchemaRequestWithFieldsData{A: TestResolveSchemaRequestWithFieldsDataInnerStruct{Bar: "baz"}}, M{}, nil) 14 | a.NoError(t, err) 15 | 16 | res, errs := s.HandleRequest( 17 | "GET", 18 | func(key string) string { 19 | switch key { 20 | case "query": 21 | return "{a {bar}}" 22 | default: 23 | return "" 24 | } 25 | }, 26 | func(key string) (string, error) { return "", errors.New("this should not be called") }, 27 | func() []byte { return nil }, 28 | "", 29 | &RequestOptions{}, 30 | ) 31 | for _, err := range errs { 32 | panic(err) 33 | } 34 | a.Equal(t, `{"data":{"a":{"bar":"baz"}}}`, string(res)) 35 | } 36 | 37 | func TestHandleRequestRequestJsonBody(t *testing.T) { 38 | s := NewSchema() 39 | err := s.Parse(TestResolveSchemaRequestWithFieldsData{A: TestResolveSchemaRequestWithFieldsDataInnerStruct{Bar: "baz"}}, M{}, nil) 40 | a.NoError(t, err) 41 | 42 | query := ` 43 | query Foo { 44 | a { 45 | foo 46 | } 47 | } 48 | query Bar { 49 | a { 50 | bar 51 | } 52 | } 53 | ` 54 | query = strings.ReplaceAll(query, "\n", "\\n") 55 | query = strings.ReplaceAll(query, "\t", "\\t") 56 | 57 | res, errs := s.HandleRequest( 58 | "POST", 59 | func(key string) string { return "" }, 60 | func(key string) (string, error) { return "", errors.New("this should not be called") }, 61 | func() []byte { 62 | return []byte(`{ 63 | "query": "` + query + `", 64 | "operationName": "Bar", 65 | "variables": {"a": "b"} 66 | }`) 67 | }, 68 | "application/json", 69 | &RequestOptions{}, 70 | ) 71 | for _, err := range errs { 72 | panic(err) 73 | } 74 | a.Equal(t, `{"data":{"a":{"bar":"baz"}}}`, string(res)) 75 | } 76 | 77 | func TestHandleRequestRequestForm(t *testing.T) { 78 | s := NewSchema() 79 | err := s.Parse(TestResolveSchemaRequestWithFieldsData{A: TestResolveSchemaRequestWithFieldsDataInnerStruct{Bar: "baz"}}, M{}, nil) 80 | a.NoError(t, err) 81 | 82 | query := ` 83 | query Foo { 84 | a { 85 | foo 86 | } 87 | } 88 | query Bar { 89 | a { 90 | bar 91 | } 92 | } 93 | ` 94 | query = strings.ReplaceAll(query, "\n", "\\n") 95 | query = strings.ReplaceAll(query, "\t", "\\t") 96 | 97 | res, errs := s.HandleRequest( 98 | "POST", 99 | func(key string) string { return "" }, 100 | func(key string) (string, error) { 101 | switch key { 102 | case "operations": 103 | return `{ 104 | "query": "` + query + `", 105 | "operationName": "Bar", 106 | "variables": {"a": "b"} 107 | }`, nil 108 | } 109 | return "", errors.New("unknown form field") 110 | }, 111 | func() []byte { return nil }, 112 | "multipart/form-data", 113 | &RequestOptions{}, 114 | ) 115 | for _, err := range errs { 116 | panic(err) 117 | } 118 | a.Equal(t, `{"data":{"a":{"bar":"baz"}}}`, string(res)) 119 | } 120 | 121 | func TestHandleRequestRequestBatch(t *testing.T) { 122 | s := NewSchema() 123 | err := s.Parse(TestResolveSchemaRequestWithFieldsData{A: TestResolveSchemaRequestWithFieldsDataInnerStruct{Bar: "baz"}}, M{}, nil) 124 | a.NoError(t, err) 125 | 126 | query := ` 127 | query Foo { 128 | a { 129 | foo 130 | } 131 | } 132 | query Bar { 133 | a { 134 | bar 135 | } 136 | } 137 | ` 138 | query = strings.ReplaceAll(query, "\n", "\\n") 139 | query = strings.ReplaceAll(query, "\t", "\\t") 140 | 141 | res, errs := s.HandleRequest( 142 | "POST", 143 | func(key string) string { return "" }, 144 | func(key string) (string, error) { return "", errors.New("this should not be called") }, 145 | func() []byte { 146 | return []byte(`[ 147 | { 148 | "query": "` + query + `", 149 | "operationName": "Bar", 150 | "variables": {"a": "b"} 151 | }, 152 | { 153 | "query": "` + query + `", 154 | "operationName": "Foo", 155 | "variables": {"b": "c"} 156 | } 157 | ]`) 158 | }, 159 | "application/json", 160 | &RequestOptions{}, 161 | ) 162 | for _, err := range errs { 163 | panic(err) 164 | } 165 | a.Equal(t, `[{"data":{"a":{"bar":"baz"}}},{"data":{"a":{"foo":null}}}]`, string(res)) 166 | } 167 | -------------------------------------------------------------------------------- /inject_schema.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "sort" 7 | 8 | h "github.com/mjarkk/yarql/helpers" 9 | ) 10 | 11 | func (s *Schema) injectQLTypes(ctx *parseCtx) { 12 | // Inject __Schema 13 | ref, err := ctx.check(reflect.TypeOf(qlSchema{}), false) 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | 18 | contents := reflect.ValueOf(s.getQLSchema()) 19 | ref.customObjValue = &contents 20 | ref.qlFieldName = []byte("__schema") 21 | ref.hidden = true 22 | 23 | s.rootQuery.objContents[getObjKey(ref.qlFieldName)] = ref 24 | 25 | // Inject __type(name: String!): __Type 26 | typeResolver := func(ctx *Ctx, args struct{ Name string }) *qlType { 27 | return ctx.schema.getTypeByName(args.Name) 28 | } 29 | typeResolverReflection := reflect.ValueOf(typeResolver) 30 | functionObj, err := ctx.checkStructFieldFunc("__type", typeResolverReflection.Type(), false, -1) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | functionObj.customObjValue = &typeResolverReflection 36 | functionObj.qlFieldName = []byte("__type") 37 | functionObj.hidden = true 38 | s.rootQuery.objContents[getObjKey(functionObj.qlFieldName)] = functionObj 39 | } 40 | 41 | func (s *Schema) getQLSchema() qlSchema { 42 | res := qlSchema{ 43 | Types: s.getAllQLTypes, 44 | Directives: s.getDirectives(), 45 | QueryType: &qlType{ 46 | Kind: typeKindObject, 47 | Name: h.StrPtr(s.rootQuery.typeName), 48 | Description: h.PtrToEmptyStr, 49 | Fields: func(isDeprecatedArgs) []qlField { 50 | fields, ok := s.graphqlObjFields[s.rootQuery.typeName] 51 | if ok { 52 | return fields 53 | } 54 | 55 | res := []qlField{} 56 | for _, item := range s.rootQuery.objContents { 57 | if item.hidden { 58 | continue 59 | } 60 | res = append(res, qlField{ 61 | Name: string(item.qlFieldName), 62 | Args: s.getObjectArgs(item), 63 | Type: *wrapQLTypeInNonNull(s.objToQLType(item)), 64 | }) 65 | } 66 | sort.Slice(res, func(a int, b int) bool { return res[a].Name < res[b].Name }) 67 | 68 | s.graphqlObjFields[s.rootQuery.typeName] = res 69 | return res 70 | }, 71 | Interfaces: []qlType{}, 72 | }, 73 | MutationType: &qlType{ 74 | Kind: typeKindObject, 75 | Name: h.StrPtr(s.rootMethod.typeName), 76 | Description: h.PtrToEmptyStr, 77 | Fields: func(isDeprecatedArgs) []qlField { 78 | fields, ok := s.graphqlObjFields[s.rootMethod.typeName] 79 | if ok { 80 | return fields 81 | } 82 | 83 | res := []qlField{} 84 | for _, item := range s.rootMethod.objContents { 85 | if item.hidden { 86 | continue 87 | } 88 | res = append(res, qlField{ 89 | Name: string(item.qlFieldName), 90 | Args: s.getObjectArgs(item), 91 | Type: *wrapQLTypeInNonNull(s.objToQLType(item)), 92 | }) 93 | } 94 | sort.Slice(res, func(a int, b int) bool { return res[a].Name < res[b].Name }) 95 | 96 | s.graphqlObjFields[s.rootQuery.typeName] = res 97 | return res 98 | }, 99 | Interfaces: []qlType{}, 100 | }, 101 | } 102 | 103 | // TODO: We currently don't support subscriptions 104 | res.SubscriptionType = nil 105 | 106 | return res 107 | } 108 | 109 | func (s *Schema) getDirectives() []qlDirective { 110 | res := []qlDirective{} 111 | 112 | for _, directiveLocation := range s.definedDirectives { 113 | outerLoop: 114 | for _, directive := range directiveLocation { 115 | locations := make([]__DirectiveLocation, len(directive.Where)) 116 | for idx, location := range directive.Where { 117 | locations[idx] = location.ToQlDirectiveLocation() 118 | } 119 | 120 | for _, entry := range res { 121 | if entry.Name == directive.Name { 122 | for _, directiveLocation := range locations { 123 | for _, entryLocation := range entry.Locations { 124 | if directiveLocation == entryLocation { 125 | continue outerLoop 126 | } 127 | } 128 | } 129 | } 130 | } 131 | res = append(res, qlDirective{ 132 | Name: directive.Name, 133 | Description: h.CheckStrPtr(directive.Description), 134 | Locations: locations, 135 | Args: s.getMethodArgs(directive.parsedMethod.inFields), 136 | }) 137 | } 138 | } 139 | 140 | sort.Slice(res, func(a int, b int) bool { return res[a].Name < res[b].Name }) 141 | 142 | return res 143 | } 144 | 145 | func (s *Schema) getAllQLTypes() []qlType { 146 | if s.graphqlTypesList == nil { 147 | // Only generate s.graphqlTypesList once as the content won't change on runtime 148 | 149 | s.graphqlTypesList = make( 150 | []qlType, 151 | len(s.types)+len(s.inTypes)+len(s.definedEnums)+len(scalars)+len(s.interfaces), 152 | ) 153 | 154 | idx := 0 155 | for _, qlType := range s.types { 156 | obj, _ := s.objToQLType(qlType) 157 | s.graphqlTypesList[idx] = *obj 158 | idx++ 159 | } 160 | for _, in := range s.inTypes { 161 | obj, _ := s.inputToQLType(in) 162 | s.graphqlTypesList[idx] = *obj 163 | idx++ 164 | } 165 | for _, enum := range s.definedEnums { 166 | s.graphqlTypesList[idx] = enum.qlType 167 | idx++ 168 | } 169 | for _, scalar := range scalars { 170 | s.graphqlTypesList[idx] = scalar 171 | idx++ 172 | } 173 | for _, qlInterface := range s.interfaces { 174 | obj, _ := s.objToQLType(qlInterface) 175 | s.graphqlTypesList[idx] = *obj 176 | idx++ 177 | } 178 | 179 | sort.Slice(s.graphqlTypesList, func(a int, b int) bool { return *s.graphqlTypesList[a].Name < *s.graphqlTypesList[b].Name }) 180 | } 181 | 182 | return s.graphqlTypesList 183 | } 184 | 185 | func (s *Schema) getTypeByName(name string) *qlType { 186 | if s.graphqlTypesMap == nil { 187 | // Build up s.graphqlTypesMap 188 | s.graphqlTypesMap = map[string]qlType{} 189 | all := s.getAllQLTypes() 190 | for _, t := range all { 191 | s.graphqlTypesMap[*t.Name] = t 192 | } 193 | } 194 | 195 | t, ok := s.graphqlTypesMap[name] 196 | if ok { 197 | return &t 198 | } 199 | return nil 200 | } 201 | 202 | func wrapQLTypeInNonNull(t *qlType, isNonNull bool) *qlType { 203 | if !isNonNull { 204 | return t 205 | } 206 | return &qlType{ 207 | Kind: typeKindNonNull, 208 | OfType: t, 209 | } 210 | } 211 | 212 | func (s *Schema) inputToQLType(in *input) (res *qlType, isNonNull bool) { 213 | if in.isID { 214 | isNonNull = true 215 | res = &scalarID 216 | return 217 | } else if in.isTime { 218 | isNonNull = true 219 | res = &scalarTime 220 | return 221 | } else if in.isFile { 222 | res = &scalarFile 223 | return 224 | } 225 | 226 | switch in.kind { 227 | case reflect.Struct: 228 | isNonNull = true 229 | 230 | res = &qlType{ 231 | Kind: typeKindInputObject, 232 | Name: h.StrPtr(in.structName), 233 | Description: h.PtrToEmptyStr, 234 | InputFields: func() []qlInputValue { 235 | res := make([]qlInputValue, len(in.structContent)) 236 | i := 0 237 | for key, item := range in.structContent { 238 | res[i] = qlInputValue{ 239 | Name: key, 240 | Description: h.PtrToEmptyStr, 241 | Type: *wrapQLTypeInNonNull(s.inputToQLType(&item)), 242 | DefaultValue: nil, // We do not support this atm 243 | } 244 | i++ 245 | } 246 | sort.Slice(res, func(a int, b int) bool { return res[a].Name < res[b].Name }) 247 | return res 248 | }, 249 | } 250 | case reflect.Array, reflect.Slice: 251 | res = &qlType{ 252 | Kind: typeKindList, 253 | OfType: wrapQLTypeInNonNull(s.inputToQLType(in.elem)), 254 | } 255 | case reflect.Ptr: 256 | // Basically sets the isNonNull to false 257 | res, _ = s.inputToQLType(in.elem) 258 | case reflect.Bool: 259 | isNonNull = true 260 | res = &scalarBoolean 261 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.UnsafePointer, reflect.Complex64, reflect.Complex128: 262 | isNonNull = true 263 | if in.isID { 264 | res = &scalarID 265 | } else { 266 | res = &scalarInt 267 | } 268 | case reflect.Float32, reflect.Float64: 269 | isNonNull = true 270 | res = &scalarFloat 271 | case reflect.String: 272 | isNonNull = true 273 | res = &scalarString 274 | default: 275 | isNonNull = true 276 | res = &qlType{ 277 | Kind: typeKindScalar, 278 | Name: h.PtrToEmptyStr, 279 | Description: h.PtrToEmptyStr, 280 | } 281 | } 282 | return 283 | } 284 | 285 | func (s *Schema) getObjectArgs(item *obj) []qlInputValue { 286 | if item.valueType != valueTypeMethod { 287 | return []qlInputValue{} 288 | } 289 | return s.getMethodArgs(item.method.inFields) 290 | } 291 | 292 | func (s *Schema) getMethodArgs(inputs map[string]referToInput) []qlInputValue { 293 | res := []qlInputValue{} 294 | for key, value := range inputs { 295 | res = append(res, qlInputValue{ 296 | Name: key, 297 | Description: h.PtrToEmptyStr, 298 | Type: *wrapQLTypeInNonNull(s.inputToQLType(&value.input)), 299 | DefaultValue: nil, 300 | }) 301 | } 302 | sort.Slice(res, func(a int, b int) bool { return res[a].Name < res[b].Name }) 303 | return res 304 | } 305 | 306 | func (s *Schema) objToQLType(item *obj) (res *qlType, isNonNull bool) { 307 | switch item.valueType { 308 | case valueTypeUndefined: 309 | // WUT??, we'll just look away and continue as if nothing happened 310 | // FIXME: maybe we should return an error here 311 | return 312 | case valueTypeArray: 313 | res = &qlType{ 314 | Kind: typeKindList, 315 | OfType: wrapQLTypeInNonNull(s.objToQLType(item.innerContent)), 316 | } 317 | return 318 | case valueTypeObjRef: 319 | return s.objToQLType(s.types[item.typeName]) 320 | case valueTypeObj: 321 | isNonNull = true 322 | interfaces := []qlType{} 323 | if len(item.implementations) != 0 { 324 | for _, implementation := range item.implementations { 325 | interfaceType, _ := s.objToQLType(implementation) 326 | interfaces = append(interfaces, *interfaceType) 327 | } 328 | } 329 | 330 | res = &qlType{ 331 | Kind: typeKindObject, 332 | Name: &item.typeName, 333 | Description: h.PtrToEmptyStr, 334 | Fields: func(args isDeprecatedArgs) []qlField { 335 | fields, ok := s.graphqlObjFields[item.typeName] 336 | if ok { 337 | return fields 338 | } 339 | 340 | res := []qlField{} 341 | for _, innerItem := range item.objContents { 342 | if innerItem.hidden { 343 | continue 344 | } 345 | res = append(res, qlField{ 346 | Name: string(innerItem.qlFieldName), 347 | Args: s.getObjectArgs(innerItem), 348 | Type: *wrapQLTypeInNonNull(s.objToQLType(innerItem)), 349 | }) 350 | } 351 | sort.Slice(res, func(a int, b int) bool { return res[a].Name < res[b].Name }) 352 | 353 | s.graphqlObjFields[item.typeName] = res 354 | return res 355 | }, 356 | Interfaces: interfaces, 357 | } 358 | return 359 | case valueTypeEnum: 360 | enumType := s.definedEnums[item.enumTypeIndex].qlType 361 | res = &enumType 362 | return res, true 363 | case valueTypePtr: 364 | // This basically sets the isNonNull to false 365 | res, _ := s.objToQLType(item.innerContent) 366 | return res, false 367 | case valueTypeMethod: 368 | res, isNonNull = s.objToQLType(&item.method.outType) 369 | if !item.method.isTypeMethod { 370 | isNonNull = false 371 | } 372 | return 373 | case valueTypeInterfaceRef: 374 | return s.objToQLType(s.interfaces[item.typeName]) 375 | case valueTypeInterface: 376 | // A interface should be non null BUT as a interface in go can be nil we set it to false 377 | isNonNull = false 378 | 379 | res = &qlType{ 380 | Kind: typeKindInterface, 381 | Name: &item.typeName, 382 | Description: h.PtrToEmptyStr, 383 | Interfaces: []qlType{}, 384 | PossibleTypes: func() []qlType { 385 | possibleTypes := make([]qlType, len(item.implementations)) 386 | for idx, implementation := range item.implementations { 387 | item, _ := s.objToQLType(implementation) 388 | possibleTypes[idx] = *item 389 | } 390 | return possibleTypes 391 | }, 392 | Fields: func(args isDeprecatedArgs) []qlField { 393 | fields, ok := s.graphqlObjFields[item.typeName] 394 | if ok { 395 | return fields 396 | } 397 | 398 | res := []qlField{} 399 | for _, innerItem := range item.objContents { 400 | if item.hidden { 401 | continue 402 | } 403 | res = append(res, qlField{ 404 | Name: string(innerItem.qlFieldName), 405 | Args: s.getObjectArgs(innerItem), 406 | Type: *wrapQLTypeInNonNull(s.objToQLType(innerItem)), 407 | }) 408 | } 409 | sort.Slice(res, func(a int, b int) bool { return res[a].Name < res[b].Name }) 410 | 411 | s.graphqlObjFields[item.typeName] = res 412 | return res 413 | }, 414 | } 415 | return 416 | default: 417 | return resolveObjToScalar(item), true 418 | } 419 | } 420 | 421 | func resolveObjToScalar(item *obj) *qlType { 422 | var res qlType 423 | switch item.valueType { 424 | case valueTypeData: 425 | if item.isID { 426 | res = scalarID 427 | } else { 428 | switch item.dataValueType { 429 | case reflect.Bool: 430 | res = scalarBoolean 431 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.UnsafePointer, reflect.Complex64, reflect.Complex128: 432 | res = scalarInt 433 | case reflect.Float32, reflect.Float64: 434 | res = scalarFloat 435 | case reflect.String: 436 | res = scalarString 437 | default: 438 | res = qlType{Kind: typeKindScalar, Name: h.PtrToEmptyStr, Description: h.PtrToEmptyStr} 439 | } 440 | } 441 | return &res 442 | case valueTypeTime: 443 | res = scalarTime 444 | return &res 445 | } 446 | return nil 447 | } 448 | -------------------------------------------------------------------------------- /interfaces.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // implementationMap is a map of interface names and the types that implement them 8 | var implementationMap = map[string][]reflect.Type{} 9 | 10 | // structImplementsMap is list of all structs and their interfaces that they implement 11 | var structImplementsMap = map[string][]reflect.Type{} 12 | 13 | // Implements registers a new type that implementation an interface 14 | // The interfaceValue should be a pointer to the interface type like: (*InterfaceType)(nil) 15 | // The typeValue should be a empty struct that implements the interfaceValue 16 | // 17 | // Example: 18 | // var _ = Implements((*InterfaceType)(nil), StructThatImplements{}) 19 | func Implements(interfaceValue interface{}, typeValue interface{}) bool { 20 | if interfaceValue == nil { 21 | panic("interfaceValue cannot be nil") 22 | } 23 | interfaceType := reflect.TypeOf(interfaceValue) 24 | if interfaceType.Kind() != reflect.Ptr { 25 | panic("interfaceValue should be a pointer to a interface") 26 | } 27 | interfaceType = interfaceType.Elem() 28 | if interfaceType.Kind() != reflect.Interface { 29 | panic("interfaceValue should be a pointer to a interface") 30 | } 31 | 32 | interfaceName := interfaceType.Name() 33 | interfacePath := interfaceType.PkgPath() 34 | if interfaceName == "" || interfacePath == "" { 35 | panic("interfaceValue should be a pointer to a named interface, not a inline interface") 36 | } 37 | 38 | if typeValue == nil { 39 | panic("typeValue cannot be nil") 40 | } 41 | typeType := reflect.TypeOf(typeValue) 42 | if typeType.Kind() != reflect.Struct { 43 | panic("typeValue must be a struct") 44 | } 45 | 46 | typeName := typeType.Name() 47 | typePath := typeType.PkgPath() 48 | if typeName == "" || typePath == "" { 49 | panic("typeName should is not allowed to be a inline struct") 50 | } 51 | 52 | if !typeType.Implements(interfaceType) { 53 | panic(typePath + "." + typePath + " does not implement " + interfacePath + "." + interfaceName) 54 | } 55 | 56 | typesThatImplementInterf, ok := implementationMap[interfaceName] 57 | if !ok { 58 | typesThatImplementInterf = []reflect.Type{} 59 | } else { 60 | for _, t := range typesThatImplementInterf { 61 | if t.Name() == typeName && t.PkgPath() == typePath { 62 | // already registered 63 | return true 64 | } 65 | } 66 | } 67 | typesThatImplementInterf = append(typesThatImplementInterf, typeType) 68 | implementationMap[interfaceName] = typesThatImplementInterf 69 | 70 | structImplementsMap[typeName] = append(structImplementsMap[typeName], interfaceType) 71 | 72 | return true 73 | } 74 | -------------------------------------------------------------------------------- /interfaces_test.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | a "github.com/mjarkk/yarql/assert" 8 | ) 9 | 10 | type InterfaceSchema struct { 11 | Bar BarWImpl 12 | Baz BazWImpl 13 | Generic InterfaceType 14 | } 15 | 16 | type InterfaceType interface { 17 | ResolveFoo() string 18 | ResolveBar() string 19 | } 20 | 21 | type BarWImpl struct { 22 | ExtraBarField string 23 | } 24 | 25 | func (BarWImpl) ResolveFoo() string { return "this is bar" } 26 | func (BarWImpl) ResolveBar() string { return "This is bar" } 27 | 28 | type BazWImpl struct { 29 | ExtraBazField string 30 | } 31 | 32 | func (BazWImpl) ResolveFoo() string { return "this is baz" } 33 | func (BazWImpl) ResolveBar() string { return "This is baz" } 34 | 35 | func TestInterfaceType(t *testing.T) { 36 | implementationMapLen := len(implementationMap) 37 | structImplementsMapLen := len(structImplementsMap) 38 | 39 | Implements((*InterfaceType)(nil), BarWImpl{}) 40 | a.Equal(t, implementationMapLen+1, len(implementationMap)) 41 | a.Equal(t, structImplementsMapLen+1, len(structImplementsMap)) 42 | 43 | Implements((*InterfaceType)(nil), BazWImpl{}) 44 | a.Equal(t, implementationMapLen+1, len(implementationMap)) 45 | a.Equal(t, structImplementsMapLen+2, len(structImplementsMap)) 46 | 47 | _, err := newParseCtx().check(reflect.TypeOf(InterfaceSchema{}), false) 48 | a.Nil(t, err) 49 | } 50 | 51 | func TestInterfaceInvalidInput(t *testing.T) { 52 | a.Panics(t, func() { 53 | Implements(nil, BarWImpl{}) 54 | }, "cannot use nil as interface value") 55 | 56 | a.Panics(t, func() { 57 | Implements((*InterfaceType)(nil), nil) 58 | }, "cannot use nil as type value") 59 | 60 | a.Panics(t, func() { 61 | Implements(struct{}{}, BarWImpl{}) 62 | }, "cannot use non interface type as interface value") 63 | 64 | a.Panics(t, func() { 65 | Implements((*InterfaceType)(nil), "this is not a valid type") 66 | }, "cannot use non struct type as type value") 67 | 68 | a.Panics(t, func() { 69 | Implements((*interface{})(nil), BarWImpl{}) 70 | }, "cannot use inline interface type as interface value") 71 | 72 | a.Panics(t, func() { 73 | Implements((*InterfaceType)(nil), struct{}{}) 74 | }, "cannot use inline struct type as type value") 75 | 76 | type InvalidStruct struct{} 77 | a.Panics(t, func() { 78 | Implements((*InterfaceType)(nil), InvalidStruct{}) 79 | }, "cannot use struct that doesn't implement the interface") 80 | } 81 | -------------------------------------------------------------------------------- /parse_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkValidGraphQlName(b *testing.B) { 8 | // On laptop 9 | // BenchmarkValidGraphQlName-12 2822854 438.1 ns/op 48 B/op 3 allocs/op 10 | // BenchmarkValidGraphQlName-12 13395151 85.27 ns/op 48 B/op 3 allocs/op 11 | 12 | validName := []byte("BananaHead") 13 | invalidName := []byte("_BananaHead") 14 | invalidName2 := []byte("0BananaHead") 15 | invalidName3 := []byte("Banana & Head") 16 | for i := 0; i < b.N; i++ { 17 | validGraphQlName(validName) 18 | validGraphQlName(invalidName) 19 | validGraphQlName(invalidName2) 20 | validGraphQlName(invalidName3) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | a "github.com/mjarkk/yarql/assert" 8 | ) 9 | 10 | func TestFormatGoNameToQL(t *testing.T) { 11 | a.Equal(t, "input", formatGoNameToQL("input")) 12 | a.Equal(t, "input", formatGoNameToQL("Input")) 13 | a.Equal(t, "INPUT", formatGoNameToQL("INPUT")) 14 | a.Equal(t, "", formatGoNameToQL("")) 15 | } 16 | 17 | type TestCheckEmptyStructData struct{} 18 | 19 | func newParseCtx() *parseCtx { 20 | return &parseCtx{ 21 | schema: NewSchema(), 22 | parsedMethods: []*objMethod{}, 23 | } 24 | } 25 | 26 | func TestCheckEmptyStruct(t *testing.T) { 27 | obj, err := newParseCtx().check(reflect.TypeOf(TestCheckEmptyStructData{}), false) 28 | a.NoError(t, err) 29 | 30 | a.Equal(t, valueTypeObjRef, obj.valueType) 31 | } 32 | 33 | type TestCheckStructSimpleDemo struct { 34 | A string 35 | B int 36 | C float64 37 | } 38 | 39 | func TestCheckStructSimple(t *testing.T) { 40 | ctx := newParseCtx() 41 | obj, err := ctx.check(reflect.TypeOf(TestCheckStructSimpleDemo{}), false) 42 | a.NoError(t, err) 43 | 44 | a.Equal(t, obj.valueType, valueTypeObjRef) 45 | typeObj, ok := ctx.schema.types[obj.typeName] 46 | a.True(t, ok) 47 | a.NotNil(t, typeObj.objContents) 48 | 49 | exists := map[string]reflect.Kind{ 50 | "a": reflect.String, 51 | "b": reflect.Int, 52 | "c": reflect.Float64, 53 | } 54 | for name, expectedType := range exists { 55 | val, ok := typeObj.objContents[getObjKey([]byte(name))] 56 | a.True(t, ok) 57 | a.Equal(t, valueTypeData, val.valueType) 58 | a.Equal(t, expectedType, val.dataValueType) 59 | } 60 | } 61 | 62 | func TestParseSchema(t *testing.T) { 63 | NewSchema().Parse(TestCheckStructSimpleDemo{}, TestCheckStructSimpleDemo{}, &SchemaOptions{noMethodEqualToQueryChecks: true}) 64 | } 65 | 66 | type TestCheckStructWArrayData struct { 67 | Foo []string 68 | } 69 | 70 | func TestCheckStructWArray(t *testing.T) { 71 | ctx := newParseCtx() 72 | ref, err := ctx.check(reflect.TypeOf(TestCheckStructWArrayData{}), false) 73 | a.NoError(t, err) 74 | obj := ctx.schema.types[ref.typeName] 75 | 76 | // Foo is an array 77 | val, ok := obj.objContents[getObjKey([]byte("foo"))] 78 | a.True(t, ok) 79 | a.Equal(t, valueTypeArray, val.valueType) 80 | 81 | // Foo array content is correct 82 | val = val.innerContent 83 | a.NotNil(t, val) 84 | a.Equal(t, valueTypeData, val.valueType) 85 | a.Equal(t, reflect.String, val.dataValueType) 86 | } 87 | 88 | type TestCheckStructWPtrData struct { 89 | Foo *string 90 | } 91 | 92 | func TestCheckStructWPtr(t *testing.T) { 93 | ctx := newParseCtx() 94 | ref, err := ctx.check(reflect.TypeOf(TestCheckStructWPtrData{}), false) 95 | a.NoError(t, err) 96 | obj := ctx.schema.types[ref.typeName] 97 | 98 | // Foo is a ptr 99 | val, ok := obj.objContents[getObjKey([]byte("foo"))] 100 | a.True(t, ok) 101 | a.Equal(t, valueTypePtr, val.valueType) 102 | 103 | // Foo array content is correct 104 | val = val.innerContent 105 | a.NotNil(t, val) 106 | a.Equal(t, valueTypeData, val.valueType) 107 | a.Equal(t, reflect.String, val.dataValueType) 108 | } 109 | 110 | type TestCheckStructTagsData struct { 111 | Name string `gq:"otherName"` 112 | HiddenField string `gq:"-"` 113 | } 114 | 115 | func TestCheckStructTags(t *testing.T) { 116 | ctx := newParseCtx() 117 | ref, err := ctx.check(reflect.TypeOf(TestCheckStructTagsData{}), false) 118 | a.NoError(t, err) 119 | obj := ctx.schema.types[ref.typeName] 120 | 121 | _, ok := obj.objContents[getObjKey([]byte("otherName"))] 122 | a.True(t, ok, "name should now be called otherName") 123 | 124 | _, ok = obj.objContents[getObjKey([]byte("name"))] 125 | a.False(t, ok, "name should now be called otherName and thus also not appear in the checkres") 126 | 127 | _, ok = obj.objContents[getObjKey([]byte("hiddenField"))] 128 | a.False(t, ok, "hiddenField should be ignored") 129 | } 130 | 131 | func TestCheckInvalidStruct(t *testing.T) { 132 | _, err := newParseCtx().check(reflect.TypeOf(struct { 133 | Foo interface{} 134 | }{}), false) 135 | a.Error(t, err) 136 | 137 | _, err = newParseCtx().check(reflect.TypeOf(struct { 138 | Foo complex64 139 | }{}), false) 140 | a.Error(t, err) 141 | 142 | _, err = newParseCtx().check(reflect.TypeOf(struct { 143 | Foo struct { 144 | Bar complex64 145 | } 146 | }{}), false) 147 | a.Error(t, err) 148 | } 149 | 150 | type TestCheckMethodsData struct{} 151 | 152 | func (TestCheckMethodsData) ResolveName(in struct{}) string { 153 | return "" 154 | } 155 | func (TestCheckMethodsData) ResolveBanana(in struct{}) (string, error) { 156 | return "", nil 157 | } 158 | func (TestCheckMethodsData) ResolvePeer(in struct{}) string { 159 | return "" 160 | } 161 | func (TestCheckMethodsData) ResolveId(in struct{}) (int, AttrIsID) { 162 | return 0, 0 163 | } 164 | 165 | func TestCheckMethods(t *testing.T) { 166 | ctx := newParseCtx() 167 | ref, err := ctx.check(reflect.TypeOf(TestCheckMethodsData{}), false) 168 | a.Nil(t, err) 169 | obj := ctx.schema.types[ref.typeName] 170 | 171 | field, ok := obj.objContents[getObjKey([]byte("name"))] 172 | a.True(t, ok) 173 | a.False(t, field.isID) 174 | a.Nil(t, field.method.errorOutNr) 175 | 176 | field, ok = obj.objContents[getObjKey([]byte("banana"))] 177 | a.True(t, ok) 178 | a.False(t, field.isID) 179 | a.NotNil(t, field.method.errorOutNr) 180 | 181 | field, ok = obj.objContents[getObjKey([]byte("peer"))] 182 | a.True(t, ok) 183 | a.False(t, field.isID) 184 | a.Nil(t, field.method.errorOutNr) 185 | 186 | field, ok = obj.objContents[getObjKey([]byte("id"))] 187 | a.True(t, ok) 188 | a.True(t, field.isID) 189 | a.Nil(t, field.method.errorOutNr) 190 | } 191 | 192 | type TestCheckMethodsFailData1 struct{} 193 | 194 | func (TestCheckMethodsFailData1) ResolveName(in int) (string, string) { 195 | return "", "" 196 | } 197 | 198 | type TestCheckMethodsFailData2 struct{} 199 | 200 | func (TestCheckMethodsFailData2) ResolveName(in int) (error, error) { 201 | return nil, nil 202 | } 203 | 204 | type TestCheckMethodsFailData3 struct{} 205 | 206 | func (TestCheckMethodsFailData3) ResolveName(in int) func(string) string { 207 | return nil 208 | } 209 | 210 | func TestCheckMethodsFail(t *testing.T) { 211 | _, err := newParseCtx().check(reflect.TypeOf(TestCheckMethodsFailData1{}), false) 212 | a.Error(t, err) 213 | 214 | _, err = newParseCtx().check(reflect.TypeOf(TestCheckMethodsFailData2{}), false) 215 | a.Error(t, err) 216 | 217 | _, err = newParseCtx().check(reflect.TypeOf(TestCheckMethodsFailData3{}), false) 218 | a.Error(t, err) 219 | } 220 | 221 | type TestCheckStructFuncsData struct { 222 | Name func(struct{}) string 223 | } 224 | 225 | func TestCheckStructFuncs(t *testing.T) { 226 | ctx := newParseCtx() 227 | ref, err := ctx.check(reflect.TypeOf(TestCheckStructFuncsData{}), false) 228 | a.Nil(t, err) 229 | obj := ctx.schema.types[ref.typeName] 230 | 231 | _, ok := obj.objContents[getObjKey([]byte("name"))] 232 | a.True(t, ok) 233 | } 234 | 235 | type ReferToSelf1 struct { 236 | Bar *ReferToSelf1 237 | } 238 | 239 | func TestReferenceLoop1(t *testing.T) { 240 | _, err := newParseCtx().check(reflect.TypeOf(ReferToSelf1{}), false) 241 | a.Nil(t, err) 242 | } 243 | 244 | type ReferToSelf2 struct { 245 | Bar []ReferToSelf1 246 | } 247 | 248 | func TestReferenceLoop2(t *testing.T) { 249 | _, err := newParseCtx().check(reflect.TypeOf(ReferToSelf2{}), false) 250 | a.Nil(t, err) 251 | } 252 | 253 | type ReferToSelf3 struct { 254 | Bar func() ReferToSelf1 255 | } 256 | 257 | func TestReferenceLoop3(t *testing.T) { 258 | _, err := newParseCtx().check(reflect.TypeOf(ReferToSelf3{}), false) 259 | a.Nil(t, err) 260 | } 261 | -------------------------------------------------------------------------------- /readme_test.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | a "github.com/mjarkk/yarql/assert" 8 | ) 9 | 10 | // Making sure the code in the readme actually works :) 11 | 12 | // QueryRoot defines the entry point for all graphql queries 13 | type QueryRoot struct{} 14 | 15 | // Post defines a post someone made 16 | type Post struct { 17 | ID uint `gq:"id,ID"` 18 | Title string `gq:"name"` 19 | } 20 | 21 | // ResolvePosts returns all posts 22 | func (QueryRoot) ResolvePosts() []Post { 23 | return []Post{ 24 | {1, "post 1"}, 25 | {2, "post 2"}, 26 | {3, "post 3"}, 27 | } 28 | } 29 | 30 | // MethodRoot defines the entry for all method graphql queries 31 | type MethodRoot struct{} 32 | 33 | func TestReadmeExample(t *testing.T) { 34 | s := NewSchema() 35 | 36 | err := s.Parse(QueryRoot{}, MethodRoot{}, nil) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | errs := s.Resolve([]byte(` 42 | { 43 | posts { 44 | id 45 | name 46 | } 47 | } 48 | `), ResolveOptions{}) 49 | for _, err := range errs { 50 | log.Fatal(err) 51 | } 52 | 53 | a.Equal(t, `{"data":{"posts":[{"id":"1","name":"post 1"},{"id":"2","name":"post 2"},{"id":"3","name":"post 3"}]}}`, string(s.Result)) 54 | } 55 | -------------------------------------------------------------------------------- /resolver_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "runtime/pprof" 7 | "testing" 8 | ) 9 | 10 | func BenchmarkResolve(b *testing.B) { 11 | // BenchmarkResolve-12 10750 102765 ns/op 4500 B/op 49 allocs/op 12 | // BenchmarkResolve-12 10186 98603 ns/op 6912 B/op 59 allocs/op 13 | 14 | s := NewSchema() 15 | s.Parse(TestResolveSchemaRequestWithFieldsData{}, M{}, nil) 16 | 17 | query := []byte(schemaQuery) 18 | 19 | opts := ResolveOptions{} 20 | 21 | f, err := os.Create("memprofile") 22 | if err != nil { 23 | log.Fatal("could not create memory profile: ", err) 24 | } 25 | defer f.Close() 26 | 27 | if err := pprof.StartCPUProfile(f); err != nil { 28 | log.Fatal("could not start CPU profile: ", err) 29 | } 30 | defer pprof.StopCPUProfile() 31 | 32 | var errs []error 33 | for i := 0; i < b.N; i++ { 34 | errs = s.Resolve(query, opts) 35 | for _, err := range errs { 36 | panic(err) 37 | } 38 | } 39 | 40 | // runtime.GC() 41 | // if err := pprof.WriteHeapProfile(f); err != nil { 42 | // log.Fatal("could not write memory profile: ", err) 43 | // } 44 | } 45 | 46 | type HelloWorldSchema struct { 47 | Hello string 48 | } 49 | 50 | func BenchmarkHelloWorldResolve(b *testing.B) { 51 | s := NewSchema() 52 | s.Parse(HelloWorldSchema{Hello: "World"}, M{}, nil) 53 | 54 | query := []byte(`{hello}`) 55 | 56 | opts := ResolveOptions{} 57 | 58 | // f, err := os.Create("memprofile") 59 | // if err != nil { 60 | // log.Fatal("could not create memory profile: ", err) 61 | // } 62 | // defer f.Close() 63 | 64 | // if err := pprof.StartCPUProfile(f); err != nil { 65 | // log.Fatal("could not start CPU profile: ", err) 66 | // } 67 | // defer pprof.StopCPUProfile() 68 | 69 | var errs []error 70 | for i := 0; i < b.N; i++ { 71 | errs = s.Resolve(query, opts) 72 | for _, err := range errs { 73 | panic(err) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tester/tester.go: -------------------------------------------------------------------------------- 1 | package tester 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | graphql "github.com/mjarkk/yarql" 9 | ) 10 | 11 | // Type contains a subset of fields from the graphql __Type type 12 | // https://spec.graphql.org/October2021/#sec-Schema-Introspection 13 | type Type struct { 14 | Kind string `json:"kind"` 15 | Fields []Field `json:"fields"` 16 | } 17 | 18 | // Field contains a subset of fields from the graphql __Field type 19 | // https://spec.graphql.org/October2021/#sec-Schema-Introspection 20 | type Field struct { 21 | Name string `json:"name"` 22 | } 23 | 24 | // GetTypeByName returns a schema type based on it's typename 25 | func GetTypeByName(s *graphql.Schema, typename string) *Type { 26 | vars := map[string]string{"typename": typename} 27 | varsJSON, _ := json.Marshal(vars) 28 | 29 | query := `query ($typename: String) { 30 | __type(name: $typename) { 31 | kind 32 | fields { 33 | name 34 | } 35 | } 36 | }` 37 | errs := s.Resolve([]byte(query), graphql.ResolveOptions{ 38 | NoMeta: true, 39 | Variables: string(varsJSON), 40 | }) 41 | if len(errs) != 0 { 42 | return nil 43 | } 44 | 45 | type Res struct { 46 | Type *Type `json:"__type"` 47 | } 48 | 49 | var res Res 50 | err := json.Unmarshal(s.Result, &res) 51 | if err != nil { 52 | return nil 53 | } 54 | 55 | return res.Type 56 | } 57 | 58 | // HasType returns true if the typename exists on the schema 59 | func HasType(s *graphql.Schema, typename string) bool { 60 | return GetTypeByName(s, typename) != nil 61 | } 62 | 63 | // TypeKind returns the kind of a type 64 | // If no type is found an empty string is returned 65 | func TypeKind(s *graphql.Schema, typename string) string { 66 | qlType := GetTypeByName(s, typename) 67 | if qlType == nil { 68 | return "" 69 | } 70 | return qlType.Kind 71 | } 72 | 73 | var ( 74 | errTypeNotFound = errors.New("type not found") 75 | ) 76 | 77 | // HasFields checks weather the type has the specified fields 78 | func HasFields(s *graphql.Schema, typename string, fields []string) error { 79 | qlType := GetTypeByName(s, typename) 80 | if qlType == nil { 81 | return errTypeNotFound 82 | } 83 | 84 | if qlType.Kind != "OBJECT" && qlType.Kind != "INTERFACE" { 85 | return errors.New("type must be a OBJECT or INTERFACE") 86 | } 87 | 88 | for _, field := range fields { 89 | found := false 90 | for _, qlField := range qlType.Fields { 91 | if qlField.Name == field { 92 | found = true 93 | break 94 | } 95 | } 96 | if !found { 97 | return fmt.Errorf("field %s not found on type %s", field, typename) 98 | } 99 | } 100 | 101 | return nil 102 | } 103 | 104 | // OnlyHasFields checks weather the type only has the specified fields and no more 105 | func OnlyHasFields(s *graphql.Schema, typename string, fields []string) error { 106 | qlType := GetTypeByName(s, typename) 107 | if qlType == nil { 108 | return errTypeNotFound 109 | } 110 | 111 | if qlType.Kind != "OBJECT" && qlType.Kind != "INTERFACE" { 112 | return errors.New("type must be a OBJECT or INTERFACE") 113 | } 114 | 115 | for _, field := range fields { 116 | found := false 117 | for _, qlField := range qlType.Fields { 118 | if qlField.Name == field { 119 | found = true 120 | break 121 | } 122 | } 123 | if !found { 124 | return fmt.Errorf("field %s not found on type %s", field, typename) 125 | } 126 | } 127 | 128 | for _, qlField := range qlType.Fields { 129 | found := false 130 | for _, field := range fields { 131 | if qlField.Name == field { 132 | found = true 133 | break 134 | } 135 | } 136 | if !found { 137 | return fmt.Errorf("found extra field with name %s on type %s", qlField.Name, typename) 138 | } 139 | } 140 | 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /tester/tester_test.go: -------------------------------------------------------------------------------- 1 | package tester 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mjarkk/yarql" 7 | "github.com/mjarkk/yarql/assert" 8 | ) 9 | 10 | type TesterQuerySchema struct { 11 | Foo FooSchemaType 12 | Bar BarSchemaType 13 | } 14 | 15 | type FooSchemaType struct { 16 | ExampleField string 17 | } 18 | 19 | type BarSchemaType struct { 20 | ExampleField string 21 | } 22 | 23 | type TesterMutationSchema struct{} 24 | 25 | func TestTester(t *testing.T) { 26 | s := yarql.NewSchema() 27 | err := s.Parse(TesterQuerySchema{}, TesterMutationSchema{}, nil) 28 | assert.NoError(t, err) 29 | 30 | t.Run("HasType", func(t *testing.T) { 31 | hasType := HasType(s, "FooSchemaType") 32 | assert.True(t, hasType) 33 | 34 | hasType = HasType(s, "BarSchemaType") 35 | assert.True(t, hasType) 36 | 37 | hasType = HasType(s, "NonExistentType") 38 | assert.False(t, hasType) 39 | }) 40 | 41 | t.Run("TypeKind", func(t *testing.T) { 42 | typeKind := TypeKind(s, "FooSchemaType") 43 | assert.Equal(t, "OBJECT", typeKind) 44 | 45 | typeKind = TypeKind(s, "BarSchemaType") 46 | assert.Equal(t, "OBJECT", typeKind) 47 | 48 | typeKind = TypeKind(s, "NonExistentType") 49 | assert.Equal(t, "", typeKind) 50 | }) 51 | 52 | t.Run("HasFields", func(t *testing.T) { 53 | err := HasFields(s, "FooSchemaType", []string{"exampleField"}) 54 | assert.NoError(t, err) 55 | 56 | err = HasFields(s, "FooSchemaType", []string{}) 57 | assert.NoError(t, err) 58 | 59 | err = HasFields(s, "FooSchemaType", []string{"this_field_does_not_exsist"}) 60 | assert.Error(t, err) 61 | 62 | err = HasFields(s, "NonExistentType", []string{"exampleField"}) 63 | assert.Error(t, err) 64 | }) 65 | 66 | t.Run("OnlyHasFields", func(t *testing.T) { 67 | err := OnlyHasFields(s, "FooSchemaType", []string{"exampleField"}) 68 | assert.NoError(t, err) 69 | 70 | err = OnlyHasFields(s, "FooSchemaType", []string{}) 71 | assert.Error(t, err) 72 | 73 | err = OnlyHasFields(s, "FooSchemaType", []string{"this_field_does_not_exsist"}) 74 | assert.Error(t, err) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /type_rename.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | var renamedTypes = map[string]string{} 10 | 11 | // TypeRename renames the graphql type of the input type 12 | // By default the typename of the struct is used but you might want to change this form time to time and with this you can 13 | func TypeRename(goType interface{}, newName string, force ...bool) string { 14 | t := reflect.TypeOf(goType) 15 | originalName := t.Name() 16 | 17 | if originalName == "" { 18 | log.Panicf("GraphQl Can only rename struct type with type name\n") 19 | } 20 | if t.Kind() != reflect.Struct { 21 | log.Panicf("GraphQl Cannot rename type of %s with name: %s and package: %s, can only rename Structs\n", t.Kind().String(), originalName, t.PkgPath()) 22 | } 23 | 24 | newName = strings.TrimSpace(newName) 25 | if len(newName) == 0 { 26 | log.Panicf("GraphQl cannot rename to empty string on type: %s %s\n", t.PkgPath(), originalName) 27 | } 28 | 29 | if len(force) == 0 || !force[0] { 30 | err := validGraphQlName([]byte(newName)) 31 | if err != nil { 32 | log.Panicf("GraphQl cannot rename typeof of %s with name %s to %s, err: %s", t.Kind().String(), originalName, newName, err.Error()) 33 | } 34 | } 35 | 36 | renamedTypes[originalName] = newName 37 | 38 | return newName 39 | } 40 | -------------------------------------------------------------------------------- /type_rename_test.go: -------------------------------------------------------------------------------- 1 | package yarql 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | a "github.com/mjarkk/yarql/assert" 8 | ) 9 | 10 | var _ = TypeRename(TestTypeRenameData{}, "Foo") 11 | 12 | type TestTypeRenameData struct{} 13 | 14 | func TestTypeRename(t *testing.T) { 15 | ctx := newParseCtx() 16 | obj, err := ctx.check(reflect.TypeOf(TestTypeRenameData{}), false) 17 | a.NoError(t, err) 18 | 19 | a.Equal(t, "Foo", obj.typeName) 20 | _, ok := ctx.schema.types["Foo"] 21 | a.True(t, ok) 22 | } 23 | 24 | func TestTypeRenameFails(t *testing.T) { 25 | a.Panics(t, func() { 26 | TypeRename(TestTypeRenameData{}, "") 27 | }, "Should panic when giving no type rename name") 28 | 29 | a.Panics(t, func() { 30 | TypeRename(struct{}{}, "Foo") 31 | }, "Should panic when giving a non global struct") 32 | 33 | a.Panics(t, func() { 34 | TypeRename(123, "Foo") 35 | }, "Should panic when giving a non struct") 36 | } 37 | --------------------------------------------------------------------------------