Add graphql server (#27)

* Add graphql server

* Update Makefile

* Update log_filters constraint

* Add GetLogFilter to repo

* Update travis (use Makefile, go fmt, go vet)

* Add logFilter schema and resolvers

* Add GetWatchedEvent to watched_events_repo

* Add watchedEventLog schema and resolvers
This commit is contained in:
Matt K 2018-02-08 10:12:08 -06:00 committed by GitHub
parent d5852654bb
commit 605b0a96ae
113 changed files with 16180 additions and 153 deletions

View File

@ -10,24 +10,21 @@ addons:
go_import_path: github.com/vulcanize/vulcanizedb go_import_path: github.com/vulcanize/vulcanizedb
before_install: before_install:
# ginkgo # ginkgo golint dep migrate
- go get -u github.com/onsi/ginkgo/ginkgo - make installtools
# migrate
- go get -u -d github.com/mattes/migrate/cli github.com/lib/pq
- sudo $(which go) build -tags 'postgres' -o /usr/local/bin/migrate github.com/mattes/migrate/cli
- sudo -u postgres createdb vulcanize_private
# geth # geth
- wget https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.7.2-1db4ecdc.tar.gz - wget https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.7.2-1db4ecdc.tar.gz
- tar -xzf geth-linux-amd64-1.7.2-1db4ecdc.tar.gz - tar -xzf geth-linux-amd64-1.7.2-1db4ecdc.tar.gz
- sudo cp geth-linux-amd64-1.7.2-1db4ecdc/geth /usr/local/bin - sudo cp geth-linux-amd64-1.7.2-1db4ecdc/geth /usr/local/bin
before_script: before_script:
- ./scripts/setup - sudo -u postgres createdb vulcanize_private
- nohup ./scripts/start_private_blockchain </dev/null &
- make migrate HOST_NAME=localhost NAME=vulcanize_private PORT=5432 - make migrate HOST_NAME=localhost NAME=vulcanize_private PORT=5432
- make createprivate
- nohup make startprivate </dev/null &
script: script:
- ginkgo -r - make test
notifications: notifications:
email: false email: false

33
Gopkg.lock generated
View File

@ -152,6 +152,27 @@
packages = ["."] packages = ["."]
revision = "b4575eea38cca1123ec2dc90c26529b5c5acfcff" revision = "b4575eea38cca1123ec2dc90c26529b5c5acfcff"
[[projects]]
branch = "master"
name = "github.com/neelance/graphql-go"
packages = [
".",
"errors",
"internal/common",
"internal/exec",
"internal/exec/packer",
"internal/exec/resolvable",
"internal/exec/selected",
"internal/query",
"internal/schema",
"internal/validation",
"introspection",
"log",
"relay",
"trace"
]
revision = "b46637030579abd312c5eea21d36845b6e9e7ca4"
[[projects]] [[projects]]
name = "github.com/onsi/ginkgo" name = "github.com/onsi/ginkgo"
packages = [ packages = [
@ -197,6 +218,16 @@
revision = "c893efa28eb45626cdaa76c9f653b62488858837" revision = "c893efa28eb45626cdaa76c9f653b62488858837"
version = "v1.2.0" version = "v1.2.0"
[[projects]]
name = "github.com/opentracing/opentracing-go"
packages = [
".",
"ext",
"log"
]
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
version = "v1.0.2"
[[projects]] [[projects]]
name = "github.com/pelletier/go-toml" name = "github.com/pelletier/go-toml"
packages = ["."] packages = ["."]
@ -348,6 +379,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "9f01f0f0bbce801c579dccd45229f8a78a21e229e5c93f8a54cb22748b0b25b2" inputs-digest = "4d1806bf6a5678261abb604455d0076906ca5aba42e8625ee09cae019fb21d40"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -40,3 +40,7 @@
[[constraint]] [[constraint]]
name = "github.com/ethereum/go-ethereum" name = "github.com/ethereum/go-ethereum"
version = "1.7.3" version = "1.7.3"
[[constraint]]
branch = "master"
name = "github.com/neelance/graphql-go"

View File

@ -1,49 +1,92 @@
HOST_NAME = BIN = $(GOPATH)/bin
PORT = BASE = $(GOPATH)/src/$(PACKAGE)
PKGS = go list ./... | grep -v "^vendor/"
#Tools
DEP = $(BIN)/dep
$(BIN)/dep:
go get -u github.com/golang/dep/cmd/dep
GINKGO = $(BIN)/ginkgo
$(BIN)/ginkgo:
go get -u github.com/onsi/ginkgo/ginkgo
MIGRATE = $(BIN)/migrate
$(BIN)/migrate:
go get -u -d github.com/mattes/migrate/cli github.com/lib/pq
go build -tags 'postgres' -o $(BIN)/migrate github.com/mattes/migrate/cli
LINT = $(BIN)/golint
$(BIN)/golint:
go get github.com/golang/lint/golint
METALINT = $(BIN)/gometalinter.v2
$(BIN)/gometalinter.v2:
go get -u gopkg.in/alecthomas/gometalinter.v2
$(METALINT) --install
.PHONY: installtools
installtools: | $(LINT) $(MIGRATE) $(GINKGO) $(DEP)
echo "Installing tools"
.PHONY: metalint
metalint: | $(METALINT)
$(METALINT) ./... --vendor \
--fast \
--exclude="exported (function)|(var)|(method)|(type).*should have comment or be unexported" \
--format="{{.Path.Abs}}:{{.Line}}:{{if .Col}}{{.Col}}{{end}}:{{.Severity}}: {{.Message}} ({{.Linter}})"
.PHONY: lint
lint:
$(LINT) $$($(PKGS)) | grep -v -E "exported (function)|(var)|(method)|(type).*should have comment or be unexported"
.PHONY: test
test: | $(GINKGO) $(LINT)
go vet ./...
go fmt ./...
$(GINKGO) -r
.PHONY: dep
dep: | $(DEP)
$(DEP) ensure
build: dep
go fmt ./...
go build
#Database
HOST_NAME = localhost
PORT = 5432
NAME = NAME =
CONNECT_STRING=postgresql://$(HOST_NAME):$(PORT)/$(NAME)?sslmode=disable CONNECT_STRING=postgresql://$(HOST_NAME):$(PORT)/$(NAME)?sslmode=disable
$(MATTESMIGRATE): .PHONY: checkdbvars
go get -u -d github.com/mattes/migrate/cli github.com/lib/pq
go build -tags 'postgres' -o /usr/local/bin/migrate github.com/mattes/migrate/cli
$(DEP):
go get -u github.com/golang/dep/cmd/dep
$(GINKGO):
go get -u github.com/onsi/ginkgo/ginkgo
checkdbvars: checkdbvars:
test -n "$(HOST_NAME)" # $$HOST_NAME test -n "$(HOST_NAME)" # $$HOST_NAME
test -n "$(PORT)" # $$PORT test -n "$(PORT)" # $$PORT
test -n "$(NAME)" # $$NAME test -n "$(NAME)" # $$NAME
rollback: checkdbvars .PHONY: rollback
migrate -database $(CONNECT_STRING) -path ./db/migrations down 1 rollback: $(MIGRATE) checkdbvars
$(MIGRATE) -database $(CONNECT_STRING) -path ./db/migrations down 1
pg_dump -O -s $(CONNECT_STRING) > db/schema.sql pg_dump -O -s $(CONNECT_STRING) > db/schema.sql
migrate: $(MATTESMIGRATE) checkdbvars .PHONY: migrate
migrate -database $(CONNECT_STRING) -path ./db/migrations up migrate: $(MIGRATE) checkdbvars
$(MIGRATE) -database $(CONNECT_STRING) -path ./db/migrations up
pg_dump -O -s $(CONNECT_STRING) > db/schema.sql pg_dump -O -s $(CONNECT_STRING) > db/schema.sql
.PHONY: import
import: import:
test -n "$(NAME)" # $$NAME test -n "$(NAME)" # $$NAME
psql $(NAME) < db/schema.sql psql $(NAME) < db/schema.sql
dep: $(DEP) #Ethereum
dep ensure
build: dep
go build
test: $(GINKGO)
ginkgo -r
createprivate: createprivate:
#!/bin/bash #!/bin/bash
echo "Deleting test blockchain" echo "Deleting test node"
rm -rf test_data_dir rm -rf test_data_dir
echo "Creating test blockchain with a new account" echo "Creating test node with a new account"
mkdir test_data_dir mkdir test_data_dir
geth --dev --datadir test_data_dir --password .private_blockchain_password account new geth --dev --datadir test_data_dir --password .private_blockchain_password account new

View File

@ -5,10 +5,10 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"github.com/spf13/cobra"
"github.com/vulcanize/vulcanizedb/pkg/filters" "github.com/vulcanize/vulcanizedb/pkg/filters"
"github.com/vulcanize/vulcanizedb/pkg/geth" "github.com/vulcanize/vulcanizedb/pkg/geth"
"github.com/vulcanize/vulcanizedb/utils" "github.com/vulcanize/vulcanizedb/utils"
"github.com/spf13/cobra"
) )
// addFilterCmd represents the addFilter command // addFilterCmd represents the addFilter command
@ -67,7 +67,7 @@ func addFilter() {
log.Fatal(err) log.Fatal(err)
} }
for _, filter := range logFilters { for _, filter := range logFilters {
err = repository.AddFilter(filter) err = repository.CreateFilter(filter)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

96
cmd/graphql.go Normal file
View File

@ -0,0 +1,96 @@
package cmd
import (
"net/http"
_ "net/http/pprof"
"log"
"github.com/neelance/graphql-go"
"github.com/neelance/graphql-go/relay"
"github.com/spf13/cobra"
"github.com/vulcanize/vulcanizedb/pkg/geth"
"github.com/vulcanize/vulcanizedb/pkg/graphql_server"
"github.com/vulcanize/vulcanizedb/utils"
)
var graphqlCmd = &cobra.Command{
Use: "graphql",
Short: "Starts Vulcanize graphql server",
Long: `Starts vulcanize graphql server
and usage of using your command. For example:
vulcanizedb graphql --port 9090 --host localhost
`,
Run: func(cmd *cobra.Command, args []string) {
schema := parseSchema()
serve(schema)
},
}
func init() {
var (
port int
host string
)
rootCmd.AddCommand(graphqlCmd)
syncCmd.Flags().IntVar(&port, "port", 9090, "graphql: port")
syncCmd.Flags().StringVar(&host, "host", "localhost", "graphql: host")
}
func parseSchema() *graphql.Schema {
blockchain := geth.NewBlockchain(ipc)
repository := utils.LoadPostgres(databaseConfig, blockchain.Node())
schema := graphql.MustParseSchema(graphql_server.Schema, graphql_server.NewResolver(repository))
return schema
}
func serve(schema *graphql.Schema) {
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(page)
}))
http.Handle("/query", &relay.Handler{Schema: schema})
log.Fatal(http.ListenAndServe(":9090", nil))
}
var page = []byte(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.1.0/fetch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.js"></script>
</head>
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
<div id="graphiql" style="height: 100vh;">Loading...</div>
<script>
function graphQLFetcher(graphQLParams) {
return fetch("/query", {
method: "post",
body: JSON.stringify(graphQLParams),
credentials: "include",
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
ReactDOM.render(
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
document.getElementById("graphiql")
);
</script>
</body>
</html>
`)

View File

@ -4,15 +4,17 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/vulcanize/vulcanizedb/pkg/config"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/vulcanize/vulcanizedb/pkg/config"
) )
var cfgFile string var (
var databaseConfig config.Database cfgFile string
var ipc string databaseConfig config.Database
ipc string
)
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "vulcanizedb", Use: "vulcanizedb",

View File

@ -0,0 +1,7 @@
BEGIN;
ALTER TABLE log_filters
DROP CONSTRAINT log_filters_to_block_check;
ALTER TABLE log_filters
ADD CONSTRAINT log_filters_from_block_check1 CHECK (to_block >= 0);
COMMIT;

View File

@ -0,0 +1,7 @@
BEGIN;
ALTER TABLE log_filters
DROP CONSTRAINT log_filters_from_block_check1;
ALTER TABLE log_filters
ADD CONSTRAINT log_filters_to_block_check CHECK (to_block >= 0);
COMMIT;

View File

@ -123,8 +123,8 @@ CREATE TABLE log_filters (
topic2 character varying(66), topic2 character varying(66),
topic3 character varying(66), topic3 character varying(66),
CONSTRAINT log_filters_from_block_check CHECK ((from_block >= 0)), CONSTRAINT log_filters_from_block_check CHECK ((from_block >= 0)),
CONSTRAINT log_filters_from_block_check1 CHECK ((from_block >= 0)), CONSTRAINT log_filters_name_check CHECK (((name)::text <> ''::text)),
CONSTRAINT log_filters_name_check CHECK (((name)::text <> ''::text)) CONSTRAINT log_filters_to_block_check CHECK ((to_block >= 0))
); );

View File

@ -7,7 +7,6 @@
}, },
{ {
"name": "NewFilter", "name": "NewFilter",
"toBlock": "0x4B34AA",
"fromBlock": "0x4B34AD", "fromBlock": "0x4B34AD",
"address": "0x06012c8cf97bead5deae237070f9587f8e7a266d", "address": "0x06012c8cf97bead5deae237070f9587f8e7a266d",
"topics": ["0x241ea03ca20251805084d27d4440371c34a0b85ff108f6bb5611248f73818b80"] "topics": ["0x241ea03ca20251805084d27d4440371c34a0b85ff108f6bb5611248f73818b80"]

View File

@ -3,10 +3,10 @@ package integration
import ( import (
"log" "log"
cfg "github.com/vulcanize/vulcanizedb/pkg/config"
"github.com/vulcanize/vulcanizedb/pkg/geth"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
cfg "github.com/vulcanize/vulcanizedb/pkg/config"
"github.com/vulcanize/vulcanizedb/pkg/geth"
) )
var _ = Describe("Rewards calculations", func() { var _ = Describe("Rewards calculations", func() {

View File

@ -5,12 +5,12 @@ import (
"log" "log"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
cfg "github.com/vulcanize/vulcanizedb/pkg/config" cfg "github.com/vulcanize/vulcanizedb/pkg/config"
"github.com/vulcanize/vulcanizedb/pkg/core" "github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/geth" "github.com/vulcanize/vulcanizedb/pkg/geth"
"github.com/vulcanize/vulcanizedb/pkg/geth/testing" "github.com/vulcanize/vulcanizedb/pkg/geth/testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("Reading contracts", func() { var _ = Describe("Reading contracts", func() {

View File

@ -3,9 +3,9 @@ package config_test
import ( import (
"path/filepath" "path/filepath"
cfg "github.com/vulcanize/vulcanizedb/pkg/config"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
cfg "github.com/vulcanize/vulcanizedb/pkg/config"
) )
var _ = Describe("Loading the config", func() { var _ = Describe("Loading the config", func() {

View File

@ -3,8 +3,8 @@ package contract_summary
import ( import (
"fmt" "fmt"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/vulcanize/vulcanizedb/pkg/core"
) )
func GenerateConsoleOutput(summary ContractSummary) string { func GenerateConsoleOutput(summary ContractSummary) string {

View File

@ -18,7 +18,7 @@ type ContractSummary struct {
} }
func NewSummary(blockchain core.Blockchain, repository repositories.Repository, contractHash string, blockNumber *big.Int) (ContractSummary, error) { func NewSummary(blockchain core.Blockchain, repository repositories.Repository, contractHash string, blockNumber *big.Int) (ContractSummary, error) {
contract, err := repository.FindContract(contractHash) contract, err := repository.GetContract(contractHash)
if err != nil { if err != nil {
return ContractSummary{}, err return ContractSummary{}, err
} else { } else {

View File

@ -0,0 +1,14 @@
package core
type WatchedEvent struct {
Name string `json:"name"` // name
BlockNumber int64 `json:"block_number" db:"block_number"` // block_number
Address string `json:"address"` // address
TxHash string `json:"tx_hash" db:"tx_hash"` // tx_hash
Index int64 `json:"index"` // index
Topic0 string `json:"topic0"` // topic0
Topic1 string `json:"topic1"` // topic1
Topic2 string `json:"topic2"` // topic2
Topic3 string `json:"topic3"` // topic3
Data string `json:"data"` // data
}

View File

@ -5,17 +5,17 @@ import (
"errors" "errors"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/vulcanize/vulcanizedb/pkg/core"
) )
type LogFilters []LogFilter type LogFilters []LogFilter
type LogFilter struct { type LogFilter struct {
Name string `json:"name"` Name string `json:"name"`
FromBlock int64 `json:"fromBlock"` FromBlock int64 `json:"fromBlock" db:"from_block"`
ToBlock int64 `json:"toBlock"` ToBlock int64 `json:"toBlock" db:"to_block"`
Address string `json:"address"` Address string `json:"address"`
core.Topics `json:"topics"` core.Topics `json:"topics"`
} }

View File

@ -3,10 +3,10 @@ package filters_test
import ( import (
"encoding/json" "encoding/json"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/filters"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/filters"
) )
var _ = Describe("Log filters", func() { var _ = Describe("Log filters", func() {

View File

@ -9,12 +9,12 @@ import (
"log" "log"
cfg "github.com/vulcanize/vulcanizedb/pkg/config"
"github.com/vulcanize/vulcanizedb/pkg/geth"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/onsi/gomega/ghttp" "github.com/onsi/gomega/ghttp"
cfg "github.com/vulcanize/vulcanizedb/pkg/config"
"github.com/vulcanize/vulcanizedb/pkg/geth"
) )
var _ = Describe("ABI files", func() { var _ = Describe("ABI files", func() {

View File

@ -1,9 +1,9 @@
package geth package geth
import ( import (
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/vulcanize/vulcanizedb/pkg/core"
) )
func CalcUnclesReward(block core.Block, uncles []*types.Header) float64 { func CalcUnclesReward(block core.Block, uncles []*types.Header) float64 {

View File

@ -5,12 +5,12 @@ import (
"context" "context"
"github.com/vulcanize/vulcanizedb/pkg/geth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/vulcanize/vulcanizedb/pkg/geth"
) )
type FakeGethClient struct { type FakeGethClient struct {

View File

@ -7,13 +7,13 @@ import (
"log" "log"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/geth/node"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/geth/node"
"golang.org/x/net/context" "golang.org/x/net/context"
) )

View File

@ -8,9 +8,9 @@ import (
"context" "context"
"math/big" "math/big"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/vulcanize/vulcanizedb/pkg/core"
) )
var ( var (
@ -50,7 +50,7 @@ func (blockchain *Blockchain) GetAttributes(contract core.Contract) (core.Contra
for _, abiElement := range parsed.Methods { for _, abiElement := range parsed.Methods {
if (len(abiElement.Outputs) > 0) && (len(abiElement.Inputs) == 0) && abiElement.Const { if (len(abiElement.Outputs) > 0) && (len(abiElement.Inputs) == 0) && abiElement.Const {
attributeType := abiElement.Outputs[0].Type.String() attributeType := abiElement.Outputs[0].Type.String()
contractAttributes = append(contractAttributes, core.ContractAttribute{abiElement.Name, attributeType}) contractAttributes = append(contractAttributes, core.ContractAttribute{Name: abiElement.Name, Type: attributeType})
} }
} }
sort.Sort(contractAttributes) sort.Sort(contractAttributes)

View File

@ -3,10 +3,10 @@ package geth
import ( import (
"strings" "strings"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/vulcanize/vulcanizedb/pkg/core"
) )
func ToCoreLogs(gethLogs []types.Log) []core.Log { func ToCoreLogs(gethLogs []types.Log) []core.Log {

View File

@ -3,13 +3,13 @@ package geth_test
import ( import (
"strings" "strings"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/geth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/geth"
) )
var _ = Describe("Conversion of GethLog to core.Log", func() { var _ = Describe("Conversion of GethLog to core.Log", func() {

View File

@ -5,10 +5,10 @@ import (
"strconv" "strconv"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/vulcanize/vulcanizedb/pkg/core"
) )
func Info(client *rpc.Client) core.Node { func Info(client *rpc.Client) core.Node {

View File

@ -5,10 +5,10 @@ import (
"bytes" "bytes"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/vulcanize/vulcanizedb/pkg/core"
) )
func BigTo64(n *big.Int) int64 { func BigTo64(n *big.Int) int64 {

View File

@ -3,13 +3,13 @@ package geth_test
import ( import (
"math/big" "math/big"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/geth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/geth"
) )
var _ = Describe("Conversion of GethReceipt to core.Receipt", func() { var _ = Describe("Conversion of GethReceipt to core.Receipt", func() {

View File

@ -0,0 +1,13 @@
package graphql_server_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestGraphqlServer(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "GraphqlServer Suite")
}

View File

@ -0,0 +1,162 @@
package graphql_server
import (
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/filters"
"github.com/vulcanize/vulcanizedb/pkg/repositories"
)
var Schema = `
schema {
query: Query
}
type Query {
logFilter(name: String!): LogFilter
watchedEvents(name: String!): WatchedEventList
}
type LogFilter {
name: String!
fromBlock: Int
toBlock: Int
address: String!
topics: [String]!
}
type WatchedEventList{
total: Int!
watchedEvents: [WatchedEvent]!
}
type WatchedEvent {
name: String!
blockNumber: Int!
address: String!
tx_hash: String!
topic0: String!
topic1: String!
topic2: String!
topic3: String!
data: String!
}
`
type Resolver struct {
repository repositories.Repository
}
func NewResolver(repository repositories.Repository) *Resolver {
return &Resolver{repository: repository}
}
func (r *Resolver) LogFilter(args struct {
Name string
}) (*logFilterResolver, error) {
logFilter, err := r.repository.GetFilter(args.Name)
if err != nil {
return &logFilterResolver{}, err
}
return &logFilterResolver{&logFilter}, nil
}
type logFilterResolver struct {
lf *filters.LogFilter
}
func (lfr *logFilterResolver) Name() string {
return lfr.lf.Name
}
func (lfr *logFilterResolver) FromBlock() *int32 {
fromBlock := int32(lfr.lf.FromBlock)
return &fromBlock
}
func (lfr *logFilterResolver) ToBlock() *int32 {
toBlock := int32(lfr.lf.ToBlock)
return &toBlock
}
func (lfr *logFilterResolver) Address() string {
return lfr.lf.Address
}
func (lfr *logFilterResolver) Topics() []*string {
var topics = make([]*string, 4)
for i := range topics {
if lfr.lf.Topics[i] != "" {
topics[i] = &lfr.lf.Topics[i]
}
}
return topics
}
func (r *Resolver) WatchedEvents(args struct {
Name string
}) (*watchedEventsResolver, error) {
watchedEvents, err := r.repository.GetWatchedEvents(args.Name)
if err != nil {
return &watchedEventsResolver{}, err
}
return &watchedEventsResolver{watchedEvents: watchedEvents}, err
}
type watchedEventsResolver struct {
watchedEvents []*core.WatchedEvent
}
func (wesr watchedEventsResolver) WatchedEvents() []*watchedEventResolver {
return resolveWatchedEvents(wesr.watchedEvents)
}
func (wesr watchedEventsResolver) Total() int32 {
return int32(len(wesr.watchedEvents))
}
func resolveWatchedEvents(watchedEvents []*core.WatchedEvent) []*watchedEventResolver {
watchedEventResolvers := make([]*watchedEventResolver, 0)
for _, watchedEvent := range watchedEvents {
watchedEventResolvers = append(watchedEventResolvers, &watchedEventResolver{watchedEvent})
}
return watchedEventResolvers
}
type watchedEventResolver struct {
we *core.WatchedEvent
}
func (wer watchedEventResolver) Name() string {
return wer.we.Name
}
func (wer watchedEventResolver) BlockNumber() int32 {
return int32(wer.we.BlockNumber)
}
func (wer watchedEventResolver) Address() string {
return wer.we.Address
}
func (wer watchedEventResolver) TxHash() string {
return wer.we.TxHash
}
func (wer watchedEventResolver) Topic0() string {
return wer.we.Topic0
}
func (wer watchedEventResolver) Topic1() string {
return wer.we.Topic1
}
func (wer watchedEventResolver) Topic2() string {
return wer.we.Topic2
}
func (wer watchedEventResolver) Topic3() string {
return wer.we.Topic3
}
func (wer watchedEventResolver) Data() string {
return wer.we.Data
}

View File

@ -0,0 +1,168 @@
package graphql_server_test
import (
"log"
"encoding/json"
"context"
"github.com/neelance/graphql-go"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/vulcanize/vulcanizedb/pkg/config"
"github.com/vulcanize/vulcanizedb/pkg/core"
"github.com/vulcanize/vulcanizedb/pkg/filters"
"github.com/vulcanize/vulcanizedb/pkg/graphql_server"
"github.com/vulcanize/vulcanizedb/pkg/repositories"
"github.com/vulcanize/vulcanizedb/pkg/repositories/postgres"
)
func formatJSON(data []byte) []byte {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
log.Fatalf("invalid JSON: %s", err)
}
formatted, err := json.Marshal(v)
if err != nil {
log.Fatal(err)
}
return formatted
}
var _ = Describe("GraphQL", func() {
var cfg config.Config
var repository repositories.Repository
BeforeEach(func() {
cfg, _ = config.NewConfig("private")
node := core.Node{GenesisBlock: "GENESIS", NetworkId: 1, Id: "x123", ClientName: "geth"}
repository = postgres.BuildRepository(node)
e := repository.CreateFilter(filters.LogFilter{
Name: "TestFilter1",
FromBlock: 1,
ToBlock: 10,
Address: "0x123456789",
Topics: core.Topics{0: "topic=1", 2: "topic=2"},
})
if e != nil {
log.Fatal(e)
}
f, e := repository.GetFilter("TestFilter1")
if e != nil {
log.Println(f)
log.Fatal(e)
}
matchingEvent := core.Log{
BlockNumber: 5,
TxHash: "0xTX1",
Address: "0x123456789",
Topics: core.Topics{0: "topic=1", 2: "topic=2"},
Index: 0,
Data: "0xDATADATADATA",
}
nonMatchingEvent := core.Log{
BlockNumber: 5,
TxHash: "0xTX2",
Address: "0xOTHERADDRESS",
Topics: core.Topics{0: "topic=1", 2: "topic=2"},
Index: 0,
Data: "0xDATADATADATA",
}
e = repository.CreateLogs([]core.Log{matchingEvent, nonMatchingEvent})
if e != nil {
log.Fatal(e)
}
})
It("Queries example schema for specific log filter", func() {
var variables map[string]interface{}
r := graphql_server.NewResolver(repository)
var schema = graphql.MustParseSchema(graphql_server.Schema, r)
response := schema.Exec(context.Background(),
`{
logFilter(name: "TestFilter1") {
name
fromBlock
toBlock
address
topics
}
}`,
"",
variables)
expected := `{
"logFilter": {
"name": "TestFilter1",
"fromBlock": 1,
"toBlock": 10,
"address": "0x123456789",
"topics": ["topic=1", null, "topic=2", null]
}
}`
var v interface{}
if len(response.Errors) != 0 {
log.Fatal(response.Errors)
}
err := json.Unmarshal(response.Data, &v)
Expect(err).ToNot(HaveOccurred())
a := formatJSON(response.Data)
e := formatJSON([]byte(expected))
Expect(a).To(Equal(e))
})
It("Queries example schema for specific watched event log", func() {
var variables map[string]interface{}
r := graphql_server.NewResolver(repository)
var schema = graphql.MustParseSchema(graphql_server.Schema, r)
response := schema.Exec(context.Background(),
`{
watchedEvents(name: "TestFilter1") {
total
watchedEvents{
name
blockNumber
address
tx_hash
topic0
topic1
topic2
topic3
data
}
}
}`,
"",
variables)
expected := `{
"watchedEvents":
{
"total": 1,
"watchedEvents": [
{"name":"TestFilter1",
"blockNumber": 5,
"address": "0x123456789",
"tx_hash": "0xTX1",
"topic0": "topic=1",
"topic1": "",
"topic2": "topic=2",
"topic3": "",
"data": "0xDATADATADATA"
}
]
}
}`
var v interface{}
if len(response.Errors) != 0 {
log.Fatal(response.Errors)
}
err := json.Unmarshal(response.Data, &v)
Expect(err).ToNot(HaveOccurred())
a := formatJSON(response.Data)
e := formatJSON([]byte(expected))
Expect(a).To(Equal(e))
})
})

View File

@ -21,7 +21,7 @@ var _ = Describe("Populating blocks", func() {
repository.CreateOrUpdateBlock(core.Block{Number: 2}) repository.CreateOrUpdateBlock(core.Block{Number: 2})
blocksAdded := history.PopulateMissingBlocks(blockchain, repository, 1) blocksAdded := history.PopulateMissingBlocks(blockchain, repository, 1)
_, err := repository.FindBlockByNumber(1) _, err := repository.GetBlock(1)
Expect(blocksAdded).To(Equal(1)) Expect(blocksAdded).To(Equal(1))
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
@ -54,15 +54,15 @@ var _ = Describe("Populating blocks", func() {
Expect(blocksAdded).To(Equal(3)) Expect(blocksAdded).To(Equal(3))
Expect(repository.BlockCount()).To(Equal(11)) Expect(repository.BlockCount()).To(Equal(11))
_, err := repository.FindBlockByNumber(4) _, err := repository.GetBlock(4)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
_, err = repository.FindBlockByNumber(5) _, err = repository.GetBlock(5)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
_, err = repository.FindBlockByNumber(8) _, err = repository.GetBlock(8)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
_, err = repository.FindBlockByNumber(10) _, err = repository.GetBlock(10)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
_, err = repository.FindBlockByNumber(13) _, err = repository.GetBlock(13)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })

View File

@ -36,7 +36,7 @@ var _ = Describe("Blocks validator", func() {
}) })
It("returns the window size", func() { It("returns the window size", func() {
window := history.ValidationWindow{1, 3} window := history.ValidationWindow{LowerBound: 1, UpperBound: 3}
Expect(window.Size()).To(Equal(2)) Expect(window.Size()).To(Equal(2))
}) })
@ -51,15 +51,15 @@ var _ = Describe("Blocks validator", func() {
validator := history.NewBlockValidator(blockchain, repository, 2) validator := history.NewBlockValidator(blockchain, repository, 2)
window := validator.ValidateBlocks() window := validator.ValidateBlocks()
Expect(window).To(Equal(history.ValidationWindow{5, 7})) Expect(window).To(Equal(history.ValidationWindow{LowerBound: 5, UpperBound: 7}))
Expect(repository.BlockCount()).To(Equal(2)) Expect(repository.BlockCount()).To(Equal(2))
Expect(repository.CreateOrUpdateBlockCallCount).To(Equal(2)) Expect(repository.CreateOrUpdateBlockCallCount).To(Equal(2))
}) })
It("logs window message", func() { It("logs window message", func() {
expectedMessage := &bytes.Buffer{} expectedMessage := &bytes.Buffer{}
window := history.ValidationWindow{5, 7} window := history.ValidationWindow{LowerBound: 5, UpperBound: 7}
history.ParsedWindowTemplate.Execute(expectedMessage, history.ValidationWindow{5, 7}) history.ParsedWindowTemplate.Execute(expectedMessage, history.ValidationWindow{LowerBound: 5, UpperBound: 7})
blockchain := fakes.NewBlockchainWithBlocks([]core.Block{}) blockchain := fakes.NewBlockchainWithBlocks([]core.Block{})
repository := inmemory.NewInMemory() repository := inmemory.NewInMemory()

View File

@ -23,7 +23,15 @@ type InMemory struct {
CreateOrUpdateBlockCallCount int CreateOrUpdateBlockCallCount int
} }
func (repository *InMemory) AddFilter(filter filters.LogFilter) error { func (repository *InMemory) GetWatchedEvents(name string) ([]*core.WatchedEvent, error) {
panic("implement me")
}
func (repository *InMemory) GetFilter(name string) (filters.LogFilter, error) {
panic("implement me")
}
func (repository *InMemory) CreateFilter(filter filters.LogFilter) error {
key := filter.Name key := filter.Name
if _, ok := repository.logFilters[key]; ok || key == "" { if _, ok := repository.logFilters[key]; ok || key == "" {
return errors.New("filter name not unique") return errors.New("filter name not unique")
@ -43,7 +51,7 @@ func NewInMemory() *InMemory {
} }
} }
func (repository *InMemory) FindReceipt(txHash string) (core.Receipt, error) { func (repository *InMemory) GetReceipt(txHash string) (core.Receipt, error) {
if receipt, ok := repository.receipts[txHash]; ok { if receipt, ok := repository.receipts[txHash]; ok {
return receipt, nil return receipt, nil
} }
@ -62,14 +70,14 @@ func (repository *InMemory) SetBlocksStatus(chainHead int64) {
func (repository *InMemory) CreateLogs(logs []core.Log) error { func (repository *InMemory) CreateLogs(logs []core.Log) error {
for _, log := range logs { for _, log := range logs {
key := fmt.Sprintf("%s%s", log.BlockNumber, log.Index) key := fmt.Sprintf("%d%d", log.BlockNumber, log.Index)
var logs []core.Log var logs []core.Log
repository.logs[key] = append(logs, log) repository.logs[key] = append(logs, log)
} }
return nil return nil
} }
func (repository *InMemory) FindLogs(address string, blockNumber int64) []core.Log { func (repository *InMemory) GetLogs(address string, blockNumber int64) []core.Log {
var matchingLogs []core.Log var matchingLogs []core.Log
for _, logs := range repository.logs { for _, logs := range repository.logs {
for _, log := range logs { for _, log := range logs {
@ -91,7 +99,7 @@ func (repository *InMemory) ContractExists(contractHash string) bool {
return present return present
} }
func (repository *InMemory) FindContract(contractHash string) (core.Contract, error) { func (repository *InMemory) GetContract(contractHash string) (core.Contract, error) {
contract, ok := repository.contracts[contractHash] contract, ok := repository.contracts[contractHash]
if !ok { if !ok {
return core.Contract{}, repositories.ErrContractDoesNotExist(contractHash) return core.Contract{}, repositories.ErrContractDoesNotExist(contractHash)
@ -130,7 +138,7 @@ func (repository *InMemory) BlockCount() int {
return len(repository.blocks) return len(repository.blocks)
} }
func (repository *InMemory) FindBlockByNumber(blockNumber int64) (core.Block, error) { func (repository *InMemory) GetBlock(blockNumber int64) (core.Block, error) {
if block, ok := repository.blocks[blockNumber]; ok { if block, ok := repository.blocks[blockNumber]; ok {
return block, nil return block, nil
} }

View File

@ -56,7 +56,7 @@ func (db DB) MissingBlockNumbers(startingBlockNumber int64, highestBlockNumber i
return numbers return numbers
} }
func (db DB) FindBlockByNumber(blockNumber int64) (core.Block, error) { func (db DB) GetBlock(blockNumber int64) (core.Block, error) {
blockRows := db.DB.QueryRowx( blockRows := db.DB.QueryRowx(
`SELECT id, `SELECT id,
number, number,

View File

@ -36,7 +36,7 @@ var _ = Describe("Saving blocks", func() {
} }
repositoryTwo := postgres.BuildRepository(nodeTwo) repositoryTwo := postgres.BuildRepository(nodeTwo)
_, err := repositoryTwo.FindBlockByNumber(123) _, err := repositoryTwo.GetBlock(123)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
@ -74,7 +74,7 @@ var _ = Describe("Saving blocks", func() {
repository.CreateOrUpdateBlock(block) repository.CreateOrUpdateBlock(block)
savedBlock, err := repository.FindBlockByNumber(blockNumber) savedBlock, err := repository.GetBlock(blockNumber)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(savedBlock.Reward).To(Equal(blockReward)) Expect(savedBlock.Reward).To(Equal(blockReward))
Expect(savedBlock.Difficulty).To(Equal(difficulty)) Expect(savedBlock.Difficulty).To(Equal(difficulty))
@ -93,7 +93,7 @@ var _ = Describe("Saving blocks", func() {
}) })
It("does not find a block when searching for a number that does not exist", func() { It("does not find a block when searching for a number that does not exist", func() {
_, err := repository.FindBlockByNumber(111) _, err := repository.GetBlock(111)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
@ -106,7 +106,7 @@ var _ = Describe("Saving blocks", func() {
repository.CreateOrUpdateBlock(block) repository.CreateOrUpdateBlock(block)
savedBlock, _ := repository.FindBlockByNumber(123) savedBlock, _ := repository.GetBlock(123)
Expect(len(savedBlock.Transactions)).To(Equal(1)) Expect(len(savedBlock.Transactions)).To(Equal(1))
}) })
@ -118,7 +118,7 @@ var _ = Describe("Saving blocks", func() {
repository.CreateOrUpdateBlock(block) repository.CreateOrUpdateBlock(block)
savedBlock, _ := repository.FindBlockByNumber(123) savedBlock, _ := repository.GetBlock(123)
Expect(len(savedBlock.Transactions)).To(Equal(2)) Expect(len(savedBlock.Transactions)).To(Equal(2))
}) })
@ -138,7 +138,7 @@ var _ = Describe("Saving blocks", func() {
repository.CreateOrUpdateBlock(blockOne) repository.CreateOrUpdateBlock(blockOne)
repository.CreateOrUpdateBlock(blockTwo) repository.CreateOrUpdateBlock(blockTwo)
savedBlock, _ := repository.FindBlockByNumber(123) savedBlock, _ := repository.GetBlock(123)
Expect(len(savedBlock.Transactions)).To(Equal(2)) Expect(len(savedBlock.Transactions)).To(Equal(2))
Expect(savedBlock.Transactions[0].Hash).To(Equal("x678")) Expect(savedBlock.Transactions[0].Hash).To(Equal("x678"))
Expect(savedBlock.Transactions[1].Hash).To(Equal("x9ab")) Expect(savedBlock.Transactions[1].Hash).To(Equal("x9ab"))
@ -163,8 +163,8 @@ var _ = Describe("Saving blocks", func() {
repository.CreateOrUpdateBlock(blockOne) repository.CreateOrUpdateBlock(blockOne)
repositoryTwo.CreateOrUpdateBlock(blockTwo) repositoryTwo.CreateOrUpdateBlock(blockTwo)
retrievedBlockOne, _ := repository.FindBlockByNumber(123) retrievedBlockOne, _ := repository.GetBlock(123)
retrievedBlockTwo, _ := repositoryTwo.FindBlockByNumber(123) retrievedBlockTwo, _ := repositoryTwo.GetBlock(123)
Expect(retrievedBlockOne.Transactions[0].Hash).To(Equal("x123")) Expect(retrievedBlockOne.Transactions[0].Hash).To(Equal("x123"))
Expect(retrievedBlockTwo.Transactions[0].Hash).To(Equal("x678")) Expect(retrievedBlockTwo.Transactions[0].Hash).To(Equal("x678"))
@ -196,7 +196,7 @@ var _ = Describe("Saving blocks", func() {
repository.CreateOrUpdateBlock(block) repository.CreateOrUpdateBlock(block)
savedBlock, _ := repository.FindBlockByNumber(123) savedBlock, _ := repository.GetBlock(123)
Expect(len(savedBlock.Transactions)).To(Equal(1)) Expect(len(savedBlock.Transactions)).To(Equal(1))
savedTransaction := savedBlock.Transactions[0] savedTransaction := savedBlock.Transactions[0]
Expect(savedTransaction.Data).To(Equal(transaction.Data)) Expect(savedTransaction.Data).To(Equal(transaction.Data))
@ -271,10 +271,10 @@ var _ = Describe("Saving blocks", func() {
repository.SetBlocksStatus(int64(blockNumberOfChainHead)) repository.SetBlocksStatus(int64(blockNumberOfChainHead))
blockOne, err := repository.FindBlockByNumber(1) blockOne, err := repository.GetBlock(1)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(blockOne.IsFinal).To(Equal(true)) Expect(blockOne.IsFinal).To(Equal(true))
blockTwo, err := repository.FindBlockByNumber(24) blockTwo, err := repository.GetBlock(24)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(blockTwo.IsFinal).To(BeFalse()) Expect(blockTwo.IsFinal).To(BeFalse())
}) })

View File

@ -36,7 +36,7 @@ func (db DB) ContractExists(contractHash string) bool {
return exists return exists
} }
func (db DB) FindContract(contractHash string) (core.Contract, error) { func (db DB) GetContract(contractHash string) (core.Contract, error) {
var hash string var hash string
var abi string var abi string
contract := db.DB.QueryRow( contract := db.DB.QueryRow(

View File

@ -25,7 +25,7 @@ var _ = Describe("Creating contracts", func() {
It("returns the contract when it exists", func() { It("returns the contract when it exists", func() {
repository.CreateContract(core.Contract{Hash: "x123"}) repository.CreateContract(core.Contract{Hash: "x123"})
contract, err := repository.FindContract("x123") contract, err := repository.GetContract("x123")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(contract.Hash).To(Equal("x123")) Expect(contract.Hash).To(Equal("x123"))
@ -34,13 +34,13 @@ var _ = Describe("Creating contracts", func() {
}) })
It("returns err if contract does not exist", func() { It("returns err if contract does not exist", func() {
_, err := repository.FindContract("x123") _, err := repository.GetContract("x123")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
It("returns empty array when no transactions 'To' a contract", func() { It("returns empty array when no transactions 'To' a contract", func() {
repository.CreateContract(core.Contract{Hash: "x123"}) repository.CreateContract(core.Contract{Hash: "x123"})
contract, err := repository.FindContract("x123") contract, err := repository.GetContract("x123")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(contract.Transactions).To(BeEmpty()) Expect(contract.Transactions).To(BeEmpty())
}) })
@ -59,7 +59,7 @@ var _ = Describe("Creating contracts", func() {
blockRepository.CreateOrUpdateBlock(block) blockRepository.CreateOrUpdateBlock(block)
repository.CreateContract(core.Contract{Hash: "x123"}) repository.CreateContract(core.Contract{Hash: "x123"})
contract, err := repository.FindContract("x123") contract, err := repository.GetContract("x123")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
sort.Slice(contract.Transactions, func(i, j int) bool { sort.Slice(contract.Transactions, func(i, j int) bool {
return contract.Transactions[i].Hash < contract.Transactions[j].Hash return contract.Transactions[i].Hash < contract.Transactions[j].Hash
@ -76,7 +76,7 @@ var _ = Describe("Creating contracts", func() {
Abi: "{\"some\": \"json\"}", Abi: "{\"some\": \"json\"}",
Hash: "x123", Hash: "x123",
}) })
contract, err := repository.FindContract("x123") contract, err := repository.GetContract("x123")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(contract.Abi).To(Equal("{\"some\": \"json\"}")) Expect(contract.Abi).To(Equal("{\"some\": \"json\"}"))
}) })
@ -90,7 +90,7 @@ var _ = Describe("Creating contracts", func() {
Abi: "{\"some\": \"different json\"}", Abi: "{\"some\": \"different json\"}",
Hash: "x123", Hash: "x123",
}) })
contract, err := repository.FindContract("x123") contract, err := repository.GetContract("x123")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(contract.Abi).To(Equal("{\"some\": \"different json\"}")) Expect(contract.Abi).To(Equal("{\"some\": \"different json\"}"))
}) })

View File

@ -1,8 +1,16 @@
package postgres package postgres
import "github.com/vulcanize/vulcanizedb/pkg/filters" import (
"database/sql"
func (db DB) AddFilter(query filters.LogFilter) error { "encoding/json"
"errors"
"github.com/vulcanize/vulcanizedb/pkg/filters"
"github.com/vulcanize/vulcanizedb/pkg/repositories"
)
func (db DB) CreateFilter(query filters.LogFilter) error {
_, err := db.DB.Exec( _, err := db.DB.Exec(
`INSERT INTO log_filters `INSERT INTO log_filters
(name, from_block, to_block, address, topic0, topic1, topic2, topic3) (name, from_block, to_block, address, topic0, topic1, topic2, topic3)
@ -13,3 +21,53 @@ func (db DB) AddFilter(query filters.LogFilter) error {
} }
return nil return nil
} }
func (db DB) GetFilter(name string) (filters.LogFilter, error) {
lf := DBLogFilter{}
err := db.DB.Get(&lf,
`SELECT
id,
name,
from_block,
to_block,
address,
json_build_array(topic0, topic1, topic2, topic3) AS topics
FROM log_filters
WHERE name = $1`, name)
if err != nil {
switch err {
case sql.ErrNoRows:
return filters.LogFilter{}, repositories.ErrFilterDoesNotExist(name)
default:
return filters.LogFilter{}, err
}
}
dbLogFilterToCoreLogFilter(lf)
return *lf.LogFilter, nil
}
type DBTopics []*string
func (t *DBTopics) Scan(src interface{}) error {
asBytes, ok := src.([]byte)
if !ok {
return error(errors.New("scan source was not []byte"))
}
json.Unmarshal(asBytes, &t)
return nil
}
type DBLogFilter struct {
ID int
*filters.LogFilter
Topics DBTopics
}
func dbLogFilterToCoreLogFilter(lf DBLogFilter) {
for i, v := range lf.Topics {
if v != nil {
lf.LogFilter.Topics[i] = *v
}
}
}

View File

@ -37,7 +37,7 @@ var _ = Describe("Logs Repository", func() {
"", "",
}, },
} }
err := repository.AddFilter(logFilter) err := repository.CreateFilter(logFilter)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
@ -54,8 +54,52 @@ var _ = Describe("Logs Repository", func() {
"", "",
}, },
} }
err := repository.AddFilter(logFilter) err := repository.CreateFilter(logFilter)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
It("gets a log filter", func() {
logFilter1 := filters.LogFilter{
Name: "TestFilter1",
FromBlock: 1,
ToBlock: 2,
Address: "0x8888f1f195afa192cfee860698584c030f4c9db1",
Topics: core.Topics{
"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"",
"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"",
},
}
err := repository.CreateFilter(logFilter1)
Expect(err).ToNot(HaveOccurred())
logFilter2 := filters.LogFilter{
Name: "TestFilter2",
FromBlock: 10,
ToBlock: 20,
Address: "0x8888f1f195afa192cfee860698584c030f4c9db1",
Topics: core.Topics{
"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"",
"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"",
},
}
err = repository.CreateFilter(logFilter2)
Expect(err).ToNot(HaveOccurred())
logFilter1, err = repository.GetFilter("TestFilter1")
Expect(err).ToNot(HaveOccurred())
Expect(logFilter1).To(Equal(logFilter1))
logFilter1, err = repository.GetFilter("TestFilter1")
Expect(err).ToNot(HaveOccurred())
Expect(logFilter2).To(Equal(logFilter2))
})
It("returns ErrFilterDoesNotExist error when log does not exist", func() {
_, err := repository.GetFilter("TestFilter1")
Expect(err).To(Equal(repositories.ErrFilterDoesNotExist("TestFilter1")))
})
}) })
}) })

View File

@ -26,7 +26,7 @@ func (db DB) CreateLogs(logs []core.Log) error {
return nil return nil
} }
func (db DB) FindLogs(address string, blockNumber int64) []core.Log { func (db DB) GetLogs(address string, blockNumber int64) []core.Log {
logRows, _ := db.DB.Query( logRows, _ := db.DB.Query(
`SELECT block_number, `SELECT block_number,
address, address,

View File

@ -35,7 +35,7 @@ var _ = Describe("Logs Repository", func() {
}}, }},
) )
log := repository.FindLogs("x123", 1) log := repository.GetLogs("x123", 1)
Expect(log).NotTo(BeNil()) Expect(log).NotTo(BeNil())
Expect(log[0].BlockNumber).To(Equal(int64(1))) Expect(log[0].BlockNumber).To(Equal(int64(1)))
@ -49,7 +49,7 @@ var _ = Describe("Logs Repository", func() {
}) })
It("returns nil if log does not exist", func() { It("returns nil if log does not exist", func() {
log := repository.FindLogs("x123", 1) log := repository.GetLogs("x123", 1)
Expect(log).To(BeNil()) Expect(log).To(BeNil())
}) })
@ -82,7 +82,7 @@ var _ = Describe("Logs Repository", func() {
}}, }},
) )
log := repository.FindLogs("x123", 1) log := repository.GetLogs("x123", 1)
type logIndex struct { type logIndex struct {
blockNumber int64 blockNumber int64
@ -168,7 +168,7 @@ var _ = Describe("Logs Repository", func() {
block := core.Block{Transactions: []core.Transaction{transaction}} block := core.Block{Transactions: []core.Transaction{transaction}}
err := blockRepository.CreateOrUpdateBlock(block) err := blockRepository.CreateOrUpdateBlock(block)
Expect(err).To(Not(HaveOccurred())) Expect(err).To(Not(HaveOccurred()))
retrievedLogs := repository.FindLogs("0x99041f808d598b782d5a3e498681c2452a31da08", 4745407) retrievedLogs := repository.GetLogs("0x99041f808d598b782d5a3e498681c2452a31da08", 4745407)
expected := logs[1:] expected := logs[1:]
Expect(retrievedLogs).To(Equal(expected)) Expect(retrievedLogs).To(Equal(expected))

View File

@ -94,7 +94,7 @@ var _ = Describe("Postgres repository", func() {
repository, _ := postgres.NewDB(cfg.Database, node) repository, _ := postgres.NewDB(cfg.Database, node)
err1 := repository.CreateOrUpdateBlock(badBlock) err1 := repository.CreateOrUpdateBlock(badBlock)
savedBlock, err2 := repository.FindBlockByNumber(123) savedBlock, err2 := repository.GetBlock(123)
Expect(err1).To(HaveOccurred()) Expect(err1).To(HaveOccurred())
Expect(err2).To(HaveOccurred()) Expect(err2).To(HaveOccurred())
@ -129,7 +129,7 @@ var _ = Describe("Postgres repository", func() {
repository, _ := postgres.NewDB(cfg.Database, node) repository, _ := postgres.NewDB(cfg.Database, node)
err := repository.CreateLogs([]core.Log{badLog}) err := repository.CreateLogs([]core.Log{badLog})
savedBlock := repository.FindLogs("x123", 1) savedBlock := repository.GetLogs("x123", 1)
Expect(err).ToNot(BeNil()) Expect(err).ToNot(BeNil())
Expect(savedBlock).To(BeNil()) Expect(savedBlock).To(BeNil())
@ -148,7 +148,7 @@ var _ = Describe("Postgres repository", func() {
repository, _ := postgres.NewDB(cfg.Database, node) repository, _ := postgres.NewDB(cfg.Database, node)
err1 := repository.CreateOrUpdateBlock(block) err1 := repository.CreateOrUpdateBlock(block)
savedBlock, err2 := repository.FindBlockByNumber(123) savedBlock, err2 := repository.GetBlock(123)
Expect(err1).To(HaveOccurred()) Expect(err1).To(HaveOccurred())
Expect(err2).To(HaveOccurred()) Expect(err2).To(HaveOccurred())

View File

@ -7,7 +7,7 @@ import (
"github.com/vulcanize/vulcanizedb/pkg/repositories" "github.com/vulcanize/vulcanizedb/pkg/repositories"
) )
func (db DB) FindReceipt(txHash string) (core.Receipt, error) { func (db DB) GetReceipt(txHash string) (core.Receipt, error) {
row := db.DB.QueryRow( row := db.DB.QueryRow(
`SELECT contract_address, `SELECT contract_address,
tx_hash, tx_hash,

View File

@ -42,7 +42,7 @@ var _ = Describe("Logs Repository", func() {
block := core.Block{Transactions: []core.Transaction{transaction}} block := core.Block{Transactions: []core.Transaction{transaction}}
blockRepository.CreateOrUpdateBlock(block) blockRepository.CreateOrUpdateBlock(block)
receipt, err := repository.FindReceipt("0xe340558980f89d5f86045ac11e5cc34e4bcec20f9f1e2a427aa39d87114e8223") receipt, err := repository.GetReceipt("0xe340558980f89d5f86045ac11e5cc34e4bcec20f9f1e2a427aa39d87114e8223")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
//Not currently serializing bloom logs //Not currently serializing bloom logs
@ -55,7 +55,7 @@ var _ = Describe("Logs Repository", func() {
}) })
It("returns ErrReceiptDoesNotExist when receipt does not exist", func() { It("returns ErrReceiptDoesNotExist when receipt does not exist", func() {
receipt, err := repository.FindReceipt("DOES NOT EXIST") receipt, err := repository.GetReceipt("DOES NOT EXIST")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(receipt).To(BeZero()) Expect(receipt).To(BeZero())
}) })
@ -76,7 +76,7 @@ var _ = Describe("Logs Repository", func() {
} }
blockRepository.CreateOrUpdateBlock(block) blockRepository.CreateOrUpdateBlock(block)
_, err := repository.FindReceipt(receipt.TxHash) _, err := repository.GetReceipt(receipt.TxHash)
Expect(err).To(Not(HaveOccurred())) Expect(err).To(Not(HaveOccurred()))
}) })

View File

@ -1,32 +1,19 @@
package postgres package postgres
type WatchedEventLog struct { import (
Name string `json:"name"` // name "github.com/vulcanize/vulcanizedb/pkg/core"
BlockNumber int64 `json:"block_number" db:"block_number"` // block_number )
Address string `json:"address"` // address
TxHash string `json:"tx_hash" db:"tx_hash"` // tx_hash
Index int64 `json:"index"` // index
Topic0 string `json:"topic0"` // topic0
Topic1 string `json:"topic1"` // topic1
Topic2 string `json:"topic2"` // topic2
Topic3 string `json:"topic3"` // topic3
Data string `json:"data"` // data
}
type WatchedEventLogs interface { func (db DB) GetWatchedEvents(name string) ([]*core.WatchedEvent, error) {
AllWatchedEventLogs() ([]*WatchedEventLog, error) rows, err := db.DB.Queryx(`SELECT name, block_number, address, tx_hash, index, topic0, topic1, topic2, topic3, data FROM watched_event_logs where name=$1`, name)
}
func (db *DB) AllWatchedEventLogs() ([]*WatchedEventLog, error) {
rows, err := db.DB.Queryx(`SELECT name, block_number, address, tx_hash, index, topic0, topic1, topic2, topic3, data FROM watched_event_logs`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
lgs := make([]*WatchedEventLog, 0) lgs := make([]*core.WatchedEvent, 0)
for rows.Next() { for rows.Next() {
lg := new(WatchedEventLog) lg := new(core.WatchedEvent)
err := rows.StructScan(lg) err := rows.StructScan(lg)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -27,7 +27,7 @@ var _ = Describe("Watched Events Repository", func() {
postgres.ClearData(repository) postgres.ClearData(repository)
}) })
It("retrieves watched logs that match the event filter", func() { It("retrieves watched event logs that match the event filter", func() {
filter := filters.LogFilter{ filter := filters.LogFilter{
Name: "Filter1", Name: "Filter1",
FromBlock: 0, FromBlock: 0,
@ -45,7 +45,7 @@ var _ = Describe("Watched Events Repository", func() {
Data: "", Data: "",
}, },
} }
expectedWatchedEventLog := []*postgres.WatchedEventLog{ expectedWatchedEventLog := []*core.WatchedEvent{
{ {
Name: "Filter1", Name: "Filter1",
BlockNumber: 0, BlockNumber: 0,
@ -57,11 +57,57 @@ var _ = Describe("Watched Events Repository", func() {
Data: "", Data: "",
}, },
} }
err := repository.AddFilter(filter) err := repository.CreateFilter(filter)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
err = repository.CreateLogs(logs) err = repository.CreateLogs(logs)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
matchingLogs, err := repository.AllWatchedEventLogs() matchingLogs, err := repository.GetWatchedEvents("Filter1")
Expect(err).ToNot(HaveOccurred())
Expect(matchingLogs).To(Equal(expectedWatchedEventLog))
})
It("retrieves a watched event log by name", func() {
filter := filters.LogFilter{
Name: "Filter1",
FromBlock: 0,
ToBlock: 10,
Address: "0x123",
Topics: core.Topics{0: "event1=10", 2: "event3=hello"},
}
logs := []core.Log{
{
BlockNumber: 0,
TxHash: "0x1",
Address: "0x123",
Topics: core.Topics{0: "event1=10", 2: "event3=hello"},
Index: 0,
Data: "",
},
{
BlockNumber: 100,
TxHash: "",
Address: "",
Topics: core.Topics{},
Index: 0,
Data: "",
},
}
expectedWatchedEventLog := []*core.WatchedEvent{{
Name: "Filter1",
BlockNumber: 0,
TxHash: "0x1",
Address: "0x123",
Topic0: "event1=10",
Topic2: "event3=hello",
Index: 0,
Data: "",
}}
err := repository.CreateFilter(filter)
Expect(err).ToNot(HaveOccurred())
err = repository.CreateLogs(logs)
Expect(err).ToNot(HaveOccurred())
matchingLogs, err := repository.GetWatchedEvents("Filter1")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(matchingLogs).To(Equal(expectedWatchedEventLog)) Expect(matchingLogs).To(Equal(expectedWatchedEventLog))

View File

@ -14,6 +14,7 @@ type Repository interface {
LogsRepository LogsRepository
ReceiptRepository ReceiptRepository
FilterRepository FilterRepository
WatchedEventsRepository
} }
var ErrBlockDoesNotExist = func(blockNumber int64) error { var ErrBlockDoesNotExist = func(blockNumber int64) error {
@ -22,7 +23,7 @@ var ErrBlockDoesNotExist = func(blockNumber int64) error {
type BlockRepository interface { type BlockRepository interface {
CreateOrUpdateBlock(block core.Block) error CreateOrUpdateBlock(block core.Block) error
FindBlockByNumber(blockNumber int64) (core.Block, error) GetBlock(blockNumber int64) (core.Block, error)
MissingBlockNumbers(startingBlockNumber int64, endingBlockNumber int64) []int64 MissingBlockNumbers(startingBlockNumber int64, endingBlockNumber int64) []int64
SetBlocksStatus(chainHead int64) SetBlocksStatus(chainHead int64)
} }
@ -33,17 +34,22 @@ var ErrContractDoesNotExist = func(contractHash string) error {
type ContractRepository interface { type ContractRepository interface {
CreateContract(contract core.Contract) error CreateContract(contract core.Contract) error
GetContract(contractHash string) (core.Contract, error)
ContractExists(contractHash string) bool ContractExists(contractHash string) bool
FindContract(contractHash string) (core.Contract, error) }
var ErrFilterDoesNotExist = func(name string) error {
return errors.New(fmt.Sprintf("filter %s does not exist", name))
} }
type FilterRepository interface { type FilterRepository interface {
AddFilter(filter filters.LogFilter) error CreateFilter(filter filters.LogFilter) error
GetFilter(name string) (filters.LogFilter, error)
} }
type LogsRepository interface { type LogsRepository interface {
FindLogs(address string, blockNumber int64) []core.Log
CreateLogs(logs []core.Log) error CreateLogs(logs []core.Log) error
GetLogs(address string, blockNumber int64) []core.Log
} }
var ErrReceiptDoesNotExist = func(txHash string) error { var ErrReceiptDoesNotExist = func(txHash string) error {
@ -51,5 +57,9 @@ var ErrReceiptDoesNotExist = func(txHash string) error {
} }
type ReceiptRepository interface { type ReceiptRepository interface {
FindReceipt(txHash string) (core.Receipt, error) GetReceipt(txHash string) (core.Receipt, error)
}
type WatchedEventsRepository interface {
GetWatchedEvents(name string) ([]*core.WatchedEvent, error)
} }

1
vendor/github.com/neelance/graphql-go/.gitignore generated vendored Normal file
View File

@ -0,0 +1 @@
/internal/tests/testdata/graphql-js

24
vendor/github.com/neelance/graphql-go/LICENSE generated vendored Normal file
View File

@ -0,0 +1,24 @@
Copyright (c) 2016 Richard Musiol. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

57
vendor/github.com/neelance/graphql-go/README.md generated vendored Normal file
View File

@ -0,0 +1,57 @@
# graphql-go
[![Sourcegraph](https://sourcegraph.com/github.com/neelance/graphql-go/-/badge.svg)](https://sourcegraph.com/github.com/neelance/graphql-go?badge)
[![Build Status](https://semaphoreci.com/api/v1/neelance/graphql-go/branches/master/badge.svg)](https://semaphoreci.com/neelance/graphql-go)
[![GoDoc](https://godoc.org/github.com/neelance/graphql-go?status.svg)](https://godoc.org/github.com/neelance/graphql-go)
## Status
The project is under heavy development. It is stable enough so we use it in production at [Sourcegraph](https://sourcegraph.com), but expect changes.
## Goals
* [ ] full support of [GraphQL spec (October 2016)](https://facebook.github.io/graphql/)
* [ ] propagation of `null` on resolver errors
* [x] everything else
* [x] minimal API
* [x] support for context.Context and OpenTracing
* [x] early error detection at application startup by type-checking if the given resolver matches the schema
* [x] resolvers are purely based on method sets (e.g. it's up to you if you want to resolve a GraphQL interface with a Go interface or a Go struct)
* [ ] nice error messages (no internal panics, even with an invalid schema or resolver; please file a bug if you see an internal panic)
* [x] nice errors on resolver validation
* [ ] nice errors on all invalid schemas
* [ ] nice errors on all invalid queries
* [x] panic handling (a panic in a resolver should not take down the whole app)
* [x] parallel execution of resolvers
## (Some) Documentation
### Resolvers
A resolver must have one method for each field of the GraphQL type it resolves. The method name has to be [exported](https://golang.org/ref/spec#Exported_identifiers) and match the field's name in a non-case-sensitive way.
The method has up to two arguments:
- Optional `context.Context` argument.
- Mandatory `*struct { ... }` argument if the corresponding GraphQL field has arguments. The names of the struct fields have to be [exported](https://golang.org/ref/spec#Exported_identifiers) and have to match the names of the GraphQL arguments in a non-case-sensitive way.
The method has up to two results:
- The GraphQL field's value as determined by the resolver.
- Optional `error` result.
Example for a simple resolver method:
```go
func (r *helloWorldResolver) Hello() string {
return "Hello world!"
}
```
The following signature is also allowed:
```go
func (r *helloWorldResolver) Hello(ctx context.Context) (string, error) {
return "Hello world!", nil
}
```

41
vendor/github.com/neelance/graphql-go/errors/errors.go generated vendored Normal file
View File

@ -0,0 +1,41 @@
package errors
import (
"fmt"
)
type QueryError struct {
Message string `json:"message"`
Locations []Location `json:"locations,omitempty"`
Path []interface{} `json:"path,omitempty"`
Rule string `json:"-"`
ResolverError error `json:"-"`
}
type Location struct {
Line int `json:"line"`
Column int `json:"column"`
}
func (a Location) Before(b Location) bool {
return a.Line < b.Line || (a.Line == b.Line && a.Column < b.Column)
}
func Errorf(format string, a ...interface{}) *QueryError {
return &QueryError{
Message: fmt.Sprintf(format, a...),
}
}
func (err *QueryError) Error() string {
if err == nil {
return "<nil>"
}
str := fmt.Sprintf("graphql: %s", err.Message)
for _, loc := range err.Locations {
str += fmt.Sprintf(" (line %d, column %d)", loc.Line, loc.Column)
}
return str
}
var _ error = &QueryError{}

View File

@ -0,0 +1,64 @@
package main
import (
"log"
"net/http"
"github.com/neelance/graphql-go"
"github.com/neelance/graphql-go/example/starwars"
"github.com/neelance/graphql-go/relay"
)
var schema *graphql.Schema
func init() {
schema = graphql.MustParseSchema(starwars.Schema, &starwars.Resolver{})
}
func main() {
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(page)
}))
http.Handle("/query", &relay.Handler{Schema: schema})
log.Fatal(http.ListenAndServe(":8080", nil))
}
var page = []byte(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.1.0/fetch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.js"></script>
</head>
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
<div id="graphiql" style="height: 100vh;">Loading...</div>
<script>
function graphQLFetcher(graphQLParams) {
return fetch("/query", {
method: "post",
body: JSON.stringify(graphQLParams),
credentials: "include",
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
ReactDOM.render(
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
document.getElementById("graphiql")
);
</script>
</body>
</html>
`)

View File

@ -0,0 +1,647 @@
// Package starwars provides a example schema and resolver based on Star Wars characters.
//
// Source: https://github.com/graphql/graphql.github.io/blob/source/site/_core/swapiSchema.js
package starwars
import (
"encoding/base64"
"fmt"
"strconv"
"strings"
graphql "github.com/neelance/graphql-go"
)
var Schema = `
schema {
query: Query
mutation: Mutation
}
# The query type, represents all of the entry points into our object graph
type Query {
hero(episode: Episode = NEWHOPE): Character
reviews(episode: Episode!): [Review]!
search(text: String!): [SearchResult]!
character(id: ID!): Character
droid(id: ID!): Droid
human(id: ID!): Human
starship(id: ID!): Starship
}
# The mutation type, represents all updates we can make to our data
type Mutation {
createReview(episode: Episode!, review: ReviewInput!): Review
}
# The episodes in the Star Wars trilogy
enum Episode {
# Star Wars Episode IV: A New Hope, released in 1977.
NEWHOPE
# Star Wars Episode V: The Empire Strikes Back, released in 1980.
EMPIRE
# Star Wars Episode VI: Return of the Jedi, released in 1983.
JEDI
}
# A character from the Star Wars universe
interface Character {
# The ID of the character
id: ID!
# The name of the character
name: String!
# The friends of the character, or an empty list if they have none
friends: [Character]
# The friends of the character exposed as a connection with edges
friendsConnection(first: Int, after: ID): FriendsConnection!
# The movies this character appears in
appearsIn: [Episode!]!
}
# Units of height
enum LengthUnit {
# The standard unit around the world
METER
# Primarily used in the United States
FOOT
}
# A humanoid creature from the Star Wars universe
type Human implements Character {
# The ID of the human
id: ID!
# What this human calls themselves
name: String!
# Height in the preferred unit, default is meters
height(unit: LengthUnit = METER): Float!
# Mass in kilograms, or null if unknown
mass: Float
# This human's friends, or an empty list if they have none
friends: [Character]
# The friends of the human exposed as a connection with edges
friendsConnection(first: Int, after: ID): FriendsConnection!
# The movies this human appears in
appearsIn: [Episode!]!
# A list of starships this person has piloted, or an empty list if none
starships: [Starship]
}
# An autonomous mechanical character in the Star Wars universe
type Droid implements Character {
# The ID of the droid
id: ID!
# What others call this droid
name: String!
# This droid's friends, or an empty list if they have none
friends: [Character]
# The friends of the droid exposed as a connection with edges
friendsConnection(first: Int, after: ID): FriendsConnection!
# The movies this droid appears in
appearsIn: [Episode!]!
# This droid's primary function
primaryFunction: String
}
# A connection object for a character's friends
type FriendsConnection {
# The total number of friends
totalCount: Int!
# The edges for each of the character's friends.
edges: [FriendsEdge]
# A list of the friends, as a convenience when edges are not needed.
friends: [Character]
# Information for paginating this connection
pageInfo: PageInfo!
}
# An edge object for a character's friends
type FriendsEdge {
# A cursor used for pagination
cursor: ID!
# The character represented by this friendship edge
node: Character
}
# Information for paginating this connection
type PageInfo {
startCursor: ID
endCursor: ID
hasNextPage: Boolean!
}
# Represents a review for a movie
type Review {
# The number of stars this review gave, 1-5
stars: Int!
# Comment about the movie
commentary: String
}
# The input object sent when someone is creating a new review
input ReviewInput {
# 0-5 stars
stars: Int!
# Comment about the movie, optional
commentary: String
}
type Starship {
# The ID of the starship
id: ID!
# The name of the starship
name: String!
# Length of the starship, along the longest axis
length(unit: LengthUnit = METER): Float!
}
union SearchResult = Human | Droid | Starship
`
type human struct {
ID graphql.ID
Name string
Friends []graphql.ID
AppearsIn []string
Height float64
Mass int
Starships []graphql.ID
}
var humans = []*human{
{
ID: "1000",
Name: "Luke Skywalker",
Friends: []graphql.ID{"1002", "1003", "2000", "2001"},
AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"},
Height: 1.72,
Mass: 77,
Starships: []graphql.ID{"3001", "3003"},
},
{
ID: "1001",
Name: "Darth Vader",
Friends: []graphql.ID{"1004"},
AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"},
Height: 2.02,
Mass: 136,
Starships: []graphql.ID{"3002"},
},
{
ID: "1002",
Name: "Han Solo",
Friends: []graphql.ID{"1000", "1003", "2001"},
AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"},
Height: 1.8,
Mass: 80,
Starships: []graphql.ID{"3000", "3003"},
},
{
ID: "1003",
Name: "Leia Organa",
Friends: []graphql.ID{"1000", "1002", "2000", "2001"},
AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"},
Height: 1.5,
Mass: 49,
},
{
ID: "1004",
Name: "Wilhuff Tarkin",
Friends: []graphql.ID{"1001"},
AppearsIn: []string{"NEWHOPE"},
Height: 1.8,
Mass: 0,
},
}
var humanData = make(map[graphql.ID]*human)
func init() {
for _, h := range humans {
humanData[h.ID] = h
}
}
type droid struct {
ID graphql.ID
Name string
Friends []graphql.ID
AppearsIn []string
PrimaryFunction string
}
var droids = []*droid{
{
ID: "2000",
Name: "C-3PO",
Friends: []graphql.ID{"1000", "1002", "1003", "2001"},
AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"},
PrimaryFunction: "Protocol",
},
{
ID: "2001",
Name: "R2-D2",
Friends: []graphql.ID{"1000", "1002", "1003"},
AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"},
PrimaryFunction: "Astromech",
},
}
var droidData = make(map[graphql.ID]*droid)
func init() {
for _, d := range droids {
droidData[d.ID] = d
}
}
type starship struct {
ID graphql.ID
Name string
Length float64
}
var starships = []*starship{
{
ID: "3000",
Name: "Millennium Falcon",
Length: 34.37,
},
{
ID: "3001",
Name: "X-Wing",
Length: 12.5,
},
{
ID: "3002",
Name: "TIE Advanced x1",
Length: 9.2,
},
{
ID: "3003",
Name: "Imperial shuttle",
Length: 20,
},
}
var starshipData = make(map[graphql.ID]*starship)
func init() {
for _, s := range starships {
starshipData[s.ID] = s
}
}
type review struct {
stars int32
commentary *string
}
var reviews = make(map[string][]*review)
type Resolver struct{}
func (r *Resolver) Hero(args struct{ Episode string }) *characterResolver {
if args.Episode == "EMPIRE" {
return &characterResolver{&humanResolver{humanData["1000"]}}
}
return &characterResolver{&droidResolver{droidData["2001"]}}
}
func (r *Resolver) Reviews(args struct{ Episode string }) []*reviewResolver {
var l []*reviewResolver
for _, review := range reviews[args.Episode] {
l = append(l, &reviewResolver{review})
}
return l
}
func (r *Resolver) Search(args struct{ Text string }) []*searchResultResolver {
var l []*searchResultResolver
for _, h := range humans {
if strings.Contains(h.Name, args.Text) {
l = append(l, &searchResultResolver{&humanResolver{h}})
}
}
for _, d := range droids {
if strings.Contains(d.Name, args.Text) {
l = append(l, &searchResultResolver{&droidResolver{d}})
}
}
for _, s := range starships {
if strings.Contains(s.Name, args.Text) {
l = append(l, &searchResultResolver{&starshipResolver{s}})
}
}
return l
}
func (r *Resolver) Character(args struct{ ID graphql.ID }) *characterResolver {
if h := humanData[args.ID]; h != nil {
return &characterResolver{&humanResolver{h}}
}
if d := droidData[args.ID]; d != nil {
return &characterResolver{&droidResolver{d}}
}
return nil
}
func (r *Resolver) Human(args struct{ ID graphql.ID }) *humanResolver {
if h := humanData[args.ID]; h != nil {
return &humanResolver{h}
}
return nil
}
func (r *Resolver) Droid(args struct{ ID graphql.ID }) *droidResolver {
if d := droidData[args.ID]; d != nil {
return &droidResolver{d}
}
return nil
}
func (r *Resolver) Starship(args struct{ ID graphql.ID }) *starshipResolver {
if s := starshipData[args.ID]; s != nil {
return &starshipResolver{s}
}
return nil
}
func (r *Resolver) CreateReview(args *struct {
Episode string
Review *reviewInput
}) *reviewResolver {
review := &review{
stars: args.Review.Stars,
commentary: args.Review.Commentary,
}
reviews[args.Episode] = append(reviews[args.Episode], review)
return &reviewResolver{review}
}
type friendsConnectionArgs struct {
First *int32
After *graphql.ID
}
type character interface {
ID() graphql.ID
Name() string
Friends() *[]*characterResolver
FriendsConnection(friendsConnectionArgs) (*friendsConnectionResolver, error)
AppearsIn() []string
}
type characterResolver struct {
character
}
func (r *characterResolver) ToHuman() (*humanResolver, bool) {
c, ok := r.character.(*humanResolver)
return c, ok
}
func (r *characterResolver) ToDroid() (*droidResolver, bool) {
c, ok := r.character.(*droidResolver)
return c, ok
}
type humanResolver struct {
h *human
}
func (r *humanResolver) ID() graphql.ID {
return r.h.ID
}
func (r *humanResolver) Name() string {
return r.h.Name
}
func (r *humanResolver) Height(args struct{ Unit string }) float64 {
return convertLength(r.h.Height, args.Unit)
}
func (r *humanResolver) Mass() *float64 {
if r.h.Mass == 0 {
return nil
}
f := float64(r.h.Mass)
return &f
}
func (r *humanResolver) Friends() *[]*characterResolver {
return resolveCharacters(r.h.Friends)
}
func (r *humanResolver) FriendsConnection(args friendsConnectionArgs) (*friendsConnectionResolver, error) {
return newFriendsConnectionResolver(r.h.Friends, args)
}
func (r *humanResolver) AppearsIn() []string {
return r.h.AppearsIn
}
func (r *humanResolver) Starships() *[]*starshipResolver {
l := make([]*starshipResolver, len(r.h.Starships))
for i, id := range r.h.Starships {
l[i] = &starshipResolver{starshipData[id]}
}
return &l
}
type droidResolver struct {
d *droid
}
func (r *droidResolver) ID() graphql.ID {
return r.d.ID
}
func (r *droidResolver) Name() string {
return r.d.Name
}
func (r *droidResolver) Friends() *[]*characterResolver {
return resolveCharacters(r.d.Friends)
}
func (r *droidResolver) FriendsConnection(args friendsConnectionArgs) (*friendsConnectionResolver, error) {
return newFriendsConnectionResolver(r.d.Friends, args)
}
func (r *droidResolver) AppearsIn() []string {
return r.d.AppearsIn
}
func (r *droidResolver) PrimaryFunction() *string {
if r.d.PrimaryFunction == "" {
return nil
}
return &r.d.PrimaryFunction
}
type starshipResolver struct {
s *starship
}
func (r *starshipResolver) ID() graphql.ID {
return r.s.ID
}
func (r *starshipResolver) Name() string {
return r.s.Name
}
func (r *starshipResolver) Length(args struct{ Unit string }) float64 {
return convertLength(r.s.Length, args.Unit)
}
type searchResultResolver struct {
result interface{}
}
func (r *searchResultResolver) ToHuman() (*humanResolver, bool) {
res, ok := r.result.(*humanResolver)
return res, ok
}
func (r *searchResultResolver) ToDroid() (*droidResolver, bool) {
res, ok := r.result.(*droidResolver)
return res, ok
}
func (r *searchResultResolver) ToStarship() (*starshipResolver, bool) {
res, ok := r.result.(*starshipResolver)
return res, ok
}
func convertLength(meters float64, unit string) float64 {
switch unit {
case "METER":
return meters
case "FOOT":
return meters * 3.28084
default:
panic("invalid unit")
}
}
func resolveCharacters(ids []graphql.ID) *[]*characterResolver {
var characters []*characterResolver
for _, id := range ids {
if c := resolveCharacter(id); c != nil {
characters = append(characters, c)
}
}
return &characters
}
func resolveCharacter(id graphql.ID) *characterResolver {
if h, ok := humanData[id]; ok {
return &characterResolver{&humanResolver{h}}
}
if d, ok := droidData[id]; ok {
return &characterResolver{&droidResolver{d}}
}
return nil
}
type reviewResolver struct {
r *review
}
func (r *reviewResolver) Stars() int32 {
return r.r.stars
}
func (r *reviewResolver) Commentary() *string {
return r.r.commentary
}
type friendsConnectionResolver struct {
ids []graphql.ID
from int
to int
}
func newFriendsConnectionResolver(ids []graphql.ID, args friendsConnectionArgs) (*friendsConnectionResolver, error) {
from := 0
if args.After != nil {
b, err := base64.StdEncoding.DecodeString(string(*args.After))
if err != nil {
return nil, err
}
i, err := strconv.Atoi(strings.TrimPrefix(string(b), "cursor"))
if err != nil {
return nil, err
}
from = i
}
to := len(ids)
if args.First != nil {
to = from + int(*args.First)
if to > len(ids) {
to = len(ids)
}
}
return &friendsConnectionResolver{
ids: ids,
from: from,
to: to,
}, nil
}
func (r *friendsConnectionResolver) TotalCount() int32 {
return int32(len(r.ids))
}
func (r *friendsConnectionResolver) Edges() *[]*friendsEdgeResolver {
l := make([]*friendsEdgeResolver, r.to-r.from)
for i := range l {
l[i] = &friendsEdgeResolver{
cursor: encodeCursor(r.from + i),
id: r.ids[r.from+i],
}
}
return &l
}
func (r *friendsConnectionResolver) Friends() *[]*characterResolver {
return resolveCharacters(r.ids[r.from:r.to])
}
func (r *friendsConnectionResolver) PageInfo() *pageInfoResolver {
return &pageInfoResolver{
startCursor: encodeCursor(r.from),
endCursor: encodeCursor(r.to - 1),
hasNextPage: r.to < len(r.ids),
}
}
func encodeCursor(i int) graphql.ID {
return graphql.ID(base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("cursor%d", i+1))))
}
type friendsEdgeResolver struct {
cursor graphql.ID
id graphql.ID
}
func (r *friendsEdgeResolver) Cursor() graphql.ID {
return r.cursor
}
func (r *friendsEdgeResolver) Node() *characterResolver {
return resolveCharacter(r.id)
}
type pageInfoResolver struct {
startCursor graphql.ID
endCursor graphql.ID
hasNextPage bool
}
func (r *pageInfoResolver) StartCursor() *graphql.ID {
return &r.startCursor
}
func (r *pageInfoResolver) EndCursor() *graphql.ID {
return &r.endCursor
}
func (r *pageInfoResolver) HasNextPage() bool {
return r.hasNextPage
}
type reviewInput struct {
Stars int32
Commentary *string
}

View File

@ -0,0 +1,67 @@
package gqltesting
import (
"bytes"
"context"
"encoding/json"
"strconv"
"testing"
graphql "github.com/neelance/graphql-go"
)
// Test is a GraphQL test case to be used with RunTest(s).
type Test struct {
Context context.Context
Schema *graphql.Schema
Query string
OperationName string
Variables map[string]interface{}
ExpectedResult string
}
// RunTests runs the given GraphQL test cases as subtests.
func RunTests(t *testing.T, tests []*Test) {
if len(tests) == 1 {
RunTest(t, tests[0])
return
}
for i, test := range tests {
t.Run(strconv.Itoa(i+1), func(t *testing.T) {
RunTest(t, test)
})
}
}
// RunTest runs a single GraphQL test case.
func RunTest(t *testing.T, test *Test) {
if test.Context == nil {
test.Context = context.Background()
}
result := test.Schema.Exec(test.Context, test.Query, test.OperationName, test.Variables)
if len(result.Errors) != 0 {
t.Fatal(result.Errors[0])
}
got := formatJSON(t, result.Data)
want := formatJSON(t, []byte(test.ExpectedResult))
if !bytes.Equal(got, want) {
t.Logf("got: %s", got)
t.Logf("want: %s", want)
t.Fail()
}
}
func formatJSON(t *testing.T, data []byte) []byte {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
t.Fatalf("invalid JSON: %s", err)
}
formatted, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
return formatted
}

