├── .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 | 
2 |
3 | [](https://pkg.go.dev/github.com/mjarkk/yarql)
4 | [](https://goreportcard.com/report/github.com/mjarkk/yarql)
5 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------