185
vendor/github.com/neelance/graphql-go/graphql.go generated vendored Normal file
View File

@ -0,0 +1,185 @@
package graphql
import (
"context"
"fmt"
"encoding/json"
"github.com/neelance/graphql-go/errors"
"github.com/neelance/graphql-go/internal/common"
"github.com/neelance/graphql-go/internal/exec"
"github.com/neelance/graphql-go/internal/exec/resolvable"
"github.com/neelance/graphql-go/internal/exec/selected"
"github.com/neelance/graphql-go/internal/query"
"github.com/neelance/graphql-go/internal/schema"
"github.com/neelance/graphql-go/internal/validation"
"github.com/neelance/graphql-go/introspection"
"github.com/neelance/graphql-go/log"
"github.com/neelance/graphql-go/trace"
)
// ParseSchema parses a GraphQL schema and attaches the given root resolver. It returns an error if
// the Go type signature of the resolvers does not match the schema. If nil is passed as the
// resolver, then the schema can not be executed, but it may be inspected (e.g. with ToJSON).
func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) (*Schema, error) {
s := &Schema{
schema: schema.New(),
maxParallelism: 10,
tracer: trace.OpenTracingTracer{},
logger: &log.DefaultLogger{},
}
for _, opt := range opts {
opt(s)
}
if err := s.schema.Parse(schemaString); err != nil {
return nil, err
}
if resolver != nil {
r, err := resolvable.ApplyResolver(s.schema, resolver)
if err != nil {
return nil, err
}
s.res = r
}
return s, nil
}
// MustParseSchema calls ParseSchema and panics on error.
func MustParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) *Schema {
s, err := ParseSchema(schemaString, resolver, opts...)
if err != nil {
panic(err)
}
return s
}
// Schema represents a GraphQL schema with an optional resolver.
type Schema struct {
schema *schema.Schema
res *resolvable.Schema
maxParallelism int
tracer trace.Tracer
logger log.Logger
}
// SchemaOpt is an option to pass to ParseSchema or MustParseSchema.
type SchemaOpt func(*Schema)
// MaxParallelism specifies the maximum number of resolvers per request allowed to run in parallel. The default is 10.
func MaxParallelism(n int) SchemaOpt {
return func(s *Schema) {
s.maxParallelism = n
}
}
// Tracer is used to trace queries and fields. It defaults to trace.OpenTracingTracer.
func Tracer(tracer trace.Tracer) SchemaOpt {
return func(s *Schema) {
s.tracer = tracer
}
}
// Logger is used to log panics durring query execution. It defaults to exec.DefaultLogger.
func Logger(logger log.Logger) SchemaOpt {
return func(s *Schema) {
s.logger = logger
}
}
// Response represents a typical response of a GraphQL server. It may be encoded to JSON directly or
// it may be further processed to a custom response type, for example to include custom error data.
type Response struct {
Data json.RawMessage `json:"data,omitempty"`
Errors []*errors.QueryError `json:"errors,omitempty"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
}
// Validate validates the given query with the schema.
func (s *Schema) Validate(queryString string) []*errors.QueryError {
doc, qErr := query.Parse(queryString)
if qErr != nil {
return []*errors.QueryError{qErr}
}
return validation.Validate(s.schema, doc)
}
// Exec executes the given query with the schema's resolver. It panics if the schema was created
// without a resolver. If the context get cancelled, no further resolvers will be called and a
// the context error will be returned as soon as possible (not immediately).
func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) *Response {
if s.res == nil {
panic("schema created without resolver, can not exec")
}
return s.exec(ctx, queryString, operationName, variables, s.res)
}
func (s *Schema) exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, res *resolvable.Schema) *Response {
doc, qErr := query.Parse(queryString)
if qErr != nil {
return &Response{Errors: []*errors.QueryError{qErr}}
}
errs := validation.Validate(s.schema, doc)
if len(errs) != 0 {
return &Response{Errors: errs}
}
op, err := getOperation(doc, operationName)
if err != nil {
return &Response{Errors: []*errors.QueryError{errors.Errorf("%s", err)}}
}
r := &exec.Request{
Request: selected.Request{
Doc: doc,
Vars: variables,
Schema: s.schema,
},
Limiter: make(chan struct{}, s.maxParallelism),
Tracer: s.tracer,
Logger: s.logger,
}
varTypes := make(map[string]*introspection.Type)
for _, v := range op.Vars {
t, err := common.ResolveType(v.Type, s.schema.Resolve)
if err != nil {
return &Response{Errors: []*errors.QueryError{err}}
}
varTypes[v.Name.Name] = introspection.WrapType(t)
}
traceCtx, finish := s.tracer.TraceQuery(ctx, queryString, operationName, variables, varTypes)
data, errs := r.Execute(traceCtx, res, op)
finish(errs)
return &Response{
Data: data,
Errors: errs,
}
}
func getOperation(document *query.Document, operationName string) (*query.Operation, error) {
if len(document.Operations) == 0 {
return nil, fmt.Errorf("no operations in query document")
}
if operationName == "" {
if len(document.Operations) > 1 {
return nil, fmt.Errorf("more than one operation in query document and no operation name given")
}
for _, op := range document.Operations {
return op, nil // return the one and only operation
}
}
op := document.Operations.Get(operationName)
if op == nil {
return nil, fmt.Errorf("no operation with name %q", operationName)
}
return op, nil
}

1755
vendor/github.com/neelance/graphql-go/graphql_test.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

30
vendor/github.com/neelance/graphql-go/id.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
package graphql
import (
"errors"
"strconv"
)
// ID represents GraphQL's "ID" scalar type. A custom type may be used instead.
type ID string
func (_ ID) ImplementsGraphQLType(name string) bool {
return name == "ID"
}
func (id *ID) UnmarshalGraphQL(input interface{}) error {
var err error
switch input := input.(type) {
case string:
*id = ID(input)
case int32:
*id = ID(strconv.Itoa(int(input)))
default:
err = errors.New("wrong type")
}
return err
}
func (id ID) MarshalJSON() ([]byte, error) {
return strconv.AppendQuote(nil, string(id)), nil
}

View File

@ -0,0 +1,32 @@
package common
type Directive struct {
Name Ident
Args ArgumentList
}
func ParseDirectives(l *Lexer) DirectiveList {
var directives DirectiveList
for l.Peek() == '@' {
l.ConsumeToken('@')
d := &Directive{}
d.Name = l.ConsumeIdentWithLoc()
d.Name.Loc.Column--
if l.Peek() == '(' {
d.Args = ParseArguments(l)
}
directives = append(directives, d)
}
return directives
}
type DirectiveList []*Directive
func (l DirectiveList) Get(name string) *Directive {
for _, d := range l {
if d.Name.Name == name {
return d
}
}
return nil
}

View File

@ -0,0 +1,122 @@
package common
import (
"fmt"
"text/scanner"
"github.com/neelance/graphql-go/errors"
)
type syntaxError string
type Lexer struct {
sc *scanner.Scanner
next rune
descComment string
}
type Ident struct {
Name string
Loc errors.Location
}
func New(sc *scanner.Scanner) *Lexer {
l := &Lexer{sc: sc}
l.Consume()
return l
}
func (l *Lexer) CatchSyntaxError(f func()) (errRes *errors.QueryError) {
defer func() {
if err := recover(); err != nil {
if err, ok := err.(syntaxError); ok {
errRes = errors.Errorf("syntax error: %s", err)
errRes.Locations = []errors.Location{l.Location()}
return
}
panic(err)
}
}()
f()
return
}
func (l *Lexer) Peek() rune {
return l.next
}
func (l *Lexer) Consume() {
l.descComment = ""
for {
l.next = l.sc.Scan()
if l.next == ',' {
continue
}
if l.next == '#' {
if l.sc.Peek() == ' ' {
l.sc.Next()
}
if l.descComment != "" {
l.descComment += "\n"
}
for {
next := l.sc.Next()
if next == '\n' || next == scanner.EOF {
break
}
l.descComment += string(next)
}
continue
}
break
}
}
func (l *Lexer) ConsumeIdent() string {
name := l.sc.TokenText()
l.ConsumeToken(scanner.Ident)
return name
}
func (l *Lexer) ConsumeIdentWithLoc() Ident {
loc := l.Location()
name := l.sc.TokenText()
l.ConsumeToken(scanner.Ident)
return Ident{name, loc}
}
func (l *Lexer) ConsumeKeyword(keyword string) {
if l.next != scanner.Ident || l.sc.TokenText() != keyword {
l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %q", l.sc.TokenText(), keyword))
}
l.Consume()
}
func (l *Lexer) ConsumeLiteral() *BasicLit {
lit := &BasicLit{Type: l.next, Text: l.sc.TokenText()}
l.Consume()
return lit
}
func (l *Lexer) ConsumeToken(expected rune) {
if l.next != expected {
l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %s", l.sc.TokenText(), scanner.TokenString(expected)))
}
l.Consume()
}
func (l *Lexer) DescComment() string {
return l.descComment
}
func (l *Lexer) SyntaxError(message string) {
panic(syntaxError(message))
}
func (l *Lexer) Location() errors.Location {
return errors.Location{
Line: l.sc.Line,
Column: l.sc.Column,
}
}

View File

@ -0,0 +1,206 @@
package common
import (
"strconv"
"strings"
"text/scanner"
"github.com/neelance/graphql-go/errors"
)
type Literal interface {
Value(vars map[string]interface{}) interface{}
String() string
Location() errors.Location
}
type BasicLit struct {
Type rune
Text string
Loc errors.Location
}
func (lit *BasicLit) Value(vars map[string]interface{}) interface{} {
switch lit.Type {
case scanner.Int:
value, err := strconv.ParseInt(lit.Text, 10, 32)
if err != nil {
panic(err)
}
return int32(value)
case scanner.Float:
value, err := strconv.ParseFloat(lit.Text, 64)
if err != nil {
panic(err)
}
return value
case scanner.String:
value, err := strconv.Unquote(lit.Text)
if err != nil {
panic(err)
}
return value
case scanner.Ident:
switch lit.Text {
case "true":
return true
case "false":
return false
default:
return lit.Text
}
default:
panic("invalid literal")
}
}
func (lit *BasicLit) String() string {
return lit.Text
}
func (lit *BasicLit) Location() errors.Location {
return lit.Loc
}
type ListLit struct {
Entries []Literal
Loc errors.Location
}
func (lit *ListLit) Value(vars map[string]interface{}) interface{} {
entries := make([]interface{}, len(lit.Entries))
for i, entry := range lit.Entries {
entries[i] = entry.Value(vars)
}
return entries
}
func (lit *ListLit) String() string {
entries := make([]string, len(lit.Entries))
for i, entry := range lit.Entries {
entries[i] = entry.String()
}
return "[" + strings.Join(entries, ", ") + "]"
}
func (lit *ListLit) Location() errors.Location {
return lit.Loc
}
type ObjectLit struct {
Fields []*ObjectLitField
Loc errors.Location
}
type ObjectLitField struct {
Name Ident
Value Literal
}
func (lit *ObjectLit) Value(vars map[string]interface{}) interface{} {
fields := make(map[string]interface{}, len(lit.Fields))
for _, f := range lit.Fields {
fields[f.Name.Name] = f.Value.Value(vars)
}
return fields
}
func (lit *ObjectLit) String() string {
entries := make([]string, 0, len(lit.Fields))
for _, f := range lit.Fields {
entries = append(entries, f.Name.Name+": "+f.Value.String())
}
return "{" + strings.Join(entries, ", ") + "}"
}
func (lit *ObjectLit) Location() errors.Location {
return lit.Loc
}
type NullLit struct {
Loc errors.Location
}
func (lit *NullLit) Value(vars map[string]interface{}) interface{} {
return nil
}
func (lit *NullLit) String() string {
return "null"
}
func (lit *NullLit) Location() errors.Location {
return lit.Loc
}
type Variable struct {
Name string
Loc errors.Location
}
func (v Variable) Value(vars map[string]interface{}) interface{} {
return vars[v.Name]
}
func (v Variable) String() string {
return "$" + v.Name
}
func (v *Variable) Location() errors.Location {
return v.Loc
}
func ParseLiteral(l *Lexer, constOnly bool) Literal {
loc := l.Location()
switch l.Peek() {
case '$':
if constOnly {
l.SyntaxError("variable not allowed")
panic("unreachable")
}
l.ConsumeToken('$')
return &Variable{l.ConsumeIdent(), loc}
case scanner.Int, scanner.Float, scanner.String, scanner.Ident:
lit := l.ConsumeLiteral()
if lit.Type == scanner.Ident && lit.Text == "null" {
return &NullLit{loc}
}
lit.Loc = loc
return lit
case '-':
l.ConsumeToken('-')
lit := l.ConsumeLiteral()
lit.Text = "-" + lit.Text
lit.Loc = loc
return lit
case '[':
l.ConsumeToken('[')
var list []Literal
for l.Peek() != ']' {
list = append(list, ParseLiteral(l, constOnly))
}
l.ConsumeToken(']')
return &ListLit{list, loc}
case '{':
l.ConsumeToken('{')
var fields []*ObjectLitField
for l.Peek() != '}' {
name := l.ConsumeIdentWithLoc()
l.ConsumeToken(':')
value := ParseLiteral(l, constOnly)
fields = append(fields, &ObjectLitField{name, value})
}
l.ConsumeToken('}')
return &ObjectLit{fields, loc}
default:
l.SyntaxError("invalid value")
panic("unreachable")
}
}

View File

@ -0,0 +1,80 @@
package common
import (
"github.com/neelance/graphql-go/errors"
)
type Type interface {
Kind() string
String() string
}
type List struct {
OfType Type
}
type NonNull struct {
OfType Type
}
type TypeName struct {
Ident
}
func (*List) Kind() string { return "LIST" }
func (*NonNull) Kind() string { return "NON_NULL" }
func (*TypeName) Kind() string { panic("TypeName needs to be resolved to actual type") }
func (t *List) String() string { return "[" + t.OfType.String() + "]" }
func (t *NonNull) String() string { return t.OfType.String() + "!" }
func (*TypeName) String() string { panic("TypeName needs to be resolved to actual type") }
func ParseType(l *Lexer) Type {
t := parseNullType(l)
if l.Peek() == '!' {
l.ConsumeToken('!')
return &NonNull{OfType: t}
}
return t
}
func parseNullType(l *Lexer) Type {
if l.Peek() == '[' {
l.ConsumeToken('[')
ofType := ParseType(l)
l.ConsumeToken(']')
return &List{OfType: ofType}
}
return &TypeName{Ident: l.ConsumeIdentWithLoc()}
}
type Resolver func(name string) Type
func ResolveType(t Type, resolver Resolver) (Type, *errors.QueryError) {
switch t := t.(type) {
case *List:
ofType, err := ResolveType(t.OfType, resolver)
if err != nil {
return nil, err
}
return &List{OfType: ofType}, nil
case *NonNull:
ofType, err := ResolveType(t.OfType, resolver)
if err != nil {
return nil, err
}
return &NonNull{OfType: ofType}, nil
case *TypeName:
refT := resolver(t.Name)
if refT == nil {
err := errors.Errorf("Unknown type %q.", t.Name)
err.Rule = "KnownTypeNames"
err.Locations = []errors.Location{t.Loc}
return nil, err
}
return refT, nil
default:
return t, nil
}
}

View File

@ -0,0 +1,77 @@
package common
import (
"github.com/neelance/graphql-go/errors"
)
type InputValue struct {
Name Ident
Type Type
Default Literal
Desc string
Loc errors.Location
TypeLoc errors.Location
}
type InputValueList []*InputValue
func (l InputValueList) Get(name string) *InputValue {
for _, v := range l {
if v.Name.Name == name {
return v
}
}
return nil
}
func ParseInputValue(l *Lexer) *InputValue {
p := &InputValue{}
p.Loc = l.Location()
p.Desc = l.DescComment()
p.Name = l.ConsumeIdentWithLoc()
l.ConsumeToken(':')
p.TypeLoc = l.Location()
p.Type = ParseType(l)
if l.Peek() == '=' {
l.ConsumeToken('=')
p.Default = ParseLiteral(l, true)
}
return p
}
type Argument struct {
Name Ident
Value Literal
}
type ArgumentList []Argument
func (l ArgumentList) Get(name string) (Literal, bool) {
for _, arg := range l {
if arg.Name.Name == name {
return arg.Value, true
}
}
return nil, false
}
func (l ArgumentList) MustGet(name string) Literal {
value, ok := l.Get(name)
if !ok {
panic("argument not found")
}
return value
}
func ParseArguments(l *Lexer) ArgumentList {
var args ArgumentList
l.ConsumeToken('(')
for l.Peek() != ')' {
name := l.ConsumeIdentWithLoc()
l.ConsumeToken(':')
value := ParseLiteral(l, false)
args = append(args, Argument{Name: name, Value: value})
}
l.ConsumeToken(')')
return args
}

View File

@ -0,0 +1,313 @@
package exec
import (
"bytes"
"context"
"encoding/json"
"reflect"
"sync"
"github.com/neelance/graphql-go/errors"
"github.com/neelance/graphql-go/internal/common"
"github.com/neelance/graphql-go/internal/exec/resolvable"
"github.com/neelance/graphql-go/internal/exec/selected"
"github.com/neelance/graphql-go/internal/query"
"github.com/neelance/graphql-go/internal/schema"
"github.com/neelance/graphql-go/log"
"github.com/neelance/graphql-go/trace"
)
type Request struct {
selected.Request
Limiter chan struct{}
Tracer trace.Tracer
Logger log.Logger
}
type fieldResult struct {
name string
value []byte
}
func (r *Request) handlePanic(ctx context.Context) {
if value := recover(); value != nil {
r.Logger.LogPanic(ctx, value)
r.AddError(makePanicError(value))
}
}
func makePanicError(value interface{}) *errors.QueryError {
return errors.Errorf("graphql: panic occurred: %v", value)
}
func (r *Request) Execute(ctx context.Context, s *resolvable.Schema, op *query.Operation) ([]byte, []*errors.QueryError) {
var out bytes.Buffer
func() {
defer r.handlePanic(ctx)
sels := selected.ApplyOperation(&r.Request, s, op)
r.execSelections(ctx, sels, nil, s.Resolver, &out, op.Type == query.Mutation)
}()
if err := ctx.Err(); err != nil {
return nil, []*errors.QueryError{errors.Errorf("%s", err)}
}
return out.Bytes(), r.Errs
}
type fieldToExec struct {
field *selected.SchemaField
sels []selected.Selection
resolver reflect.Value
out *bytes.Buffer
}
func (r *Request) execSelections(ctx context.Context, sels []selected.Selection, path *pathSegment, resolver reflect.Value, out *bytes.Buffer, serially bool) {
async := !serially && selected.HasAsyncSel(sels)
var fields []*fieldToExec
collectFieldsToResolve(sels, resolver, &fields, make(map[string]*fieldToExec))
if async {
var wg sync.WaitGroup
wg.Add(len(fields))
for _, f := range fields {
go func(f *fieldToExec) {
defer wg.Done()
defer r.handlePanic(ctx)
f.out = new(bytes.Buffer)
execFieldSelection(ctx, r, f, &pathSegment{path, f.field.Alias}, true)
}(f)
}
wg.Wait()
}
out.WriteByte('{')
for i, f := range fields {
if i > 0 {
out.WriteByte(',')
}
out.WriteByte('"')
out.WriteString(f.field.Alias)
out.WriteByte('"')
out.WriteByte(':')
if async {
out.Write(f.out.Bytes())
continue
}
f.out = out
execFieldSelection(ctx, r, f, &pathSegment{path, f.field.Alias}, false)
}
out.WriteByte('}')
}
func collectFieldsToResolve(sels []selected.Selection, resolver reflect.Value, fields *[]*fieldToExec, fieldByAlias map[string]*fieldToExec) {
for _, sel := range sels {
switch sel := sel.(type) {
case *selected.SchemaField:
field, ok := fieldByAlias[sel.Alias]
if !ok { // validation already checked for conflict (TODO)
field = &fieldToExec{field: sel, resolver: resolver}
fieldByAlias[sel.Alias] = field
*fields = append(*fields, field)
}
field.sels = append(field.sels, sel.Sels...)
case *selected.TypenameField:
sf := &selected.SchemaField{
Field: resolvable.MetaFieldTypename,
Alias: sel.Alias,
FixedResult: reflect.ValueOf(typeOf(sel, resolver)),
}
*fields = append(*fields, &fieldToExec{field: sf, resolver: resolver})
case *selected.TypeAssertion:
out := resolver.Method(sel.MethodIndex).Call(nil)
if !out[1].Bool() {
continue
}
collectFieldsToResolve(sel.Sels, out[0], fields, fieldByAlias)
default:
panic("unreachable")
}
}
}
func typeOf(tf *selected.TypenameField, resolver reflect.Value) string {
if len(tf.TypeAssertions) == 0 {
return tf.Name
}
for name, a := range tf.TypeAssertions {
out := resolver.Method(a.MethodIndex).Call(nil)
if out[1].Bool() {
return name
}
}
return ""
}
func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *pathSegment, applyLimiter bool) {
if applyLimiter {
r.Limiter <- struct{}{}
}
var result reflect.Value
var err *errors.QueryError
traceCtx, finish := r.Tracer.TraceField(ctx, f.field.TraceLabel, f.field.TypeName, f.field.Name, !f.field.Async, f.field.Args)
defer func() {
finish(err)
}()
err = func() (err *errors.QueryError) {
defer func() {
if panicValue := recover(); panicValue != nil {
r.Logger.LogPanic(ctx, panicValue)
err = makePanicError(panicValue)
err.Path = path.toSlice()
}
}()
if f.field.FixedResult.IsValid() {
result = f.field.FixedResult
return nil
}
if err := traceCtx.Err(); err != nil {
return errors.Errorf("%s", err) // don't execute any more resolvers if context got cancelled
}
var in []reflect.Value
if f.field.HasContext {
in = append(in, reflect.ValueOf(traceCtx))
}
if f.field.ArgsPacker != nil {
in = append(in, f.field.PackedArgs)
}
callOut := f.resolver.Method(f.field.MethodIndex).Call(in)
result = callOut[0]
if f.field.HasError && !callOut[1].IsNil() {
resolverErr := callOut[1].Interface().(error)
err := errors.Errorf("%s", resolverErr)
err.Path = path.toSlice()
err.ResolverError = resolverErr
return err
}
return nil
}()
if applyLimiter {
<-r.Limiter
}
if err != nil {
r.AddError(err)
f.out.WriteString("null") // TODO handle non-nil
return
}
r.execSelectionSet(traceCtx, f.sels, f.field.Type, path, result, f.out)
}
func (r *Request) execSelectionSet(ctx context.Context, sels []selected.Selection, typ common.Type, path *pathSegment, resolver reflect.Value, out *bytes.Buffer) {
t, nonNull := unwrapNonNull(typ)
switch t := t.(type) {
case *schema.Object, *schema.Interface, *schema.Union:
if resolver.Kind() == reflect.Ptr && resolver.IsNil() {
if nonNull {
panic(errors.Errorf("got nil for non-null %q", t))
}
out.WriteString("null")
return
}
r.execSelections(ctx, sels, path, resolver, out, false)
return
}
if !nonNull {
if resolver.IsNil() {
out.WriteString("null")
return
}
resolver = resolver.Elem()
}
switch t := t.(type) {
case *common.List:
l := resolver.Len()
if selected.HasAsyncSel(sels) {
var wg sync.WaitGroup
wg.Add(l)
entryouts := make([]bytes.Buffer, l)
for i := 0; i < l; i++ {
go func(i int) {
defer wg.Done()
defer r.handlePanic(ctx)
r.execSelectionSet(ctx, sels, t.OfType, &pathSegment{path, i}, resolver.Index(i), &entryouts[i])
}(i)
}
wg.Wait()
out.WriteByte('[')
for i, entryout := range entryouts {
if i > 0 {
out.WriteByte(',')
}
out.Write(entryout.Bytes())
}
out.WriteByte(']')
return
}
out.WriteByte('[')
for i := 0; i < l; i++ {
if i > 0 {
out.WriteByte(',')
}
r.execSelectionSet(ctx, sels, t.OfType, &pathSegment{path, i}, resolver.Index(i), out)
}
out.WriteByte(']')
case *schema.Scalar:
v := resolver.Interface()
data, err := json.Marshal(v)
if err != nil {
panic(errors.Errorf("could not marshal %v", v))
}
out.Write(data)
case *schema.Enum:
out.WriteByte('"')
out.WriteString(resolver.String())
out.WriteByte('"')
default:
panic("unreachable")
}
}
func unwrapNonNull(t common.Type) (common.Type, bool) {
if nn, ok := t.(*common.NonNull); ok {
return nn.OfType, true
}
return t, false
}
type marshaler interface {
MarshalJSON() ([]byte, error)
}
type pathSegment struct {
parent *pathSegment
value interface{}
}
func (p *pathSegment) toSlice() []interface{} {
if p == nil {
return nil
}
return append(p.parent.toSlice(), p.value)
}

View File

@ -0,0 +1,367 @@
package packer
import (
"fmt"
"math"
"reflect"
"strings"
"github.com/neelance/graphql-go/errors"
"github.com/neelance/graphql-go/internal/common"
"github.com/neelance/graphql-go/internal/schema"
)
type packer interface {
Pack(value interface{}) (reflect.Value, error)
}
type Builder struct {
packerMap map[typePair]*packerMapEntry
structPackers []*StructPacker
}
type typePair struct {
graphQLType common.Type
resolverType reflect.Type
}
type packerMapEntry struct {
packer packer
targets []*packer
}
func NewBuilder() *Builder {
return &Builder{
packerMap: make(map[typePair]*packerMapEntry),
}
}
func (b *Builder) Finish() error {
for _, entry := range b.packerMap {
for _, target := range entry.targets {
*target = entry.packer
}
}
for _, p := range b.structPackers {
p.defaultStruct = reflect.New(p.structType).Elem()
for _, f := range p.fields {
if defaultVal := f.field.Default; defaultVal != nil {
v, err := f.fieldPacker.Pack(defaultVal.Value(nil))
if err != nil {
return err
}
p.defaultStruct.FieldByIndex(f.fieldIndex).Set(v)
}
}
}
return nil
}
func (b *Builder) assignPacker(target *packer, schemaType common.Type, reflectType reflect.Type) error {
k := typePair{schemaType, reflectType}
ref, ok := b.packerMap[k]
if !ok {
ref = &packerMapEntry{}
b.packerMap[k] = ref
var err error
ref.packer, err = b.makePacker(schemaType, reflectType)
if err != nil {
return err
}
}
ref.targets = append(ref.targets, target)
return nil
}
func (b *Builder) makePacker(schemaType common.Type, reflectType reflect.Type) (packer, error) {
t, nonNull := unwrapNonNull(schemaType)
if !nonNull {
if reflectType.Kind() != reflect.Ptr {
return nil, fmt.Errorf("%s is not a pointer", reflectType)
}
elemType := reflectType.Elem()
addPtr := true
if _, ok := t.(*schema.InputObject); ok {
elemType = reflectType // keep pointer for input objects
addPtr = false
}
elem, err := b.makeNonNullPacker(t, elemType)
if err != nil {
return nil, err
}
return &nullPacker{
elemPacker: elem,
valueType: reflectType,
addPtr: addPtr,
}, nil
}
return b.makeNonNullPacker(t, reflectType)
}
func (b *Builder) makeNonNullPacker(schemaType common.Type, reflectType reflect.Type) (packer, error) {
if u, ok := reflect.New(reflectType).Interface().(Unmarshaler); ok {
if !u.ImplementsGraphQLType(schemaType.String()) {
return nil, fmt.Errorf("can not unmarshal %s into %s", schemaType, reflectType)
}
return &unmarshalerPacker{
ValueType: reflectType,
}, nil
}
switch t := schemaType.(type) {
case *schema.Scalar:
return &ValuePacker{
ValueType: reflectType,
}, nil
case *schema.Enum:
want := reflect.TypeOf("")
if reflectType != want {
return nil, fmt.Errorf("wrong type, expected %s", want)
}
return &ValuePacker{
ValueType: reflectType,
}, nil
case *schema.InputObject:
e, err := b.MakeStructPacker(t.Values, reflectType)
if err != nil {
return nil, err
}
return e, nil
case *common.List:
if reflectType.Kind() != reflect.Slice {
return nil, fmt.Errorf("expected slice, got %s", reflectType)
}
p := &listPacker{
sliceType: reflectType,
}
if err := b.assignPacker(&p.elem, t.OfType, reflectType.Elem()); err != nil {
return nil, err
}
return p, nil
case *schema.Object, *schema.Interface, *schema.Union:
return nil, fmt.Errorf("type of kind %s can not be used as input", t.Kind())
default:
panic("unreachable")
}
}
func (b *Builder) MakeStructPacker(values common.InputValueList, typ reflect.Type) (*StructPacker, error) {
structType := typ
usePtr := false
if typ.Kind() == reflect.Ptr {
structType = typ.Elem()
usePtr = true
}
if structType.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected struct or pointer to struct, got %s", typ)
}
var fields []*structPackerField
for _, v := range values {
fe := &structPackerField{field: v}
fx := func(n string) bool {
return strings.EqualFold(stripUnderscore(n), stripUnderscore(v.Name.Name))
}
sf, ok := structType.FieldByNameFunc(fx)
if !ok {
return nil, fmt.Errorf("missing argument %q", v.Name)
}
if sf.PkgPath != "" {
return nil, fmt.Errorf("field %q must be exported", sf.Name)
}
fe.fieldIndex = sf.Index
ft := v.Type
if v.Default != nil {
ft, _ = unwrapNonNull(ft)
ft = &common.NonNull{OfType: ft}
}
if err := b.assignPacker(&fe.fieldPacker, ft, sf.Type); err != nil {
return nil, fmt.Errorf("field %q: %s", sf.Name, err)
}
fields = append(fields, fe)
}
p := &StructPacker{
structType: structType,
usePtr: usePtr,
fields: fields,
}
b.structPackers = append(b.structPackers, p)
return p, nil
}
type StructPacker struct {
structType reflect.Type
usePtr bool
defaultStruct reflect.Value
fields []*structPackerField
}
type structPackerField struct {
field *common.InputValue
fieldIndex []int
fieldPacker packer
}
func (p *StructPacker) Pack(value interface{}) (reflect.Value, error) {
if value == nil {
return reflect.Value{}, errors.Errorf("got null for non-null")
}
values := value.(map[string]interface{})
v := reflect.New(p.structType)
v.Elem().Set(p.defaultStruct)
for _, f := range p.fields {
if value, ok := values[f.field.Name.Name]; ok {
packed, err := f.fieldPacker.Pack(value)
if err != nil {
return reflect.Value{}, err
}
v.Elem().FieldByIndex(f.fieldIndex).Set(packed)
}
}
if !p.usePtr {
return v.Elem(), nil
}
return v, nil
}
type listPacker struct {
sliceType reflect.Type
elem packer
}
func (e *listPacker) Pack(value interface{}) (reflect.Value, error) {
list, ok := value.([]interface{})
if !ok {
list = []interface{}{value}
}
v := reflect.MakeSlice(e.sliceType, len(list), len(list))
for i := range list {
packed, err := e.elem.Pack(list[i])
if err != nil {
return reflect.Value{}, err
}
v.Index(i).Set(packed)
}
return v, nil
}
type nullPacker struct {
elemPacker packer
valueType reflect.Type
addPtr bool
}
func (p *nullPacker) Pack(value interface{}) (reflect.Value, error) {
if value == nil {
return reflect.Zero(p.valueType), nil
}
v, err := p.elemPacker.Pack(value)
if err != nil {
return reflect.Value{}, err
}
if p.addPtr {
ptr := reflect.New(p.valueType.Elem())
ptr.Elem().Set(v)
return ptr, nil
}
return v, nil
}
type ValuePacker struct {
ValueType reflect.Type
}
func (p *ValuePacker) Pack(value interface{}) (reflect.Value, error) {
if value == nil {
return reflect.Value{}, errors.Errorf("got null for non-null")
}
coerced, err := unmarshalInput(p.ValueType, value)
if err != nil {
return reflect.Value{}, fmt.Errorf("could not unmarshal %#v (%T) into %s: %s", value, value, p.ValueType, err)
}
return reflect.ValueOf(coerced), nil
}
type unmarshalerPacker struct {
ValueType reflect.Type
}
func (p *unmarshalerPacker) Pack(value interface{}) (reflect.Value, error) {
if value == nil {
return reflect.Value{}, errors.Errorf("got null for non-null")
}
v := reflect.New(p.ValueType)
if err := v.Interface().(Unmarshaler).UnmarshalGraphQL(value); err != nil {
return reflect.Value{}, err
}
return v.Elem(), nil
}
type Unmarshaler interface {
ImplementsGraphQLType(name string) bool
UnmarshalGraphQL(input interface{}) error
}
func unmarshalInput(typ reflect.Type, input interface{}) (interface{}, error) {
if reflect.TypeOf(input) == typ {
return input, nil
}
switch typ.Kind() {
case reflect.Int32:
switch input := input.(type) {
case int:
if input < math.MinInt32 || input > math.MaxInt32 {
return nil, fmt.Errorf("not a 32-bit integer")
}
return int32(input), nil
case float64:
coerced := int32(input)
if input < math.MinInt32 || input > math.MaxInt32 || float64(coerced) != input {
return nil, fmt.Errorf("not a 32-bit integer")
}
return coerced, nil
}
case reflect.Float64:
switch input := input.(type) {
case int32:
return float64(input), nil
case int:
return float64(input), nil
}
}
return nil, fmt.Errorf("incompatible type")
}
func unwrapNonNull(t common.Type) (common.Type, bool) {
if nn, ok := t.(*common.NonNull); ok {
return nn.OfType, true
}
return t, false
}
func stripUnderscore(s string) string {
return strings.Replace(s, "_", "", -1)
}

View File

@ -0,0 +1,58 @@
package resolvable
import (
"fmt"
"reflect"
"github.com/neelance/graphql-go/internal/common"
"github.com/neelance/graphql-go/internal/schema"
"github.com/neelance/graphql-go/introspection"
)
var MetaSchema *Object
var MetaType *Object
func init() {
var err error
b := newBuilder(schema.Meta)
metaSchema := schema.Meta.Types["__Schema"].(*schema.Object)
MetaSchema, err = b.makeObjectExec(metaSchema.Name, metaSchema.Fields, nil, false, reflect.TypeOf(&introspection.Schema{}))
if err != nil {
panic(err)
}
metaType := schema.Meta.Types["__Type"].(*schema.Object)
MetaType, err = b.makeObjectExec(metaType.Name, metaType.Fields, nil, false, reflect.TypeOf(&introspection.Type{}))
if err != nil {
panic(err)
}
if err := b.finish(); err != nil {
panic(err)
}
}
var MetaFieldTypename = Field{
Field: schema.Field{
Name: "__typename",
Type: &common.NonNull{OfType: schema.Meta.Types["String"]},
},
TraceLabel: fmt.Sprintf("GraphQL field: __typename"),
}
var MetaFieldSchema = Field{
Field: schema.Field{
Name: "__schema",
Type: schema.Meta.Types["__Schema"],
},
TraceLabel: fmt.Sprintf("GraphQL field: __schema"),
}
var MetaFieldType = Field{
Field: schema.Field{
Name: "__type",
Type: schema.Meta.Types["__Type"],
},
TraceLabel: fmt.Sprintf("GraphQL field: __type"),
}

View File

@ -0,0 +1,331 @@
package resolvable
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/neelance/graphql-go/internal/common"
"github.com/neelance/graphql-go/internal/exec/packer"
"github.com/neelance/graphql-go/internal/schema"
)
type Schema struct {
schema.Schema
Query Resolvable
Mutation Resolvable
Resolver reflect.Value
}
type Resolvable interface {
isResolvable()
}
type Object struct {
Name string
Fields map[string]*Field
TypeAssertions map[string]*TypeAssertion
}
type Field struct {
schema.Field
TypeName string
MethodIndex int
HasContext bool
ArgsPacker *packer.StructPacker
HasError bool
ValueExec Resolvable
TraceLabel string
}
type TypeAssertion struct {
MethodIndex int
TypeExec Resolvable
}
type List struct {
Elem Resolvable
}
type Scalar struct{}
func (*Object) isResolvable() {}
func (*List) isResolvable() {}
func (*Scalar) isResolvable() {}
func ApplyResolver(s *schema.Schema, resolver interface{}) (*Schema, error) {
b := newBuilder(s)
var query, mutation Resolvable
if t, ok := s.EntryPoints["query"]; ok {
if err := b.assignExec(&query, t, reflect.TypeOf(resolver)); err != nil {
return nil, err
}
}
if t, ok := s.EntryPoints["mutation"]; ok {
if err := b.assignExec(&mutation, t, reflect.TypeOf(resolver)); err != nil {
return nil, err
}
}
if err := b.finish(); err != nil {
return nil, err
}
return &Schema{
Schema: *s,
Resolver: reflect.ValueOf(resolver),
Query: query,
Mutation: mutation,
}, nil
}
type execBuilder struct {
schema *schema.Schema
resMap map[typePair]*resMapEntry
packerBuilder *packer.Builder
}
type typePair struct {
graphQLType common.Type
resolverType reflect.Type
}
type resMapEntry struct {
exec Resolvable
targets []*Resolvable
}
func newBuilder(s *schema.Schema) *execBuilder {
return &execBuilder{
schema: s,
resMap: make(map[typePair]*resMapEntry),
packerBuilder: packer.NewBuilder(),
}
}
func (b *execBuilder) finish() error {
for _, entry := range b.resMap {
for _, target := range entry.targets {
*target = entry.exec
}
}
return b.packerBuilder.Finish()
}
func (b *execBuilder) assignExec(target *Resolvable, t common.Type, resolverType reflect.Type) error {
k := typePair{t, resolverType}
ref, ok := b.resMap[k]
if !ok {
ref = &resMapEntry{}
b.resMap[k] = ref
var err error
ref.exec, err = b.makeExec(t, resolverType)
if err != nil {
return err
}
}
ref.targets = append(ref.targets, target)
return nil
}
func (b *execBuilder) makeExec(t common.Type, resolverType reflect.Type) (Resolvable, error) {
var nonNull bool
t, nonNull = unwrapNonNull(t)
switch t := t.(type) {
case *schema.Object:
return b.makeObjectExec(t.Name, t.Fields, nil, nonNull, resolverType)
case *schema.Interface:
return b.makeObjectExec(t.Name, t.Fields, t.PossibleTypes, nonNull, resolverType)
case *schema.Union:
return b.makeObjectExec(t.Name, nil, t.PossibleTypes, nonNull, resolverType)
}
if !nonNull {
if resolverType.Kind() != reflect.Ptr {
return nil, fmt.Errorf("%s is not a pointer", resolverType)
}
resolverType = resolverType.Elem()
}
switch t := t.(type) {
case *schema.Scalar:
return makeScalarExec(t, resolverType)
case *schema.Enum:
return &Scalar{}, nil
case *common.List:
if resolverType.Kind() != reflect.Slice {
return nil, fmt.Errorf("%s is not a slice", resolverType)
}
e := &List{}
if err := b.assignExec(&e.Elem, t.OfType, resolverType.Elem()); err != nil {
return nil, err
}
return e, nil
default:
panic("invalid type")
}
}
func makeScalarExec(t *schema.Scalar, resolverType reflect.Type) (Resolvable, error) {
implementsType := false
switch r := reflect.New(resolverType).Interface().(type) {
case *int32:
implementsType = (t.Name == "Int")
case *float64:
implementsType = (t.Name == "Float")
case *string:
implementsType = (t.Name == "String")
case *bool:
implementsType = (t.Name == "Boolean")
case packer.Unmarshaler:
implementsType = r.ImplementsGraphQLType(t.Name)
}
if !implementsType {
return nil, fmt.Errorf("can not use %s as %s", resolverType, t.Name)
}
return &Scalar{}, nil
}
func (b *execBuilder) makeObjectExec(typeName string, fields schema.FieldList, possibleTypes []*schema.Object, nonNull bool, resolverType reflect.Type) (*Object, error) {
if !nonNull {
if resolverType.Kind() != reflect.Ptr && resolverType.Kind() != reflect.Interface {
return nil, fmt.Errorf("%s is not a pointer or interface", resolverType)
}
}
methodHasReceiver := resolverType.Kind() != reflect.Interface
Fields := make(map[string]*Field)
for _, f := range fields {
methodIndex := findMethod(resolverType, f.Name)
if methodIndex == -1 {
hint := ""
if findMethod(reflect.PtrTo(resolverType), f.Name) != -1 {
hint = " (hint: the method exists on the pointer type)"
}
return nil, fmt.Errorf("%s does not resolve %q: missing method for field %q%s", resolverType, typeName, f.Name, hint)
}
m := resolverType.Method(methodIndex)
fe, err := b.makeFieldExec(typeName, f, m, methodIndex, methodHasReceiver)
if err != nil {
return nil, fmt.Errorf("%s\n\treturned by (%s).%s", err, resolverType, m.Name)
}
Fields[f.Name] = fe
}
typeAssertions := make(map[string]*TypeAssertion)
for _, impl := range possibleTypes {
methodIndex := findMethod(resolverType, "to"+impl.Name)
if methodIndex == -1 {
return nil, fmt.Errorf("%s does not resolve %q: missing method %q to convert to %q", resolverType, typeName, "to"+impl.Name, impl.Name)
}
if resolverType.Method(methodIndex).Type.NumOut() != 2 {
return nil, fmt.Errorf("%s does not resolve %q: method %q should return a value and a bool indicating success", resolverType, typeName, "to"+impl.Name)
}
a := &TypeAssertion{
MethodIndex: methodIndex,
}
if err := b.assignExec(&a.TypeExec, impl, resolverType.Method(methodIndex).Type.Out(0)); err != nil {
return nil, err
}
typeAssertions[impl.Name] = a
}
return &Object{
Name: typeName,
Fields: Fields,
TypeAssertions: typeAssertions,
}, nil
}
var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
var errorType = reflect.TypeOf((*error)(nil)).Elem()
func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect.Method, methodIndex int, methodHasReceiver bool) (*Field, error) {
in := make([]reflect.Type, m.Type.NumIn())
for i := range in {
in[i] = m.Type.In(i)
}
if methodHasReceiver {
in = in[1:] // first parameter is receiver
}
hasContext := len(in) > 0 && in[0] == contextType
if hasContext {
in = in[1:]
}
var argsPacker *packer.StructPacker
if len(f.Args) > 0 {
if len(in) == 0 {
return nil, fmt.Errorf("must have parameter for field arguments")
}
var err error
argsPacker, err = b.packerBuilder.MakeStructPacker(f.Args, in[0])
if err != nil {
return nil, err
}
in = in[1:]
}
if len(in) > 0 {
return nil, fmt.Errorf("too many parameters")
}
if m.Type.NumOut() > 2 {
return nil, fmt.Errorf("too many return values")
}
hasError := m.Type.NumOut() == 2
if hasError {
if m.Type.Out(1) != errorType {
return nil, fmt.Errorf(`must have "error" as its second return value`)
}
}
fe := &Field{
Field: *f,
TypeName: typeName,
MethodIndex: methodIndex,
HasContext: hasContext,
ArgsPacker: argsPacker,
HasError: hasError,
TraceLabel: fmt.Sprintf("GraphQL field: %s.%s", typeName, f.Name),
}
if err := b.assignExec(&fe.ValueExec, f.Type, m.Type.Out(0)); err != nil {
return nil, err
}
return fe, nil
}
func findMethod(t reflect.Type, name string) int {
for i := 0; i < t.NumMethod(); i++ {
if strings.EqualFold(stripUnderscore(name), stripUnderscore(t.Method(i).Name)) {
return i
}
}
return -1
}
func unwrapNonNull(t common.Type) (common.Type, bool) {
if nn, ok := t.(*common.NonNull); ok {
return nn.OfType, true
}
return t, false
}
func stripUnderscore(s string) string {
return strings.Replace(s, "_", "", -1)
}

View File

@ -0,0 +1,238 @@
package selected
import (
"fmt"
"reflect"
"sync"
"github.com/neelance/graphql-go/errors"
"github.com/neelance/graphql-go/internal/common"
"github.com/neelance/graphql-go/internal/exec/packer"
"github.com/neelance/graphql-go/internal/exec/resolvable"
"github.com/neelance/graphql-go/internal/query"
"github.com/neelance/graphql-go/internal/schema"
"github.com/neelance/graphql-go/introspection"
)
type Request struct {
Schema *schema.Schema
Doc *query.Document
Vars map[string]interface{}
Mu sync.Mutex
Errs []*errors.QueryError
}
func (r *Request) AddError(err *errors.QueryError) {
r.Mu.Lock()
r.Errs = append(r.Errs, err)
r.Mu.Unlock()
}
func ApplyOperation(r *Request, s *resolvable.Schema, op *query.Operation) []Selection {
var obj *resolvable.Object
switch op.Type {
case query.Query:
obj = s.Query.(*resolvable.Object)
case query.Mutation:
obj = s.Mutation.(*resolvable.Object)
}
return applySelectionSet(r, obj, op.Selections)
}
type Selection interface {
isSelection()
}
type SchemaField struct {
resolvable.Field
Alias string
Args map[string]interface{}
PackedArgs reflect.Value
Sels []Selection
Async bool
FixedResult reflect.Value
}
type TypeAssertion struct {
resolvable.TypeAssertion
Sels []Selection
}
type TypenameField struct {
resolvable.Object
Alias string
}
func (*SchemaField) isSelection() {}
func (*TypeAssertion) isSelection() {}
func (*TypenameField) isSelection() {}
func applySelectionSet(r *Request, e *resolvable.Object, sels []query.Selection) (flattenedSels []Selection) {
for _, sel := range sels {
switch sel := sel.(type) {
case *query.Field:
field := sel
if skipByDirective(r, field.Directives) {
continue
}
switch field.Name.Name {
case "__typename":
flattenedSels = append(flattenedSels, &TypenameField{
Object: *e,
Alias: field.Alias.Name,
})
case "__schema":
flattenedSels = append(flattenedSels, &SchemaField{
Field: resolvable.MetaFieldSchema,
Alias: field.Alias.Name,
Sels: applySelectionSet(r, resolvable.MetaSchema, field.Selections),
Async: true,
FixedResult: reflect.ValueOf(introspection.WrapSchema(r.Schema)),
})
case "__type":
p := packer.ValuePacker{ValueType: reflect.TypeOf("")}
v, err := p.Pack(field.Arguments.MustGet("name").Value(r.Vars))
if err != nil {
r.AddError(errors.Errorf("%s", err))
return nil
}
t, ok := r.Schema.Types[v.String()]
if !ok {
return nil
}
flattenedSels = append(flattenedSels, &SchemaField{
Field: resolvable.MetaFieldType,
Alias: field.Alias.Name,
Sels: applySelectionSet(r, resolvable.MetaType, field.Selections),
Async: true,
FixedResult: reflect.ValueOf(introspection.WrapType(t)),
})
default:
fe := e.Fields[field.Name.Name]
var args map[string]interface{}
var packedArgs reflect.Value
if fe.ArgsPacker != nil {
args = make(map[string]interface{})
for _, arg := range field.Arguments {
args[arg.Name.Name] = arg.Value.Value(r.Vars)
}
var err error
packedArgs, err = fe.ArgsPacker.Pack(args)
if err != nil {
r.AddError(errors.Errorf("%s", err))
return
}
}
fieldSels := applyField(r, fe.ValueExec, field.Selections)
flattenedSels = append(flattenedSels, &SchemaField{
Field: *fe,
Alias: field.Alias.Name,
Args: args,
PackedArgs: packedArgs,
Sels: fieldSels,
Async: fe.HasContext || fe.ArgsPacker != nil || fe.HasError || HasAsyncSel(fieldSels),
})
}
case *query.InlineFragment:
frag := sel
if skipByDirective(r, frag.Directives) {
continue
}
flattenedSels = append(flattenedSels, applyFragment(r, e, &frag.Fragment)...)
case *query.FragmentSpread:
spread := sel
if skipByDirective(r, spread.Directives) {
continue
}
flattenedSels = append(flattenedSels, applyFragment(r, e, &r.Doc.Fragments.Get(spread.Name.Name).Fragment)...)
default:
panic("invalid type")
}
}
return
}
func applyFragment(r *Request, e *resolvable.Object, frag *query.Fragment) []Selection {
if frag.On.Name != "" && frag.On.Name != e.Name {
a, ok := e.TypeAssertions[frag.On.Name]
if !ok {
panic(fmt.Errorf("%q does not implement %q", frag.On, e.Name)) // TODO proper error handling
}
return []Selection{&TypeAssertion{
TypeAssertion: *a,
Sels: applySelectionSet(r, a.TypeExec.(*resolvable.Object), frag.Selections),
}}
}
return applySelectionSet(r, e, frag.Selections)
}
func applyField(r *Request, e resolvable.Resolvable, sels []query.Selection) []Selection {
switch e := e.(type) {
case *resolvable.Object:
return applySelectionSet(r, e, sels)
case *resolvable.List:
return applyField(r, e.Elem, sels)
case *resolvable.Scalar:
return nil
default:
panic("unreachable")
}
}
func skipByDirective(r *Request, directives common.DirectiveList) bool {
if d := directives.Get("skip"); d != nil {
p := packer.ValuePacker{ValueType: reflect.TypeOf(false)}
v, err := p.Pack(d.Args.MustGet("if").Value(r.Vars))
if err != nil {
r.AddError(errors.Errorf("%s", err))
}
if err == nil && v.Bool() {
return true
}
}
if d := directives.Get("include"); d != nil {
p := packer.ValuePacker{ValueType: reflect.TypeOf(false)}
v, err := p.Pack(d.Args.MustGet("if").Value(r.Vars))
if err != nil {
r.AddError(errors.Errorf("%s", err))
}
if err == nil && !v.Bool() {
return true
}
}
return false
}
func HasAsyncSel(sels []Selection) bool {
for _, sel := range sels {
switch sel := sel.(type) {
case *SchemaField:
if sel.Async {
return true
}
case *TypeAssertion:
if HasAsyncSel(sel.Sels) {
return true
}
case *TypenameField:
// sync
default:
panic("unreachable")
}
}
return false
}

View File

@ -0,0 +1,240 @@
package query
import (
"fmt"
"strings"
"text/scanner"
"github.com/neelance/graphql-go/errors"
"github.com/neelance/graphql-go/internal/common"
)
type Document struct {
Operations OperationList
Fragments FragmentList
}
type OperationList []*Operation
func (l OperationList) Get(name string) *Operation {
for _, f := range l {
if f.Name.Name == name {
return f
}
}
return nil
}
type FragmentList []*FragmentDecl
func (l FragmentList) Get(name string) *FragmentDecl {
for _, f := range l {
if f.Name.Name == name {
return f
}
}
return nil
}
type Operation struct {
Type OperationType
Name common.Ident
Vars common.InputValueList
Selections []Selection
Directives common.DirectiveList
Loc errors.Location
}
type OperationType string
const (
Query OperationType = "QUERY"
Mutation = "MUTATION"
Subscription = "SUBSCRIPTION"
)
type Fragment struct {
On common.TypeName
Selections []Selection
}
type FragmentDecl struct {
Fragment
Name common.Ident
Directives common.DirectiveList
Loc errors.Location
}
type Selection interface {
isSelection()
}
type Field struct {
Alias common.Ident
Name common.Ident
Arguments common.ArgumentList
Directives common.DirectiveList
Selections []Selection
SelectionSetLoc errors.Location
}
type InlineFragment struct {
Fragment
Directives common.DirectiveList
Loc errors.Location
}
type FragmentSpread struct {
Name common.Ident
Directives common.DirectiveList
Loc errors.Location
}
func (Field) isSelection() {}
func (InlineFragment) isSelection() {}
func (FragmentSpread) isSelection() {}
func Parse(queryString string) (*Document, *errors.QueryError) {
sc := &scanner.Scanner{
Mode: scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings,
}
sc.Init(strings.NewReader(queryString))
l := common.New(sc)
var doc *Document
err := l.CatchSyntaxError(func() {
doc = parseDocument(l)
})
if err != nil {
return nil, err
}
return doc, nil
}
func parseDocument(l *common.Lexer) *Document {
d := &Document{}
for l.Peek() != scanner.EOF {
if l.Peek() == '{' {
op := &Operation{Type: Query, Loc: l.Location()}
op.Selections = parseSelectionSet(l)
d.Operations = append(d.Operations, op)
continue
}
loc := l.Location()
switch x := l.ConsumeIdent(); x {
case "query":
op := parseOperation(l, Query)
op.Loc = loc
d.Operations = append(d.Operations, op)
case "mutation":
d.Operations = append(d.Operations, parseOperation(l, Mutation))
case "subscription":
d.Operations = append(d.Operations, parseOperation(l, Subscription))
case "fragment":
frag := parseFragment(l)
frag.Loc = loc
d.Fragments = append(d.Fragments, frag)
default:
l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "fragment"`, x))
}
}
return d
}
func parseOperation(l *common.Lexer, opType OperationType) *Operation {
op := &Operation{Type: opType}
op.Name.Loc = l.Location()
if l.Peek() == scanner.Ident {
op.Name = l.ConsumeIdentWithLoc()
}
op.Directives = common.ParseDirectives(l)
if l.Peek() == '(' {
l.ConsumeToken('(')
for l.Peek() != ')' {
loc := l.Location()
l.ConsumeToken('$')
iv := common.ParseInputValue(l)
iv.Loc = loc
op.Vars = append(op.Vars, iv)
}
l.ConsumeToken(')')
}
op.Selections = parseSelectionSet(l)
return op
}
func parseFragment(l *common.Lexer) *FragmentDecl {
f := &FragmentDecl{}
f.Name = l.ConsumeIdentWithLoc()
l.ConsumeKeyword("on")
f.On = common.TypeName{Ident: l.ConsumeIdentWithLoc()}
f.Directives = common.ParseDirectives(l)
f.Selections = parseSelectionSet(l)
return f
}
func parseSelectionSet(l *common.Lexer) []Selection {
var sels []Selection
l.ConsumeToken('{')
for l.Peek() != '}' {
sels = append(sels, parseSelection(l))
}
l.ConsumeToken('}')
return sels
}
func parseSelection(l *common.Lexer) Selection {
if l.Peek() == '.' {
return parseSpread(l)
}
return parseField(l)
}
func parseField(l *common.Lexer) *Field {
f := &Field{}
f.Alias = l.ConsumeIdentWithLoc()
f.Name = f.Alias
if l.Peek() == ':' {
l.ConsumeToken(':')
f.Name = l.ConsumeIdentWithLoc()
}
if l.Peek() == '(' {
f.Arguments = common.ParseArguments(l)
}
f.Directives = common.ParseDirectives(l)
if l.Peek() == '{' {
f.SelectionSetLoc = l.Location()
f.Selections = parseSelectionSet(l)
}
return f
}
func parseSpread(l *common.Lexer) Selection {
loc := l.Location()
l.ConsumeToken('.')
l.ConsumeToken('.')
l.ConsumeToken('.')
f := &InlineFragment{Loc: loc}
if l.Peek() == scanner.Ident {
ident := l.ConsumeIdentWithLoc()
if ident.Name != "on" {
fs := &FragmentSpread{
Name: ident,
Loc: loc,
}
fs.Directives = common.ParseDirectives(l)
return fs
}
f.On = common.TypeName{Ident: l.ConsumeIdentWithLoc()}
}
f.Directives = common.ParseDirectives(l)
f.Selections = parseSelectionSet(l)
return f
}

View File

@ -0,0 +1,190 @@
package schema
var Meta *Schema
func init() {
Meta = &Schema{} // bootstrap
Meta = New()
if err := Meta.Parse(metaSrc); err != nil {
panic(err)
}
}
var metaSrc = `
# The ` + "`" + `Int` + "`" + ` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
scalar Int
# The ` + "`" + `Float` + "`" + ` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).
scalar Float
# 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.
scalar String
# The ` + "`" + `Boolean` + "`" + ` scalar type represents ` + "`" + `true` + "`" + ` or ` + "`" + `false` + "`" + `.
scalar Boolean
# The ` + "`" + `ID` + "`" + ` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as ` + "`" + `"4"` + "`" + `) or integer (such as ` + "`" + `4` + "`" + `) input value will be accepted as an ID.
scalar ID
# Directs the executor to include this field or fragment only when the ` + "`" + `if` + "`" + ` argument is true.
directive @include(
# Included when true.
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
# Directs the executor to skip this field or fragment when the ` + "`" + `if` + "`" + ` argument is true.
directive @skip(
# Skipped when true.
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
# Marks an element of a GraphQL schema as no longer supported.
directive @deprecated(
# Explains why this element was deprecated, usually also including a suggestion
# for how to access supported similar data. Formatted in
# [Markdown](https://daringfireball.net/projects/markdown/).
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
# A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
#
# In some cases, you need to provide options to alter GraphQL's execution behavior
# in ways field arguments will not suffice, such as conditionally including or
# skipping a field. Directives provide this by describing additional information
# to the executor.
type __Directive {
name: String!
description: String
locations: [__DirectiveLocation!]!
args: [__InputValue!]!
}
# A Directive can be adjacent to many parts of the GraphQL language, a
# __DirectiveLocation describes one such possible adjacencies.
enum __DirectiveLocation {
# Location adjacent to a query operation.
QUERY
# Location adjacent to a mutation operation.
MUTATION
# Location adjacent to a subscription operation.
SUBSCRIPTION
# Location adjacent to a field.
FIELD
# Location adjacent to a fragment definition.
FRAGMENT_DEFINITION
# Location adjacent to a fragment spread.
FRAGMENT_SPREAD
# Location adjacent to an inline fragment.
INLINE_FRAGMENT
# Location adjacent to a schema definition.
SCHEMA
# Location adjacent to a scalar definition.
SCALAR
# Location adjacent to an object type definition.
OBJECT
# Location adjacent to a field definition.
FIELD_DEFINITION
# Location adjacent to an argument definition.
ARGUMENT_DEFINITION
# Location adjacent to an interface definition.
INTERFACE
# Location adjacent to a union definition.
UNION
# Location adjacent to an enum definition.
ENUM
# Location adjacent to an enum value definition.
ENUM_VALUE
# Location adjacent to an input object type definition.
INPUT_OBJECT
# Location adjacent to an input object field definition.
INPUT_FIELD_DEFINITION
}
# One possible value for a given Enum. Enum values are unique values, not a
# placeholder for a string or numeric value. However an Enum value is returned in
# a JSON response as a string.
type __EnumValue {
name: String!
description: String
isDeprecated: Boolean!
deprecationReason: String
}
# Object and Interface types are described by a list of Fields, each of which has
# a name, potentially a list of arguments, and a return type.
type __Field {
name: String!
description: String
args: [__InputValue!]!
type: __Type!
isDeprecated: Boolean!
deprecationReason: String
}
# Arguments provided to Fields or Directives and the input fields of an
# InputObject are represented as Input Values which describe their type and
# optionally a default value.
type __InputValue {
name: String!
description: String
type: __Type!
# A GraphQL-formatted string representing the default value for this input value.
defaultValue: String
}
# A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all
# available types and directives on the server, as well as the entry points for
# query, mutation, and subscription operations.
type __Schema {
# A list of all types supported by this server.
types: [__Type!]!
# The type that query operations will be rooted at.
queryType: __Type!
# If this server supports mutation, the type that mutation operations will be rooted at.
mutationType: __Type
# If this server support subscription, the type that subscription operations will be rooted at.
subscriptionType: __Type
# A list of all directives supported by this server.
directives: [__Directive!]!
}
# The fundamental unit of any GraphQL Schema is the type. There are many kinds of
# types in GraphQL as represented by the ` + "`" + `__TypeKind` + "`" + ` enum.
#
# Depending on the kind of a type, certain fields describe information about that
# type. Scalar types provide no information beyond a name and description, while
# Enum types provide their values. Object and Interface types provide the fields
# they describe. Abstract types, Union and Interface, provide the Object types
# possible at runtime. List and NonNull types compose other types.
type __Type {
kind: __TypeKind!
name: String
description: String
fields(includeDeprecated: Boolean = false): [__Field!]
interfaces: [__Type!]
possibleTypes: [__Type!]
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
inputFields: [__InputValue!]
ofType: __Type
}
# An enum describing what kind of type a given ` + "`" + `__Type` + "`" + ` is.
enum __TypeKind {
# Indicates this type is a scalar.
SCALAR
# Indicates this type is an object. ` + "`" + `fields` + "`" + ` and ` + "`" + `interfaces` + "`" + ` are valid fields.
OBJECT
# Indicates this type is an interface. ` + "`" + `fields` + "`" + ` and ` + "`" + `possibleTypes` + "`" + ` are valid fields.
INTERFACE
# Indicates this type is a union. ` + "`" + `possibleTypes` + "`" + ` is a valid field.
UNION
# Indicates this type is an enum. ` + "`" + `enumValues` + "`" + ` is a valid field.
ENUM
# Indicates this type is an input object. ` + "`" + `inputFields` + "`" + ` is a valid field.
INPUT_OBJECT
# Indicates this type is a list. ` + "`" + `ofType` + "`" + ` is a valid field.
LIST
# Indicates this type is a non-null. ` + "`" + `ofType` + "`" + ` is a valid field.
NON_NULL
}
`

View File

@ -0,0 +1,462 @@
package schema
import (
"fmt"
"strings"
"text/scanner"
"github.com/neelance/graphql-go/errors"
"github.com/neelance/graphql-go/internal/common"
)
type Schema struct {
EntryPoints map[string]NamedType
Types map[string]NamedType
Directives map[string]*DirectiveDecl
entryPointNames map[string]string
objects []*Object
unions []*Union
enums []*Enum
}
func (s *Schema) Resolve(name string) common.Type {
return s.Types[name]
}
type NamedType interface {
common.Type
TypeName() string
Description() string
}
type Scalar struct {
Name string
Desc string
}
type Object struct {
Name string
Interfaces []*Interface
Fields FieldList
Desc string
interfaceNames []string
}
type Interface struct {
Name string
PossibleTypes []*Object
Fields FieldList
Desc string
}
type Union struct {
Name string
PossibleTypes []*Object
Desc string
typeNames []string
}
type Enum struct {
Name string
Values []*EnumValue
Desc string
}
type EnumValue struct {
Name string
Directives common.DirectiveList
Desc string
}
type InputObject struct {
Name string
Desc string
Values common.InputValueList
}
type FieldList []*Field
func (l FieldList) Get(name string) *Field {
for _, f := range l {
if f.Name == name {
return f
}
}
return nil
}
func (l FieldList) Names() []string {
names := make([]string, len(l))
for i, f := range l {
names[i] = f.Name
}
return names
}
type DirectiveDecl struct {
Name string
Desc string
Locs []string
Args common.InputValueList
}
func (*Scalar) Kind() string { return "SCALAR" }
func (*Object) Kind() string { return "OBJECT" }
func (*Interface) Kind() string { return "INTERFACE" }
func (*Union) Kind() string { return "UNION" }
func (*Enum) Kind() string { return "ENUM" }
func (*InputObject) Kind() string { return "INPUT_OBJECT" }
func (t *Scalar) String() string { return t.Name }
func (t *Object) String() string { return t.Name }
func (t *Interface) String() string { return t.Name }
func (t *Union) String() string { return t.Name }
func (t *Enum) String() string { return t.Name }
func (t *InputObject) String() string { return t.Name }
func (t *Scalar) TypeName() string { return t.Name }
func (t *Object) TypeName() string { return t.Name }
func (t *Interface) TypeName() string { return t.Name }
func (t *Union) TypeName() string { return t.Name }
func (t *Enum) TypeName() string { return t.Name }
func (t *InputObject) TypeName() string { return t.Name }
func (t *Scalar) Description() string { return t.Desc }
func (t *Object) Description() string { return t.Desc }
func (t *Interface) Description() string { return t.Desc }
func (t *Union) Description() string { return t.Desc }
func (t *Enum) Description() string { return t.Desc }
func (t *InputObject) Description() string { return t.Desc }
type Field struct {
Name string
Args common.InputValueList
Type common.Type
Directives common.DirectiveList
Desc string
}
func New() *Schema {
s := &Schema{
entryPointNames: make(map[string]string),
Types: make(map[string]NamedType),
Directives: make(map[string]*DirectiveDecl),
}
for n, t := range Meta.Types {
s.Types[n] = t
}
for n, d := range Meta.Directives {
s.Directives[n] = d
}
return s
}
func (s *Schema) Parse(schemaString string) error {
sc := &scanner.Scanner{
Mode: scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings,
}
sc.Init(strings.NewReader(schemaString))
l := common.New(sc)
err := l.CatchSyntaxError(func() {
parseSchema(s, l)
})
if err != nil {
return err
}
for _, t := range s.Types {
if err := resolveNamedType(s, t); err != nil {
return err
}
}
for _, d := range s.Directives {
for _, arg := range d.Args {
t, err := common.ResolveType(arg.Type, s.Resolve)
if err != nil {
return err
}
arg.Type = t
}
}
s.EntryPoints = make(map[string]NamedType)
for key, name := range s.entryPointNames {
t, ok := s.Types[name]
if !ok {
if !ok {
return errors.Errorf("type %q not found", name)
}
}
s.EntryPoints[key] = t
}
for _, obj := range s.objects {
obj.Interfaces = make([]*Interface, len(obj.interfaceNames))
for i, intfName := range obj.interfaceNames {
t, ok := s.Types[intfName]
if !ok {
return errors.Errorf("interface %q not found", intfName)
}
intf, ok := t.(*Interface)
if !ok {
return errors.Errorf("type %q is not an interface", intfName)
}
obj.Interfaces[i] = intf
intf.PossibleTypes = append(intf.PossibleTypes, obj)
}
}
for _, union := range s.unions {
union.PossibleTypes = make([]*Object, len(union.typeNames))
for i, name := range union.typeNames {
t, ok := s.Types[name]
if !ok {
return errors.Errorf("object type %q not found", name)
}
obj, ok := t.(*Object)
if !ok {
return errors.Errorf("type %q is not an object", name)
}
union.PossibleTypes[i] = obj
}
}
for _, enum := range s.enums {
for _, value := range enum.Values {
if err := resolveDirectives(s, value.Directives); err != nil {
return err
}
}
}
return nil
}
func resolveNamedType(s *Schema, t NamedType) error {
switch t := t.(type) {
case *Object:
for _, f := range t.Fields {
if err := resolveField(s, f); err != nil {
return err
}
}
case *Interface:
for _, f := range t.Fields {
if err := resolveField(s, f); err != nil {
return err
}
}
case *InputObject:
if err := resolveInputObject(s, t.Values); err != nil {
return err
}
}
return nil
}
func resolveField(s *Schema, f *Field) error {
t, err := common.ResolveType(f.Type, s.Resolve)
if err != nil {
return err
}
f.Type = t
if err := resolveDirectives(s, f.Directives); err != nil {
return err
}
return resolveInputObject(s, f.Args)
}
func resolveDirectives(s *Schema, directives common.DirectiveList) error {
for _, d := range directives {
dirName := d.Name.Name
dd, ok := s.Directives[dirName]
if !ok {
return errors.Errorf("directive %q not found", dirName)
}
for _, arg := range d.Args {
if dd.Args.Get(arg.Name.Name) == nil {
return errors.Errorf("invalid argument %q for directive %q", arg.Name.Name, dirName)
}
}
for _, arg := range dd.Args {
if _, ok := d.Args.Get(arg.Name.Name); !ok {
d.Args = append(d.Args, common.Argument{Name: arg.Name, Value: arg.Default})
}
}
}
return nil
}
func resolveInputObject(s *Schema, values common.InputValueList) error {
for _, v := range values {
t, err := common.ResolveType(v.Type, s.Resolve)
if err != nil {
return err
}
v.Type = t
}
return nil
}
func parseSchema(s *Schema, l *common.Lexer) {
for l.Peek() != scanner.EOF {
desc := l.DescComment()
switch x := l.ConsumeIdent(); x {
case "schema":
l.ConsumeToken('{')
for l.Peek() != '}' {
name := l.ConsumeIdent()
l.ConsumeToken(':')
typ := l.ConsumeIdent()
s.entryPointNames[name] = typ
}
l.ConsumeToken('}')
case "type":
obj := parseObjectDecl(l)
obj.Desc = desc
s.Types[obj.Name] = obj
s.objects = append(s.objects, obj)
case "interface":
intf := parseInterfaceDecl(l)
intf.Desc = desc
s.Types[intf.Name] = intf
case "union":
union := parseUnionDecl(l)
union.Desc = desc
s.Types[union.Name] = union
s.unions = append(s.unions, union)
case "enum":
enum := parseEnumDecl(l)
enum.Desc = desc
s.Types[enum.Name] = enum
s.enums = append(s.enums, enum)
case "input":
input := parseInputDecl(l)
input.Desc = desc
s.Types[input.Name] = input
case "scalar":
name := l.ConsumeIdent()
s.Types[name] = &Scalar{Name: name, Desc: desc}
case "directive":
directive := parseDirectiveDecl(l)
directive.Desc = desc
s.Directives[directive.Name] = directive
default:
l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "schema", "type", "enum", "interface", "union", "input", "scalar" or "directive"`, x))
}
}
}
func parseObjectDecl(l *common.Lexer) *Object {
o := &Object{}
o.Name = l.ConsumeIdent()
if l.Peek() == scanner.Ident {
l.ConsumeKeyword("implements")
for {
o.interfaceNames = append(o.interfaceNames, l.ConsumeIdent())
if l.Peek() == '{' {
break
}
}
}
l.ConsumeToken('{')
o.Fields = parseFields(l)
l.ConsumeToken('}')
return o
}
func parseInterfaceDecl(l *common.Lexer) *Interface {
i := &Interface{}
i.Name = l.ConsumeIdent()
l.ConsumeToken('{')
i.Fields = parseFields(l)
l.ConsumeToken('}')
return i
}
func parseUnionDecl(l *common.Lexer) *Union {
union := &Union{}
union.Name = l.ConsumeIdent()
l.ConsumeToken('=')
union.typeNames = []string{l.ConsumeIdent()}
for l.Peek() == '|' {
l.ConsumeToken('|')
union.typeNames = append(union.typeNames, l.ConsumeIdent())
}
return union
}
func parseInputDecl(l *common.Lexer) *InputObject {
i := &InputObject{}
i.Name = l.ConsumeIdent()
l.ConsumeToken('{')
for l.Peek() != '}' {
i.Values = append(i.Values, common.ParseInputValue(l))
}
l.ConsumeToken('}')
return i
}
func parseEnumDecl(l *common.Lexer) *Enum {
enum := &Enum{}
enum.Name = l.ConsumeIdent()
l.ConsumeToken('{')
for l.Peek() != '}' {
v := &EnumValue{}
v.Desc = l.DescComment()
v.Name = l.ConsumeIdent()
v.Directives = common.ParseDirectives(l)
enum.Values = append(enum.Values, v)
}
l.ConsumeToken('}')
return enum
}
func parseDirectiveDecl(l *common.Lexer) *DirectiveDecl {
d := &DirectiveDecl{}
l.ConsumeToken('@')
d.Name = l.ConsumeIdent()
if l.Peek() == '(' {
l.ConsumeToken('(')
for l.Peek() != ')' {
v := common.ParseInputValue(l)
d.Args = append(d.Args, v)
}
l.ConsumeToken(')')
}
l.ConsumeKeyword("on")
for {
loc := l.ConsumeIdent()
d.Locs = append(d.Locs, loc)
if l.Peek() != '|' {
break
}
l.ConsumeToken('|')
}
return d
}
func parseFields(l *common.Lexer) FieldList {
var fields FieldList
for l.Peek() != '}' {
f := &Field{}
f.Desc = l.DescComment()
f.Name = l.ConsumeIdent()
if l.Peek() == '(' {
l.ConsumeToken('(')
for l.Peek() != ')' {
f.Args = append(f.Args, common.ParseInputValue(l))
}
l.ConsumeToken(')')
}
l.ConsumeToken(':')
f.Type = common.ParseType(l)
f.Directives = common.ParseDirectives(l)
fields = append(fields, f)
}
return fields
}

View File

@ -0,0 +1,75 @@
package tests
import (
"os"
"reflect"
"sort"
"testing"
"encoding/json"
"github.com/neelance/graphql-go/errors"
"github.com/neelance/graphql-go/internal/query"
"github.com/neelance/graphql-go/internal/schema"
"github.com/neelance/graphql-go/internal/validation"
)
type Test struct {
Name string
Rule string
Schema int
Query string
Errors []*errors.QueryError
}
func TestAll(t *testing.T) {
f, err := os.Open("testdata/tests.json")
if err != nil {
t.Fatal(err)
}
var testData struct {
Schemas []string
Tests []*Test
}
if err := json.NewDecoder(f).Decode(&testData); err != nil {
t.Fatal(err)
}
schemas := make([]*schema.Schema, len(testData.Schemas))
for i, schemaStr := range testData.Schemas {
schemas[i] = schema.New()
if err := schemas[i].Parse(schemaStr); err != nil {
t.Fatal(err)
}
}
for _, test := range testData.Tests {
t.Run(test.Name, func(t *testing.T) {
d, err := query.Parse(test.Query)
if err != nil {
t.Fatal(err)
}
errs := validation.Validate(schemas[test.Schema], d)
got := []*errors.QueryError{}
for _, err := range errs {
if err.Rule == test.Rule {
err.Rule = ""
got = append(got, err)
}
}
sortLocations(test.Errors)
sortLocations(got)
if !reflect.DeepEqual(test.Errors, got) {
t.Errorf("wrong errors\nexpected: %v\ngot: %v", test.Errors, got)
}
})
}
}
func sortLocations(errs []*errors.QueryError) {
for _, err := range errs {
locs := err.Locations
sort.Slice(locs, func(i, j int) bool { return locs[i].Before(locs[j]) })
}
}

View File

@ -0,0 +1 @@
package tests

View File

@ -0,0 +1,33 @@
The files in this testdata directory are derived from the graphql-js project:
https://github.com/graphql/graphql-js
BSD License
For GraphQL software
Copyright (c) 2015, Facebook, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Facebook nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,110 @@
import fs from 'fs';
import Module from 'module';
import { testSchema } from './src/validation/__tests__/harness';
import { printSchema } from './src/utilities';
let schemas = [];
function registerSchema(schema) {
for (let i = 0; i < schemas.length; i++) {
if (schemas[i] == schema) {
return i;
}
}
schemas.push(schema);
return schemas.length - 1;
}
const harness = {
expectPassesRule(rule, queryString) {
harness.expectPassesRuleWithSchema(testSchema, rule, queryString);
},
expectPassesRuleWithSchema(schema, rule, queryString, errors) {
tests.push({
name: names.join('/'),
rule: rule.name,
schema: registerSchema(schema),
query: queryString,
errors: [],
});
},
expectFailsRule(rule, queryString, errors) {
harness.expectFailsRuleWithSchema(testSchema, rule, queryString, errors);
},
expectFailsRuleWithSchema(schema, rule, queryString, errors) {
tests.push({
name: names.join('/'),
rule: rule.name,
schema: registerSchema(schema),
query: queryString,
errors: errors,
});
}
};
let tests = [];
let names = []
const fakeModules = {
'mocha': {
describe(name, f) {
switch (name) {
case 'within schema language':
return;
}
names.push(name);
f();
names.pop();
},
it(name, f) {
switch (name) {
case 'ignores type definitions':
case 'reports correctly when a non-exclusive follows an exclusive':
case 'disallows differing subfields':
return;
}
names.push(name);
f();
names.pop();
},
},
'./harness': harness,
};
const originalLoader = Module._load;
Module._load = function(request, parent, isMain) {
return fakeModules[request] || originalLoader(request, parent, isMain);
};
require('./src/validation/__tests__/ArgumentsOfCorrectType-test');
require('./src/validation/__tests__/DefaultValuesOfCorrectType-test');
require('./src/validation/__tests__/FieldsOnCorrectType-test');
require('./src/validation/__tests__/FragmentsOnCompositeTypes-test');
require('./src/validation/__tests__/KnownArgumentNames-test');
require('./src/validation/__tests__/KnownDirectives-test');
require('./src/validation/__tests__/KnownFragmentNames-test');
require('./src/validation/__tests__/KnownTypeNames-test');
require('./src/validation/__tests__/LoneAnonymousOperation-test');
require('./src/validation/__tests__/NoFragmentCycles-test');
require('./src/validation/__tests__/NoUndefinedVariables-test');
require('./src/validation/__tests__/NoUnusedFragments-test');
require('./src/validation/__tests__/NoUnusedVariables-test');
require('./src/validation/__tests__/OverlappingFieldsCanBeMerged-test');
require('./src/validation/__tests__/PossibleFragmentSpreads-test');
require('./src/validation/__tests__/ProvidedNonNullArguments-test');
require('./src/validation/__tests__/ScalarLeafs-test');
require('./src/validation/__tests__/UniqueArgumentNames-test');
require('./src/validation/__tests__/UniqueDirectivesPerLocation-test');
require('./src/validation/__tests__/UniqueFragmentNames-test');
require('./src/validation/__tests__/UniqueInputFieldNames-test');
require('./src/validation/__tests__/UniqueOperationNames-test');
require('./src/validation/__tests__/UniqueVariableNames-test');
require('./src/validation/__tests__/VariablesAreInputTypes-test');
require('./src/validation/__tests__/VariablesInAllowedPosition-test');
let output = JSON.stringify({
schemas: schemas.map(s => printSchema(s)),
tests: tests,
}, null, 2)
output = output.replace(' Did you mean to use an inline fragment on \\"Dog\\" or \\"Cat\\"?', '');
output = output.replace(' Did you mean to use an inline fragment on \\"Being\\", \\"Pet\\", \\"Canine\\", \\"Dog\\", or \\"Cat\\"?', '');
output = output.replace(' Did you mean \\"Pet\\"?', '');
fs.writeFileSync("tests.json", output);

View File

@ -0,0 +1,4 @@
package testdata
//go:generate cp export.js graphql-js/export.js
//go:generate babel-node graphql-js/export.js

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
package validation
import (
"fmt"
"sort"
"strconv"
"strings"
)
func makeSuggestion(prefix string, options []string, input string) string {
var selected []string
distances := make(map[string]int)
for _, opt := range options {
distance := levenshteinDistance(input, opt)
threshold := max(len(input)/2, max(len(opt)/2, 1))
if distance < threshold {
selected = append(selected, opt)
distances[opt] = distance
}
}
if len(selected) == 0 {
return ""
}
sort.Slice(selected, func(i, j int) bool {
return distances[selected[i]] < distances[selected[j]]
})
parts := make([]string, len(selected))
for i, opt := range selected {
parts[i] = strconv.Quote(opt)
}
if len(parts) > 1 {
parts[len(parts)-1] = "or " + parts[len(parts)-1]
}
return fmt.Sprintf(" %s %s?", prefix, strings.Join(parts, ", "))
}
func levenshteinDistance(s1, s2 string) int {
column := make([]int, len(s1)+1)
for y := range s1 {
column[y+1] = y + 1
}
for x, rx := range s2 {
column[0] = x + 1
lastdiag := x
for y, ry := range s1 {
olddiag := column[y+1]
if rx != ry {
lastdiag++
}
column[y+1] = min(column[y+1]+1, min(column[y]+1, lastdiag))
lastdiag = olddiag
}
}
return column[len(s1)]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}

View File

@ -0,0 +1,860 @@
package validation
import (
"fmt"
"math"
"reflect"
"strconv"
"strings"
"text/scanner"
"github.com/neelance/graphql-go/errors"
"github.com/neelance/graphql-go/internal/common"
"github.com/neelance/graphql-go/internal/query"
"github.com/neelance/graphql-go/internal/schema"
)
type varSet map[*common.InputValue]struct{}
type selectionPair struct{ a, b query.Selection }
type fieldInfo struct {
sf *schema.Field
parent schema.NamedType
}
type context struct {
schema *schema.Schema
doc *query.Document
errs []*errors.QueryError
opErrs map[*query.Operation][]*errors.QueryError
usedVars map[*query.Operation]varSet
fieldMap map[*query.Field]fieldInfo
overlapValidated map[selectionPair]struct{}
}
func (c *context) addErr(loc errors.Location, rule string, format string, a ...interface{}) {
c.addErrMultiLoc([]errors.Location{loc}, rule, format, a...)
}
func (c *context) addErrMultiLoc(locs []errors.Location, rule string, format string, a ...interface{}) {
c.errs = append(c.errs, &errors.QueryError{
Message: fmt.Sprintf(format, a...),
Locations: locs,
Rule: rule,
})
}
type opContext struct {
*context
ops []*query.Operation
}
func Validate(s *schema.Schema, doc *query.Document) []*errors.QueryError {
c := &context{
schema: s,
doc: doc,
opErrs: make(map[*query.Operation][]*errors.QueryError),
usedVars: make(map[*query.Operation]varSet),
fieldMap: make(map[*query.Field]fieldInfo),
overlapValidated: make(map[selectionPair]struct{}),
}
opNames := make(nameSet)
fragUsedBy := make(map[*query.FragmentDecl][]*query.Operation)
for _, op := range doc.Operations {
c.usedVars[op] = make(varSet)
opc := &opContext{c, []*query.Operation{op}}
if op.Name.Name == "" && len(doc.Operations) != 1 {
c.addErr(op.Loc, "LoneAnonymousOperation", "This anonymous operation must be the only defined operation.")
}
if op.Name.Name != "" {
validateName(c, opNames, op.Name, "UniqueOperationNames", "operation")
}
validateDirectives(opc, string(op.Type), op.Directives)
varNames := make(nameSet)
for _, v := range op.Vars {
validateName(c, varNames, v.Name, "UniqueVariableNames", "variable")
t := resolveType(c, v.Type)
if !canBeInput(t) {
c.addErr(v.TypeLoc, "VariablesAreInputTypes", "Variable %q cannot be non-input type %q.", "$"+v.Name.Name, t)
}
if v.Default != nil {
validateLiteral(opc, v.Default)
if t != nil {
if nn, ok := t.(*common.NonNull); ok {
c.addErr(v.Default.Location(), "DefaultValuesOfCorrectType", "Variable %q of type %q is required and will not use the default value. Perhaps you meant to use type %q.", "$"+v.Name.Name, t, nn.OfType)
}
if ok, reason := validateValueType(opc, v.Default, t); !ok {
c.addErr(v.Default.Location(), "DefaultValuesOfCorrectType", "Variable %q of type %q has invalid default value %s.\n%s", "$"+v.Name.Name, t, v.Default, reason)
}
}
}
}
var entryPoint schema.NamedType
switch op.Type {
case query.Query:
entryPoint = s.EntryPoints["query"]
case query.Mutation:
entryPoint = s.EntryPoints["mutation"]
case query.Subscription:
entryPoint = s.EntryPoints["subscription"]
default:
panic("unreachable")
}
validateSelectionSet(opc, op.Selections, entryPoint)
fragUsed := make(map[*query.FragmentDecl]struct{})
markUsedFragments(c, op.Selections, fragUsed)
for frag := range fragUsed {
fragUsedBy[frag] = append(fragUsedBy[frag], op)
}
}
fragNames := make(nameSet)
fragVisited := make(map[*query.FragmentDecl]struct{})
for _, frag := range doc.Fragments {
opc := &opContext{c, fragUsedBy[frag]}
validateName(c, fragNames, frag.Name, "UniqueFragmentNames", "fragment")
validateDirectives(opc, "FRAGMENT_DEFINITION", frag.Directives)
t := unwrapType(resolveType(c, &frag.On))
// continue even if t is nil
if t != nil && !canBeFragment(t) {
c.addErr(frag.On.Loc, "FragmentsOnCompositeTypes", "Fragment %q cannot condition on non composite type %q.", frag.Name.Name, t)
continue
}
validateSelectionSet(opc, frag.Selections, t)
if _, ok := fragVisited[frag]; !ok {
detectFragmentCycle(c, frag.Selections, fragVisited, nil, map[string]int{frag.Name.Name: 0})
}
}
for _, frag := range doc.Fragments {
if len(fragUsedBy[frag]) == 0 {
c.addErr(frag.Loc, "NoUnusedFragments", "Fragment %q is never used.", frag.Name.Name)
}
}
for _, op := range doc.Operations {
c.errs = append(c.errs, c.opErrs[op]...)
opUsedVars := c.usedVars[op]
for _, v := range op.Vars {
if _, ok := opUsedVars[v]; !ok {
opSuffix := ""
if op.Name.Name != "" {
opSuffix = fmt.Sprintf(" in operation %q", op.Name.Name)
}
c.addErr(v.Loc, "NoUnusedVariables", "Variable %q is never used%s.", "$"+v.Name.Name, opSuffix)
}
}
}
return c.errs
}
func validateSelectionSet(c *opContext, sels []query.Selection, t schema.NamedType) {
for _, sel := range sels {
validateSelection(c, sel, t)
}
for i, a := range sels {
for _, b := range sels[i+1:] {
c.validateOverlap(a, b, nil, nil)
}
}
}
func validateSelection(c *opContext, sel query.Selection, t schema.NamedType) {
switch sel := sel.(type) {
case *query.Field:
validateDirectives(c, "FIELD", sel.Directives)
fieldName := sel.Name.Name
var f *schema.Field
switch fieldName {
case "__typename":
f = &schema.Field{
Name: "__typename",
Type: c.schema.Types["String"],
}
case "__schema":
f = &schema.Field{
Name: "__schema",
Type: c.schema.Types["__Schema"],
}
case "__type":
f = &schema.Field{
Name: "__type",
Args: common.InputValueList{
&common.InputValue{
Name: common.Ident{Name: "name"},
Type: &common.NonNull{OfType: c.schema.Types["String"]},
},
},
Type: c.schema.Types["__Type"],
}
default:
f = fields(t).Get(fieldName)
if f == nil && t != nil {
suggestion := makeSuggestion("Did you mean", fields(t).Names(), fieldName)
c.addErr(sel.Alias.Loc, "FieldsOnCorrectType", "Cannot query field %q on type %q.%s", fieldName, t, suggestion)
}
}
c.fieldMap[sel] = fieldInfo{sf: f, parent: t}
validateArgumentLiterals(c, sel.Arguments)
if f != nil {
validateArgumentTypes(c, sel.Arguments, f.Args, sel.Alias.Loc,
func() string { return fmt.Sprintf("field %q of type %q", fieldName, t) },
func() string { return fmt.Sprintf("Field %q", fieldName) },
)
}
var ft common.Type
if f != nil {
ft = f.Type
sf := hasSubfields(ft)
if sf && sel.Selections == nil {
c.addErr(sel.Alias.Loc, "ScalarLeafs", "Field %q of type %q must have a selection of subfields. Did you mean \"%s { ... }\"?", fieldName, ft, fieldName)
}
if !sf && sel.Selections != nil {
c.addErr(sel.SelectionSetLoc, "ScalarLeafs", "Field %q must not have a selection since type %q has no subfields.", fieldName, ft)
}
}
if sel.Selections != nil {
validateSelectionSet(c, sel.Selections, unwrapType(ft))
}
case *query.InlineFragment:
validateDirectives(c, "INLINE_FRAGMENT", sel.Directives)
if sel.On.Name != "" {
fragTyp := unwrapType(resolveType(c.context, &sel.On))
if fragTyp != nil && !compatible(t, fragTyp) {
c.addErr(sel.Loc, "PossibleFragmentSpreads", "Fragment cannot be spread here as objects of type %q can never be of type %q.", t, fragTyp)
}
t = fragTyp
// continue even if t is nil
}
if t != nil && !canBeFragment(t) {
c.addErr(sel.On.Loc, "FragmentsOnCompositeTypes", "Fragment cannot condition on non composite type %q.", t)
return
}
validateSelectionSet(c, sel.Selections, unwrapType(t))
case *query.FragmentSpread:
validateDirectives(c, "FRAGMENT_SPREAD", sel.Directives)
frag := c.doc.Fragments.Get(sel.Name.Name)
if frag == nil {
c.addErr(sel.Name.Loc, "KnownFragmentNames", "Unknown fragment %q.", sel.Name.Name)
return
}
fragTyp := c.schema.Types[frag.On.Name]
if !compatible(t, fragTyp) {
c.addErr(sel.Loc, "PossibleFragmentSpreads", "Fragment %q cannot be spread here as objects of type %q can never be of type %q.", frag.Name.Name, t, fragTyp)
}
default:
panic("unreachable")
}
}
func compatible(a, b common.Type) bool {
for _, pta := range possibleTypes(a) {
for _, ptb := range possibleTypes(b) {
if pta == ptb {
return true
}
}
}
return false
}
func possibleTypes(t common.Type) []*schema.Object {
switch t := t.(type) {
case *schema.Object:
return []*schema.Object{t}
case *schema.Interface:
return t.PossibleTypes
case *schema.Union:
return t.PossibleTypes
default:
return nil
}
}
func markUsedFragments(c *context, sels []query.Selection, fragUsed map[*query.FragmentDecl]struct{}) {
for _, sel := range sels {
switch sel := sel.(type) {
case *query.Field:
if sel.Selections != nil {
markUsedFragments(c, sel.Selections, fragUsed)
}
case *query.InlineFragment:
markUsedFragments(c, sel.Selections, fragUsed)
case *query.FragmentSpread:
frag := c.doc.Fragments.Get(sel.Name.Name)
if frag == nil {
return
}
if _, ok := fragUsed[frag]; ok {
return
}
fragUsed[frag] = struct{}{}
markUsedFragments(c, frag.Selections, fragUsed)
default:
panic("unreachable")
}
}
}
func detectFragmentCycle(c *context, sels []query.Selection, fragVisited map[*query.FragmentDecl]struct{}, spreadPath []*query.FragmentSpread, spreadPathIndex map[string]int) {
for _, sel := range sels {
detectFragmentCycleSel(c, sel, fragVisited, spreadPath, spreadPathIndex)
}
}
func detectFragmentCycleSel(c *context, sel query.Selection, fragVisited map[*query.FragmentDecl]struct{}, spreadPath []*query.FragmentSpread, spreadPathIndex map[string]int) {
switch sel := sel.(type) {
case *query.Field:
if sel.Selections != nil {
detectFragmentCycle(c, sel.Selections, fragVisited, spreadPath, spreadPathIndex)
}
case *query.InlineFragment:
detectFragmentCycle(c, sel.Selections, fragVisited, spreadPath, spreadPathIndex)
case *query.FragmentSpread:
frag := c.doc.Fragments.Get(sel.Name.Name)
if frag == nil {
return
}
spreadPath = append(spreadPath, sel)
if i, ok := spreadPathIndex[frag.Name.Name]; ok {
cyclePath := spreadPath[i:]
via := ""
if len(cyclePath) > 1 {
names := make([]string, len(cyclePath)-1)
for i, frag := range cyclePath[:len(cyclePath)-1] {
names[i] = frag.Name.Name
}
via = " via " + strings.Join(names, ", ")
}
locs := make([]errors.Location, len(cyclePath))
for i, frag := range cyclePath {
locs[i] = frag.Loc
}
c.addErrMultiLoc(locs, "NoFragmentCycles", "Cannot spread fragment %q within itself%s.", frag.Name.Name, via)
return
}
if _, ok := fragVisited[frag]; ok {
return
}
fragVisited[frag] = struct{}{}
spreadPathIndex[frag.Name.Name] = len(spreadPath)
detectFragmentCycle(c, frag.Selections, fragVisited, spreadPath, spreadPathIndex)
delete(spreadPathIndex, frag.Name.Name)
default:
panic("unreachable")
}
}
func (c *context) validateOverlap(a, b query.Selection, reasons *[]string, locs *[]errors.Location) {
if a == b {
return
}
if _, ok := c.overlapValidated[selectionPair{a, b}]; ok {
return
}
c.overlapValidated[selectionPair{a, b}] = struct{}{}
c.overlapValidated[selectionPair{b, a}] = struct{}{}
switch a := a.(type) {
case *query.Field:
switch b := b.(type) {
case *query.Field:
if b.Alias.Loc.Before(a.Alias.Loc) {
a, b = b, a
}
if reasons2, locs2 := c.validateFieldOverlap(a, b); len(reasons2) != 0 {
locs2 = append(locs2, a.Alias.Loc, b.Alias.Loc)
if reasons == nil {
c.addErrMultiLoc(locs2, "OverlappingFieldsCanBeMerged", "Fields %q conflict because %s. Use different aliases on the fields to fetch both if this was intentional.", a.Alias.Name, strings.Join(reasons2, " and "))
return
}
for _, r := range reasons2 {
*reasons = append(*reasons, fmt.Sprintf("subfields %q conflict because %s", a.Alias.Name, r))
}
*locs = append(*locs, locs2...)
}
case *query.InlineFragment:
for _, sel := range b.Selections {
c.validateOverlap(a, sel, reasons, locs)
}
case *query.FragmentSpread:
if frag := c.doc.Fragments.Get(b.Name.Name); frag != nil {
for _, sel := range frag.Selections {
c.validateOverlap(a, sel, reasons, locs)
}
}
default:
panic("unreachable")
}
case *query.InlineFragment:
for _, sel := range a.Selections {
c.validateOverlap(sel, b, reasons, locs)
}
case *query.FragmentSpread:
if frag := c.doc.Fragments.Get(a.Name.Name); frag != nil {
for _, sel := range frag.Selections {
c.validateOverlap(sel, b, reasons, locs)
}
}
default:
panic("unreachable")
}
}
func (c *context) validateFieldOverlap(a, b *query.Field) ([]string, []errors.Location) {
if a.Alias.Name != b.Alias.Name {
return nil, nil
}
if asf := c.fieldMap[a].sf; asf != nil {
if bsf := c.fieldMap[b].sf; bsf != nil {
if !typesCompatible(asf.Type, bsf.Type) {
return []string{fmt.Sprintf("they return conflicting types %s and %s", asf.Type, bsf.Type)}, nil
}
}
}
at := c.fieldMap[a].parent
bt := c.fieldMap[b].parent
if at == nil || bt == nil || at == bt {
if a.Name.Name != b.Name.Name {
return []string{fmt.Sprintf("%s and %s are different fields", a.Name.Name, b.Name.Name)}, nil
}
if argumentsConflict(a.Arguments, b.Arguments) {
return []string{"they have differing arguments"}, nil
}
}
var reasons []string
var locs []errors.Location
for _, a2 := range a.Selections {
for _, b2 := range b.Selections {
c.validateOverlap(a2, b2, &reasons, &locs)
}
}
return reasons, locs
}
func argumentsConflict(a, b common.ArgumentList) bool {
if len(a) != len(b) {
return true
}
for _, argA := range a {
valB, ok := b.Get(argA.Name.Name)
if !ok || !reflect.DeepEqual(argA.Value.Value(nil), valB.Value(nil)) {
return true
}
}
return false
}
func fields(t common.Type) schema.FieldList {
switch t := t.(type) {
case *schema.Object:
return t.Fields
case *schema.Interface:
return t.Fields
default:
return nil
}
}
func unwrapType(t common.Type) schema.NamedType {
if t == nil {
return nil
}
for {
switch t2 := t.(type) {
case schema.NamedType:
return t2
case *common.List:
t = t2.OfType
case *common.NonNull:
t = t2.OfType
default:
panic("unreachable")
}
}
}
func resolveType(c *context, t common.Type) common.Type {
t2, err := common.ResolveType(t, c.schema.Resolve)
if err != nil {
c.errs = append(c.errs, err)
}
return t2
}
func validateDirectives(c *opContext, loc string, directives common.DirectiveList) {
directiveNames := make(nameSet)
for _, d := range directives {
dirName := d.Name.Name
validateNameCustomMsg(c.context, directiveNames, d.Name, "UniqueDirectivesPerLocation", func() string {
return fmt.Sprintf("The directive %q can only be used once at this location.", dirName)
})
validateArgumentLiterals(c, d.Args)
dd, ok := c.schema.Directives[dirName]
if !ok {
c.addErr(d.Name.Loc, "KnownDirectives", "Unknown directive %q.", dirName)
continue
}
locOK := false
for _, allowedLoc := range dd.Locs {
if loc == allowedLoc {
locOK = true
break
}
}
if !locOK {
c.addErr(d.Name.Loc, "KnownDirectives", "Directive %q may not be used on %s.", dirName, loc)
}
validateArgumentTypes(c, d.Args, dd.Args, d.Name.Loc,
func() string { return fmt.Sprintf("directive %q", "@"+dirName) },
func() string { return fmt.Sprintf("Directive %q", "@"+dirName) },
)
}
return
}
type nameSet map[string]errors.Location
func validateName(c *context, set nameSet, name common.Ident, rule string, kind string) {
validateNameCustomMsg(c, set, name, rule, func() string {
return fmt.Sprintf("There can be only one %s named %q.", kind, name.Name)
})
}
func validateNameCustomMsg(c *context, set nameSet, name common.Ident, rule string, msg func() string) {
if loc, ok := set[name.Name]; ok {
c.addErrMultiLoc([]errors.Location{loc, name.Loc}, rule, msg())
return
}
set[name.Name] = name.Loc
return
}
func validateArgumentTypes(c *opContext, args common.ArgumentList, argDecls common.InputValueList, loc errors.Location, owner1, owner2 func() string) {
for _, selArg := range args {
arg := argDecls.Get(selArg.Name.Name)
if arg == nil {
c.addErr(selArg.Name.Loc, "KnownArgumentNames", "Unknown argument %q on %s.", selArg.Name.Name, owner1())
continue
}
value := selArg.Value
if ok, reason := validateValueType(c, value, arg.Type); !ok {
c.addErr(value.Location(), "ArgumentsOfCorrectType", "Argument %q has invalid value %s.\n%s", arg.Name.Name, value, reason)
}
}
for _, decl := range argDecls {
if _, ok := decl.Type.(*common.NonNull); ok {
if _, ok := args.Get(decl.Name.Name); !ok {
c.addErr(loc, "ProvidedNonNullArguments", "%s argument %q of type %q is required but not provided.", owner2(), decl.Name.Name, decl.Type)
}
}
}
}
func validateArgumentLiterals(c *opContext, args common.ArgumentList) {
argNames := make(nameSet)
for _, arg := range args {
validateName(c.context, argNames, arg.Name, "UniqueArgumentNames", "argument")
validateLiteral(c, arg.Value)
}
}
func validateLiteral(c *opContext, l common.Literal) {
switch l := l.(type) {
case *common.ObjectLit:
fieldNames := make(nameSet)
for _, f := range l.Fields {
validateName(c.context, fieldNames, f.Name, "UniqueInputFieldNames", "input field")
validateLiteral(c, f.Value)
}
case *common.ListLit:
for _, entry := range l.Entries {
validateLiteral(c, entry)
}
case *common.Variable:
for _, op := range c.ops {
v := op.Vars.Get(l.Name)
if v == nil {
byOp := ""
if op.Name.Name != "" {
byOp = fmt.Sprintf(" by operation %q", op.Name.Name)
}
c.opErrs[op] = append(c.opErrs[op], &errors.QueryError{
Message: fmt.Sprintf("Variable %q is not defined%s.", "$"+l.Name, byOp),
Locations: []errors.Location{l.Loc, op.Loc},
Rule: "NoUndefinedVariables",
})
continue
}
c.usedVars[op][v] = struct{}{}
}
}
}
func validateValueType(c *opContext, v common.Literal, t common.Type) (bool, string) {
if v, ok := v.(*common.Variable); ok {
for _, op := range c.ops {
if v2 := op.Vars.Get(v.Name); v2 != nil {
t2, err := common.ResolveType(v2.Type, c.schema.Resolve)
if _, ok := t2.(*common.NonNull); !ok && v2.Default != nil {
t2 = &common.NonNull{OfType: t2}
}
if err == nil && !typeCanBeUsedAs(t2, t) {
c.addErrMultiLoc([]errors.Location{v2.Loc, v.Loc}, "VariablesInAllowedPosition", "Variable %q of type %q used in position expecting type %q.", "$"+v.Name, t2, t)
}
}
}
return true, ""
}
if nn, ok := t.(*common.NonNull); ok {
if isNull(v) {
return false, fmt.Sprintf("Expected %q, found null.", t)
}
t = nn.OfType
}
if isNull(v) {
return true, ""
}
switch t := t.(type) {
case *schema.Scalar, *schema.Enum:
if lit, ok := v.(*common.BasicLit); ok {
if validateBasicLit(lit, t) {
return true, ""
}
}
case *common.List:
list, ok := v.(*common.ListLit)
if !ok {
return validateValueType(c, v, t.OfType) // single value instead of list
}
for i, entry := range list.Entries {
if ok, reason := validateValueType(c, entry, t.OfType); !ok {
return false, fmt.Sprintf("In element #%d: %s", i, reason)
}
}
return true, ""
case *schema.InputObject:
v, ok := v.(*common.ObjectLit)
if !ok {
return false, fmt.Sprintf("Expected %q, found not an object.", t)
}
for _, f := range v.Fields {
name := f.Name.Name
iv := t.Values.Get(name)
if iv == nil {
return false, fmt.Sprintf("In field %q: Unknown field.", name)
}
if ok, reason := validateValueType(c, f.Value, iv.Type); !ok {
return false, fmt.Sprintf("In field %q: %s", name, reason)
}
}
for _, iv := range t.Values {
found := false
for _, f := range v.Fields {
if f.Name.Name == iv.Name.Name {
found = true
break
}
}
if !found {
if _, ok := iv.Type.(*common.NonNull); ok && iv.Default == nil {
return false, fmt.Sprintf("In field %q: Expected %q, found null.", iv.Name.Name, iv.Type)
}
}
}
return true, ""
}
return false, fmt.Sprintf("Expected type %q, found %s.", t, v)
}
func validateBasicLit(v *common.BasicLit, t common.Type) bool {
switch t := t.(type) {
case *schema.Scalar:
switch t.Name {
case "Int":
if v.Type != scanner.Int {
return false
}
f, err := strconv.ParseFloat(v.Text, 64)
if err != nil {
panic(err)
}
return f >= math.MinInt32 && f <= math.MaxInt32
case "Float":
return v.Type == scanner.Int || v.Type == scanner.Float
case "String":
return v.Type == scanner.String
case "Boolean":
return v.Type == scanner.Ident && (v.Text == "true" || v.Text == "false")
case "ID":
return v.Type == scanner.Int || v.Type == scanner.String
default:
//TODO: Type-check against expected type by Unmarshalling
return true
}
case *schema.Enum:
if v.Type != scanner.Ident {
return false
}
for _, option := range t.Values {
if option.Name == v.Text {
return true
}
}
return false
}
return false
}
func canBeFragment(t common.Type) bool {
switch t.(type) {
case *schema.Object, *schema.Interface, *schema.Union:
return true
default:
return false
}
}
func canBeInput(t common.Type) bool {
switch t := t.(type) {
case *schema.InputObject, *schema.Scalar, *schema.Enum:
return true
case *common.List:
return canBeInput(t.OfType)
case *common.NonNull:
return canBeInput(t.OfType)
default:
return false
}
}
func hasSubfields(t common.Type) bool {
switch t := t.(type) {
case *schema.Object, *schema.Interface, *schema.Union:
return true
case *common.List:
return hasSubfields(t.OfType)
case *common.NonNull:
return hasSubfields(t.OfType)
default:
return false
}
}
func isLeaf(t common.Type) bool {
switch t.(type) {
case *schema.Scalar, *schema.Enum:
return true
default:
return false
}
}
func isNull(lit interface{}) bool {
_, ok := lit.(*common.NullLit)
return ok
}
func typesCompatible(a, b common.Type) bool {
al, aIsList := a.(*common.List)
bl, bIsList := b.(*common.List)
if aIsList || bIsList {
return aIsList && bIsList && typesCompatible(al.OfType, bl.OfType)
}
ann, aIsNN := a.(*common.NonNull)
bnn, bIsNN := b.(*common.NonNull)
if aIsNN || bIsNN {
return aIsNN && bIsNN && typesCompatible(ann.OfType, bnn.OfType)
}
if isLeaf(a) || isLeaf(b) {
return a == b
}
return true
}
func typeCanBeUsedAs(t, as common.Type) bool {
nnT, okT := t.(*common.NonNull)
if okT {
t = nnT.OfType
}
nnAs, okAs := as.(*common.NonNull)
if okAs {
as = nnAs.OfType
if !okT {
return false // nullable can not be used as non-null
}
}
if t == as {
return true
}
if lT, ok := t.(*common.List); ok {
if lAs, ok := as.(*common.List); ok {
return typeCanBeUsedAs(lT.OfType, lAs.OfType)
}
}
return false
}

117
vendor/github.com/neelance/graphql-go/introspection.go generated vendored Normal file
View File

@ -0,0 +1,117 @@
package graphql
import (
"context"
"encoding/json"
"github.com/neelance/graphql-go/internal/exec/resolvable"
"github.com/neelance/graphql-go/introspection"
)
// Inspect allows inspection of the given schema.
func (s *Schema) Inspect() *introspection.Schema {
return introspection.WrapSchema(s.schema)
}
// ToJSON encodes the schema in a JSON format used by tools like Relay.
func (s *Schema) ToJSON() ([]byte, error) {
result := s.exec(context.Background(), introspectionQuery, "", nil, &resolvable.Schema{
Query: &resolvable.Object{},
Schema: *s.schema,
})
if len(result.Errors) != 0 {
panic(result.Errors[0])
}
return json.MarshalIndent(result.Data, "", "\t")
}
var introspectionQuery = `
query {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
`

View File

@ -0,0 +1,313 @@
package introspection
import (
"sort"
"github.com/neelance/graphql-go/internal/common"
"github.com/neelance/graphql-go/internal/schema"
)
type Schema struct {
schema *schema.Schema
}
// WrapSchema is only used internally.
func WrapSchema(schema *schema.Schema) *Schema {
return &Schema{schema}
}
func (r *Schema) Types() []*Type {
var names []string
for name := range r.schema.Types {
names = append(names, name)
}
sort.Strings(names)
l := make([]*Type, len(names))
for i, name := range names {
l[i] = &Type{r.schema.Types[name]}
}
return l
}
func (r *Schema) Directives() []*Directive {
var names []string
for name := range r.schema.Directives {
names = append(names, name)
}
sort.Strings(names)
l := make([]*Directive, len(names))
for i, name := range names {
l[i] = &Directive{r.schema.Directives[name]}
}
return l
}
func (r *Schema) QueryType() *Type {
t, ok := r.schema.EntryPoints["query"]
if !ok {
return nil
}
return &Type{t}
}
func (r *Schema) MutationType() *Type {
t, ok := r.schema.EntryPoints["mutation"]
if !ok {
return nil
}
return &Type{t}
}
func (r *Schema) SubscriptionType() *Type {
t, ok := r.schema.EntryPoints["subscription"]
if !ok {
return nil
}
return &Type{t}
}
type Type struct {
typ common.Type
}
// WrapType is only used internally.
func WrapType(typ common.Type) *Type {
return &Type{typ}
}
func (r *Type) Kind() string {
return r.typ.Kind()
}
func (r *Type) Name() *string {
if named, ok := r.typ.(schema.NamedType); ok {
name := named.TypeName()
return &name
}
return nil
}
func (r *Type) Description() *string {
if named, ok := r.typ.(schema.NamedType); ok {
desc := named.Description()
if desc == "" {
return nil
}
return &desc
}
return nil
}
func (r *Type) Fields(args *struct{ IncludeDeprecated bool }) *[]*Field {
var fields schema.FieldList
switch t := r.typ.(type) {
case *schema.Object:
fields = t.Fields
case *schema.Interface:
fields = t.Fields
default:
return nil
}
var l []*Field
for _, f := range fields {
if d := f.Directives.Get("deprecated"); d == nil || args.IncludeDeprecated {
l = append(l, &Field{f})
}
}
return &l
}
func (r *Type) Interfaces() *[]*Type {
t, ok := r.typ.(*schema.Object)
if !ok {
return nil
}
l := make([]*Type, len(t.Interfaces))
for i, intf := range t.Interfaces {
l[i] = &Type{intf}
}
return &l
}
func (r *Type) PossibleTypes() *[]*Type {
var possibleTypes []*schema.Object
switch t := r.typ.(type) {
case *schema.Interface:
possibleTypes = t.PossibleTypes
case *schema.Union:
possibleTypes = t.PossibleTypes
default:
return nil
}
l := make([]*Type, len(possibleTypes))
for i, intf := range possibleTypes {
l[i] = &Type{intf}
}
return &l
}
func (r *Type) EnumValues(args *struct{ IncludeDeprecated bool }) *[]*EnumValue {
t, ok := r.typ.(*schema.Enum)
if !ok {
return nil
}
var l []*EnumValue
for _, v := range t.Values {
if d := v.Directives.Get("deprecated"); d == nil || args.IncludeDeprecated {
l = append(l, &EnumValue{v})
}
}
return &l
}
func (r *Type) InputFields() *[]*InputValue {
t, ok := r.typ.(*schema.InputObject)
if !ok {
return nil
}
l := make([]*InputValue, len(t.Values))
for i, v := range t.Values {
l[i] = &InputValue{v}
}
return &l
}
func (r *Type) OfType() *Type {
switch t := r.typ.(type) {
case *common.List:
return &Type{t.OfType}
case *common.NonNull:
return &Type{t.OfType}
default:
return nil
}
}
type Field struct {
field *schema.Field
}
func (r *Field) Name() string {
return r.field.Name
}
func (r *Field) Description() *string {
if r.field.Desc == "" {
return nil
}
return &r.field.Desc
}
func (r *Field) Args() []*InputValue {
l := make([]*InputValue, len(r.field.Args))
for i, v := range r.field.Args {
l[i] = &InputValue{v}
}
return l
}
func (r *Field) Type() *Type {
return &Type{r.field.Type}
}
func (r *Field) IsDeprecated() bool {
return r.field.Directives.Get("deprecated") != nil
}
func (r *Field) DeprecationReason() *string {
d := r.field.Directives.Get("deprecated")
if d == nil {
return nil
}
reason := d.Args.MustGet("reason").Value(nil).(string)
return &reason
}
type InputValue struct {
value *common.InputValue
}
func (r *InputValue) Name() string {
return r.value.Name.Name
}
func (r *InputValue) Description() *string {
if r.value.Desc == "" {
return nil
}
return &r.value.Desc
}
func (r *InputValue) Type() *Type {
return &Type{r.value.Type}
}
func (r *InputValue) DefaultValue() *string {
if r.value.Default == nil {
return nil
}
s := r.value.Default.String()
return &s
}
type EnumValue struct {
value *schema.EnumValue
}
func (r *EnumValue) Name() string {
return r.value.Name
}
func (r *EnumValue) Description() *string {
if r.value.Desc == "" {
return nil
}
return &r.value.Desc
}
func (r *EnumValue) IsDeprecated() bool {
return r.value.Directives.Get("deprecated") != nil
}
func (r *EnumValue) DeprecationReason() *string {
d := r.value.Directives.Get("deprecated")
if d == nil {
return nil
}
reason := d.Args.MustGet("reason").Value(nil).(string)
return &reason
}
type Directive struct {
directive *schema.DirectiveDecl
}
func (r *Directive) Name() string {
return r.directive.Name
}
func (r *Directive) Description() *string {
if r.directive.Desc == "" {
return nil
}
return &r.directive.Desc
}
func (r *Directive) Locations() []string {
return r.directive.Locs
}
func (r *Directive) Args() []*InputValue {
l := make([]*InputValue, len(r.directive.Args))
for i, v := range r.directive.Args {
l[i] = &InputValue{v}
}
return l
}

23
vendor/github.com/neelance/graphql-go/log/log.go generated vendored Normal file
View File

@ -0,0 +1,23 @@
package log
import (
"context"
"log"
"runtime"
)
// Logger is the interface used to log panics that occur durring query execution. It is setable via graphql.ParseSchema
type Logger interface {
LogPanic(ctx context.Context, value interface{})
}
// DefaultLogger is the default logger used to log panics that occur durring query execution
type DefaultLogger struct{}
// LogPanic is used to log recovered panic values that occur durring query execution
func (l *DefaultLogger) LogPanic(_ context.Context, value interface{}) {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("graphql: panic occurred: %v\n%s", value, buf)
}

70
vendor/github.com/neelance/graphql-go/relay/relay.go generated vendored Normal file
View File

@ -0,0 +1,70 @@
package relay
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
graphql "github.com/neelance/graphql-go"
)
func MarshalID(kind string, spec interface{}) graphql.ID {
d, err := json.Marshal(spec)
if err != nil {
panic(fmt.Errorf("relay.MarshalID: %s", err))
}
return graphql.ID(base64.URLEncoding.EncodeToString(append([]byte(kind+":"), d...)))
}
func UnmarshalKind(id graphql.ID) string {
s, err := base64.URLEncoding.DecodeString(string(id))
if err != nil {
return ""
}
i := strings.IndexByte(string(s), ':')
if i == -1 {
return ""
}
return string(s[:i])
}
func UnmarshalSpec(id graphql.ID, v interface{}) error {
s, err := base64.URLEncoding.DecodeString(string(id))
if err != nil {
return err
}
i := strings.IndexByte(string(s), ':')
if i == -1 {
return errors.New("invalid graphql.ID")
}
return json.Unmarshal([]byte(s[i+1:]), v)
}
type Handler struct {
Schema *graphql.Schema
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var params struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
response := h.Schema.Exec(r.Context(), params.Query, params.OperationName, params.Variables)
responseJSON, err := json.Marshal(response)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(responseJSON)
}

View File

@ -0,0 +1,36 @@
package relay_test
import (
"net/http/httptest"
"strings"
"testing"
"github.com/neelance/graphql-go"
"github.com/neelance/graphql-go/example/starwars"
"github.com/neelance/graphql-go/relay"
)
var starwarsSchema = graphql.MustParseSchema(starwars.Schema, &starwars.Resolver{})
func TestServeHTTP(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/some/path/here", strings.NewReader(`{"query":"{ hero { name } }", "operationName":"", "variables": null}`))
h := relay.Handler{Schema: starwarsSchema}
h.ServeHTTP(w, r)
if w.Code != 200 {
t.Fatalf("Expected status code 200, got %d.", w.Code)
}
contentType := w.Header().Get("Content-Type")
if contentType != "application/json" {
t.Fatalf("Invalid content-type. Expected [application/json], but instead got [%s]", contentType)
}
expectedResponse := `{"data":{"hero":{"name":"R2-D2"}}}`
actualResponse := w.Body.String()
if expectedResponse != actualResponse {
t.Fatalf("Invalid response. Expected [%s], but instead got [%s]", expectedResponse, actualResponse)
}
}

36
vendor/github.com/neelance/graphql-go/time.go generated vendored Normal file
View File

@ -0,0 +1,36 @@
package graphql
import (
"fmt"
"time"
)
// Time is a custom GraphQL type to represent an instant in time. It has to be added to a schema
// via "scalar Time" since it is not a predeclared GraphQL type like "ID".
type Time struct {
time.Time
}
func (_ Time) ImplementsGraphQLType(name string) bool {
return name == "Time"
}
func (t *Time) UnmarshalGraphQL(input interface{}) error {
switch input := input.(type) {
case time.Time:
t.Time = input
return nil
case string:
var err error
t.Time, err = time.Parse(time.RFC3339, input)
return err
case int:
t.Time = time.Unix(int64(input), 0)
return nil
case float64:
t.Time = time.Unix(int64(input), 0)
return nil
default:
return fmt.Errorf("wrong type")
}
}

80
vendor/github.com/neelance/graphql-go/trace/trace.go generated vendored Normal file
View File

@ -0,0 +1,80 @@
package trace
import (
"context"
"fmt"
"github.com/neelance/graphql-go/errors"
"github.com/neelance/graphql-go/introspection"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
)
type TraceQueryFinishFunc func([]*errors.QueryError)
type TraceFieldFinishFunc func(*errors.QueryError)
type Tracer interface {
TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc)
TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc)
}
type OpenTracingTracer struct{}
func (OpenTracingTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) {
span, spanCtx := opentracing.StartSpanFromContext(ctx, "GraphQL request")
span.SetTag("graphql.query", queryString)
if operationName != "" {
span.SetTag("graphql.operationName", operationName)
}
if len(variables) != 0 {
span.LogFields(log.Object("graphql.variables", variables))
}
return spanCtx, func(errs []*errors.QueryError) {
if len(errs) > 0 {
msg := errs[0].Error()
if len(errs) > 1 {
msg += fmt.Sprintf(" (and %d more errors)", len(errs)-1)
}
ext.Error.Set(span, true)
span.SetTag("graphql.error", msg)
}
span.Finish()
}
}
func (OpenTracingTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) {
if trivial {
return ctx, noop
}
span, spanCtx := opentracing.StartSpanFromContext(ctx, label)
span.SetTag("graphql.type", typeName)
span.SetTag("graphql.field", fieldName)
for name, value := range args {
span.SetTag("graphql.args."+name, value)
}
return spanCtx, func(err *errors.QueryError) {
if err != nil {
ext.Error.Set(span, true)
span.SetTag("graphql.error", err.Error())
}
span.Finish()
}
}
func noop(*errors.QueryError) {}
type NoopTracer struct{}
func (NoopTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) {
return ctx, func(errs []*errors.QueryError) {}
}
func (NoopTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) {
return ctx, func(err *errors.QueryError) {}
}

View File

@ -0,0 +1,13 @@
# IntelliJ project files
.idea/
opentracing-go.iml
opentracing-go.ipr
opentracing-go.iws
# Test results
*.cov
*.html
test.log
# Build dir
build/

View File

@ -0,0 +1,14 @@
language: go
go:
- 1.6
- 1.7
- 1.8
- tip
install:
- go get -d -t github.com/opentracing/opentracing-go/...
- go get -u github.com/golang/lint/...
script:
- make test lint
- go build ./...

View File

@ -0,0 +1,14 @@
Changes by Version
==================
1.1.0 (unreleased)
-------------------
- Deprecate InitGlobalTracer() in favor of SetGlobalTracer()
1.0.0 (2016-09-26)
-------------------
- This release implements OpenTracing Specification 1.0 (http://opentracing.io/spec)

21
vendor/github.com/opentracing/opentracing-go/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 The OpenTracing Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

32
vendor/github.com/opentracing/opentracing-go/Makefile generated vendored Normal file
View File

@ -0,0 +1,32 @@
PACKAGES := . ./mocktracer/... ./ext/...
.DEFAULT_GOAL := test-and-lint
.PHONE: test-and-lint
test-and-lint: test lint
.PHONY: test
test:
go test -v -cover ./...
cover:
@rm -rf cover-all.out
$(foreach pkg, $(PACKAGES), $(MAKE) cover-pkg PKG=$(pkg) || true;)
@grep mode: cover.out > coverage.out
@cat cover-all.out >> coverage.out
go tool cover -html=coverage.out -o cover.html
@rm -rf cover.out cover-all.out coverage.out
cover-pkg:
go test -coverprofile cover.out $(PKG)
@grep -v mode: cover.out >> cover-all.out
.PHONY: lint
lint:
go fmt ./...
golint ./...
@# Run again with magic to exit non-zero if golint outputs anything.
@! (golint ./... | read dummy)
go vet ./...

147
vendor/github.com/opentracing/opentracing-go/README.md generated vendored Normal file
View File

@ -0,0 +1,147 @@
[![Gitter chat](http://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/opentracing/public) [![Build Status](https://travis-ci.org/opentracing/opentracing-go.svg?branch=master)](https://travis-ci.org/opentracing/opentracing-go) [![GoDoc](https://godoc.org/github.com/opentracing/opentracing-go?status.svg)](http://godoc.org/github.com/opentracing/opentracing-go)
# OpenTracing API for Go
This package is a Go platform API for OpenTracing.
## Required Reading
In order to understand the Go platform API, one must first be familiar with the
[OpenTracing project](http://opentracing.io) and
[terminology](http://opentracing.io/documentation/pages/spec.html) more specifically.
## API overview for those adding instrumentation
Everyday consumers of this `opentracing` package really only need to worry
about a couple of key abstractions: the `StartSpan` function, the `Span`
interface, and binding a `Tracer` at `main()`-time. Here are code snippets
demonstrating some important use cases.
#### Singleton initialization
The simplest starting point is `./default_tracer.go`. As early as possible, call
```go
import "github.com/opentracing/opentracing-go"
import ".../some_tracing_impl"
func main() {
opentracing.InitGlobalTracer(
// tracing impl specific:
some_tracing_impl.New(...),
)
...
}
```
##### Non-Singleton initialization
If you prefer direct control to singletons, manage ownership of the
`opentracing.Tracer` implementation explicitly.
#### Creating a Span given an existing Go `context.Context`
If you use `context.Context` in your application, OpenTracing's Go library will
happily rely on it for `Span` propagation. To start a new (blocking child)
`Span`, you can use `StartSpanFromContext`.
```go
func xyz(ctx context.Context, ...) {
...
span, ctx := opentracing.StartSpanFromContext(ctx, "operation_name")
defer span.Finish()
span.LogFields(
log.String("event", "soft error"),
log.String("type", "cache timeout"),
log.Int("waited.millis", 1500))
...
}
```
#### Starting an empty trace by creating a "root span"
It's always possible to create a "root" `Span` with no parent or other causal
reference.
```go
func xyz() {
...
sp := opentracing.StartSpan("operation_name")
defer sp.Finish()
...
}
```
#### Creating a (child) Span given an existing (parent) Span
```go
func xyz(parentSpan opentracing.Span, ...) {
...
sp := opentracing.StartSpan(
"operation_name",
opentracing.ChildOf(parentSpan.Context()))
defer sp.Finish()
...
}
```
#### Serializing to the wire
```go
func makeSomeRequest(ctx context.Context) ... {
if span := opentracing.SpanFromContext(ctx); span != nil {
httpClient := &http.Client{}
httpReq, _ := http.NewRequest("GET", "http://myservice/", nil)
// Transmit the span's TraceContext as HTTP headers on our
// outbound request.
opentracing.GlobalTracer().Inject(
span.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(httpReq.Header))
resp, err := httpClient.Do(httpReq)
...
}
...
}
```
#### Deserializing from the wire
```go
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
var serverSpan opentracing.Span
appSpecificOperationName := ...
wireContext, err := opentracing.GlobalTracer().Extract(
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header))
if err != nil {
// Optionally record something about err here
}
// Create the span referring to the RPC client if available.
// If wireContext == nil, a root span will be created.
serverSpan = opentracing.StartSpan(
appSpecificOperationName,
ext.RPCServerOption(wireContext))
defer serverSpan.Finish()
ctx := opentracing.ContextWithSpan(context.Background(), serverSpan)
...
}
```
#### Goroutine-safety
The entire public API is goroutine-safe and does not require external
synchronization.
## API pointers for those implementing a tracing system
Tracing system implementors may be able to reuse or copy-paste-modify the `basictracer` package, found [here](https://github.com/opentracing/basictracer-go). In particular, see `basictracer.New(...)`.
## API compatibility
For the time being, "mild" backwards-incompatible changes may be made without changing the major version number. As OpenTracing and `opentracing-go` mature, backwards compatibility will become more of a priority.

View File

@ -0,0 +1,198 @@
package ext
import opentracing "github.com/opentracing/opentracing-go"
// These constants define common tag names recommended for better portability across
// tracing systems and languages/platforms.
//
// The tag names are defined as typed strings, so that in addition to the usual use
//
// span.setTag(TagName, value)
//
// they also support value type validation via this additional syntax:
//
// TagName.Set(span, value)
//
var (
//////////////////////////////////////////////////////////////////////
// SpanKind (client/server or producer/consumer)
//////////////////////////////////////////////////////////////////////
// SpanKind hints at relationship between spans, e.g. client/server
SpanKind = spanKindTagName("span.kind")
// SpanKindRPCClient marks a span representing the client-side of an RPC
// or other remote call
SpanKindRPCClientEnum = SpanKindEnum("client")
SpanKindRPCClient = opentracing.Tag{Key: string(SpanKind), Value: SpanKindRPCClientEnum}
// SpanKindRPCServer marks a span representing the server-side of an RPC
// or other remote call
SpanKindRPCServerEnum = SpanKindEnum("server")
SpanKindRPCServer = opentracing.Tag{Key: string(SpanKind), Value: SpanKindRPCServerEnum}
// SpanKindProducer marks a span representing the producer-side of a
// message bus
SpanKindProducerEnum = SpanKindEnum("producer")
SpanKindProducer = opentracing.Tag{Key: string(SpanKind), Value: SpanKindProducerEnum}
// SpanKindConsumer marks a span representing the consumer-side of a
// message bus
SpanKindConsumerEnum = SpanKindEnum("consumer")
SpanKindConsumer = opentracing.Tag{Key: string(SpanKind), Value: SpanKindConsumerEnum}
//////////////////////////////////////////////////////////////////////
// Component name
//////////////////////////////////////////////////////////////////////
// Component is a low-cardinality identifier of the module, library,
// or package that is generating a span.
Component = stringTagName("component")
//////////////////////////////////////////////////////////////////////
// Sampling hint
//////////////////////////////////////////////////////////////////////
// SamplingPriority determines the priority of sampling this Span.
SamplingPriority = uint16TagName("sampling.priority")
//////////////////////////////////////////////////////////////////////
// Peer tags. These tags can be emitted by either client-side of
// server-side to describe the other side/service in a peer-to-peer
// communications, like an RPC call.
//////////////////////////////////////////////////////////////////////
// PeerService records the service name of the peer.
PeerService = stringTagName("peer.service")
// PeerAddress records the address name of the peer. This may be a "ip:port",
// a bare "hostname", a FQDN or even a database DSN substring
// like "mysql://username@127.0.0.1:3306/dbname"
PeerAddress = stringTagName("peer.address")
// PeerHostname records the host name of the peer
PeerHostname = stringTagName("peer.hostname")
// PeerHostIPv4 records IP v4 host address of the peer
PeerHostIPv4 = uint32TagName("peer.ipv4")
// PeerHostIPv6 records IP v6 host address of the peer
PeerHostIPv6 = stringTagName("peer.ipv6")
// PeerPort records port number of the peer
PeerPort = uint16TagName("peer.port")
//////////////////////////////////////////////////////////////////////
// HTTP Tags
//////////////////////////////////////////////////////////////////////
// HTTPUrl should be the URL of the request being handled in this segment
// of the trace, in standard URI format. The protocol is optional.
HTTPUrl = stringTagName("http.url")
// HTTPMethod is the HTTP method of the request, and is case-insensitive.
HTTPMethod = stringTagName("http.method")
// HTTPStatusCode is the numeric HTTP status code (200, 404, etc) of the
// HTTP response.
HTTPStatusCode = uint16TagName("http.status_code")
//////////////////////////////////////////////////////////////////////
// DB Tags
//////////////////////////////////////////////////////////////////////
// DBInstance is database instance name.
DBInstance = stringTagName("db.instance")
// DBStatement is a database statement for the given database type.
// It can be a query or a prepared statement (i.e., before substitution).
DBStatement = stringTagName("db.statement")
// DBType is a database type. For any SQL database, "sql".
// For others, the lower-case database category, e.g. "redis"
DBType = stringTagName("db.type")
// DBUser is a username for accessing database.
DBUser = stringTagName("db.user")
//////////////////////////////////////////////////////////////////////
// Message Bus Tag
//////////////////////////////////////////////////////////////////////
// MessageBusDestination is an address at which messages can be exchanged
MessageBusDestination = stringTagName("message_bus.destination")
//////////////////////////////////////////////////////////////////////
// Error Tag
//////////////////////////////////////////////////////////////////////
// Error indicates that operation represented by the span resulted in an error.
Error = boolTagName("error")
)
// ---
// SpanKindEnum represents common span types
type SpanKindEnum string
type spanKindTagName string
// Set adds a string tag to the `span`
func (tag spanKindTagName) Set(span opentracing.Span, value SpanKindEnum) {
span.SetTag(string(tag), value)
}
type rpcServerOption struct {
clientContext opentracing.SpanContext
}
func (r rpcServerOption) Apply(o *opentracing.StartSpanOptions) {
if r.clientContext != nil {
opentracing.ChildOf(r.clientContext).Apply(o)
}
SpanKindRPCServer.Apply(o)
}
// RPCServerOption returns a StartSpanOption appropriate for an RPC server span
// with `client` representing the metadata for the remote peer Span if available.
// In case client == nil, due to the client not being instrumented, this RPC
// server span will be a root span.
func RPCServerOption(client opentracing.SpanContext) opentracing.StartSpanOption {
return rpcServerOption{client}
}
// ---
type stringTagName string
// Set adds a string tag to the `span`
func (tag stringTagName) Set(span opentracing.Span, value string) {
span.SetTag(string(tag), value)
}
// ---
type uint32TagName string
// Set adds a uint32 tag to the `span`
func (tag uint32TagName) Set(span opentracing.Span, value uint32) {
span.SetTag(string(tag), value)
}
// ---
type uint16TagName string
// Set adds a uint16 tag to the `span`
func (tag uint16TagName) Set(span opentracing.Span, value uint16) {
span.SetTag(string(tag), value)
}
// ---
type boolTagName string
// Add adds a bool tag to the `span`
func (tag boolTagName) Set(span opentracing.Span, value bool) {
span.SetTag(string(tag), value)
}

View File

@ -0,0 +1,148 @@
package ext_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/mocktracer"
)
func TestPeerTags(t *testing.T) {
if ext.PeerService != "peer.service" {
t.Fatalf("Invalid PeerService %v", ext.PeerService)
}
tracer := mocktracer.New()
span := tracer.StartSpan("my-trace")
ext.PeerService.Set(span, "my-service")
ext.PeerAddress.Set(span, "my-hostname:8080")
ext.PeerHostname.Set(span, "my-hostname")
ext.PeerHostIPv4.Set(span, uint32(127<<24|1))
ext.PeerHostIPv6.Set(span, "::")
ext.PeerPort.Set(span, uint16(8080))
ext.SamplingPriority.Set(span, uint16(1))
ext.SpanKind.Set(span, ext.SpanKindRPCServerEnum)
ext.SpanKindRPCClient.Set(span)
span.Finish()
rawSpan := tracer.FinishedSpans()[0]
assert.Equal(t, map[string]interface{}{
"peer.service": "my-service",
"peer.address": "my-hostname:8080",
"peer.hostname": "my-hostname",
"peer.ipv4": uint32(127<<24 | 1),
"peer.ipv6": "::",
"peer.port": uint16(8080),
"span.kind": ext.SpanKindRPCClientEnum,
}, rawSpan.Tags())
assert.True(t, span.Context().(mocktracer.MockSpanContext).Sampled)
ext.SamplingPriority.Set(span, uint16(0))
assert.False(t, span.Context().(mocktracer.MockSpanContext).Sampled)
}
func TestHTTPTags(t *testing.T) {
tracer := mocktracer.New()
span := tracer.StartSpan("my-trace", ext.SpanKindRPCServer)
ext.HTTPUrl.Set(span, "test.biz/uri?protocol=false")
ext.HTTPMethod.Set(span, "GET")
ext.HTTPStatusCode.Set(span, 301)
span.Finish()
rawSpan := tracer.FinishedSpans()[0]
assert.Equal(t, map[string]interface{}{
"http.url": "test.biz/uri?protocol=false",
"http.method": "GET",
"http.status_code": uint16(301),
"span.kind": ext.SpanKindRPCServerEnum,
}, rawSpan.Tags())
}
func TestDBTags(t *testing.T) {
tracer := mocktracer.New()
span := tracer.StartSpan("my-trace", ext.SpanKindRPCClient)
ext.DBInstance.Set(span, "127.0.0.1:3306/customers")
ext.DBStatement.Set(span, "SELECT * FROM user_table")
ext.DBType.Set(span, "sql")
ext.DBUser.Set(span, "customer_user")
span.Finish()
rawSpan := tracer.FinishedSpans()[0]
assert.Equal(t, map[string]interface{}{
"db.instance": "127.0.0.1:3306/customers",
"db.statement": "SELECT * FROM user_table",
"db.type": "sql",
"db.user": "customer_user",
"span.kind": ext.SpanKindRPCClientEnum,
}, rawSpan.Tags())
}
func TestMiscTags(t *testing.T) {
tracer := mocktracer.New()
span := tracer.StartSpan("my-trace")
ext.Component.Set(span, "my-awesome-library")
ext.SamplingPriority.Set(span, 1)
ext.Error.Set(span, true)
span.Finish()
rawSpan := tracer.FinishedSpans()[0]
assert.Equal(t, map[string]interface{}{
"component": "my-awesome-library",
"error": true,
}, rawSpan.Tags())
}
func TestRPCServerOption(t *testing.T) {
tracer := mocktracer.New()
parent := tracer.StartSpan("my-trace")
parent.SetBaggageItem("bag", "gage")
carrier := opentracing.HTTPHeadersCarrier{}
err := tracer.Inject(parent.Context(), opentracing.HTTPHeaders, carrier)
if err != nil {
t.Fatal(err)
}
parCtx, err := tracer.Extract(opentracing.HTTPHeaders, carrier)
if err != nil {
t.Fatal(err)
}
tracer.StartSpan("my-child", ext.RPCServerOption(parCtx)).Finish()
rawSpan := tracer.FinishedSpans()[0]
assert.Equal(t, map[string]interface{}{
"span.kind": ext.SpanKindRPCServerEnum,
}, rawSpan.Tags())
assert.Equal(t, map[string]string{
"bag": "gage",
}, rawSpan.Context().(mocktracer.MockSpanContext).Baggage)
}
func TestMessageBusProducerTags(t *testing.T) {
tracer := mocktracer.New()
span := tracer.StartSpan("my-trace", ext.SpanKindProducer)
ext.MessageBusDestination.Set(span, "topic name")
span.Finish()
rawSpan := tracer.FinishedSpans()[0]
assert.Equal(t, map[string]interface{}{
"message_bus.destination": "topic name",
"span.kind": ext.SpanKindProducerEnum,
}, rawSpan.Tags())
}
func TestMessageBusConsumerTags(t *testing.T) {
tracer := mocktracer.New()
span := tracer.StartSpan("my-trace", ext.SpanKindConsumer)
ext.MessageBusDestination.Set(span, "topic name")
span.Finish()
rawSpan := tracer.FinishedSpans()[0]
assert.Equal(t, map[string]interface{}{
"message_bus.destination": "topic name",
"span.kind": ext.SpanKindConsumerEnum,
}, rawSpan.Tags())
}

View File

@ -0,0 +1,32 @@
package opentracing
var (
globalTracer Tracer = NoopTracer{}
)
// SetGlobalTracer sets the [singleton] opentracing.Tracer returned by
// GlobalTracer(). Those who use GlobalTracer (rather than directly manage an
// opentracing.Tracer instance) should call SetGlobalTracer as early as
// possible in main(), prior to calling the `StartSpan` global func below.
// Prior to calling `SetGlobalTracer`, any Spans started via the `StartSpan`
// (etc) globals are noops.
func SetGlobalTracer(tracer Tracer) {
globalTracer = tracer
}
// GlobalTracer returns the global singleton `Tracer` implementation.
// Before `SetGlobalTracer()` is called, the `GlobalTracer()` is a noop
// implementation that drops all data handed to it.
func GlobalTracer() Tracer {
return globalTracer
}
// StartSpan defers to `Tracer.StartSpan`. See `GlobalTracer()`.
func StartSpan(operationName string, opts ...StartSpanOption) Span {
return globalTracer.StartSpan(operationName, opts...)
}
// InitGlobalTracer is deprecated. Please use SetGlobalTracer.
func InitGlobalTracer(tracer Tracer) {
SetGlobalTracer(tracer)
}

View File

@ -0,0 +1,57 @@
package opentracing
import "golang.org/x/net/context"
type contextKey struct{}
var activeSpanKey = contextKey{}
// ContextWithSpan returns a new `context.Context` that holds a reference to
// `span`'s SpanContext.
func ContextWithSpan(ctx context.Context, span Span) context.Context {
return context.WithValue(ctx, activeSpanKey, span)
}
// SpanFromContext returns the `Span` previously associated with `ctx`, or
// `nil` if no such `Span` could be found.
//
// NOTE: context.Context != SpanContext: the former is Go's intra-process
// context propagation mechanism, and the latter houses OpenTracing's per-Span
// identity and baggage information.
func SpanFromContext(ctx context.Context) Span {
val := ctx.Value(activeSpanKey)
if sp, ok := val.(Span); ok {
return sp
}
return nil
}
// StartSpanFromContext starts and returns a Span with `operationName`, using
// any Span found within `ctx` as a ChildOfRef. If no such parent could be
// found, StartSpanFromContext creates a root (parentless) Span.
//
// The second return value is a context.Context object built around the
// returned Span.
//
// Example usage:
//
// SomeFunction(ctx context.Context, ...) {
// sp, ctx := opentracing.StartSpanFromContext(ctx, "SomeFunction")
// defer sp.Finish()
// ...
// }
func StartSpanFromContext(ctx context.Context, operationName string, opts ...StartSpanOption) (Span, context.Context) {
return startSpanFromContextWithTracer(ctx, GlobalTracer(), operationName, opts...)
}
// startSpanFromContextWithTracer is factored out for testing purposes.
func startSpanFromContextWithTracer(ctx context.Context, tracer Tracer, operationName string, opts ...StartSpanOption) (Span, context.Context) {
var span Span
if parentSpan := SpanFromContext(ctx); parentSpan != nil {
opts = append(opts, ChildOf(parentSpan.Context()))
span = tracer.StartSpan(operationName, opts...)
} else {
span = tracer.StartSpan(operationName, opts...)
}
return span, ContextWithSpan(ctx, span)
}

View File

@ -0,0 +1,81 @@
package opentracing
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestContextWithSpan(t *testing.T) {
span := &noopSpan{}
ctx := ContextWithSpan(context.Background(), span)
span2 := SpanFromContext(ctx)
if span != span2 {
t.Errorf("Not the same span returned from context, expected=%+v, actual=%+v", span, span2)
}
ctx = context.Background()
span2 = SpanFromContext(ctx)
if span2 != nil {
t.Errorf("Expected nil span, found %+v", span2)
}
ctx = ContextWithSpan(ctx, span)
span2 = SpanFromContext(ctx)
if span != span2 {
t.Errorf("Not the same span returned from context, expected=%+v, actual=%+v", span, span2)
}
}
func TestStartSpanFromContext(t *testing.T) {
testTracer := testTracer{}
// Test the case where there *is* a Span in the Context.
{
parentSpan := &testSpan{}
parentCtx := ContextWithSpan(context.Background(), parentSpan)
childSpan, childCtx := startSpanFromContextWithTracer(parentCtx, testTracer, "child")
if !childSpan.Context().(testSpanContext).HasParent {
t.Errorf("Failed to find parent: %v", childSpan)
}
if !childSpan.(testSpan).Equal(SpanFromContext(childCtx)) {
t.Errorf("Unable to find child span in context: %v", childCtx)
}
}
// Test the case where there *is not* a Span in the Context.
{
emptyCtx := context.Background()
childSpan, childCtx := startSpanFromContextWithTracer(emptyCtx, testTracer, "child")
if childSpan.Context().(testSpanContext).HasParent {
t.Errorf("Should not have found parent: %v", childSpan)
}
if !childSpan.(testSpan).Equal(SpanFromContext(childCtx)) {
t.Errorf("Unable to find child span in context: %v", childCtx)
}
}
}
func TestStartSpanFromContextOptions(t *testing.T) {
testTracer := testTracer{}
// Test options are passed to tracer
startTime := time.Now().Add(-10 * time.Second) // ten seconds ago
span, ctx := startSpanFromContextWithTracer(
context.Background(), testTracer, "parent", StartTime(startTime), Tag{"component", "test"})
assert.Equal(t, "test", span.(testSpan).Tags["component"])
assert.Equal(t, startTime, span.(testSpan).StartTime)
// Test it also works for a child span
childStartTime := startTime.Add(3 * time.Second)
childSpan, _ := startSpanFromContextWithTracer(
ctx, testTracer, "child", StartTime(childStartTime))
assert.Equal(t, childSpan.(testSpan).Tags["component"], nil)
assert.Equal(t, childSpan.(testSpan).StartTime, childStartTime)
}

View File

@ -0,0 +1,245 @@
package log
import (
"fmt"
"math"
)
type fieldType int
const (
stringType fieldType = iota
boolType
intType
int32Type
uint32Type
int64Type
uint64Type
float32Type
float64Type
errorType
objectType
lazyLoggerType
)
// Field instances are constructed via LogBool, LogString, and so on.
// Tracing implementations may then handle them via the Field.Marshal
// method.
//
// "heavily influenced by" (i.e., partially stolen from)
// https://github.com/uber-go/zap
type Field struct {
key string
fieldType fieldType
numericVal int64
stringVal string
interfaceVal interface{}
}
// String adds a string-valued key:value pair to a Span.LogFields() record
func String(key, val string) Field {
return Field{
key: key,
fieldType: stringType,
stringVal: val,
}
}
// Bool adds a bool-valued key:value pair to a Span.LogFields() record
func Bool(key string, val bool) Field {
var numericVal int64
if val {
numericVal = 1
}
return Field{
key: key,
fieldType: boolType,
numericVal: numericVal,
}
}
// Int adds an int-valued key:value pair to a Span.LogFields() record
func Int(key string, val int) Field {
return Field{
key: key,
fieldType: intType,
numericVal: int64(val),
}
}
// Int32 adds an int32-valued key:value pair to a Span.LogFields() record
func Int32(key string, val int32) Field {
return Field{
key: key,
fieldType: int32Type,
numericVal: int64(val),
}
}
// Int64 adds an int64-valued key:value pair to a Span.LogFields() record
func Int64(key string, val int64) Field {
return Field{
key: key,
fieldType: int64Type,
numericVal: val,
}
}
// Uint32 adds a uint32-valued key:value pair to a Span.LogFields() record
func Uint32(key string, val uint32) Field {
return Field{
key: key,
fieldType: uint32Type,
numericVal: int64(val),
}
}
// Uint64 adds a uint64-valued key:value pair to a Span.LogFields() record
func Uint64(key string, val uint64) Field {
return Field{
key: key,
fieldType: uint64Type,
numericVal: int64(val),
}
}
// Float32 adds a float32-valued key:value pair to a Span.LogFields() record
func Float32(key string, val float32) Field {
return Field{
key: key,
fieldType: float32Type,
numericVal: int64(math.Float32bits(val)),
}
}
// Float64 adds a float64-valued key:value pair to a Span.LogFields() record
func Float64(key string, val float64) Field {
return Field{
key: key,
fieldType: float64Type,
numericVal: int64(math.Float64bits(val)),
}
}
// Error adds an error with the key "error" to a Span.LogFields() record
func Error(err error) Field {
return Field{
key: "error",
fieldType: errorType,
interfaceVal: err,
}
}
// Object adds an object-valued key:value pair to a Span.LogFields() record
func Object(key string, obj interface{}) Field {
return Field{
key: key,
fieldType: objectType,
interfaceVal: obj,
}
}
// LazyLogger allows for user-defined, late-bound logging of arbitrary data
type LazyLogger func(fv Encoder)
// Lazy adds a LazyLogger to a Span.LogFields() record; the tracing
// implementation will call the LazyLogger function at an indefinite time in
// the future (after Lazy() returns).
func Lazy(ll LazyLogger) Field {
return Field{
fieldType: lazyLoggerType,
interfaceVal: ll,
}
}
// Encoder allows access to the contents of a Field (via a call to
// Field.Marshal).
//
// Tracer implementations typically provide an implementation of Encoder;
// OpenTracing callers typically do not need to concern themselves with it.
type Encoder interface {
EmitString(key, value string)
EmitBool(key string, value bool)
EmitInt(key string, value int)
EmitInt32(key string, value int32)
EmitInt64(key string, value int64)
EmitUint32(key string, value uint32)
EmitUint64(key string, value uint64)
EmitFloat32(key string, value float32)
EmitFloat64(key string, value float64)
EmitObject(key string, value interface{})
EmitLazyLogger(value LazyLogger)
}
// Marshal passes a Field instance through to the appropriate
// field-type-specific method of an Encoder.
func (lf Field) Marshal(visitor Encoder) {
switch lf.fieldType {
case stringType:
visitor.EmitString(lf.key, lf.stringVal)
case boolType:
visitor.EmitBool(lf.key, lf.numericVal != 0)
case intType:
visitor.EmitInt(lf.key, int(lf.numericVal))
case int32Type:
visitor.EmitInt32(lf.key, int32(lf.numericVal))
case int64Type:
visitor.EmitInt64(lf.key, int64(lf.numericVal))
case uint32Type:
visitor.EmitUint32(lf.key, uint32(lf.numericVal))
case uint64Type:
visitor.EmitUint64(lf.key, uint64(lf.numericVal))
case float32Type:
visitor.EmitFloat32(lf.key, math.Float32frombits(uint32(lf.numericVal)))
case float64Type:
visitor.EmitFloat64(lf.key, math.Float64frombits(uint64(lf.numericVal)))
case errorType:
if err, ok := lf.interfaceVal.(error); ok {
visitor.EmitString(lf.key, err.Error())
} else {
visitor.EmitString(lf.key, "<nil>")
}
case objectType:
visitor.EmitObject(lf.key, lf.interfaceVal)
case lazyLoggerType:
visitor.EmitLazyLogger(lf.interfaceVal.(LazyLogger))
}
}
// Key returns the field's key.
func (lf Field) Key() string {
return lf.key
}
// Value returns the field's value as interface{}.
func (lf Field) Value() interface{} {
switch lf.fieldType {
case stringType:
return lf.stringVal
case boolType:
return lf.numericVal != 0
case intType:
return int(lf.numericVal)
case int32Type:
return int32(lf.numericVal)
case int64Type:
return int64(lf.numericVal)
case uint32Type:
return uint32(lf.numericVal)
case uint64Type:
return uint64(lf.numericVal)
case float32Type:
return math.Float32frombits(uint32(lf.numericVal))
case float64Type:
return math.Float64frombits(uint64(lf.numericVal))
case errorType, objectType, lazyLoggerType:
return lf.interfaceVal
default:
return nil
}
}
// String returns a string representation of the key and value.
func (lf Field) String() string {
return fmt.Sprint(lf.key, ":", lf.Value())
}

View File

@ -0,0 +1,39 @@
package log
import (
"fmt"
"testing"
)
func TestFieldString(t *testing.T) {
testCases := []struct {
field Field
expected string
}{
{
field: String("key", "value"),
expected: "key:value",
},
{
field: Bool("key", true),
expected: "key:true",
},
{
field: Int("key", 5),
expected: "key:5",
},
{
field: Error(fmt.Errorf("err msg")),
expected: "error:err msg",
},
{
field: Error(nil),
expected: "error:<nil>",
},
}
for i, tc := range testCases {
if str := tc.field.String(); str != tc.expected {
t.Errorf("%d: expected '%s', got '%s'", i, tc.expected, str)
}
}
}

Some files were not shown because too many files have changed in this diff Show More