├── .chglog
├── CHANGELOG.tpl.md
└── config.yml
├── .githooks
└── commit-msg
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── objective.md
├── renovate.json
├── stale.yml
└── workflows
│ ├── changelog.yml
│ ├── compile.yml
│ ├── release.yml
│ ├── security.yml
│ └── test.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── build
├── .keep
├── compile.mk
├── deps.mk
├── docker.mk
├── document.mk
├── lint.mk
├── package
│ └── Dockerfile
├── release.mk
├── test.mk
├── tools.mk
└── util.mk
├── cla.md
├── cmd
├── .keep
└── tutone
│ └── main.go
├── configs
└── tutone.yml
├── docs
├── .keep
├── package-schema.md
└── tutone.md
├── generators
├── command
│ ├── command_util.go
│ └── generator.go
├── nerdgraphclient
│ └── generator.go
├── terraform
│ └── generator.go
└── typegen
│ └── generator.go
├── go.mod
├── go.sum
├── internal
├── .keep
├── codegen
│ ├── codegen.go
│ ├── generator.go
│ └── helpers.go
├── config
│ ├── config.go
│ └── config_test.go
├── filesystem
│ └── filesystem.go
├── output
│ └── output.go
├── schema
│ ├── enum.go
│ ├── expander.go
│ ├── field.go
│ ├── kind.go
│ ├── query.go
│ ├── schema.go
│ ├── schema_test.go
│ ├── schema_util.go
│ ├── schema_util_test.go
│ ├── testdata
│ │ ├── TestSchema_GetQueryStringForEndpoint_entities.txt
│ │ ├── TestSchema_GetQueryStringForEndpoint_entitySearch.txt
│ │ ├── TestSchema_GetQueryStringForEndpoint_entitySearchArgs.txt
│ │ ├── TestSchema_GetQueryStringForEndpoint_linkedAccounts.txt
│ │ ├── TestSchema_GetQueryStringForEndpoint_policy.txt
│ │ ├── TestSchema_GetQueryStringForEndpoint_user.txt
│ │ ├── TestSchema_GetQueryStringForMutation_Pattern_alertsMutingRuleCreate.txt
│ │ ├── TestSchema_GetQueryStringForMutation_Pattern_dashboardCreate.txt
│ │ ├── TestSchema_GetQueryStringForMutation_Pattern_dashboardUpdate.txt
│ │ ├── TestSchema_GetQueryStringForMutation_Pattern_edgeCreateTraceFilterRules.txt
│ │ ├── TestSchema_GetQueryStringForMutation_Pattern_edgeCreateTraceObserver.txt
│ │ ├── TestSchema_GetQueryStringForMutation_Pattern_edgeDeleteTraceFilterRules.txt
│ │ ├── TestSchema_GetQueryStringForMutation_Pattern_edgeDeleteTraceObservers.txt
│ │ ├── TestSchema_GetQueryStringForMutation_Pattern_edgeUpdateTraceObservers.txt
│ │ ├── TestSchema_GetQueryStringForMutation_alertsMutingRuleCreate.txt
│ │ ├── TestSchema_GetQueryStringForMutation_apiAccessCreateKeys.txt
│ │ ├── TestSchema_GetQueryStringForMutation_cloudRenameAccount.txt
│ │ ├── TestType_GetQueryFieldsString_AlertsNrqlBaselineCondition.txt
│ │ ├── TestType_GetQueryFieldsString_AlertsNrqlCondition.txt
│ │ ├── TestType_GetQueryFieldsString_AlertsNrqlOutlierCondition.txt
│ │ ├── TestType_GetQueryFieldsString_CloudDisableIntegrationPayload.txt
│ │ └── TestType_GetQueryFieldsString_CloudLinkedAccount.txt
│ ├── type.go
│ ├── type_test.go
│ └── typeref.go
├── util
│ ├── log.go
│ ├── log_test.go
│ ├── strings.go
│ ├── strings_test.go
│ ├── template_funcs.go
│ └── template_funcs_test.go
└── version
│ ├── version.go
│ └── version_test.go
├── pkg
├── .keep
├── accountmanagement
│ └── types.go
├── fetch
│ ├── command.go
│ └── http.go
├── generate
│ ├── command.go
│ └── generate.go
└── lang
│ ├── command.go
│ └── golang.go
├── scripts
└── release.sh
├── templates
├── clientgo
│ └── types.go.tmpl
├── command
│ └── command.go.tmpl
├── nerdgraphclient
│ ├── client.go.tmpl
│ └── integration_test.go.tmpl
└── typegen
│ └── types.go.tmpl
├── testdata
└── goodConfig_fixture.yml
└── tools
├── go.mod
├── go.sum
└── tools.go
/.chglog/CHANGELOG.tpl.md:
--------------------------------------------------------------------------------
1 | {{ if .Versions -}}
2 | {{ if .Unreleased.CommitGroups -}}
3 |
4 | ## [Unreleased]
5 |
6 | {{ range .Unreleased.CommitGroups -}}
7 | ### {{ .Title }}
8 | {{ range .Commits -}}
9 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
10 | {{ end -}}
11 | {{ end -}}
12 | {{ end -}}
13 |
14 | {{ range .Versions -}}
15 |
16 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }}
17 | {{ range .CommitGroups -}}
18 | ### {{ .Title }}
19 | {{ range .Commits -}}
20 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
21 | {{ end }}
22 | {{ end -}}
23 |
24 | {{ if .NoteGroups -}}
25 | {{ range .NoteGroups -}}
26 | ### {{ .Title }}
27 | {{ range .Notes }}
28 | {{ .Body }}
29 | {{ end }}
30 | {{ end -}}
31 | {{ end -}}
32 | {{ end -}}
33 |
34 | [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD
35 | {{ range .Versions -}}
36 | {{ if .Tag.Previous -}}
37 | [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}
38 | {{ end -}}
39 | {{ end -}}
40 | {{ end -}}
41 |
--------------------------------------------------------------------------------
/.chglog/config.yml:
--------------------------------------------------------------------------------
1 | style: github
2 | template: CHANGELOG.tpl.md
3 | info:
4 | title: CHANGELOG
5 | repository_url: https://github.com/newrelic/tutone
6 | options:
7 | commits:
8 | filters:
9 | Type:
10 | - docs
11 | - feat
12 | - fix
13 |
14 | commit_groups:
15 | title_maps:
16 | docs: Documentation Updates
17 | feat: Features
18 | fix: Bug Fixes
19 |
20 | refs:
21 | actions:
22 | - Closes
23 | - Fixes
24 | - Resolves
25 |
26 | issues:
27 | prefix:
28 | - #
29 |
30 | header:
31 | pattern: "^(\\w*)(?:\\(([\\/\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$"
32 | pattern_maps:
33 | - Type
34 | - Scope
35 | - Subject
36 |
37 | notes:
38 | keywords:
39 | - BREAKING CHANGE
40 |
--------------------------------------------------------------------------------
/.githooks/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | RED="\033[0;31m"
4 | END_COLOR="\033[0m"
5 |
6 | commit_types="(chore|docs|feat|fix|refactor|tests?)"
7 | conventional_commit_regex="^${commit_types}(\([a-z \-]+\))?!?: .+$"
8 |
9 | commit_message=$(cat "$1")
10 |
11 | if [[ "$commit_message" =~ $conventional_commit_regex ]]; then
12 | echo "Commit message meets Conventional Commit standards..."
13 | exit 0
14 | fi
15 | echo
16 | echo "${RED}Commit lint failed. Please update your commit message format. ${END_COLOR}"
17 | echo "Example commit messages:"
18 | echo " feat(scope): add your feature description here"
19 | echo " fix(scope): add your fix description here"
20 | echo " chore(scope): add your chore description here"
21 | echo " docs(scope): add your docs description here"
22 | echo " refactor(scope): add your refactor description here"
23 | echo " tests(scope): add your tests description here"
24 | echo
25 |
26 | exit 1
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: 'Report a bug '
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Description
11 | A clear and concise description of what the problem is.
12 |
13 | ### Go Version
14 | Provide the output of `go version` here please
15 |
16 | ### Current behavior
17 | A clear and concise description of what you're currently experiencing.
18 |
19 | ### Expected behavior
20 | A clear and concise description of what you expected to happen.
21 |
22 | ### Steps To Reproduce
23 | Steps to reproduce the behavior:
24 | 1. Do this...
25 | 2. Then do this... etc
26 | 3. See error
27 |
28 | ### Debug Output (if applicable)
29 | If applicable, add associated log output to help explain the problem.
30 |
31 | ### Additional Context
32 | Add any other context about the problem here.
33 |
34 | ### References or Related Issues
35 | Are there any other related GitHub issues (open or closed) or Pull Requests that should be linked here?
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Feature Description
11 | A clear and concise description of the feature you want or need.
12 |
13 | ### Describe Alternatives
14 | A clear and concise description of any alternative solutions or features you've considered. Are there examples you could link us to?
15 |
16 | ### Additional context
17 | Add any other context here.
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/objective.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Objective
3 | about: Custom objective template
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Objective
11 | State the desired outcome of this ticket.
12 |
13 | ### Acceptance Criteria
14 | - Acceptance criterion 1
15 | - Acceptance criterion 2
16 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "gomodTidy": true,
4 | "ignoreDeps": [
5 | "golang.org/x/crypto",
6 | "golang.org/x/net",
7 | "golang.org/x/sys",
8 | "golang.org/x/tools"
9 | ],
10 | "commitMessagePrefix": "chore(deps):",
11 | "reviewers": ["team:developer-toolkit"],
12 | "labels": ["dependencies"],
13 | "prConcurrentLimit": 4,
14 | "postUpdateOptions": ["gomodTidy"]
15 | }
16 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-stale - https://github.com/probot/stale
2 |
3 | # Number of days of inactivity before an Issue or Pull Request becomes stale
4 | daysUntilStale: 30
5 |
6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
8 | daysUntilClose: 14
9 |
10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
11 | onlyLabels: []
12 |
13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
14 | exemptLabels:
15 | - enhancement
16 | - help wanted
17 | - priority:low
18 | - priority:medium
19 | - priority:high
20 | - priority:blocker
21 | - good first issue
22 |
23 | # Set to true to ignore issues in a project (defaults to false)
24 | exemptProjects: false
25 |
26 | # Set to true to ignore issues in a milestone (defaults to false)
27 | exemptMilestones: false
28 |
29 | # Set to true to ignore issues with an assignee (defaults to false)
30 | exemptAssignees: false
31 |
32 | # Label to use when marking as stale
33 | staleLabel: stale
34 |
35 | # Comment to post when marking as stale. Set to `false` to disable
36 | markComment: >
37 | This issue has been automatically marked as stale because it has not had
38 | any recent activity. It will be closed if no further activity occurs.
39 |
40 | # Comment to post when removing the stale label.
41 | # unmarkComment: >
42 | # Your comment here.
43 |
44 | # Comment to post when closing a stale Issue or Pull Request.
45 | closeComment: >
46 | This issue has been automatically closed due to a lack of activity
47 | for an extended period of time.
48 |
49 | # Limit to only `issues` or `pulls`
50 | only: issues
51 |
--------------------------------------------------------------------------------
/.github/workflows/changelog.yml:
--------------------------------------------------------------------------------
1 | name: Changelog
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*"
7 |
8 | jobs:
9 | createPullRequest:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Install Go
13 | uses: actions/setup-go@v3
14 | with:
15 | go-version: 1.20.x
16 |
17 | - name: Add GOBIN to PATH
18 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
19 | shell: bash
20 | - uses: actions/checkout@v3
21 | with:
22 | fetch-depth: 0
23 |
24 | - name: Update Changelog
25 | run: |
26 | make changelog
27 | make spell-check-fix
28 |
29 | - name: Create Pull Request
30 | id: cpr
31 | uses: peter-evans/create-pull-request@v3
32 | with:
33 | token: ${{ secrets.DEV_TOOLKIT_TOKEN }}
34 | commit-message: "chore: update changelog"
35 | committer: GitHub
36 | author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
37 | signoff: false
38 | branch: update/changelog
39 | title: "${{ github.ref }} Changelog Update"
40 | base: main
41 | body: |
42 | Update changelog
43 | - Auto-generated by [create-pull-request][1]
44 |
45 | [1]: https://github.com/peter-evans/create-pull-request
46 | labels: |
47 | changelog
48 | automated pr
49 |
50 | - name: Check output
51 | run: |
52 | echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
53 |
--------------------------------------------------------------------------------
/.github/workflows/compile.yml:
--------------------------------------------------------------------------------
1 | name: Compiling
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | # Compile on all supported OSes
11 | compile:
12 | strategy:
13 | matrix:
14 | go-version:
15 | - 1.20.x
16 | platform:
17 | - ubuntu-latest
18 | - macos-latest
19 | - windows-latest
20 | runs-on: ${{ matrix.platform }}
21 | steps:
22 | - name: Install Go
23 | uses: actions/setup-go@v3
24 | with:
25 | go-version: ${{ matrix.go-version }}
26 |
27 | - name: Add GOBIN to PATH
28 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
29 | shell: bash
30 |
31 | - name: Checkout code
32 | uses: actions/checkout@v3
33 |
34 | - name: Compile
35 | run: make compile-only
36 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | # Triggered via GitHub Actions UI
4 | on:
5 | workflow_dispatch:
6 |
7 | permissions: write-all
8 |
9 | jobs:
10 | release:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Install Go
14 | uses: actions/setup-go@v3
15 | with:
16 | go-version: 1.20.x
17 |
18 | - name: Add GOBIN to PATH
19 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
20 | shell: bash
21 |
22 | - name: Checkout code
23 | uses: actions/checkout@v3
24 | with:
25 | fetch-depth: 0
26 | token: ${{ secrets.RELEASE_TOKEN }}
27 |
28 | - name: Publish Release
29 | shell: bash
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
32 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
33 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
34 | run: |
35 | git config --global user.name nr-developer-toolkit
36 | git config --global user.email 62031461+nr-developer-toolkit@users.noreply.github.com
37 | make release
38 |
--------------------------------------------------------------------------------
/.github/workflows/security.yml:
--------------------------------------------------------------------------------
1 | name: Security Scan
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | security:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@master
14 | - name: Run Snyk to check for vulnerabilities
15 | uses: snyk/actions/golang@master
16 | env:
17 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
18 | with:
19 | args: --severity-threshold=high
20 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Testing
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | lint:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Install Go
14 | uses: actions/setup-go@v3
15 | with:
16 | go-version: 1.20.x
17 |
18 | - name: Add GOBIN to PATH
19 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
20 | shell: bash
21 |
22 | - name: Checkout code
23 | uses: actions/checkout@v3
24 | with:
25 | fetch-depth: 0
26 |
27 | - name: Cache deps
28 | uses: actions/cache@v3
29 | with:
30 | path: ~/go/pkg/mod
31 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
32 | restore-keys: |
33 | ${{ runner.os }}-go-
34 |
35 | - name: Lint
36 | run: make lint
37 |
38 | test-unit:
39 | runs-on: ubuntu-latest
40 | steps:
41 | - name: Install Go
42 | uses: actions/setup-go@v3
43 | with:
44 | go-version: 1.20.x
45 |
46 | - name: Add GOBIN to PATH
47 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
48 | shell: bash
49 |
50 | - name: Checkout code
51 | uses: actions/checkout@v3
52 |
53 | - name: Cache deps
54 | uses: actions/cache@v3
55 | with:
56 | path: ~/go/pkg/mod
57 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
58 | restore-keys: |
59 | ${{ runner.os }}-go-
60 |
61 | - name: Schema Cache
62 | uses: actions/cache@v3
63 | with:
64 | path: testdata/schema.json
65 | key: ${{ runner.os }}-schema
66 |
67 | - name: Unit Tests
68 | run: make test-unit
69 | env:
70 | NEW_RELIC_API_KEY: ${{ secrets.NEW_RELIC_API_KEY }}
71 |
72 | test-integration:
73 | runs-on: ubuntu-latest
74 | steps:
75 | - name: Install Go
76 | uses: actions/setup-go@v3
77 | with:
78 | go-version: 1.20.x
79 |
80 | - name: Add GOBIN to PATH
81 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
82 | shell: bash
83 |
84 | - name: Checkout code
85 | uses: actions/checkout@v3
86 |
87 | - name: Cache deps
88 | uses: actions/cache@v3
89 | with:
90 | path: ~/go/pkg/mod
91 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
92 | restore-keys: |
93 | ${{ runner.os }}-go-
94 |
95 | - name: Integration Tests
96 | run: make test-integration
97 | env:
98 | NEW_RELIC_API_KEY: ${{ secrets.NEW_RELIC_API_KEY }}
99 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode/
3 | bin/*
4 | coverage/*
5 | dist/*
6 | schema.json
7 | tmp/*
8 | vendor/*
9 | *.json
10 | *.yaml
11 | *.yml
12 | testdata/schema.json
13 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | # options for analysis running
2 | run:
3 | # timeout for analysis, e.g. 30s, 5m, default is 1m
4 | deadline: 5m
5 |
6 | # exit code when at least one issue was found, default is 1
7 | issues-exit-code: 1
8 |
9 | # include test files or not, default is true
10 | tests: true
11 |
12 | build-tags:
13 | - integration
14 | - unit
15 |
16 | modules-download-mode: readonly
17 |
18 | # all available settings of specific linters
19 | linters-settings:
20 | govet:
21 | # report about shadowed variables
22 | check-shadowing: true
23 | golint:
24 | # minimal confidence for issues, default is 0.8
25 | min-confidence: 0.5
26 | gocyclo:
27 | # minimal code complexity to report, 30 by default (but we recommend 10-20)
28 | min-complexity: 26
29 | maligned:
30 | # print struct with more effective memory layout or not, false by default
31 | suggest-new: true
32 | nolintlint:
33 | # Enable to ensure that nolint directives are all used. Default is true.
34 | allow-unused: false
35 | # Enable to require nolint directives to mention the specific linter being suppressed. Default is false.
36 | require-specific: true
37 | depguard:
38 | list-type: blacklist
39 | include-go-root: false
40 | packages:
41 | - github.com/davecgh/go-spew/spew
42 | misspell:
43 | ignore-words:
44 | - newrelic
45 | lll:
46 | # max line length, lines longer will be reported. Default is 120.
47 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option
48 | line-length: 150
49 |
50 | linters:
51 | disable-all: true
52 | enable:
53 | - errcheck
54 | - goimports
55 | - gocyclo
56 | - gofmt
57 | - gosimple
58 | - govet
59 | - ineffassign
60 | - misspell
61 | - nolintlint
62 | - staticcheck
63 | - thelper
64 | - unconvert
65 | - unused
66 | - vet
67 | - whitespace
68 |
69 | issues:
70 | # disable limits on issue reporting
71 | max-per-linter: 0
72 | max-same-issues: 0
73 |
74 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | project_name: tutone
2 |
3 | env:
4 | - GO111MODULE=on
5 |
6 | before:
7 | hooks:
8 | - go mod download
9 |
10 | builds:
11 | - id: tutone
12 | dir: cmd/tutone
13 | binary: tutone
14 | env:
15 | - CGO_ENABLED=0
16 | goos:
17 | - linux
18 | - darwin
19 | - windows
20 | goarch:
21 | - amd64
22 | ldflags:
23 | - -s -w -X main.version={{.Version}} -X main.appName={{.Binary}}
24 |
25 | release:
26 | name_template: "{{.ProjectName}} v{{.Version}}"
27 |
28 | archives:
29 | - id: "default"
30 | builds:
31 | - newrelic
32 | name_template: >-
33 | {{- .ProjectName }}_
34 | {{- title .Os }}_
35 | {{- if eq .Arch "amd64" }}x86_64
36 | {{- else if eq .Arch "386" }}i386
37 | {{- else }}{{ .Arch }}{{ end }}
38 | {{- if .Arm }}v{{ .Arm }}{{ end -}}
39 | format_overrides:
40 | - goos: windows
41 | format: zip
42 | files:
43 | - CHANGELOG.md
44 | - LICENSE
45 | - README.md
46 |
47 | # dockers:
48 | # - dockerfile: build/package/Dockerfile
49 | # image_templates:
50 | # - "newrelic/tutone:{{ .Tag }}"
51 | # - "newrelic/tutone:v{{ .Major }}.{{ .Minor }}"
52 | # - "newrelic/tutone:latest"
53 | # ids:
54 | # - tutone
55 | # build_flag_templates:
56 | # - "--pull"
57 | # - "--label=repository=http://github.com/newrelic/tutone"
58 | # - "--label=homepage=https://developer.newrelic.com/"
59 | # - "--label=maintainer=Developer Toolkit "
60 |
61 | # Already using git-chglog
62 | changelog:
63 | skip: true
64 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are always welcome. Before contributing please read the
4 | [code of conduct](blog/main/CODE_OF_CONDUCT.md) and [search the issue tracker](issues); your issue may have already been discussed or fixed in `main`. To contribute,
5 | [fork](https://help.github.com/articles/fork-a-repo/) this repository, commit your changes, and [send a Pull Request](https://help.github.com/articles/using-pull-requests/).
6 |
7 | Note that our [code of conduct](blog/main/CODE_OF_CONDUCT.md) applies to all platforms and venues related to this project; please follow it in all your interactions with the project and its participants.
8 |
9 | ## Feature Requests
10 |
11 | Feature requests should be submitted in the [Issue tracker](issues), with a description of the expected behavior & use case, where they’ll remain closed until sufficient interest, [e.g. :+1: reactions](https://help.github.com/articles/about-discussions-in-issues-and-pull-requests/), has been [shown by the community](issues?q=label%3A%22votes+needed%22+sort%3Areactions-%2B1-desc).
12 | Before submitting an Issue, please search for similar ones in the
13 | [closed issues](issues?q=is%3Aissue+is%3Aclosed+label%3Aenhancement).
14 |
15 | ## Pull Requests
16 |
17 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build.
18 | 2. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
19 | 3. You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you.
20 |
21 | ## Contributor License Agreement
22 |
23 | Keep in mind that when you submit your Pull Request, you'll need to sign the CLA via the click-through using CLA-Assistant. If you'd like to execute our corporate CLA, or if you have any questions, please drop us an email at opensource@newrelic.com.
24 |
25 | For more information about CLAs, please check out Alex Russell’s excellent post,
26 | [“Why Do I Need to Sign This?”](https://infrequently.org/2008/06/why-do-i-need-to-sign-this/).
27 |
28 | # Slack
29 |
30 | For contributors and maintainers of open source projects hosted by New Relic, we host a public Slack with a channel dedicated to this project. If you are contributing to this project, you're welcome to request access to that community space.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #############################
2 | # Global vars
3 | #############################
4 | PROJECT_NAME := $(shell basename $(shell pwd))
5 | PROJECT_VER ?= $(shell git describe --tags --always --dirty | sed -e '/^v/s/^v\(.*\)$$/\1/g')
6 | # Last released version (not dirty)
7 | PROJECT_VER_TAGGED := $(shell git describe --tags --always --abbrev=0 | sed -e '/^v/s/^v\(.*\)$$/\1/g')
8 |
9 | SRCDIR ?= .
10 | GO = go
11 |
12 | # The root module (from go.mod)
13 | PROJECT_MODULE ?= $(shell $(GO) list -m)
14 |
15 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
16 | ifeq (,$(shell go env GOBIN))
17 | GOBIN=$(shell go env GOPATH)/bin
18 | else
19 | GOBIN=$(shell go env GOBIN)
20 | endif
21 |
22 | #############################
23 | # Targets
24 | #############################
25 | all: build
26 |
27 | # Humans running make:
28 | build: git-hooks check-version clean lint test cover-report compile
29 |
30 | # Build command for CI tooling
31 | build-ci: check-version clean lint test compile-only
32 |
33 | # All clean commands
34 | clean: cover-clean compile-clean release-clean
35 |
36 | # Import fragments
37 | include build/compile.mk
38 | include build/deps.mk
39 | include build/docker.mk
40 | include build/document.mk
41 | include build/lint.mk
42 | include build/release.mk
43 | include build/test.mk
44 | include build/tools.mk
45 | include build/util.mk
46 |
47 | .PHONY: all build build-ci clean
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/newrelic/open-source-office/blob/master/examples/categories/index.md#category-community-project)
2 |
3 | # Tutone
4 |
5 | [](https://github.com/newrelic/tutone/actions)
6 | [](https://github.com/newrelic/tutone/actions)
7 | [](https://goreportcard.com/report/github.com/newrelic/tutone)
8 | [](https://godoc.org/github.com/newrelic/tutone)
9 | [](https://github.com/newrelic/tutone/blob/main/LICENSE)
10 | [](https://cla-assistant.io/newrelic/tutone)
11 | [](https://github.com/newrelic/tutone/releases/latest)
12 |
13 | Code generation tool
14 |
15 | Generate code from GraphQL schema introspection.
16 |
17 | ## Summary
18 |
19 | At a high level, the following workflow is used to generate code.
20 |
21 | - `tutone fetch` calls the NerdGraph API to introspect the schema.
22 | - The schema is cached in `schema.json`. This is information about the GraphQL schema
23 | - `tutone generate` uses the `schema.json` + the configuration + the templates to output generated text.
24 |
25 | ## Installation
26 |
27 | ```
28 | go install github.com/newrelic/tutone/cmd/tutone@latest
29 | ```
30 |
31 | ## Command Flags
32 |
33 | Flags for running the typegen command:
34 |
35 | | Flag | Description |
36 | | ------------------- | ------------------------------------------------------------------------------ |
37 | | `-p ` | Package name used within the generated file. Overrides the configuration file. |
38 | | `-v` | Enable verbose logging |
39 |
40 | ## Getting Started
41 |
42 | 1. Create a project configuration file, see `configs/tutone.yaml` for an example.
43 | 2. Generate a `schema.json` using the following command:
44 |
45 | ```bash
46 | $ tutone fetch \
47 | --config path/to/tutone.yaml \
48 | --schema path/to/schema.json
49 | ```
50 |
51 | 3. Add a `./path/to/package/typegen.yaml` configuration with the type you want generated:
52 |
53 | ```yaml
54 | ---
55 | types:
56 | - name: MyGraphQLTypeName
57 | - name: AnotherTypeInGraphQL
58 | createAs: map[string]int
59 | ```
60 |
61 | 4. Add a generation command inside the `main.go` (or equivalent)
62 |
63 | ```go
64 | // Package CoolPackage provides cool stuff, based on generated types
65 | //go:generate tutone generate -p $GOPACKAGE
66 | package CoolPackage
67 | // ... implementation ...
68 | ```
69 |
70 | 5. Run `go generate`
71 | 6. Add the `./path/to/package/types.go` file to your repo
72 |
73 | # Configuration
74 |
75 | ## Configuration File
76 |
77 | An example configuration can be found in [this project repo][example_config].
78 |
79 | A configuration file is meant to represent a single project, with
80 | specifications for which parts of the schema to process.
81 |
82 | Please see the [config documentation][pkg_go_dev] for details about specific fields.
83 |
84 | ## Package Schema
85 |
86 | The `packages` field in the configuration contains the details about which
87 | types and mutations to include from the schema, and where the package is located.
88 |
89 | | Name | Required | Description |
90 | | ---------- | -------- | ----------------------------------------------------------------------- |
91 | | name | Yes | The name of the package |
92 | | path | Yes | Name of the package the output file will be part of (see `-p` flag) |
93 | | generators | Yes | A list of generator names from the `generators` field |
94 | | mutations | No | A list of mutations from which to infer types |
95 | | types | No | A list of types from which to start expanding the inferred set of types |
96 |
97 | Please see the [package schema doc](docs/package-schema.md) for detailed information about the schema structure.
98 |
99 | #### Type Configuration
100 |
101 | To fine-tune the types that are created, or not create them at all, the
102 | following options are supported:
103 |
104 | | Name | Required | Description |
105 | | --------------------- | -------- | ----------- |
106 | | `name` | yes | Name of the type to match |
107 | | `create_as` | no | Used when creating a new scalar type to determine which Go type to use. |
108 | | `field_type_override` | no | Golang type to override whatever the default detected type would be for a given field. |
109 | | `interface_methods` | no | List of additional methods that are added to an interface definition. The methods are not defined in the code, so must be implemented by the user. |
110 | | `skip_type_create` | no | Allows the user to skip creating a type. |
111 |
112 | ### Generators
113 |
114 | The `generators` field is used to describe a given generator. The generator is
115 | where the bulk of the work is done. Note that the configuration name
116 | referenced must match the name of the attached generated in the
117 | `pkg/generate/generate.go` file.
118 |
119 | The generator configuration specifies details about how the generator should adjust the output of the work.
120 |
121 | | Name | Required | Description |
122 | | -------- | -------- | ---------------------------------------------------------------------------- |
123 | | name | Yes | The name of the generator used in `pkg/generate/generate.go` file |
124 | | fileName | No | Where to write the output of the generated code within the specified package |
125 |
126 | ## Templates
127 |
128 | Example templates are available in the [templates/](templates/) directory, and are dynamically loaded from your project.
129 |
130 | * Rendered using [text/template](https://golang.org/pkg/text/template/)
131 | * Additional pipeline functions from [sprig](http://masterminds.github.io/sprig/)
132 |
133 |
134 | ## Community
135 |
136 | New Relic hosts and moderates an online forum where customers can interact with New Relic employees as well as other customers to get help and share best practices.
137 |
138 | - [Roadmap](https://newrelic.github.io/developer-toolkit/roadmap/) - As part of the Developer Toolkit, the roadmap for this project follows the same RFC process
139 | - [Issues or Enhancement Requests](https://github.com/newrelic/tutone/issues) - Issues and enhancement requests can be submitted in the Issues tab of this repository. Please search for and review the existing open issues before submitting a new issue.
140 | - [Contributors Guide](CONTRIBUTING.md) - Contributions are welcome (and if you submit a Enhancement Request, expect to be invited to contribute it yourself :grin:).
141 | - [Community discussion board](https://discuss.newrelic.com/c/build-on-new-relic/developer-toolkit) - Like all official New Relic open source projects, there's a related Community topic in the New Relic Explorers Hub.
142 |
143 | Keep in mind that when you submit your pull request, you'll need to sign the CLA via the click-through using CLA-Assistant. If you'd like to execute our corporate CLA, or if you have any questions, please drop us an email at opensource@newrelic.com.
144 |
145 | ## Development
146 |
147 | ### Requirements
148 |
149 | - Go 1.19.0+
150 | - GNU Make
151 | - git
152 |
153 | ### Building
154 |
155 | # Default target is 'build'
156 | $ make
157 |
158 | # Explicitly run build
159 | $ make build
160 |
161 | # Locally test the CI build scripts
162 | # make build-ci
163 |
164 | ### Testing
165 |
166 | Before contributing, all linting and tests must pass. Tests can be run directly via:
167 |
168 | # Tests and Linting
169 | $ make test
170 |
171 | # Only unit tests
172 | $ make test-unit
173 |
174 | # Only integration tests
175 | $ make test-integration
176 |
177 | *Note:* You'll need to update `testdata/schema.json` to the latest GraphQL schema for tests to run
178 | correctly.
179 |
180 | ### Commit Messages
181 |
182 | Using the following format for commit messages allows for auto-generation of
183 | the [CHANGELOG](CHANGELOG.md):
184 |
185 | #### Format:
186 |
187 | `(): `
188 |
189 | | Type | Description | Change log? |
190 | | ---------- | --------------------- | :---------: |
191 | | `chore` | Maintenance type work | No |
192 | | `docs` | Documentation Updates | Yes |
193 | | `feat` | New Features | Yes |
194 | | `fix` | Bug Fixes | Yes |
195 | | `refactor` | Code Refactoring | No |
196 |
197 | #### Scope
198 |
199 | This refers to what part of the code is the focus of the work. For example:
200 |
201 | **General:**
202 |
203 | - `build` - Work related to the build system (linting, makefiles, CI/CD, etc)
204 | - `release` - Work related to cutting a new release
205 |
206 | **Package Specific:**
207 |
208 | - `newrelic` - Work related to the New Relic package
209 | - `http` - Work related to the `internal/http` package
210 | - `alerts` - Work related to the `pkg/alerts` package
211 |
212 | ### Documentation
213 |
214 | **Note:** This requires the repo to be in your GOPATH [(godoc issue)](https://github.com/golang/go/issues/26827)
215 |
216 | $ make docs
217 |
218 |
219 | ### Releasing
220 |
221 | Releases are automated via the Release Github Action on merges to the default branch. No user interaction is required.
222 |
223 | Using [svu](https://github.com/caarlos0/svu), commit messages are parsed to identify if a new release is needed, and to what extent. Here's the breakdown:
224 |
225 | | Commit message | Tag increase |
226 | | -------------------------------------------------------------------------------------- | ------------ |
227 | | `fix: fixed something` | Patch |
228 | | `feat: added new button to do X` | Minor |
229 | | `fix: fixed thing xyz`
`BREAKING CHANGE: this will break users because of blah` | Major |
230 | | `fix!: fixed something` | Major |
231 | | `feat!: added blah` | Major |
232 | | `chore: foo` | Nothing |
233 | | `refactor: updated bar` | Nothing |
234 |
235 |
236 | ## Support
237 |
238 | New Relic has open-sourced this project. This project is provided AS-IS WITHOUT WARRANTY OR SUPPORT, although you can report issues and contribute to the project here on GitHub.
239 |
240 | _Please do not report issues with this software to New Relic Global Technical Support._
241 |
242 | ## Open Source License
243 |
244 | This project is distributed under the [Apache 2 license](LICENSE).
245 |
246 | [example_config]: https://github.com/newrelic/tutone/blob/main/configs/tutone.yml
247 |
248 | [pkg_go_dev]: https://pkg.go.dev/github.com/newrelic/tutone@v0.2.3/internal/config?tab=doc
249 |
--------------------------------------------------------------------------------
/build/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newrelic/tutone/11d56607628a3619e58f4a7ce39fc144ed2b8bb9/build/.keep
--------------------------------------------------------------------------------
/build/compile.mk:
--------------------------------------------------------------------------------
1 | #
2 | # Makefile Fragment for Compiling
3 | #
4 |
5 | GO ?= go
6 | BUILD_DIR ?= ./bin/
7 | PROJECT_MODULE ?= $(shell $(GO) list -m)
8 | # $b replaced by the binary name in the compile loop, -s/w remove debug symbols
9 | LDFLAGS ?= "-s -w -X main.version=$(PROJECT_VER) -X main.appName=$$b -X $(PROJECT_MODULE)/internal/client.version=$(PROJECT_VER)"
10 | SRCDIR ?= .
11 | COMPILE_OS ?= darwin linux windows
12 |
13 | # Determine commands by looking into cmd/*
14 | COMMANDS ?= $(wildcard ${SRCDIR}/cmd/*)
15 |
16 | # Determine binary names by stripping out the dir names
17 | BINS := $(foreach cmd,${COMMANDS},$(notdir ${cmd}))
18 |
19 |
20 | compile-clean:
21 | @echo "=== $(PROJECT_NAME) === [ compile-clean ]: removing binaries..."
22 | @rm -rfv $(BUILD_DIR)/*
23 |
24 | compile: deps compile-only
25 |
26 | compile-all: deps-only
27 | @echo "=== $(PROJECT_NAME) === [ compile ]: building commands:"
28 | @mkdir -p $(BUILD_DIR)/$(GOOS)
29 | @for b in $(BINS); do \
30 | for os in $(COMPILE_OS); do \
31 | echo "=== $(PROJECT_NAME) === [ compile ]: $(BUILD_DIR)$$os/$$b"; \
32 | BUILD_FILES=`find $(SRCDIR)/cmd/$$b -type f -name "*.go"` ; \
33 | GOOS=$$os $(GO) build -ldflags=$(LDFLAGS) -o $(BUILD_DIR)/$$os/$$b $$BUILD_FILES ; \
34 | done \
35 | done
36 |
37 | compile-only: deps-only
38 | @echo "=== $(PROJECT_NAME) === [ compile ]: building commands:"
39 | @mkdir -p $(BUILD_DIR)/$(GOOS)
40 | @for b in $(BINS); do \
41 | echo "=== $(PROJECT_NAME) === [ compile ]: $(BUILD_DIR)$(GOOS)/$$b"; \
42 | BUILD_FILES=`find $(SRCDIR)/cmd/$$b -type f -name "*.go"` ; \
43 | GOOS=$(GOOS) $(GO) build -ldflags=$(LDFLAGS) -o $(BUILD_DIR)/$(GOOS)/$$b $$BUILD_FILES ; \
44 | done
45 |
46 | # Override GOOS for these specific targets
47 | compile-darwin: GOOS=darwin
48 | compile-darwin: deps-only compile-only
49 |
50 | compile-linux: GOOS=linux
51 | compile-linux: deps-only compile-only
52 |
53 | compile-windows: GOOS=windows
54 | compile-windows: deps-only compile-only
55 |
56 |
57 | .PHONY: clean-compile compile compile-darwin compile-linux compile-only compile-windows
58 |
--------------------------------------------------------------------------------
/build/deps.mk:
--------------------------------------------------------------------------------
1 | #
2 | # Makefile fragment for installing deps
3 | #
4 |
5 | GO ?= go
6 | VENDOR_CMD ?= ${GO} mod tidy
7 |
8 | deps: tools deps-only
9 |
10 | deps-only:
11 | @echo "=== $(PROJECT_NAME) === [ deps ]: Installing package dependencies required by the project..."
12 | @$(VENDOR_CMD)
13 |
14 | .PHONY: deps deps-only
15 |
--------------------------------------------------------------------------------
/build/docker.mk:
--------------------------------------------------------------------------------
1 | #
2 | # Makefile fragment for Docker actions
3 | #
4 | DOCKER ?= docker
5 | DOCKER_FILE ?= build/package/Dockerfile
6 | DOCKER_IMAGE ?= newrelic/cli
7 | DOCKER_IMAGE_TAG ?= snapshot
8 |
9 | # Build the docker image
10 | docker-build: compile-linux
11 | @echo "=== $(PROJECT_NAME) === [ docker-build ]: Creating docker image: $(DOCKER_IMAGE):$(DOCKER_IMAGE_TAG) ..."
12 | docker build -f $(DOCKER_FILE) -t $(DOCKER_IMAGE):$(DOCKER_IMAGE_TAG) $(BUILD_DIR)/linux/
13 |
14 |
15 | docker-login:
16 | @echo "=== $(PROJECT_NAME) === [ docker-login ]: logging into docker hub"
17 | @if [ -z "${DOCKER_USERNAME}" ]; then \
18 | echo "Failure: DOCKER_USERNAME not set" ; \
19 | exit 1 ; \
20 | fi
21 | @if [ -z "${DOCKER_PASSWORD}" ]; then \
22 | echo "Failure: DOCKER_PASSWORD not set" ; \
23 | exit 1 ; \
24 | fi
25 | @echo "=== $(PROJECT_NAME) === [ docker-login ]: username: '$$DOCKER_USERNAME'"
26 | @echo ${DOCKER_PASSWORD} | $(DOCKER) login -u ${DOCKER_USERNAME} --password-stdin
27 |
28 |
29 | # Push the docker image
30 | docker-push: docker-login docker-build
31 | @echo "=== $(PROJECT_NAME) === [ docker-push ]: Pushing docker image: $(DOCKER_IMAGE):$(DOCKER_IMAGE_TAG) ..."
32 | @$(DOCKER) push $(DOCKER_IMAGE):$(DOCKER_IMAGE_TAG)
33 |
34 | .PHONY: docker-build docker-login docker-push
35 |
--------------------------------------------------------------------------------
/build/document.mk:
--------------------------------------------------------------------------------
1 | #
2 | # Makefile fragment for displaying auto-generated documentation
3 | #
4 |
5 | GODOC ?= godoc
6 | GODOC_HTTP ?= "localhost:6060"
7 |
8 | CHANGELOG_CMD ?= git-chglog
9 | CHANGELOG_FILE ?= CHANGELOG.md
10 | RELEASE_NOTES_FILE ?= relnotes.md
11 |
12 | docs: tools
13 | @echo "=== $(PROJECT_NAME) === [ docs ]: Starting godoc server..."
14 | @echo "=== $(PROJECT_NAME) === [ docs ]:"
15 | @echo "=== $(PROJECT_NAME) === [ docs ]: NOTE: This only works if this codebase is in your GOPATH!"
16 | @echo "=== $(PROJECT_NAME) === [ docs ]: godoc issue: https://github.com/golang/go/issues/26827"
17 | @echo "=== $(PROJECT_NAME) === [ docs ]:"
18 | @echo "=== $(PROJECT_NAME) === [ docs ]: Module Docs: http://$(GODOC_HTTP)/pkg/$(PROJECT_MODULE)"
19 | @$(GODOC) -http=$(GODOC_HTTP)
20 |
21 | changelog: tools
22 | @echo "=== $(PROJECT_NAME) === [ changelog ]: Generating changelog..."
23 | @$(CHANGELOG_CMD) --silent -o $(CHANGELOG_FILE)
24 |
25 | release-notes: tools
26 | @echo "=== $(PROJECT_NAME) === [ release-notes ]: Generating release notes for v$(PROJECT_VER_TAGGED) ..."
27 | @mkdir -p $(SRCDIR)/tmp
28 | @$(CHANGELOG_CMD) --silent -o $(SRCDIR)/tmp/$(RELEASE_NOTES_FILE) v$(PROJECT_VER_TAGGED)
29 |
30 | .PHONY: docs changelog release-notes
31 |
--------------------------------------------------------------------------------
/build/lint.mk:
--------------------------------------------------------------------------------
1 | #
2 | # Makefile fragment for Linting
3 | #
4 |
5 | GO ?= go
6 | MISSPELL ?= misspell
7 | GOFMT ?= gofmt
8 | GOIMPORTS ?= goimports
9 |
10 | GOLINTER = golangci-lint
11 |
12 | EXCLUDEDIR ?= .git
13 | SRCDIR ?= .
14 | GO_PKGS ?= $(shell ${GO} list ./... | grep -v -e "/vendor/" -e "/example")
15 | SPELL_FILES ?= $(shell find ${SRCDIR} -type f | grep -v -e '.git/' -e '/vendor/' -e 'go.\(mod\|sum\)' -e '/testdata/' -e '.golangci.yml')
16 | GO_FILES ?= $(shell find $(SRCDIR) -type f -name "*.go" | grep -v -e ".git/" -e '/vendor/' -e '/example/')
17 | PROJECT_MODULE ?= $(shell $(GO) list -m)
18 |
19 | GO_MOD_OUTDATED ?= go-mod-outdated
20 |
21 | lint: deps spell-check gofmt golangci goimports outdated
22 | lint-fix: deps spell-check-fix gofmt-fix goimports
23 |
24 | #
25 | # Check spelling on all the files, not just source code
26 | #
27 | spell-check: deps
28 | @echo "=== $(PROJECT_NAME) === [ spell-check ]: Checking for spelling mistakes with $(MISSPELL)..."
29 | @$(MISSPELL) -source text $(SPELL_FILES)
30 |
31 | spell-check-fix: deps
32 | @echo "=== $(PROJECT_NAME) === [ spell-check-fix ]: Fixing spelling mistakes with $(MISSPELL)..."
33 | @$(MISSPELL) -source text -w $(SPELL_FILES)
34 |
35 | gofmt: deps
36 | @echo "=== $(PROJECT_NAME) === [ gofmt ]: Checking file format with $(GOFMT)..."
37 | @find . -path "$(EXCLUDEDIR)" -prune -print0 | xargs -0 $(GOFMT) -e -l -s -d ${SRCDIR}
38 |
39 | gofmt-fix: deps
40 | @echo "=== $(PROJECT_NAME) === [ gofmt-fix ]: Fixing file format with $(GOFMT)..."
41 | @find . -path "$(EXCLUDEDIR)" -prune -print0 | xargs -0 $(GOFMT) -e -l -s -w ${SRCDIR}
42 |
43 | goimports: deps
44 | @echo "=== $(PROJECT_NAME) === [ goimports ]: Checking imports with $(GOIMPORTS)..."
45 | @$(GOIMPORTS) -l -w -local $(PROJECT_MODULE) $(GO_FILES)
46 |
47 | golangci: deps
48 | @echo "=== $(PROJECT_NAME) === [ golangci-lint ]: Linting using $(GOLINTER) ($(COMMIT_LINT_CMD))..."
49 | @$(GOLINTER) run
50 |
51 | outdated: deps tools-outdated
52 | @echo "=== $(PROJECT_NAME) === [ outdated ]: Finding outdated deps with $(GO_MOD_OUTDATED)..."
53 | @$(GO) list -u -m -json all | $(GO_MOD_OUTDATED) -direct -update
54 |
55 | .PHONY: lint spell-check spell-check-fix gofmt gofmt-fix lint-fix outdated goimports
56 |
--------------------------------------------------------------------------------
/build/package/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.16
2 |
3 | # Add the binary
4 | COPY tutone /bin/tutone
5 |
6 | ENTRYPOINT ["/bin/tutone"]
7 |
--------------------------------------------------------------------------------
/build/release.mk:
--------------------------------------------------------------------------------
1 | RELEASE_SCRIPT ?= ./scripts/release.sh
2 |
3 | REL_CMD ?= $(GOBIN)/goreleaser
4 | DIST_DIR ?= ./dist
5 |
6 | # Versioning info
7 | VER_CMD ?= $(GOBIN)/svu
8 | VER_BUMP ?= $(GOBIN)/gobump
9 | VER_PKG ?= internal/version
10 |
11 | # Technically relies on tools, but we don't want the status output
12 | version: tools
13 | @echo "=== $(PROJECT_NAME) === [ version ]: Versions:"
14 | @printf "Next: "
15 | @$(VER_CMD) next
16 | @printf "Tag: "
17 | @$(VER_CMD) current
18 | @printf "Code: v"
19 | @$(VER_BUMP) show -r $(VER_PKG)
20 |
21 | # Example usage: make release
22 | release: clean tools
23 | @echo "=== $(PROJECT_NAME) === [ release ]: Generating release..."
24 | @$(RELEASE_SCRIPT)
25 |
26 | release-clean:
27 | @echo "=== $(PROJECT_NAME) === [ release-clean ]: distribution files..."
28 | @rm -rfv $(DIST_DIR) $(SRCDIR)/tmp
29 |
30 | release-build: clean tools release-notes
31 | @echo "=== $(PROJECT_NAME) === [ release-build ]: Building release..."
32 | $(REL_CMD) build
33 |
34 | release-package: clean tools release-notes
35 | @echo "=== $(PROJECT_NAME) === [ release-publish ]: Packaging release..."
36 | $(REL_CMD) release --skip-publish --release-notes=$(SRCDIR)/tmp/$(RELEASE_NOTES_FILE)
37 |
38 | # Local Snapshot
39 | snapshot: clean tools release-notes
40 | @echo "=== $(PROJECT_NAME) === [ snapshot ]: Creating release snapshot..."
41 | @echo "=== $(PROJECT_NAME) === [ snapshot ]: THIS WILL NOT BE PUBLISHED!"
42 | @$(REL_CMD) --skip-publish --snapshot --release-notes=$(SRCDIR)/tmp/$(RELEASE_NOTES_FILE)
43 |
44 | .PHONY: release release-clean release-publish snapshot
45 |
--------------------------------------------------------------------------------
/build/test.mk:
--------------------------------------------------------------------------------
1 | #
2 | # Makefile fragment for Testing
3 | #
4 |
5 | GO ?= go
6 | GOLINTER ?= golangci-lint
7 | MISSPELL ?= misspell
8 | GOFMT ?= gofmt
9 | TEST_RUNNER ?= gotestsum
10 |
11 | COVERAGE_DIR ?= ./coverage/
12 | COVERMODE ?= atomic
13 | SRCDIR ?= .
14 | GO_PKGS ?= $(shell $(GO) list ./... | grep -v -e "/vendor/" -e "/example")
15 | FILES ?= $(shell find $(SRCDIR) -type f | grep -v -e '.git/' -e '/vendor/')
16 |
17 | PROJECT_MODULE ?= $(shell $(GO) list -m)
18 |
19 | LDFLAGS_UNIT ?= '-X $(PROJECT_MODULE)/internal/version.GitTag=$(PROJECT_VER_TAGGED)'
20 |
21 | test: test-only
22 | test-only: test-unit test-integration
23 |
24 | test-prep: compile-only
25 | @echo "=== $(PROJECT_NAME) === [ test-prep ]: caching schema for tests..."
26 | @$(BUILD_DIR)$(GOOS)/tutone fetch -s testdata/schema.json -c configs/tutone.yml
27 |
28 | test-unit: tools test-prep
29 | @echo "=== $(PROJECT_NAME) === [ test-unit ]: running unit tests..."
30 | @mkdir -p $(COVERAGE_DIR)
31 | @$(TEST_RUNNER) -f testname --junitfile $(COVERAGE_DIR)/unit.xml -- -v -ldflags=$(LDFLAGS_UNIT) -parallel 4 -tags unit -covermode=$(COVERMODE) -coverprofile $(COVERAGE_DIR)/unit.tmp $(GO_PKGS)
32 |
33 | test-integration: tools test-prep
34 | @echo "=== $(PROJECT_NAME) === [ test-integration ]: running integration tests..."
35 | @mkdir -p $(COVERAGE_DIR)
36 | @$(TEST_RUNNER) -f testname --junitfile $(COVERAGE_DIR)/integration.xml --rerun-fails=3 --packages "$(GO_PKGS)" -- -v -parallel 4 -tags integration -covermode=$(COVERMODE) -coverprofile $(COVERAGE_DIR)/integration.tmp $(GO_PKGS)
37 |
38 |
39 | #
40 | # Coverage
41 | #
42 | cover-clean:
43 | @echo "=== $(PROJECT_NAME) === [ cover-clean ]: removing coverage files..."
44 | @rm -rfv $(COVERAGE_DIR)/*
45 |
46 | cover-report:
47 | @echo "=== $(PROJECT_NAME) === [ cover-report ]: generating coverage results..."
48 | @mkdir -p $(COVERAGE_DIR)
49 | @echo 'mode: $(COVERMODE)' > $(COVERAGE_DIR)/coverage.out
50 | @cat $(COVERAGE_DIR)/*.tmp | grep -v 'mode: $(COVERMODE)' >> $(COVERAGE_DIR)/coverage.out || true
51 | @$(GO) tool cover -html=$(COVERAGE_DIR)/coverage.out -o $(COVERAGE_DIR)/coverage.html
52 | @echo "=== $(PROJECT_NAME) === [ cover-report ]: $(COVERAGE_DIR)coverage.html"
53 |
54 | cover-view: cover-report
55 | @$(GO) tool cover -html=$(COVERAGE_DIR)/coverage.out
56 |
57 | .PHONY: test test-only test-unit test-integration cover-report cover-view
58 |
--------------------------------------------------------------------------------
/build/tools.mk:
--------------------------------------------------------------------------------
1 | #
2 | # Makefile fragment for installing tools
3 | #
4 |
5 | GO ?= go
6 | GOFMT ?= gofmt
7 | GO_MOD_OUTDATED ?= go-mod-outdated
8 | BUILD_DIR ?= ./bin/
9 |
10 | # Go file to track tool deps with go modules
11 | TOOL_DIR ?= tools
12 | TOOL_CONFIG ?= $(TOOL_DIR)/tools.go
13 |
14 | GOTOOLS ?= $(shell cd $(TOOL_DIR) && go list -f '{{ .Imports }}' -tags tools |tr -d '[]')
15 |
16 | tools: check-version
17 | @echo "=== $(PROJECT_NAME) === [ tools ]: Installing tools required by the project..."
18 | @cd $(TOOL_DIR) && $(GO) mod download
19 | @cd $(TOOL_DIR) && $(GO) install $(GOTOOLS)
20 | @cd $(TOOL_DIR) && $(GO) mod tidy
21 |
22 | tools-outdated: check-version
23 | @echo "=== $(PROJECT_NAME) === [ tools-outdated ]: Finding outdated tool deps with $(GO_MOD_OUTDATED)..."
24 | @cd $(TOOL_DIR) && $(GO) list -u -m -json all | $(GO_MOD_OUTDATED) -direct -update
25 |
26 | tools-update: check-version
27 | @echo "=== $(PROJECT_NAME) === [ tools-update ]: Updating tools required by the project..."
28 | @cd $(TOOL_DIR) && for x in $(GOTOOLS); do \
29 | $(GO) get -u $$x; \
30 | done
31 | @cd $(TOOL_DIR) && $(GO) mod tidy
32 |
33 | .PHONY: tools tools-update tools-outdated
34 |
--------------------------------------------------------------------------------
/build/util.mk:
--------------------------------------------------------------------------------
1 | #
2 | # Makefile fragment for utility items
3 | #
4 |
5 | NATIVEOS ?= $(shell go version | awk -F '[ /]' '{print $$4}')
6 | NATIVEARCH ?= $(shell go version | awk -F '[ /]' '{print $$5}')
7 |
8 | GIT_HOOKS_PATH ?= .githooks
9 |
10 | git-hooks:
11 | @echo "=== $(PROJECT_NAME) === [ git-hooks ]: Configuring git hooks..."
12 | @git config core.hooksPath $(GIT_HOOKS_PATH)
13 |
14 | check-version:
15 | ifdef GOOS
16 | ifneq "$(GOOS)" "$(NATIVEOS)"
17 | $(error GOOS is not $(NATIVEOS). Cross-compiling is only allowed for 'clean', 'deps-only' and 'compile-only' targets)
18 | endif
19 | else
20 | GOOS = ${NATIVEOS}
21 | endif
22 | ifdef GOARCH
23 | ifneq "$(GOARCH)" "$(NATIVEARCH)"
24 | $(error GOARCH variable is not $(NATIVEARCH). Cross-compiling is only allowed for 'clean', 'deps-only' and 'compile-only' targets)
25 | endif
26 | else
27 | GOARCH = ${NATIVEARCH}
28 | endif
29 |
30 | .PHONY: check-version
31 |
--------------------------------------------------------------------------------
/cla.md:
--------------------------------------------------------------------------------
1 | # NEW RELIC, INC.
2 | ## INDIVIDUAL CONTRIBUTOR LICENSE AGREEMENT
3 | Thank you for your interest in contributing to the open source projects of New Relic, Inc. (“New Relic”). In order to clarify the intellectual property license granted with Contributions from any person or entity, New Relic must have a Contributor License Agreement ("Agreement") on file that has been signed by each Contributor, indicating agreement to the license terms below. This Agreement is for your protection as a Contributor as well as the protection of New Relic; it does not change your rights to use your own Contributions for any other purpose.
4 |
5 | You accept and agree to the following terms and conditions for Your present and future Contributions submitted to New Relic. Except for the licenses granted herein to New Relic and recipients of software distributed by New Relic, You reserve all right, title, and interest in and to Your Contributions.
6 |
7 | ## Definitions.
8 | 1. "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is entering into this Agreement with New Relic. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
9 | 2. "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to New Relic for inclusion in, or documentation of, any of the products managed or maintained by New Relic (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to New Relic or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, New Relic for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
10 | 3. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to New Relic and to recipients of software distributed by New Relic a perpetual, worldwide, non-exclusive, no-charge, royalty-free, transferable, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
11 | 4. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to New Relic and to recipients of software distributed by New Relic a perpetual, worldwide, non-exclusive, no-charge, royalty-free, transferable, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contributions alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Work to which You have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
12 | 5. You represent that You are legally entitled to grant the above licenses. If Your employer(s) has rights to intellectual property that You create that includes Your Contributions, You represent that You have received permission to make Contributions on behalf of that employer, that Your employer has waived such rights for Your Contributions to New Relic, or that Your employer has executed a separate Agreement with New Relic.
13 | 6. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which You are personally aware and which are associated with any part of Your Contributions.
14 | 7. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
15 | 8. Should You wish to submit work that is not Your original creation, You may submit it to New Relic separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which You are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
16 | 9. You agree to notify New Relic of any facts or circumstances of which You become aware that would make these representations inaccurate in any respect.
--------------------------------------------------------------------------------
/cmd/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newrelic/tutone/11d56607628a3619e58f4a7ce39fc144ed2b8bb9/cmd/.keep
--------------------------------------------------------------------------------
/cmd/tutone/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | log "github.com/sirupsen/logrus"
5 | "github.com/spf13/cobra"
6 | "github.com/spf13/viper"
7 |
8 | "github.com/newrelic/tutone/internal/util"
9 | "github.com/newrelic/tutone/internal/version"
10 | "github.com/newrelic/tutone/pkg/fetch"
11 | "github.com/newrelic/tutone/pkg/generate"
12 | )
13 |
14 | var (
15 | appName = "tutone"
16 | cfgFile string
17 | )
18 |
19 | // Command represents the base command when called without any subcommands
20 | var Command = &cobra.Command{
21 | Use: appName,
22 | Short: "Golang code generation from GraphQL",
23 | Long: `Generate Go code based on the introspection of a GraphQL server`,
24 | Version: version.Version,
25 | DisableAutoGenTag: true, // Do not print generation date on documentation
26 | }
27 |
28 | func main() {
29 | err := Command.Execute()
30 | if err != nil {
31 | log.Fatal(err)
32 | }
33 | }
34 |
35 | func init() {
36 | // Setup basic log stuff
37 | logFormatter := &log.TextFormatter{
38 | FullTimestamp: true,
39 | PadLevelText: true,
40 | }
41 | log.SetFormatter(logFormatter)
42 |
43 | // Get Cobra going on flags
44 | cobra.OnInitialize(initConfig)
45 |
46 | // Config File
47 | Command.PersistentFlags().StringVarP(&cfgFile, "config", "c", ".tutone.yml", "Path to a configuration file")
48 |
49 | // Log level flag
50 | Command.PersistentFlags().StringP("loglevel", "l", "info", "Log level")
51 | viper.SetDefault("log_level", "info")
52 | util.LogIfError(log.ErrorLevel, viper.BindPFlag("log_level", Command.PersistentFlags().Lookup("loglevel")))
53 |
54 | // Add sub commands
55 | Command.AddCommand(fetch.Command)
56 | Command.AddCommand(generate.Command)
57 | }
58 |
59 | func initConfig() {
60 | viper.SetEnvPrefix("TUTONE")
61 | viper.AutomaticEnv()
62 |
63 | // Read config using Viper
64 | if cfgFile != "" {
65 | // Use config file from the flag.
66 | viper.SetConfigFile(cfgFile)
67 | } else {
68 | viper.SetConfigName("tutone")
69 | viper.SetConfigType("yaml")
70 | viper.AddConfigPath(".")
71 | viper.AddConfigPath(".tutone")
72 | }
73 |
74 | err := viper.ReadInConfig()
75 | if err != nil {
76 | switch e := err.(type) {
77 | case viper.ConfigFileNotFoundError:
78 | log.Debug("no config file found, using defaults")
79 | case viper.ConfigParseError:
80 | log.Errorf("error parsing config file: %v", e)
81 | default:
82 | log.Errorf("unknown error: %v", e)
83 | }
84 | }
85 |
86 | logLevel, err := log.ParseLevel(viper.GetString("log_level"))
87 | if err != nil {
88 | log.Fatal(err)
89 | }
90 | log.SetLevel(logLevel)
91 | }
92 |
--------------------------------------------------------------------------------
/configs/tutone.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # Log level for running tutone
3 | # Default: info
4 | log_level: debug
5 |
6 | # File to store a copy of the schema
7 | # Default: schema.json
8 | cache:
9 | schema_file: schema.json
10 |
11 | # GraphQL endpoint to query for schema
12 | # Required
13 | endpoint: https://api.newrelic.com/graphql
14 |
15 | # How to authenticate to the API
16 | auth:
17 | # Header set with the API key for authentication
18 | # Default: Api-Key
19 | header: Api-Key
20 |
21 | # Environment variable to get the API key from
22 | # Default: TUTONE_API_KEY
23 | api_key_env_var: NEW_RELIC_API_KEY
24 |
25 | packages:
26 | - name: nerdgraph
27 | path: pkg/nerdgraph
28 | import_path: github.com/newrelic/newrelic-client-go/pkg/nerdgraph
29 | generators:
30 | - typegen
31 | mutations:
32 | - name: apiAccessCreateKeys
33 | types:
34 | - name: AlertsPolicy
35 | - name: ID
36 | field_type_override: string
37 | skip_type_create: true
38 |
39 | - name: cloud
40 | path: pkg/cloud
41 | imports:
42 | - github.com/newrelic/newrelic-client-go/internal/serialization
43 | - encoding/json
44 | - fmt
45 | generators:
46 | - typegen
47 | - nerdgraphclient
48 | queries:
49 | - path: ["actor", "cloud"]
50 | endpoints:
51 | - name: linkedAccounts
52 | max_query_field_depth: 2
53 | include_arguments:
54 | - "provider"
55 | mutations:
56 | - name: cloudConfigureIntegration
57 | - name: cloudDisableIntegration
58 | - name: cloudLinkAccount
59 | argument_type_overrides:
60 | accountId: "Int!"
61 | accounts: "CloudLinkCloudAccountsInput!"
62 | - name: cloudRenameAccount
63 | argument_type_overrides:
64 | accountId: "Int!"
65 | accounts: "[CloudRenameAccountsInput!]!"
66 | - name: cloudUnlinkAccount
67 |
68 | generators:
69 | - name: typegen
70 | fileName: "types.go"
71 |
--------------------------------------------------------------------------------
/docs/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newrelic/tutone/11d56607628a3619e58f4a7ce39fc144ed2b8bb9/docs/.keep
--------------------------------------------------------------------------------
/docs/package-schema.md:
--------------------------------------------------------------------------------
1 |
2 | ### Package Schema
3 |
4 | The following YAML schema facilitates adding and updating Go package in a project.
5 |
6 | ```yaml
7 | name: string, required # The top-level name of the GraphQL endpoint scope - example: "alerts"
8 |
9 | path: string, required # Directory path to generate (Required) - example: "pkg/alerts"
10 |
11 | imports: []string, optional # Optional array of Go package imports to inject. - example: "- encoding/json"
12 |
13 | generators: []string, required # Which generators to utilize. At least one of [typegen, nerdgraphclient, command]
14 |
15 | # Queries schema: The GraphQL queries you want to generate code for. Requires the `nerdgraphclient` generator.
16 | queries: []object, optional
17 | - path: []object, required # The path to follow for the query - example: ["actor", "cloud"] maps to actor.cloud GraphQL query scope
18 | endpoints: []object, # The endpoints to generate code for.
19 | - name: string, required # Must be endpoints under the associated query path - example: "linkedAccounts" (under cloud query scope)
20 | max_query_field_depth: int, optional # Max recursion iterations for inferring required arguments and associated types. Typically is set to 2
21 | include_arguments: []string, optional # Query arguments to include
22 | argument_type_overrides: []object, optional # Array of key:values where the key is the argument name and the value is the GraphQL type override
23 |
24 | # Mutations schema: The GraphQL mutations you want to generate code for. Requires the `nerdgraphclient` generator.
25 | mutations: []object, optional
26 | - name: string, required # The name of the mutation to generator code for - example: "alertsPolicyCreate"
27 | argument_type_overrides: object, optional # A key:value map of where the key is the argument name and the value is the GraphQL type override
28 | exclude_fields: []string, optional # A list of fields to exclude from the mutation
29 |
30 | # Types schema: The GraphQL types to use for generating Go types. Requires the `typegen` generator
31 | types: []object, optional
32 | - name: string, required # The type to generate
33 | field_type_override: string, optional # A list of fields to exclude from the query
34 | skip_type_create: bool, optional # Skips creating the Go type. Usually specified along with `field_type_override`
35 | skip_fields: []string, optional # A list of fields to exclude from the Go type
36 | create_as: string, optional # Used when creating a new scalar type to determine which Go type to use
37 | interface_methods: []string, optional # List of additional methods that are added to an interface definition. The methods are not defined in the code, so must be implemented by the user.
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/tutone.md:
--------------------------------------------------------------------------------
1 | ```
2 | Generate Go code based on the introspection of a GraphQL server
3 |
4 | Usage:
5 | tutone [command]
6 |
7 | Available Commands:
8 | completion Generate the autocompletion script for the specified shell
9 | fetch Fetch GraphQL Schema
10 | generate Generate code from GraphQL Schema
11 | help Help about any command
12 |
13 | Flags:
14 | -c, --config string Path to a configuration file (default ".tutone.yml")
15 | -h, --help help for tutone
16 | -l, --loglevel string Log level (default "info")
17 | -v, --version version for tutone
18 |
19 | Use "tutone [command] --help" for more information about a command.
20 | ```
21 |
--------------------------------------------------------------------------------
/generators/command/command_util.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 |
8 | "github.com/huandu/xstrings"
9 | log "github.com/sirupsen/logrus"
10 |
11 | "github.com/newrelic/tutone/internal/codegen"
12 | "github.com/newrelic/tutone/internal/config"
13 | "github.com/newrelic/tutone/internal/schema"
14 | "github.com/newrelic/tutone/pkg/lang"
15 | )
16 |
17 | var goTypesToCobraFlagMethodMap = map[string]string{
18 | "int": "IntVar",
19 | "string": "StringVar",
20 | }
21 |
22 | // getReadCommandMetadata returns the associated types to generate a "read" command
23 | func getReadCommandMetadata(s *schema.Schema, queryPath []string) (*schema.Field, error) {
24 | if len(queryPath) == 0 {
25 | return nil, fmt.Errorf("query path is empty")
26 | }
27 |
28 | rootQuery, err := s.LookupRootQueryTypeFieldByName(queryPath[0])
29 | if err != nil {
30 | return nil, fmt.Errorf("root query field not found: %s", err)
31 | }
32 |
33 | rootQueryType, err := s.LookupTypeByName(rootQuery.GetName())
34 | if err != nil {
35 | return nil, fmt.Errorf("%s", err) // TODO: Do better
36 | }
37 |
38 | // Remove the root query field from the slice since
39 | // we extracted its type above.
40 | queryPath = queryPath[1:]
41 |
42 | found := s.RecursiveLookupFieldByPath(queryPath, rootQueryType)
43 | if found != nil {
44 | return found, nil
45 | }
46 |
47 | return nil, fmt.Errorf("could not find matching introspection data for provided query path")
48 | }
49 |
50 | func hydrateCommand(s *schema.Schema, command config.Command, pkgConfig *config.PackageConfig) lang.Command {
51 | isBaseCommand := true
52 | cmdVarName := "Command"
53 |
54 | // Handle case where this command is a subcommand of a parent entry point
55 | if !isBaseCommand {
56 | cmdVarName = fmt.Sprintf("cmd%s", xstrings.ToCamelCase(command.Name))
57 | }
58 |
59 | cmd := lang.Command{
60 | Name: command.Name,
61 | CmdVariableName: cmdVarName,
62 | ShortDescription: command.ShortDescription,
63 | LongDescription: command.LongDescription,
64 | Example: command.Example,
65 | GraphQLPath: command.GraphQLPath,
66 | }
67 |
68 | if len(command.Subcommands) == 0 {
69 | return lang.Command{}
70 | }
71 |
72 | cmd.Subcommands = make([]lang.Command, len(command.Subcommands))
73 |
74 | for i, subCmdConfig := range command.Subcommands {
75 | var err error
76 | var subcommandMetadata *schema.Field
77 |
78 | // Check to see if the commands CRUD action is a mutation.
79 | // If it's not a mutation, then it's a query (read) request.
80 | subcommandMetadata, err = s.LookupMutationByName(subCmdConfig.Name)
81 |
82 | if subcommandMetadata == nil {
83 | log.Debugf("no mutation reference found, assuming query request type")
84 | }
85 |
86 | // If the command is not a mutation, move forward with
87 | // generating a command to perform a query request.
88 | if err != nil {
89 | subcommandMetadata, err = getReadCommandMetadata(s, subCmdConfig.GraphQLPath)
90 | if err != nil {
91 | log.Fatal(err)
92 | }
93 | }
94 |
95 | subcommand := hydrateSubcommand(s, subcommandMetadata, subCmdConfig)
96 |
97 | exampleData := lang.CommandExampleData{
98 | CLIName: "newrelic",
99 | PackageName: pkgConfig.Name,
100 | Command: cmd.Name,
101 | Subcommand: subcommand.Name,
102 | Flags: subcommand.Flags,
103 | }
104 |
105 | subcommand.Example = subCmdConfig.Example
106 | if subCmdConfig.Example == "" {
107 | sCmdExample, err := generateCommandExample(subcommandMetadata, exampleData)
108 | if err != nil {
109 | log.Fatal(err)
110 | }
111 |
112 | subcommand.Example = sCmdExample
113 | }
114 |
115 | cmd.Subcommands[i] = *subcommand
116 | }
117 |
118 | return cmd
119 | }
120 |
121 | // TODO: Consolidate common parts of hydrateCommand, hydrateSubcommand
122 | func hydrateSubcommand(s *schema.Schema, sCmd *schema.Field, cmdConfig config.Command) *lang.Command {
123 | flags := hydrateFlagsFromSchema(sCmd.Args, cmdConfig)
124 |
125 | var err error
126 | var clientMethodArgs []string
127 | for _, f := range flags {
128 | varName := f.VariableName
129 | // If the client method argument is an `INPUT_OBJECT`,
130 | // we need the regular name to unmarshal.
131 | if f.IsInputType {
132 | varName = f.Name
133 | }
134 |
135 | if f.IsEnumType {
136 | varName, err = wrapEnumTypeVariable(varName, f.ClientType)
137 | if err != nil {
138 | log.Fatal(err)
139 | }
140 | }
141 |
142 | clientMethodArgs = append(clientMethodArgs, varName)
143 | }
144 |
145 | shortDescription := sCmd.Description
146 | // Allow configuration to override the description that comes from NerdGraph
147 | if cmdConfig.ShortDescription != "" {
148 | shortDescription = cmdConfig.ShortDescription
149 | }
150 |
151 | cmdName := sCmd.Name
152 | if cmdConfig.Name != "" {
153 | cmdName = cmdConfig.Name
154 | }
155 |
156 | cmdResult := lang.Command{
157 | Name: cmdName,
158 | CmdVariableName: fmt.Sprintf("cmd%s", xstrings.FirstRuneToUpper(sCmd.Name)),
159 | ShortDescription: shortDescription,
160 | LongDescription: cmdConfig.LongDescription,
161 | ClientMethod: cmdConfig.ClientMethod,
162 | ClientMethodArgs: clientMethodArgs,
163 | Example: cmdConfig.Example,
164 | Flags: flags,
165 | }
166 |
167 | return &cmdResult
168 | }
169 |
170 | // Returns a string representation of a variable wrapped/typed with an enum type ref
171 | //
172 | // e.g apiaccess.APIAccessKeyType("KEY_TYPE")
173 | func wrapEnumTypeVariable(varName string, clientTypeRefString string) (string, error) {
174 | data := struct {
175 | VarName string
176 | TypeRef string
177 | }{
178 | VarName: varName,
179 | TypeRef: clientTypeRefString,
180 | }
181 |
182 | // TODO: Consider passing in `pkgName` instead of a previously constructed
183 | // string so functionality is bit more usable/portable.
184 | // i.e. TypeRef in this case will look like this: `apiaccess.SomeType`
185 | // But we shouldn't make the dependent function create that string
186 | // and then pass it to this function.
187 | t := `{{ .TypeRef }}({{ .VarName }})`
188 |
189 | return codegen.RenderTemplate(varName, t, data)
190 | }
191 |
192 | func hydrateFlagsFromSchema(args []schema.Field, cmdConfig config.Command) []lang.CommandFlag {
193 | var flags []lang.CommandFlag
194 |
195 | for _, arg := range args {
196 | var variableName string
197 |
198 | isInputObject := arg.Type.IsInputObject()
199 | if isInputObject {
200 | // Add 'Input' suffix to the input variable name
201 | variableName = fmt.Sprintf("%sInput", cmdConfig.Name)
202 | } else {
203 | // TODO: Use helper mthod arg.GetName() to format this properly?
204 | variableName = fmt.Sprintf("%s%s", cmdConfig.Name, xstrings.FirstRuneToUpper(arg.Name))
205 | }
206 |
207 | typ, _, _ := arg.Type.GetType()
208 |
209 | if arg.IsScalarID() {
210 | typ = "string"
211 | }
212 |
213 | variableType := "string"
214 | if arg.IsPrimitiveType() {
215 | variableType = typ
216 | }
217 |
218 | clientType := fmt.Sprintf("%s.%s", cmdConfig.ClientPackageName, typ)
219 |
220 | flag := lang.CommandFlag{
221 | Name: arg.Name,
222 | Type: typ,
223 | FlagMethodName: getCobraFlagMethodName(typ),
224 | DefaultValue: "",
225 | Description: arg.Description,
226 | VariableName: variableName,
227 | VariableType: variableType,
228 | Required: arg.IsRequired(),
229 | IsInputType: isInputObject,
230 | ClientType: clientType,
231 | IsEnumType: arg.IsEnum(),
232 | }
233 |
234 | flags = append(flags, flag)
235 | }
236 |
237 | return flags
238 | }
239 |
240 | func fetchRemoteTemplate(url string) (string, error) {
241 | resp, err := http.Get(url)
242 | if err != nil {
243 | return "", err
244 | }
245 | defer resp.Body.Close()
246 |
247 | var respString string
248 | if resp.StatusCode == http.StatusOK {
249 | respBytes, err := io.ReadAll(resp.Body)
250 | if err != nil {
251 | return "", err
252 | }
253 | respString = string(respBytes)
254 | }
255 |
256 | return respString, nil
257 | }
258 |
259 | func getCobraFlagMethodName(typeString string) string {
260 | if v, ok := goTypesToCobraFlagMethodMap[typeString]; ok {
261 | return v
262 | }
263 |
264 | // Almost all CRUD inputs will be a JSON string
265 | return "StringVar"
266 | }
267 |
268 | func generateCommandExample(sCmd *schema.Field, data lang.CommandExampleData) (string, error) {
269 | t := `{{ .CLIName }} {{ .Command }} {{ .Subcommand }}{{- range .Flags }} --{{ .Name }}{{ end }}`
270 |
271 | return codegen.RenderTemplate("commandExample", t, data)
272 | }
273 |
--------------------------------------------------------------------------------
/generators/command/generator.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 |
6 | log "github.com/sirupsen/logrus"
7 |
8 | "github.com/newrelic/tutone/internal/codegen"
9 | "github.com/newrelic/tutone/internal/config"
10 | "github.com/newrelic/tutone/internal/filesystem"
11 | "github.com/newrelic/tutone/internal/schema"
12 | "github.com/newrelic/tutone/pkg/lang"
13 | )
14 |
15 | type Generator struct {
16 | lang.CommandGenerator
17 | }
18 |
19 | func (g *Generator) Generate(s *schema.Schema, genConfig *config.GeneratorConfig, pkgConfig *config.PackageConfig) error {
20 | log.Debugf("Generate...")
21 |
22 | g.CommandGenerator.PackageName = pkgConfig.Name
23 | g.CommandGenerator.Imports = pkgConfig.Imports
24 |
25 | cmds := make([]lang.Command, len(pkgConfig.Commands))
26 | for i, c := range pkgConfig.Commands {
27 | cmds[i] = hydrateCommand(s, c, pkgConfig)
28 | }
29 |
30 | g.CommandGenerator.Commands = cmds
31 |
32 | return nil
33 | }
34 |
35 | func (g *Generator) Execute(genConfig *config.GeneratorConfig, pkgConfig *config.PackageConfig) error {
36 | log.Debugf("Generating commands...")
37 |
38 | var templateStr string
39 | var err error
40 |
41 | destinationPath := pkgConfig.GetDestinationPath()
42 | if err = filesystem.MakeDir(destinationPath, 0775); err != nil {
43 | return err
44 | }
45 |
46 | hasTemplateURL := genConfig.TemplateURL != ""
47 | hasTemplateDir := genConfig.TemplateDir != ""
48 |
49 | if hasTemplateURL {
50 | templateStr, err = fetchRemoteTemplate(genConfig.TemplateURL)
51 | if err != nil {
52 | return err
53 | }
54 | }
55 |
56 | for _, command := range pkgConfig.Commands {
57 | fileName := "command.go"
58 |
59 | if command.FileName != "" {
60 | fileName = command.FileName
61 | }
62 |
63 | // Default template name is '{{ packageName }}.go.tmpl'
64 | templateName := "command.go.tmpl"
65 | if genConfig.TemplateName != "" {
66 | templateName = genConfig.TemplateName
67 | }
68 |
69 | fPath := fmt.Sprintf("%s/%s", destinationPath, fileName)
70 | destinationFile, err := codegen.RenderStringFromGenerator(fPath, g)
71 | if err != nil {
72 | return err
73 | }
74 |
75 | if hasTemplateURL && hasTemplateDir {
76 | return fmt.Errorf("generator configuration error: please set `templateDir` or `templateURL`, but not both")
77 | }
78 |
79 | templateDir := "templates/command"
80 | if hasTemplateDir {
81 | templateDir, err = codegen.RenderStringFromGenerator(genConfig.TemplateDir, g)
82 | if err != nil {
83 | return err
84 | }
85 | }
86 |
87 | c := codegen.CodeGen{
88 | TemplateDir: templateDir,
89 | TemplateName: templateName,
90 | DestinationFile: destinationFile,
91 | DestinationDir: destinationPath,
92 | }
93 |
94 | if templateStr != "" {
95 | if err := c.WriteFileFromTemplateString(g, templateStr); err != nil {
96 | return err
97 | }
98 | } else {
99 | if err := c.WriteFile(g); err != nil {
100 | return err
101 | }
102 | }
103 | }
104 |
105 | return nil
106 | }
107 |
--------------------------------------------------------------------------------
/generators/nerdgraphclient/generator.go:
--------------------------------------------------------------------------------
1 | package nerdgraphclient
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 |
8 | log "github.com/sirupsen/logrus"
9 |
10 | "github.com/newrelic/tutone/internal/codegen"
11 | "github.com/newrelic/tutone/internal/config"
12 | "github.com/newrelic/tutone/internal/output"
13 | "github.com/newrelic/tutone/internal/schema"
14 | "github.com/newrelic/tutone/pkg/lang"
15 | )
16 |
17 | type Generator struct {
18 | lang.GolangGenerator
19 | }
20 |
21 | func (g *Generator) Generate(s *schema.Schema, genConfig *config.GeneratorConfig, pkgConfig *config.PackageConfig) error {
22 | if genConfig == nil {
23 | return fmt.Errorf("unable to Generate with nil genConfig")
24 | }
25 |
26 | if pkgConfig == nil {
27 | return fmt.Errorf("unable to Generate with nil pkgConfig")
28 | }
29 |
30 | expandedTypes, err := schema.ExpandTypes(s, pkgConfig)
31 | if err != nil {
32 | log.Error(err)
33 | }
34 |
35 | structsForGen, enumsForGen, scalarsForGen, interfacesForGen, err := lang.GenerateGoTypesForPackage(s, genConfig, pkgConfig, expandedTypes)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | // TODO idea:
41 | // err = lang.GenerateTypesForPackage(&g)
42 | // if err != nil {
43 | // return err
44 | // }
45 |
46 | // lang.Normalize(&g, genConfig, pkgConfig)
47 |
48 | g.GolangGenerator.PackageName = pkgConfig.Name
49 | g.GolangGenerator.Imports = pkgConfig.Imports
50 |
51 | if structsForGen != nil {
52 | g.GolangGenerator.Types = *structsForGen
53 | }
54 |
55 | if enumsForGen != nil {
56 | g.GolangGenerator.Enums = *enumsForGen
57 | }
58 |
59 | if scalarsForGen != nil {
60 | g.GolangGenerator.Scalars = *scalarsForGen
61 | }
62 |
63 | if interfacesForGen != nil {
64 | g.GolangGenerator.Interfaces = *interfacesForGen
65 | }
66 |
67 | mutationsForGen, err := lang.GenerateGoMethodMutationsForPackage(s, genConfig, pkgConfig)
68 | if err != nil {
69 | return err
70 | }
71 |
72 | if mutationsForGen != nil {
73 | g.GolangGenerator.Mutations = *mutationsForGen
74 | }
75 |
76 | queriesForGen, err := lang.GenerateGoMethodQueriesForPackage(s, genConfig, pkgConfig)
77 | if err != nil {
78 | return err
79 | }
80 |
81 | if queriesForGen != nil {
82 | g.GolangGenerator.Queries = *queriesForGen
83 | }
84 |
85 | return nil
86 | }
87 |
88 | func (g *Generator) Execute(genConfig *config.GeneratorConfig, pkgConfig *config.PackageConfig) error {
89 | var err error
90 | var generatedFiles []string
91 |
92 | // Default to project root for types
93 | destinationPath := "./"
94 | if pkgConfig.Path != "" {
95 | destinationPath = pkgConfig.Path
96 | }
97 |
98 | if _, err = os.Stat(destinationPath); os.IsNotExist(err) {
99 | if err = os.Mkdir(destinationPath, 0755); err != nil {
100 | log.Error(err)
101 | }
102 | }
103 |
104 | // Default file name is 'nerdgraph.go'
105 | fileName := "nerdgraphclient.go"
106 | if genConfig.FileName != "" {
107 | fileName = genConfig.FileName
108 | if err != nil {
109 | return err
110 | }
111 | }
112 |
113 | templateName := "client.go.tmpl"
114 | if genConfig.TemplateName != "" {
115 | templateName = genConfig.TemplateName
116 | if err != nil {
117 | return err
118 | }
119 | }
120 |
121 | filePath, err := codegen.RenderStringFromGenerator(fmt.Sprintf("%s/%s", destinationPath, fileName), g)
122 | if err != nil {
123 | return err
124 | }
125 |
126 | templateDir := "templates/nerdgraphclient"
127 | if genConfig.TemplateDir != "" {
128 | templateDir, err = codegen.RenderStringFromGenerator(genConfig.TemplateDir, g)
129 | if err != nil {
130 | return err
131 | }
132 | }
133 |
134 | c := codegen.CodeGen{
135 | TemplateDir: templateDir,
136 | TemplateName: templateName,
137 | DestinationFile: filePath,
138 | DestinationDir: destinationPath,
139 | }
140 |
141 | err = c.WriteFile(g)
142 | if err != nil {
143 | return err
144 | }
145 |
146 | generatedFiles = []string{filePath}
147 |
148 | if pkgConfig.IncludeIntegrationTest {
149 | testFileName := fmt.Sprintf("%s_integration_test.go", strings.ToLower(pkgConfig.Name))
150 | testFilePath := fmt.Sprintf("%s/%s", destinationPath, testFileName)
151 |
152 | // Only create an integration test file if it doesn't exist. We don't want to overwrite
153 | // existing integration test files when regenerating package code because integration tests
154 | // are only scaffolded at time of generation and then manually edited.
155 | if !fileExists(testFilePath) {
156 | cg := codegen.CodeGen{
157 | TemplateDir: templateDir,
158 | TemplateName: "integration_test.go.tmpl",
159 | DestinationFile: testFilePath,
160 | DestinationDir: destinationPath,
161 | }
162 |
163 | err = cg.WriteFile(g)
164 | if err != nil {
165 | log.Warnf("Error generating integration test file.")
166 | }
167 |
168 | generatedFiles = append(generatedFiles, testFilePath)
169 | }
170 | }
171 |
172 | output.PrintSuccessMessage(c.DestinationDir, generatedFiles)
173 |
174 | return nil
175 | }
176 |
177 | func fileExists(filePath string) bool {
178 | _, error := os.Stat(filePath)
179 | return !os.IsNotExist(error)
180 | }
181 |
--------------------------------------------------------------------------------
/generators/terraform/generator.go:
--------------------------------------------------------------------------------
1 | package terraform
2 |
3 | import (
4 | log "github.com/sirupsen/logrus"
5 |
6 | "github.com/newrelic/tutone/internal/schema"
7 | )
8 |
9 | type Generator struct {
10 | }
11 |
12 | func (g *Generator) Generate(s *schema.Schema) error {
13 | log.Debugf("s: %+v", *s)
14 |
15 | return nil
16 | }
17 |
--------------------------------------------------------------------------------
/generators/typegen/generator.go:
--------------------------------------------------------------------------------
1 | package typegen
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | log "github.com/sirupsen/logrus"
8 |
9 | "github.com/newrelic/tutone/internal/codegen"
10 | "github.com/newrelic/tutone/internal/config"
11 | "github.com/newrelic/tutone/internal/schema"
12 | "github.com/newrelic/tutone/pkg/lang"
13 | )
14 |
15 | type Generator struct {
16 | lang.GolangGenerator
17 | }
18 |
19 | // Generate is the entry point for this Generator.
20 | func (g *Generator) Generate(s *schema.Schema, genConfig *config.GeneratorConfig, pkgConfig *config.PackageConfig) error {
21 | if genConfig == nil {
22 | return fmt.Errorf("unable to Generate with nil genConfig")
23 | }
24 |
25 | if pkgConfig == nil {
26 | return fmt.Errorf("unable to Generate with nil pkgConfig")
27 | }
28 |
29 | expandedTypes, err := schema.ExpandTypes(s, pkgConfig)
30 | if err != nil {
31 | log.Error(err)
32 | }
33 |
34 | structsForGen, enumsForGen, scalarsForGen, interfacesForGen, err := lang.GenerateGoTypesForPackage(s, genConfig, pkgConfig, expandedTypes)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | // The Execute() below expects to have Generator g populated for use in the template files.
40 | g.GolangGenerator.PackageName = pkgConfig.Name
41 | g.GolangGenerator.Imports = pkgConfig.Imports
42 |
43 | if structsForGen != nil {
44 | g.GolangGenerator.Types = *structsForGen
45 | }
46 |
47 | if enumsForGen != nil {
48 | g.GolangGenerator.Enums = *enumsForGen
49 | }
50 |
51 | if scalarsForGen != nil {
52 | g.GolangGenerator.Scalars = *scalarsForGen
53 | }
54 |
55 | if interfacesForGen != nil {
56 | g.GolangGenerator.Interfaces = *interfacesForGen
57 | }
58 |
59 | return nil
60 | }
61 |
62 | // Execute performs the template render and file writement, according to the received configurations for the current Generator instance.
63 | func (g *Generator) Execute(genConfig *config.GeneratorConfig, pkgConfig *config.PackageConfig) error {
64 | var err error
65 |
66 | // Default to project root for types
67 | destinationPath := "./"
68 | if pkgConfig.Path != "" {
69 | destinationPath = pkgConfig.Path
70 | }
71 |
72 | if _, err = os.Stat(destinationPath); os.IsNotExist(err) {
73 | if err = os.Mkdir(destinationPath, 0755); err != nil {
74 | log.Error(err)
75 | }
76 | }
77 |
78 | // Default file name is 'types.go'
79 | fileName := "types.go"
80 | if genConfig.FileName != "" {
81 | fileName = genConfig.FileName
82 | }
83 |
84 | filePath := fmt.Sprintf("%s/%s", destinationPath, fileName)
85 | file, err := os.Create(filePath)
86 | if err != nil {
87 | log.Error(err)
88 | }
89 | defer file.Close()
90 |
91 | templateName := "types.go.tmpl"
92 | if genConfig.TemplateName != "" {
93 | templateName = genConfig.TemplateName
94 | }
95 |
96 | templateDir := "templates/typegen"
97 | if genConfig.TemplateDir != "" {
98 | templateDir = genConfig.TemplateDir
99 | }
100 |
101 | c := codegen.CodeGen{
102 | TemplateDir: templateDir,
103 | TemplateName: templateName,
104 | DestinationFile: filePath,
105 | DestinationDir: destinationPath,
106 | }
107 |
108 | return c.WriteFile(g)
109 | }
110 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/newrelic/tutone
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/Masterminds/sprig/v3 v3.2.3
7 | github.com/huandu/xstrings v1.3.3
8 | github.com/sirupsen/logrus v1.9.0
9 | github.com/spf13/cobra v1.6.1
10 | github.com/spf13/viper v1.13.0
11 | github.com/stretchr/testify v1.8.1
12 | golang.org/x/text v0.14.0
13 | golang.org/x/tools v0.6.0
14 | gopkg.in/yaml.v2 v2.4.0
15 | )
16 |
17 | require (
18 | github.com/Masterminds/goutils v1.1.1 // indirect
19 | github.com/Masterminds/semver/v3 v3.2.0 // indirect
20 | github.com/davecgh/go-spew v1.1.1 // indirect
21 | github.com/fsnotify/fsnotify v1.5.4 // indirect
22 | github.com/google/uuid v1.1.2 // indirect
23 | github.com/hashicorp/hcl v1.0.0 // indirect
24 | github.com/imdario/mergo v0.3.11 // indirect
25 | github.com/inconshreveable/mousetrap v1.0.1 // indirect
26 | github.com/magiconair/properties v1.8.6 // indirect
27 | github.com/mitchellh/copystructure v1.0.0 // indirect
28 | github.com/mitchellh/mapstructure v1.5.0 // indirect
29 | github.com/mitchellh/reflectwalk v1.0.1 // indirect
30 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
31 | github.com/pelletier/go-toml v1.9.5 // indirect
32 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect
33 | github.com/pmezard/go-difflib v1.0.0 // indirect
34 | github.com/shopspring/decimal v1.2.0 // indirect
35 | github.com/spf13/afero v1.8.2 // indirect
36 | github.com/spf13/cast v1.5.0 // indirect
37 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
38 | github.com/spf13/pflag v1.0.5 // indirect
39 | github.com/subosito/gotenv v1.4.1 // indirect
40 | golang.org/x/crypto v0.17.0 // indirect
41 | golang.org/x/mod v0.8.0 // indirect
42 | golang.org/x/sys v0.15.0 // indirect
43 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
44 | gopkg.in/ini.v1 v1.67.0 // indirect
45 | gopkg.in/yaml.v3 v3.0.1 // indirect
46 | )
47 |
--------------------------------------------------------------------------------
/internal/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newrelic/tutone/11d56607628a3619e58f4a7ce39fc144ed2b8bb9/internal/.keep
--------------------------------------------------------------------------------
/internal/codegen/codegen.go:
--------------------------------------------------------------------------------
1 | package codegen
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 | "path"
8 | "text/template"
9 |
10 | log "github.com/sirupsen/logrus"
11 | "golang.org/x/tools/imports"
12 |
13 | "github.com/newrelic/tutone/internal/output"
14 | "github.com/newrelic/tutone/internal/util"
15 | )
16 |
17 | type CodeGen struct {
18 | TemplateDir string
19 | TemplateName string
20 | DestinationDir string
21 | DestinationFile string
22 | Source Path
23 | Destination Path
24 | }
25 |
26 | type Path struct {
27 | // Directory is the path to directory that will store the file, eg: pkg/nerdgraph
28 | Directory string
29 | // File is the name of the file within the directory
30 | File string
31 | }
32 |
33 | // WriteFile creates a new file, where the output from rendering template using the received Generator will be stored.
34 | func (c *CodeGen) WriteFile(g Generator) error {
35 | var err error
36 |
37 | if _, err = os.Stat(c.DestinationDir); os.IsNotExist(err) {
38 | if err = os.Mkdir(c.DestinationDir, 0755); err != nil {
39 | return err
40 | }
41 | }
42 |
43 | file, err := os.Create(c.DestinationFile)
44 | if err != nil {
45 | return err
46 | }
47 |
48 | defer file.Close()
49 |
50 | templatePath := path.Join(c.TemplateDir, c.TemplateName)
51 | templateName := path.Base(templatePath)
52 |
53 | tmpl, err := template.New(templateName).Funcs(util.GetTemplateFuncs()).ParseFiles(templatePath)
54 | if err != nil {
55 | return err
56 | }
57 |
58 | var resultBuf bytes.Buffer
59 |
60 | err = tmpl.Execute(&resultBuf, g)
61 | if err != nil {
62 | return err
63 | }
64 |
65 | formatted, err := imports.Process(file.Name(), resultBuf.Bytes(), nil)
66 | if err != nil {
67 | log.Error(resultBuf.String())
68 |
69 | _, err = file.WriteAt(resultBuf.Bytes(), 0)
70 | if err != nil {
71 | log.Error(err)
72 | }
73 | }
74 |
75 | _, err = file.WriteAt(formatted, 0)
76 | if err != nil {
77 | return err
78 | }
79 |
80 | return nil
81 | }
82 |
83 | func (c *CodeGen) WriteFileFromTemplateString(g Generator, templateString string) error {
84 | var err error
85 |
86 | if _, err = os.Stat(c.DestinationDir); os.IsNotExist(err) {
87 | if err = os.Mkdir(c.DestinationDir, 0755); err != nil {
88 | return err
89 | }
90 | }
91 |
92 | file, err := os.Create(c.DestinationFile)
93 | if err != nil {
94 | return err
95 | }
96 |
97 | defer file.Close()
98 |
99 | templatePath := path.Join(c.TemplateDir, c.TemplateName)
100 | templateName := path.Base(templatePath)
101 |
102 | tmpl, err := template.New(templateName).Funcs(util.GetTemplateFuncs()).Parse(templateString)
103 | if err != nil {
104 | return err
105 | }
106 |
107 | var resultBuf bytes.Buffer
108 |
109 | err = tmpl.Execute(&resultBuf, g)
110 | if err != nil {
111 | return err
112 | }
113 |
114 | formatted, err := imports.Process(file.Name(), resultBuf.Bytes(), nil)
115 | if err != nil {
116 | log.Error(resultBuf.String())
117 | return fmt.Errorf("failed to format buffer: %s", err)
118 | }
119 |
120 | _, err = file.WriteAt(formatted, 0)
121 | if err != nil {
122 | return err
123 | }
124 |
125 | output.PrintSuccessMessage(c.DestinationDir, []string{c.DestinationFile})
126 |
127 | return nil
128 | }
129 |
--------------------------------------------------------------------------------
/internal/codegen/generator.go:
--------------------------------------------------------------------------------
1 | package codegen
2 |
3 | import (
4 | "github.com/newrelic/tutone/internal/config"
5 | "github.com/newrelic/tutone/internal/schema"
6 | )
7 |
8 | // Generator aspires to implement the interface between a NerdGraph schema and
9 | // generated code for another project.
10 | type Generator interface {
11 | Generate(*schema.Schema, *config.GeneratorConfig, *config.PackageConfig) error
12 | Execute(*config.GeneratorConfig, *config.PackageConfig) error
13 | }
14 |
--------------------------------------------------------------------------------
/internal/codegen/helpers.go:
--------------------------------------------------------------------------------
1 | package codegen
2 |
3 | import (
4 | "bytes"
5 | "text/template"
6 |
7 | "github.com/newrelic/tutone/internal/util"
8 | )
9 |
10 | // RenderStringFromGenerator receives a Generator that is used to render the received template string.
11 | func RenderStringFromGenerator(s string, g Generator) (string, error) {
12 | tmpl, err := template.New("string").Funcs(util.GetTemplateFuncs()).Parse(s)
13 | if err != nil {
14 | return "", err
15 | }
16 |
17 | var resultBuf bytes.Buffer
18 |
19 | err = tmpl.Execute(&resultBuf, g)
20 | if err != nil {
21 | return "", err
22 | }
23 |
24 | return resultBuf.String(), nil
25 | }
26 |
27 | // RenderTemplate parses and returns the rendered string of the provided template.
28 | // The template is also assigned the provided name for reference.
29 | //
30 | // TODO: How can we compose a template of embedded templates to help scale?
31 | //
32 | // Templates are stored as map[string]*Template - ("someName": *Template).
33 | // https://stackoverflow.com/questions/41176355/go-template-name
34 | func RenderTemplate(templateName string, templateString string, data interface{}) (string, error) {
35 | tmpl, err := template.New(templateName).Funcs(util.GetTemplateFuncs()).Parse(templateString)
36 | if err != nil {
37 | return "", err
38 | }
39 |
40 | var buffer bytes.Buffer
41 | err = tmpl.Execute(&buffer, data)
42 | if err != nil {
43 | return "", err
44 | }
45 |
46 | return buffer.String(), nil
47 | }
48 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "errors"
5 | "os"
6 |
7 | log "github.com/sirupsen/logrus"
8 | "gopkg.in/yaml.v2"
9 | )
10 |
11 | // Config is the information keeper for generating go structs from type names.
12 | type Config struct {
13 | // LogLevel sets the logging level
14 | LogLevel string `yaml:"log_level,omitempty"`
15 | // Endpoint is the URL for the GraphQL API
16 | Endpoint string `yaml:"endpoint"`
17 | // Auth contains details about how to authenticate to the API in the case that it's required.
18 | Auth AuthConfig `yaml:"auth"`
19 | // Cache contains information on how and where to store the schema.
20 | Cache CacheConfig `yaml:"cache"`
21 | // Packages contain the information on how to break up the schema into code packages.
22 | Packages []PackageConfig `yaml:"packages,omitempty"`
23 | // Generators configure the work engine of this project.
24 | Generators []GeneratorConfig `yaml:"generators,omitempty"`
25 | }
26 |
27 | // AuthConfig is the information necessary to authenticate to the NerdGraph API.
28 | type AuthConfig struct {
29 | // Header is the name of the API request header that is used to authenticate.
30 | Header string `yaml:"header,omitempty"`
31 | // EnvVar is the name of the environment variable to attach to the above header.
32 | EnvVar string `yaml:"api_key_env_var,omitempty"`
33 | }
34 |
35 | // CacheConfig is the information necessary to store the NerdGraph schema in JSON.
36 | type CacheConfig struct {
37 | // Enable or disable the schema caching.
38 | Enable bool `yaml:",omitempty"`
39 | // SchemaFile is the location where the schema should be cached.
40 | SchemaFile string `yaml:"schema_file,omitempty"`
41 | }
42 |
43 | // PackageConfig is the information about a single package, which types to include from the schema, and which generators to use for this package.
44 | type PackageConfig struct {
45 | // Name is the string that is used to refer to the name of the package.
46 | Name string `yaml:"name,omitempty"`
47 | // Path is the relative path within the project.
48 | Path string `yaml:"path,omitempty"`
49 | // ImportPath is the full path used for importing this package into a Go project
50 | ImportPath string `yaml:"import_path,omitempty"`
51 | // Types is a list of Type configurations to include in the package.
52 | Types []TypeConfig `yaml:"types,omitempty"`
53 | // Mutations is a list of Method configurations to include in the package.
54 | Mutations []MutationConfig `yaml:"mutations,omitempty"`
55 | // Generators is a list of names that reference a generator in the Config struct.
56 | Generators []string `yaml:"generators,omitempty"`
57 | // Imports is a list of strings to represent what pacakges to import for a given package.
58 | Imports []string `yaml:"imports,omitempty"`
59 |
60 | Commands []Command `yaml:"commands,omitempty"`
61 |
62 | Queries []Query `yaml:"queries,omitempty"`
63 |
64 | // Transient property which is set by using the --include-integration-test flag.
65 | IncludeIntegrationTest bool
66 | }
67 |
68 | // Query is the information necessary to build a query method. The Paths
69 | // reference the the place in the hierarchy, while the names reference the
70 | // objects within those paths to query.
71 | type Query struct {
72 | // Path is the path of TypeNames in GraphQL that precede the objects being queried.
73 | Path []string `yaml:"path,omitempty"`
74 | // Names is a list of TypeName entries that will be found at the above Path.
75 | Endpoints []EndpointConfig `yaml:"endpoints,omitempty"`
76 | }
77 |
78 | type Command struct {
79 | Name string `yaml:"name,omitempty"`
80 | FileName string `yaml:"fileName,omitempty"`
81 | ShortDescription string `yaml:"shortDescription,omitempty"`
82 | LongDescription string `yaml:"longDescription,omitempty"`
83 | Example string `yaml:"example,omitempty"`
84 | InputType string `yaml:"inputType,omitempty"`
85 | ClientPackageName string `yaml:"clientPackageName,omitempty"`
86 | ClientMethod string `yaml:"clientMethod,omitempty"`
87 | Flags []CommandFlag `yaml:"flags,omitempty"`
88 | Subcommands []Command `yaml:"subcommands,omitempty"`
89 | GraphQLPath []string `yaml:"path,omitempty"`
90 | }
91 |
92 | type CommandFlag struct {
93 | Name string `yaml:"name,omitempty"`
94 | Type string `yaml:"type,omitempty"`
95 | DefaultValue string `yaml:"defaultValue"`
96 | Description string `yaml:"description"`
97 | VariableName string `yaml:"variableName"`
98 | Required bool `yaml:"required"`
99 | }
100 |
101 | // GeneratorConfig is the information necessary to execute a generator.
102 | type GeneratorConfig struct {
103 | // Name is the string that is used to reference a generator.
104 | Name string `yaml:"name,omitempty"`
105 | // TemplateDir is the path to the directory that contains all of the templates.
106 | TemplateDir string `yaml:"templateDir,omitempty"`
107 | // FileName is the target file that is to be generated.
108 | FileName string `yaml:"fileName,omitempty"`
109 | // TemplateName is the name of the template within the TemplateDir.
110 | TemplateName string `yaml:"templateName,omitempty"`
111 | // TemplateURL is a URL to a downloadable file to use as a Go template
112 | TemplateURL string `yaml:"templateURL,omitempty"`
113 | }
114 |
115 | // MutationConfig is the information about the GraphQL mutations.
116 | type MutationConfig struct {
117 | // Name is the name of the GraphQL method.
118 | Name string `yaml:"name"`
119 | MaxQueryFieldDepth int `yaml:"max_query_field_depth,omitempty"`
120 | ArgumentTypeOverrides map[string]string `yaml:"argument_type_overrides,omitempty"`
121 | ExcludeFields []string `yaml:"exclude_fields,omitempty"`
122 | }
123 |
124 | type EndpointConfig struct {
125 | Name string `yaml:"name,omitempty"`
126 | MaxQueryFieldDepth int `yaml:"max_query_field_depth,omitempty"`
127 | IncludeArguments []string `yaml:"include_arguments,omitempty"`
128 | ExcludeFields []string `yaml:"exclude_fields,omitempty"`
129 | }
130 |
131 | // TypeConfig is the information about which types to render and any data specific to handling of the type.
132 | type TypeConfig struct {
133 | // InterfaceMethods is a list of additional methods that are added to an interface definition. The methods are not
134 | // defined in the code, so must be implemented by the user.
135 | InterfaceMethods []string `yaml:"interface_methods,omitempty"`
136 | // Name of the type (required)
137 | Name string `yaml:"name"`
138 | // FieldTypeOverride is the Golang type to override whatever the default detected type would be for a given field.
139 | FieldTypeOverride string `yaml:"field_type_override,omitempty"`
140 | // CreateAs is used when creating a new scalar type to determine which Go type to use.
141 | CreateAs string `yaml:"create_as,omitempty"`
142 | // SkipTypeCreate allows the user to skip creating a Scalar type.
143 | SkipTypeCreate bool `yaml:"skip_type_create,omitempty"`
144 | // SkipFields allows the user to skip generating specific fields within a type.
145 | SkipFields []string `yaml:"skip_fields,omitempty"`
146 | // GenerateStructGetters enables the auto-generation of field getters for all fields on a struct.
147 | // i.e. if a struct has a field `name` then a function would be created called `GetName()`
148 | GenerateStructGetters bool `yaml:"generate_struct_getters,omitempty"`
149 | // Applies to all fields of the struct
150 | StructTags *StructTags `yaml:"struct_tags,omitempty"`
151 | }
152 |
153 | type StructTags struct {
154 | // Set the type of struct tags - e.g. ["json"] or for multiple ["json", "yaml", etc...]
155 | // Note this will apply to ALL fields within the struct. Use with caution.
156 | Tags []string `yaml:"tags"`
157 |
158 | // Set to `false` to exclude `omitempty` from struct tags
159 | // Note this will apply to ALL fields within the struct. Use with caution.
160 | OmitEmpty *bool `yaml:"omitempty"`
161 | }
162 |
163 | const (
164 | DefaultCacheEnable = false
165 | DefaultCacheSchemaFile = "schema.json"
166 | DefaultLogLevel = "info"
167 | DefaultAuthHeader = "Api-Key"
168 | DefaultAuthEnvVar = "TUTONE_API_KEY"
169 | )
170 |
171 | // LoadConfig will load a config file at the specified path or error.
172 | func LoadConfig(file string) (*Config, error) {
173 | if file == "" {
174 | return nil, errors.New("config file name required")
175 | }
176 | log.WithFields(log.Fields{
177 | "file": file,
178 | }).Debug("loading package definition")
179 |
180 | yamlFile, err := os.ReadFile(file)
181 | if err != nil {
182 | return nil, err
183 | }
184 |
185 | var config Config
186 | err = yaml.Unmarshal(yamlFile, &config)
187 | if err != nil {
188 | return nil, err
189 | }
190 | log.Tracef("definition: %+v", config)
191 |
192 | return &config, nil
193 | }
194 |
195 | func (c *PackageConfig) GetDestinationPath() string {
196 | if c.Path != "" {
197 | return c.Path
198 | }
199 |
200 | return "./"
201 | }
202 |
203 | func (c *PackageConfig) GetTypeConfigByName(name string) *TypeConfig {
204 | for _, typeConfig := range c.Types {
205 | if typeConfig.Name == name {
206 | return &typeConfig
207 | }
208 | }
209 |
210 | return nil
211 | }
212 |
--------------------------------------------------------------------------------
/internal/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestLoadConfig(t *testing.T) {
10 | t.Parallel()
11 |
12 | config, err := LoadConfig("doesnotexist")
13 | assert.Error(t, err)
14 | assert.Nil(t, config)
15 |
16 | config, err = LoadConfig("../../testdata/goodConfig_fixture.yml")
17 | assert.NoError(t, err)
18 | assert.NotNil(t, config)
19 |
20 | expected := &Config{
21 | LogLevel: "trace",
22 | Endpoint: "https://api222.newrelic.com/graphql",
23 | Auth: AuthConfig{
24 | Header: "Api-Key",
25 | EnvVar: "NEW_RELIC_API_KEY",
26 | },
27 | Cache: CacheConfig{
28 | Enable: false,
29 | SchemaFile: "testing.schema.json",
30 | },
31 | Packages: []PackageConfig{
32 | {
33 | Name: "alerts",
34 | Path: "pkg/alerts",
35 | ImportPath: "github.com/newrelic/newrelic-client-go/pkg/alerts",
36 | Types: []TypeConfig{
37 | {
38 | Name: "AlertsMutingRuleConditionInput",
39 | },
40 | {
41 | Name: "AlertsPolicy",
42 | GenerateStructGetters: true,
43 | },
44 | {
45 | Name: "ID",
46 | FieldTypeOverride: "string",
47 | SkipTypeCreate: true,
48 | },
49 | {
50 | Name: "InterfaceImplementation",
51 | InterfaceMethods: []string{
52 | "Get() string",
53 | },
54 | },
55 | },
56 | Generators: []string{"typegen"},
57 | Queries: []Query{
58 | {
59 | Path: []string{
60 | "actor",
61 | "cloud",
62 | },
63 | Endpoints: []EndpointConfig{
64 | {
65 | Name: "linkedAccounts",
66 | MaxQueryFieldDepth: 2,
67 | IncludeArguments: []string{"provider"},
68 | ExcludeFields: []string{"updatedAt"},
69 | },
70 | },
71 | },
72 | },
73 | Mutations: []MutationConfig{
74 | {
75 | Name: "cloudConfigureIntegration",
76 | MaxQueryFieldDepth: 1,
77 | },
78 | {
79 | Name: "cloudLinkAccount",
80 | MaxQueryFieldDepth: 1,
81 | ArgumentTypeOverrides: map[string]string{
82 | "accountId": "Int!",
83 | "accounts": "CloudLinkCloudAccountsInput!",
84 | },
85 | ExcludeFields: []string{"updatedAt"},
86 | },
87 | },
88 | },
89 | },
90 | Generators: []GeneratorConfig{
91 | {
92 | Name: "typegen",
93 | // DestinationFile:
94 | // TemplateDir:
95 | FileName: "types.go",
96 | // TemplateName:
97 | },
98 | },
99 | }
100 |
101 | assert.Equal(t, config, expected)
102 | }
103 |
--------------------------------------------------------------------------------
/internal/filesystem/filesystem.go:
--------------------------------------------------------------------------------
1 | package filesystem
2 |
3 | import (
4 | "os"
5 | )
6 |
7 | // MakeDir creates a directory if it does't exist yet and sets
8 | // directory's mode and permission bits - e.g. 0775.
9 | func MakeDir(path string, permissions os.FileMode) error {
10 | if _, err := os.Stat(path); os.IsNotExist(err) {
11 | if err := os.MkdirAll(path, 0775); err != nil {
12 | return err
13 | }
14 | }
15 |
16 | return nil
17 | }
18 |
--------------------------------------------------------------------------------
/internal/output/output.go:
--------------------------------------------------------------------------------
1 | package output
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // PrintSuccessMessage prints a message to the console informing
8 | // the user that code generation was a success and outputs the
9 | // package and file path for reference.
10 | //
11 | // Emoji unicode reference: http://www.unicode.org/emoji/charts/emoji-list.html
12 | func PrintSuccessMessage(packagePath string, filePaths []string) {
13 | // Emoji = \u2705
14 | fmt.Print("\n\u2705 Code generation complete: \n\n")
15 | fmt.Printf(" Package: %v \n", packagePath)
16 |
17 | for _, f := range filePaths {
18 | fmt.Printf(" File: %v \n", f)
19 | }
20 |
21 | fmt.Println("")
22 | }
23 |
--------------------------------------------------------------------------------
/internal/schema/enum.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "strings"
5 |
6 | "golang.org/x/text/cases"
7 | "golang.org/x/text/language"
8 | )
9 |
10 | type EnumValue struct {
11 | Name string `json:"name,omitempty"`
12 | Description string `json:"description,omitempty"`
13 | Kind Kind `json:"kind,omitempty"`
14 |
15 | IsDeprecated bool `json:"isDeprecated"`
16 | DeprecationReason string `json:"deprecationReason"`
17 | }
18 |
19 | // GetDescription formats the description into a GoDoc comment.
20 | func (e *EnumValue) GetDescription() string {
21 | if strings.TrimSpace(e.Description) == "" {
22 | return ""
23 | }
24 |
25 | return formatDescription("", e.Description)
26 | }
27 |
28 | // GetName returns a recusive lookup of the type name
29 | func (e *EnumValue) GetName() string {
30 | var fieldName string
31 |
32 | switch strings.ToLower(e.Name) {
33 | case "ids":
34 | // special case to avoid the struct field Ids, and prefer IDs instead
35 | fieldName = "IDs"
36 | case "id":
37 | fieldName = "ID"
38 | case "accountid":
39 | fieldName = "AccountID"
40 | default:
41 | caser := cases.Title(language.Und, cases.NoLower)
42 | fieldName = caser.String(e.Name)
43 | }
44 |
45 | return fieldName
46 | }
47 |
--------------------------------------------------------------------------------
/internal/schema/expander.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "sync"
7 |
8 | log "github.com/sirupsen/logrus"
9 |
10 | "github.com/newrelic/tutone/internal/util"
11 | )
12 |
13 | // Expander is mean to hold the state while the schema is being expanded.
14 | type Expander struct {
15 | sync.Mutex
16 | schema *Schema
17 | expandedTypes []*Type
18 | skipTypes []string
19 | }
20 |
21 | // NewExpander is to return a sane Expander.
22 | func NewExpander(schema *Schema, skipTypes []string) *Expander {
23 | return &Expander{
24 | schema: schema,
25 | skipTypes: skipTypes,
26 | }
27 | }
28 |
29 | // ExpandedTypes is the final report of all the expanded types.
30 | func (x *Expander) ExpandedTypes() *[]*Type {
31 | x.Lock()
32 | expandedTypes := x.expandedTypes
33 | x.Unlock()
34 |
35 | for _, expandedType := range expandedTypes {
36 | log.WithFields(log.Fields{
37 | "name": expandedType.Name,
38 | "kind": expandedType.Kind,
39 | }).Debug("type included")
40 | }
41 |
42 | sort.SliceStable(expandedTypes, func(i, j int) bool {
43 | return expandedTypes[i].Name < expandedTypes[j].Name
44 | })
45 |
46 | return &expandedTypes
47 | }
48 |
49 | // ExpandType is used to populate the expander, one Type at a time.
50 | func (x *Expander) ExpandType(t *Type) (err error) {
51 | if t == nil {
52 | return fmt.Errorf("unable to expand nil Type")
53 | }
54 |
55 | if util.StringInStrings(t.Name, x.skipTypes) {
56 | log.WithFields(log.Fields{
57 | "name": t.Name,
58 | "skip_type_create": true,
59 | }).Debug("Not expanding skipped type")
60 |
61 | return nil
62 | }
63 |
64 | if x.includeType(t) {
65 | err := x.expandType(t)
66 | if err != nil {
67 | log.WithFields(log.Fields{
68 | "name": t.Name,
69 | }).Errorf("failed to expand type: %s", err)
70 | }
71 | }
72 |
73 | return nil
74 | }
75 |
76 | // ExpandTypeFromName will expand a named type if found or error.
77 | func (x *Expander) ExpandTypeFromName(name string) error {
78 | t, err := x.schema.LookupTypeByName(name)
79 | if err != nil {
80 | return fmt.Errorf("failed lookup method argument: %s", err)
81 | }
82 |
83 | return x.ExpandType(t)
84 | }
85 |
86 | // includeType is used make sure a Type has been expanded. A boolean ok is
87 | // returned if the type was included. A false value means that the type was
88 | // already included.
89 | func (x *Expander) includeType(t *Type) bool {
90 | var ok bool
91 |
92 | x.Lock()
93 | if !hasType(t, x.expandedTypes) {
94 | log.WithFields(log.Fields{
95 | "name": t.Name,
96 | }).Trace("including type")
97 |
98 | x.expandedTypes = append(x.expandedTypes, t)
99 | ok = true
100 | }
101 | x.Unlock()
102 |
103 | return ok
104 | }
105 |
106 | // expandType receives a Type which is used to determine the Type for all
107 | // nested fields.
108 | func (x *Expander) expandType(t *Type) error {
109 | if t == nil {
110 | return fmt.Errorf("unable to expand nil type")
111 | }
112 |
113 | // InputFields and Fields are handled the same way, so combine them to loop over.
114 | var fields []Field
115 | fields = append(fields, t.Fields...)
116 | fields = append(fields, t.InputFields...)
117 |
118 | log.WithFields(log.Fields{
119 | "name": t.GetName(),
120 | "interfaces": t.Interfaces,
121 | "possibleTypes": t.PossibleTypes,
122 | "kind": t.Kind,
123 | "fields_count": len(t.Fields),
124 | "inputFields_count": len(t.InputFields),
125 | }).Debug("expanding type")
126 |
127 | // Collect the nested types from InputFields and Fields.
128 | for _, i := range fields {
129 | log.WithFields(log.Fields{
130 | "args": len(i.Args),
131 | "name": i.GetName(),
132 | "type": i.Type.Kind,
133 | }).Debug("expanding field")
134 |
135 | var err error
136 |
137 | if i.Type.OfType != nil {
138 | err = x.ExpandTypeFromName(i.Type.OfType.GetTypeName())
139 | if err != nil {
140 | log.WithFields(log.Fields{
141 | "ofType": i.Type.OfType.GetTypeName(),
142 | "type": i.Type.Name,
143 | }).Errorf("failed to expand OfType for Type: %s", err)
144 | // continue
145 | }
146 | }
147 |
148 | err = x.ExpandTypeFromName(i.Type.GetTypeName())
149 | if err != nil {
150 | log.WithFields(log.Fields{
151 | "type": i.Type.Name,
152 | }).Errorf("failed to expand Type.Name: %s", err)
153 | }
154 |
155 | for _, arg := range i.Args {
156 | err := x.ExpandTypeFromName(arg.Type.GetTypeName())
157 | if err != nil {
158 | log.WithFields(log.Fields{
159 | "name": arg.Type.GetTypeName(),
160 | }).Errorf("failed to expand type from name: %s", err)
161 | }
162 | }
163 |
164 | for _, possibleType := range t.PossibleTypes {
165 | err := x.ExpandTypeFromName(possibleType.Name)
166 | if err != nil {
167 | log.WithFields(log.Fields{
168 | "name": possibleType.Name,
169 | }).Errorf("failed to expand type from name: %s", err)
170 | }
171 | }
172 |
173 | for _, typeInterface := range t.Interfaces {
174 | err := x.ExpandTypeFromName(typeInterface.Name)
175 | if err != nil {
176 | log.WithFields(log.Fields{
177 | "name": typeInterface.Name,
178 | }).Errorf("failed to expand type from name: %s", err)
179 | }
180 | }
181 | }
182 |
183 | return nil
184 | }
185 |
--------------------------------------------------------------------------------
/internal/schema/field.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "strings"
5 |
6 | log "github.com/sirupsen/logrus"
7 |
8 | "github.com/newrelic/tutone/internal/config"
9 | )
10 |
11 | // Field is an attribute of a schema Type object.
12 | type Field struct {
13 | Name string `json:"name,omitempty"`
14 | Description string `json:"description,omitempty"`
15 | Kind Kind `json:"kind,omitempty"`
16 |
17 | Type TypeRef `json:"type"`
18 | Args []Field `json:"args,omitempty"`
19 | DefaultValue interface{} `json:"defaultValue,omitempty"`
20 | }
21 |
22 | // GetDescription formats the description into a GoDoc comment.
23 | func (f *Field) GetDescription() string {
24 | if strings.TrimSpace(f.Description) == "" {
25 | return ""
26 | }
27 |
28 | return formatDescription("", f.Description)
29 | }
30 |
31 | // GetTypeNameWithOverride returns the typeName, taking into consideration any FieldTypeOverride specified in the PackageConfig.
32 | func (f *Field) GetTypeNameWithOverride(pkgConfig *config.PackageConfig) (string, error) {
33 | var typeName string
34 | var overrideType string
35 | var err error
36 |
37 | // Discover any FieldTypeOverride override for the current field.
38 | nameToMatch := f.Type.GetTypeName()
39 |
40 | for _, p := range pkgConfig.Types {
41 | if p.Name == nameToMatch {
42 | if p.FieldTypeOverride != "" {
43 | log.WithFields(log.Fields{
44 | "name": nameToMatch,
45 | "field_type_override": p.FieldTypeOverride,
46 | }).Trace("overriding typeref")
47 | overrideType = p.FieldTypeOverride
48 | }
49 | }
50 | }
51 |
52 | // Set the typeName to the override or use what is specified in the schema.
53 | if overrideType != "" {
54 | typeName = overrideType
55 | } else {
56 | typeName, _, err = f.Type.GetType()
57 | if err != nil {
58 | return "", err
59 | }
60 | }
61 |
62 | return typeName, nil
63 | }
64 |
65 | // GetName returns a recusive lookup of the type name
66 | func (f *Field) GetName() string {
67 | return formatGoName(f.Name)
68 | }
69 |
70 | func (f *Field) GetTagsWithOverrides(parentType Type, pkgConfig *config.PackageConfig) string {
71 | if f == nil {
72 | return ""
73 | }
74 |
75 | // Get the parent type config to apply any field struct tag overrides
76 | parentTypeConfig := pkgConfig.GetTypeConfigByName(parentType.Name)
77 |
78 | var tags string
79 | if parentTypeConfig != nil && parentTypeConfig.StructTags != nil {
80 | tags = f.buildStructTags(f.Name, *parentTypeConfig.StructTags)
81 | }
82 |
83 | if tags == "" {
84 | return f.GetTags()
85 | }
86 |
87 | return tags
88 | }
89 |
90 | func (f *Field) buildStructTags(fieldName string, structTags config.StructTags) string {
91 | tagsString := "`"
92 |
93 | tagsCount := len(structTags.Tags)
94 | if tagsCount == 0 && structTags.OmitEmpty == nil {
95 | return f.GetTags()
96 | }
97 |
98 | if tagsCount == 0 && structTags.OmitEmpty != nil {
99 | structTags.Tags = []string{"json"} // default is to include json struct tags
100 | }
101 |
102 | canIncludeOmitEmpty := true // default is to add `omitempty` to struct tags
103 | if structTags.OmitEmpty != nil {
104 | canIncludeOmitEmpty = *structTags.OmitEmpty
105 | }
106 |
107 | for i, tagType := range structTags.Tags {
108 | tagEnd := "\" "
109 | if i == tagsCount-1 {
110 | tagEnd = "\"" // no trailing space if last tag
111 | }
112 |
113 | tagsString = tagsString + tagType + ":\"" + f.Name
114 |
115 | if canIncludeOmitEmpty && (f.Type.IsInputObject() || !f.Type.IsNonNull()) {
116 | tagsString = tagsString + ",omitempty"
117 | }
118 |
119 | tagsString = tagsString + tagEnd
120 | }
121 |
122 | // Add closing back tick
123 | tagsString += "`"
124 |
125 | return tagsString
126 | }
127 |
128 | // GetTags is used to return the Go struct tags for a field.
129 | func (f *Field) GetTags() string {
130 | if f == nil {
131 | return ""
132 | }
133 |
134 | jsonTag := "`json:\"" + f.Name
135 |
136 | if f.Type.IsInputObject() || !f.Type.IsNonNull() {
137 | jsonTag += ",omitempty"
138 | }
139 |
140 | tags := jsonTag + "\"`"
141 |
142 | return tags
143 | }
144 |
145 | func (f *Field) IsPrimitiveType() bool {
146 | goTypes := []string{
147 | "int",
148 | "string",
149 | "bool",
150 | "boolean",
151 | }
152 |
153 | name := strings.ToLower(f.Type.GetTypeName())
154 |
155 | for _, x := range goTypes {
156 | if x == name {
157 | return true
158 | }
159 | }
160 |
161 | return false
162 | }
163 |
164 | // Convenience method that proxies to TypeRef method
165 | func (f *Field) IsScalarID() bool {
166 | return f.Type.IsScalarID()
167 | }
168 |
169 | // Convenience method that proxies to TypeRef method
170 | func (f *Field) IsRequired() bool {
171 | return f.Type.IsNonNull()
172 | }
173 |
174 | func (f *Field) IsEnum() bool {
175 | if f.Type.Kind == KindENUM {
176 | return true
177 | }
178 |
179 | return f.Type.OfType != nil && f.Type.OfType.Kind == KindENUM
180 | }
181 |
182 | func (f *Field) HasRequiredArg() bool {
183 | for _, a := range f.Args {
184 | if a.IsRequired() {
185 | return true
186 | }
187 | }
188 |
189 | return false
190 | }
191 |
--------------------------------------------------------------------------------
/internal/schema/kind.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | type Kind string
4 |
5 | const (
6 | KindENUM Kind = "ENUM"
7 | KindInputObject Kind = "INPUT_OBJECT"
8 | KindInterface Kind = "INTERFACE"
9 | KindList Kind = "LIST"
10 | KindNonNull Kind = "NON_NULL"
11 | KindObject Kind = "OBJECT"
12 | KindScalar Kind = "SCALAR"
13 | )
14 |
--------------------------------------------------------------------------------
/internal/schema/query.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | // https://github.com/graphql/graphql-js/blob/master/src/utilities/getIntrospectionQuery.js#L35
4 | //
5 | // Modified from the following as we only care about the Types
6 | // query IntrospectionQuery {
7 | // __schema {
8 | // directives { name description locations args { ...InputValue } }
9 | // mutationType { name }
10 | // queryType { name }
11 | // subscriptionType { name }
12 | // types { ...FullType }
13 | // }
14 | // }
15 | const (
16 | // QuerySchema gets basic info about the schema
17 | QuerySchema = `query { __schema { mutationType { name } queryType { name } subscriptionType { name } } }`
18 |
19 | // QuerySchemaTypes is used to fetch all of the data types in the schema
20 | QuerySchemaTypes = `query { __schema { types { ...FullType } } }` + " " + fragmentFullType
21 |
22 | // QueryType returns all of the data on that specific type, useful for fetching queries / mutations
23 | QueryType = `query($typeName:String!) { __type(name: $typeName) { ...FullType } }` + " " + fragmentFullType
24 |
25 | // Reusable query fragments
26 | fragmentFullType = `
27 | fragment FullType on __Type {
28 | kind
29 | name
30 | description
31 | fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason }
32 | inputFields { ...InputValue }
33 | interfaces { ...TypeRef }
34 | enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason }
35 | possibleTypes { ...TypeRef }
36 | }` + " " + fragmentInputValue + " " + fragmentTypeRef
37 |
38 | fragmentInputValue = `fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue }`
39 | fragmentTypeRef = `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 } } } } } } } }`
40 | )
41 |
42 | // Helper function to make queries, lives here with the constant query
43 | type QueryTypeVars struct {
44 | Name string `json:"typeName"`
45 | }
46 |
47 | type QueryResponse struct {
48 | Data struct {
49 | Type Type `json:"__type"`
50 | Schema Schema `json:"__schema"`
51 | } `json:"data"`
52 | }
53 |
--------------------------------------------------------------------------------
/internal/schema/schema_test.go:
--------------------------------------------------------------------------------
1 | //go:build unit
2 | // +build unit
3 |
4 | package schema
5 |
6 | import (
7 | "fmt"
8 | "os"
9 | "strings"
10 | "testing"
11 |
12 | "github.com/stretchr/testify/assert"
13 | "github.com/stretchr/testify/require"
14 |
15 | "github.com/newrelic/tutone/internal/config"
16 | )
17 |
18 | //nolint:deadcode,unused //used to update fixtures as needed
19 | func saveFixture(t *testing.T, n, s string) {
20 | t.Helper()
21 | wd, _ := os.Getwd()
22 | fileName := fmt.Sprintf("testdata/%s_%s.txt", t.Name(), n)
23 | t.Logf("saving fixture to %s/%s", wd, fileName)
24 |
25 | err := os.MkdirAll("testdata", 0750)
26 | require.NoError(t, err)
27 |
28 | f, err := os.Create(fileName)
29 | require.NoError(t, err)
30 | defer f.Close()
31 |
32 | _, err = f.WriteString(s)
33 | require.NoError(t, err)
34 | }
35 |
36 | func loadFixture(t *testing.T, n string) string {
37 | t.Helper()
38 | fileName := fmt.Sprintf("testdata/%s_%s.txt", t.Name(), n)
39 | t.Logf("loading fixture %s", strings.TrimPrefix(fileName, "testdata/"))
40 |
41 | content, err := os.ReadFile(fileName)
42 | require.NoError(t, err)
43 |
44 | return strings.TrimSpace(string(content))
45 | }
46 |
47 | func TestSchema_BuildQueryArgsForEndpoint(t *testing.T) {
48 | t.Parallel()
49 |
50 | // schema cached by 'make test-prep'
51 | s, err := Load("../../testdata/schema.json")
52 | require.NoError(t, err)
53 |
54 | cases := map[string]struct {
55 | Name string
56 | Fields []string
57 | IncludeArgs []string
58 | Results []QueryArg
59 | }{
60 | "accountEntities": {
61 | Name: "Actor",
62 | Fields: []string{"account", "entities"},
63 | Results: []QueryArg{
64 | {Key: "id", Value: "Int!"},
65 | {Key: "guids", Value: "[EntityGuid]!"},
66 | },
67 | },
68 | "entities": {
69 | Name: "Actor",
70 | Fields: []string{"entities"},
71 | Results: []QueryArg{
72 | {Key: "guids", Value: "[EntityGuid]!"},
73 | },
74 | },
75 | "account": {
76 | Name: "Actor",
77 | Fields: []string{"account"},
78 | Results: []QueryArg{
79 | {Key: "id", Value: "Int!"},
80 | },
81 | },
82 | "entitySearch": {
83 | Name: "Actor",
84 | Fields: []string{"entitySearch"},
85 | IncludeArgs: []string{
86 | "options",
87 | "query",
88 | "queryBuilder",
89 | "sortBy",
90 | },
91 | Results: []QueryArg{
92 | {Key: "options", Value: "EntitySearchOptions"},
93 | {Key: "query", Value: "String"},
94 | {Key: "queryBuilder", Value: "EntitySearchQueryBuilder"},
95 | {Key: "sortBy", Value: "[EntitySearchSortCriteria]"},
96 | },
97 | },
98 | "entity": {
99 | Name: "Actor",
100 | Fields: []string{"entity"},
101 | Results: []QueryArg{
102 | {Key: "guid", Value: "EntityGuid!"},
103 | },
104 | },
105 | "accountOutline": {
106 | Name: "AccountOutline",
107 | Fields: []string{"reportingEventTypes"},
108 | IncludeArgs: []string{"filter", "timeWindow"},
109 | Results: []QueryArg{
110 | {Key: "filter", Value: "[String]"},
111 | {Key: "timeWindow", Value: "TimeWindowInput"},
112 | },
113 | },
114 | "linkedAccounts": {
115 | Name: "CloudActorFields",
116 | Fields: []string{"linkedAccounts"},
117 | IncludeArgs: []string{"provider"},
118 | Results: []QueryArg{
119 | {Key: "provider", Value: "String"},
120 | },
121 | },
122 | "linkedAccountsWithoutNullable": {
123 | Name: "CloudActorFields",
124 | Fields: []string{"linkedAccounts"},
125 | Results: []QueryArg{},
126 | },
127 | "linkedAccountsWithInvalidIncludeArgument": {
128 | Name: "CloudActorFields",
129 | Fields: []string{"linkedAccounts"},
130 | IncludeArgs: []string{"this-argument-does-not-exist"},
131 | Results: []QueryArg{},
132 | },
133 | }
134 |
135 | for _, tc := range cases {
136 | x, err := s.LookupTypeByName(tc.Name)
137 | require.NoError(t, err)
138 |
139 | result := s.BuildQueryArgsForEndpoint(x, tc.Fields, tc.IncludeArgs)
140 | assert.Equal(t, tc.Results, result)
141 | }
142 | }
143 |
144 | func TestSchema_LookupTypesByFieldPath(t *testing.T) {
145 | t.Parallel()
146 |
147 | // schema cached by 'make test-prep'
148 | s, err := Load("../../testdata/schema.json")
149 | require.NoError(t, err)
150 |
151 | actorType, err := s.LookupTypeByName("Actor")
152 | require.NoError(t, err)
153 | cloudType, err := s.LookupTypeByName("CloudActorFields")
154 | require.NoError(t, err)
155 |
156 | cases := map[string]struct {
157 | FieldPath []string
158 | Result []*Type
159 | }{
160 | "cloud": {
161 | FieldPath: []string{"actor", "cloud"},
162 | Result: []*Type{actorType, cloudType},
163 | },
164 | }
165 |
166 | for n, tc := range cases {
167 | t.Logf("TestCase: %s", n)
168 |
169 | result, err := s.LookupQueryTypesByFieldPath(tc.FieldPath)
170 | require.NoError(t, err)
171 |
172 | require.Equal(t, len(tc.Result), len(result))
173 |
174 | for i := range tc.Result {
175 | assert.Equal(t, tc.Result[i], result[i])
176 | }
177 | }
178 | }
179 |
180 | func TestSchema_GetQueryStringForEndpoint(t *testing.T) {
181 | t.Parallel()
182 |
183 | // schema cached by 'make test-prep'
184 | s, err := Load("../../testdata/schema.json")
185 | require.NoError(t, err)
186 |
187 | cases := map[string]struct {
188 | Path []string
189 | Endpoint config.EndpointConfig
190 | }{
191 | "entitySearch": {
192 | Path: []string{"actor"},
193 | Endpoint: config.EndpointConfig{
194 | Name: "entitySearch",
195 | MaxQueryFieldDepth: 3,
196 | },
197 | },
198 | "entitySearchArgs": {
199 | Path: []string{"actor"},
200 | Endpoint: config.EndpointConfig{
201 | Name: "entitySearch",
202 | MaxQueryFieldDepth: 3,
203 | IncludeArguments: []string{"query"},
204 | },
205 | },
206 | "entities": {
207 | Path: []string{"actor"},
208 | Endpoint: config.EndpointConfig{
209 | Name: "entities",
210 | // Zero set here because we have the field coverage above with greater depth. Here we want to ensure that required arguments on the entities endpoint has the correct syntax.
211 | MaxQueryFieldDepth: 0,
212 | },
213 | },
214 | "linkedAccounts": {
215 | Path: []string{"actor", "cloud"},
216 | Endpoint: config.EndpointConfig{
217 | Name: "linkedAccounts",
218 | MaxQueryFieldDepth: 2,
219 | IncludeArguments: []string{"provider"},
220 | },
221 | },
222 | "policy": {
223 | Path: []string{"actor", "account", "alerts"},
224 | Endpoint: config.EndpointConfig{
225 | Name: "policy",
226 | MaxQueryFieldDepth: 2,
227 | },
228 | },
229 | "user": {
230 | Path: []string{"actor"},
231 | Endpoint: config.EndpointConfig{
232 | Name: "user",
233 | MaxQueryFieldDepth: 2,
234 | ExcludeFields: []string{
235 | "email",
236 | },
237 | },
238 | },
239 | }
240 |
241 | for n, tc := range cases {
242 | t.Logf("TestCase: %s", n)
243 | typePath, err := s.LookupQueryTypesByFieldPath(tc.Path)
244 | require.NoError(t, err)
245 |
246 | result := s.GetQueryStringForEndpoint(typePath, tc.Path, tc.Endpoint)
247 | // saveFixture(t, n, result)
248 | expected := loadFixture(t, n)
249 | assert.Equal(t, expected, result)
250 | }
251 | }
252 |
253 | func TestSchema_GetQueryStringForMutation(t *testing.T) {
254 | t.Parallel()
255 |
256 | // schema cached by 'make test-prep'
257 | s, err := Load("../../testdata/schema.json")
258 | require.NoError(t, err)
259 |
260 | cases := []config.MutationConfig{
261 | {
262 | Name: "alertsMutingRuleCreate",
263 | MaxQueryFieldDepth: 3,
264 | ArgumentTypeOverrides: map[string]string{},
265 | },
266 | {
267 | Name: "cloudRenameAccount",
268 | MaxQueryFieldDepth: 1,
269 | ArgumentTypeOverrides: map[string]string{
270 | "accountId": "Int!",
271 | "accounts": "[CloudRenameAccountsInput!]!",
272 | },
273 | },
274 | {
275 | Name: "apiAccessCreateKeys",
276 | MaxQueryFieldDepth: 3,
277 | ArgumentTypeOverrides: map[string]string{},
278 | ExcludeFields: []string{
279 | "notes",
280 | },
281 | },
282 | }
283 |
284 | for _, tc := range cases {
285 | t.Logf("TestCase: %s", tc.Name)
286 | field, err := s.LookupMutationByName(tc.Name)
287 | require.NoError(t, err)
288 |
289 | result := s.GetQueryStringForMutation(field, tc)
290 | // saveFixture(t, tc.Name, result)
291 | expected := loadFixture(t, tc.Name)
292 | assert.Equal(t, expected, result)
293 | }
294 | }
295 |
296 | func TestSchema_GetQueryStringForMutation_Pattern(t *testing.T) {
297 | t.Parallel()
298 |
299 | // schema cached by 'make test-prep'
300 | s, err := Load("../../testdata/schema.json")
301 | require.NoError(t, err)
302 |
303 | cases := []config.MutationConfig{
304 | {
305 | Name: "alertsMutingRuleCreate",
306 | MaxQueryFieldDepth: 3,
307 | ArgumentTypeOverrides: map[string]string{},
308 | },
309 | {
310 | Name: "edge.*",
311 | MaxQueryFieldDepth: 3,
312 | },
313 | {
314 | Name: "dashboardCreate",
315 | },
316 | {
317 | Name: "^dashboardUpdate$",
318 | },
319 | }
320 |
321 | for _, tc := range cases {
322 | t.Logf("TestCase: %s", tc.Name)
323 | fields := s.LookupMutationsByPattern(tc.Name)
324 |
325 | for _, field := range fields {
326 | result := s.GetQueryStringForMutation(&field, tc)
327 | // saveFixture(t, field.Name, result)
328 | expected := loadFixture(t, field.Name)
329 | assert.Equal(t, expected, result)
330 | }
331 | }
332 | }
333 |
334 | func TestSchema_GetInputFieldsForQueryPath(t *testing.T) {
335 | t.Parallel()
336 |
337 | // schema cached by 'make test-prep'
338 | s, err := Load("../../testdata/schema.json")
339 | require.NoError(t, err)
340 |
341 | cases := map[string]struct {
342 | QueryPath []string
343 | Fields map[string][]string
344 | }{
345 | "accountCloud": {
346 | QueryPath: []string{"actor", "account", "cloud"},
347 | Fields: map[string][]string{
348 | "account": {"id"},
349 | },
350 | },
351 | "entities": {
352 | QueryPath: []string{"actor", "entities"},
353 | Fields: map[string][]string{
354 | "entities": {"guids"},
355 | },
356 | },
357 | "apiAccessKey": {
358 | QueryPath: []string{"actor", "apiAccess", "key"},
359 | Fields: map[string][]string{
360 | "key": {"id", "keyType"},
361 | },
362 | },
363 | }
364 |
365 | for _, tc := range cases {
366 | result := s.GetInputFieldsForQueryPath(tc.QueryPath)
367 | assert.Equal(t, len(tc.Fields), len(result))
368 |
369 | for pathName, fields := range tc.Fields {
370 | for i, name := range fields {
371 | assert.Equal(t, name, result[pathName][i].Name)
372 | }
373 | }
374 | }
375 | }
376 |
--------------------------------------------------------------------------------
/internal/schema/schema_util.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strings"
7 |
8 | "golang.org/x/text/cases"
9 | "golang.org/x/text/language"
10 |
11 | log "github.com/sirupsen/logrus"
12 |
13 | "github.com/newrelic/tutone/internal/config"
14 | "github.com/newrelic/tutone/internal/util"
15 | )
16 |
17 | // filterDescription uses a regex to parse certain data out of the
18 | // description of an item
19 | func filterDescription(description string) string {
20 | var ret string
21 |
22 | re := regexp.MustCompile(`(?s)(.*)\n---\n`)
23 | desc := re.FindStringSubmatch(description)
24 |
25 | if len(desc) > 1 {
26 | ret = desc[1]
27 | } else {
28 | ret = description
29 | }
30 |
31 | return strings.TrimSpace(ret)
32 | }
33 |
34 | // PrefixLineTab adds a \t character to the beginning of each line.
35 | func PrefixLineTab(s string) string {
36 | var lines []string
37 |
38 | for _, t := range strings.Split(s, "\n") {
39 | lines = append(lines, fmt.Sprintf("\t%s", t))
40 | }
41 |
42 | return strings.Join(lines, "\n")
43 | }
44 |
45 | func formatDescription(name string, description string) string {
46 | if strings.TrimSpace(description) == "" {
47 | return ""
48 | }
49 |
50 | filtered := filterDescription(description)
51 | lines := strings.Split(filtered, "\n")
52 |
53 | var resultLines []string
54 |
55 | for i, l := range lines {
56 | if i == 0 && name != "" {
57 | resultLines = append(resultLines, fmt.Sprintf("// %s - %s", name, l))
58 | } else {
59 | resultLines = append(resultLines, fmt.Sprintf("// %s", l))
60 | }
61 | }
62 |
63 | return strings.Join(resultLines, "\n")
64 | }
65 |
66 | // typeNameInTypes determines if a name is already present in a set of config.TypeConfig
67 | func typeNameInTypes(s string, types []config.TypeConfig) bool {
68 | for _, t := range types {
69 | if strings.EqualFold(t.Name, s) {
70 | return !(t.SkipTypeCreate)
71 | }
72 | }
73 |
74 | return false
75 | }
76 |
77 | // mutationNameInMutations determines if a name is already present in a set of config.MutationConfig.
78 | func mutationNameInMutations(s string, mutations []config.MutationConfig) bool {
79 | for _, t := range mutations {
80 | if found, _ := regexp.MatchString(t.Name, s); found {
81 | return true
82 | }
83 | }
84 |
85 | return false
86 | }
87 |
88 | // hasType determines if a Type is already present in a slice of Type objects.
89 | func hasType(t *Type, types []*Type) bool {
90 | if t == nil {
91 | log.Warn("hasType(nil)")
92 | }
93 |
94 | for _, tt := range types {
95 | if t.Name == tt.Name {
96 | return true
97 | }
98 | }
99 |
100 | return false
101 | }
102 |
103 | // ExpandTypes receives a set of config.TypeConfig, which is then expanded to include
104 | // all the nested types from the fields.
105 | func ExpandTypes(s *Schema, pkgConfig *config.PackageConfig) (*[]*Type, error) {
106 | if s == nil {
107 | return nil, fmt.Errorf("unable to expand types from nil schema")
108 | }
109 |
110 | if pkgConfig == nil {
111 | return nil, fmt.Errorf("unable to expand types from nil PackageConfig")
112 | }
113 |
114 | skipTypes := make([]string, 0, len(pkgConfig.Types))
115 | for _, t := range pkgConfig.Types {
116 | if t.SkipTypeCreate {
117 | skipTypes = append(skipTypes, t.Name)
118 | }
119 | }
120 |
121 | var err error
122 | expander := NewExpander(s, skipTypes)
123 |
124 | queries := []string{}
125 | for _, pkgQuery := range pkgConfig.Queries {
126 | for _, q := range pkgQuery.Endpoints {
127 | queries = append(queries, q.Name)
128 | }
129 | }
130 |
131 | for _, schemaType := range s.Types {
132 | if schemaType == nil {
133 | continue
134 | }
135 |
136 | // Constrain our handling to include only the type names which are mentioned in the configuration
137 | if typeNameInTypes(schemaType.Name, pkgConfig.Types) {
138 | log.WithFields(log.Fields{
139 | "schema_name": schemaType.GetName(),
140 | }).Debug("config type")
141 |
142 | err = expander.ExpandType(schemaType)
143 | if err != nil {
144 | log.Error(err)
145 | }
146 | }
147 |
148 | for _, field := range schemaType.Fields {
149 | if util.StringInStrings(field.Name, queries) {
150 | err = expander.ExpandTypeFromName(field.Type.GetTypeName())
151 | if err != nil {
152 | log.Error(err)
153 | }
154 | }
155 | }
156 | }
157 |
158 | var mutationFields []Field
159 | mutationFields = append(mutationFields, s.MutationType.Fields...)
160 | mutationFields = append(mutationFields, s.MutationType.InputFields...)
161 |
162 | for _, field := range mutationFields {
163 | // Constrain our handling to include only the mutation names which are mentioned in the configuration.
164 | if mutationNameInMutations(field.Name, pkgConfig.Mutations) {
165 | err = expander.ExpandTypeFromName(field.Type.GetTypeName())
166 | if err != nil {
167 | log.WithFields(log.Fields{
168 | "name": field.Type.Name,
169 | "field": field.Name,
170 | }).Errorf("unable to expand mutation field type: %s", err)
171 | }
172 |
173 | for _, mutationArg := range field.Args {
174 | err := expander.ExpandTypeFromName(mutationArg.Type.GetTypeName())
175 | if err != nil {
176 | log.WithFields(log.Fields{
177 | "name": mutationArg.Name,
178 | "type": mutationArg.Type,
179 | }).Errorf("failed to expand mutation argument: %s", err)
180 | }
181 | }
182 | }
183 | }
184 |
185 | return expander.ExpandedTypes(), nil
186 | }
187 |
188 | // formatGoName formats a name string using a few special cases for proper capitalization.
189 | func formatGoName(name string) string {
190 | var fieldName string
191 |
192 | switch strings.ToLower(name) {
193 | case "ids":
194 | // special case to avoid the struct field Ids, and prefer IDs instead
195 | fieldName = "IDs"
196 | case "id":
197 | fieldName = "ID"
198 | case "accountid":
199 | fieldName = "AccountID"
200 | case "accountids":
201 | fieldName = "AccountIDs"
202 | case "userid":
203 | fieldName = "UserID"
204 | case "userids":
205 | fieldName = "UserIDs"
206 | case "ingestkeyids":
207 | fieldName = "IngestKeyIDs"
208 | case "userkeyids":
209 | fieldName = "UserKeyIDs"
210 | case "keyid":
211 | fieldName = "KeyID"
212 | case "policyid":
213 | fieldName = "PolicyID"
214 | default:
215 | caser := cases.Title(language.Und, cases.NoLower)
216 | fieldName = caser.String(name)
217 | }
218 |
219 | r := strings.NewReplacer(
220 | "Api", "API",
221 | "Guid", "GUID",
222 | "Nrql", "NRQL",
223 | "Nrdb", "NRDB",
224 | "Url", "URL",
225 | "ApplicationId", "ApplicationID",
226 | )
227 |
228 | fieldName = r.Replace(fieldName)
229 |
230 | return fieldName
231 | }
232 |
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForEndpoint_entities.txt:
--------------------------------------------------------------------------------
1 | query(
2 | $guids: [EntityGuid]!,
3 | ) { actor { entities(
4 | guids: $guids,
5 | ) {
6 |
7 | } } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForEndpoint_entitySearch.txt:
--------------------------------------------------------------------------------
1 | query { actor { entitySearch {
2 | count
3 | query
4 | results {
5 | entities {
6 | __typename
7 | account {
8 | id
9 | name
10 | reportingEventTypes
11 | }
12 | accountId
13 | alertSeverity
14 | domain
15 | entityType
16 | firstIndexedAt
17 | guid
18 | indexedAt
19 | lastReportingChangeAt
20 | name
21 | permalink
22 | reporting
23 | tags {
24 | key
25 | values
26 | }
27 | type
28 | ... on ApmApplicationEntityOutline {
29 | __typename
30 | applicationId
31 | language
32 | }
33 | ... on ApmDatabaseInstanceEntityOutline {
34 | __typename
35 | host
36 | portOrPath
37 | vendor
38 | }
39 | ... on ApmExternalServiceEntityOutline {
40 | __typename
41 | host
42 | }
43 | ... on BrowserApplicationEntityOutline {
44 | __typename
45 | agentInstallType
46 | applicationId
47 | servingApmApplicationId
48 | }
49 | ... on DashboardEntityOutline {
50 | __typename
51 | createdAt
52 | dashboardParentGuid
53 | permissions
54 | updatedAt
55 | }
56 | ... on ExternalEntityOutline {
57 | __typename
58 | }
59 | ... on GenericEntityOutline {
60 | __typename
61 | }
62 | ... on GenericInfrastructureEntityOutline {
63 | __typename
64 | integrationTypeCode
65 | }
66 | ... on InfrastructureAwsLambdaFunctionEntityOutline {
67 | __typename
68 | integrationTypeCode
69 | runtime
70 | }
71 | ... on InfrastructureHostEntityOutline {
72 | __typename
73 | }
74 | ... on KeyTransactionEntityOutline {
75 | __typename
76 | }
77 | ... on MobileApplicationEntityOutline {
78 | __typename
79 | applicationId
80 | }
81 | ... on SecureCredentialEntityOutline {
82 | __typename
83 | description
84 | secureCredentialId
85 | updatedAt
86 | }
87 | ... on SyntheticMonitorEntityOutline {
88 | __typename
89 | monitorId
90 | monitorType
91 | monitoredUrl
92 | period
93 | }
94 | ... on ThirdPartyServiceEntityOutline {
95 | __typename
96 | }
97 | ... on UnavailableEntityOutline {
98 | __typename
99 | }
100 | ... on WorkloadEntityOutline {
101 | __typename
102 | createdAt
103 | updatedAt
104 | }
105 | }
106 | nextCursor
107 | }
108 | types {
109 | count
110 | domain
111 | entityType
112 | type
113 | }
114 | } } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForEndpoint_entitySearchArgs.txt:
--------------------------------------------------------------------------------
1 | query(
2 | $query: String,
3 | ) { actor { entitySearch(
4 | query: $query,
5 | ) {
6 | count
7 | query
8 | results {
9 | entities {
10 | __typename
11 | account {
12 | id
13 | name
14 | reportingEventTypes
15 | }
16 | accountId
17 | alertSeverity
18 | domain
19 | entityType
20 | firstIndexedAt
21 | guid
22 | indexedAt
23 | lastReportingChangeAt
24 | name
25 | permalink
26 | reporting
27 | tags {
28 | key
29 | values
30 | }
31 | type
32 | ... on ApmApplicationEntityOutline {
33 | __typename
34 | applicationId
35 | language
36 | }
37 | ... on ApmDatabaseInstanceEntityOutline {
38 | __typename
39 | host
40 | portOrPath
41 | vendor
42 | }
43 | ... on ApmExternalServiceEntityOutline {
44 | __typename
45 | host
46 | }
47 | ... on BrowserApplicationEntityOutline {
48 | __typename
49 | agentInstallType
50 | applicationId
51 | servingApmApplicationId
52 | }
53 | ... on DashboardEntityOutline {
54 | __typename
55 | createdAt
56 | dashboardParentGuid
57 | permissions
58 | updatedAt
59 | }
60 | ... on ExternalEntityOutline {
61 | __typename
62 | }
63 | ... on GenericEntityOutline {
64 | __typename
65 | }
66 | ... on GenericInfrastructureEntityOutline {
67 | __typename
68 | integrationTypeCode
69 | }
70 | ... on InfrastructureAwsLambdaFunctionEntityOutline {
71 | __typename
72 | integrationTypeCode
73 | runtime
74 | }
75 | ... on InfrastructureHostEntityOutline {
76 | __typename
77 | }
78 | ... on KeyTransactionEntityOutline {
79 | __typename
80 | }
81 | ... on MobileApplicationEntityOutline {
82 | __typename
83 | applicationId
84 | }
85 | ... on SecureCredentialEntityOutline {
86 | __typename
87 | description
88 | secureCredentialId
89 | updatedAt
90 | }
91 | ... on SyntheticMonitorEntityOutline {
92 | __typename
93 | monitorId
94 | monitorType
95 | monitoredUrl
96 | period
97 | }
98 | ... on ThirdPartyServiceEntityOutline {
99 | __typename
100 | }
101 | ... on UnavailableEntityOutline {
102 | __typename
103 | }
104 | ... on WorkloadEntityOutline {
105 | __typename
106 | createdAt
107 | updatedAt
108 | }
109 | }
110 | nextCursor
111 | }
112 | types {
113 | count
114 | domain
115 | entityType
116 | type
117 | }
118 | } } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForEndpoint_policy.txt:
--------------------------------------------------------------------------------
1 | query(
2 | $accountID: Int!,
3 | $id: ID!,
4 | ) { actor { account(id: $accountID) { alerts { policy(
5 | id: $id,
6 | ) {
7 | accountId
8 | id
9 | incidentPreference
10 | name
11 | } } } } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForEndpoint_user.txt:
--------------------------------------------------------------------------------
1 | query { actor { user {
2 | id
3 | name
4 | } } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_Pattern_alertsMutingRuleCreate.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $accountId: Int!,
3 | $rule: AlertsMutingRuleInput!,
4 | ) { alertsMutingRuleCreate(
5 | accountId: $accountId,
6 | rule: $rule,
7 | ) {
8 | accountId
9 | condition {
10 | conditions {
11 | attribute
12 | operator
13 | values
14 | }
15 | operator
16 | }
17 | createdAt
18 | createdBy
19 | createdByUser {
20 | email
21 | gravatar
22 | id
23 | name
24 | }
25 | description
26 | enabled
27 | id
28 | name
29 | schedule {
30 | endRepeat
31 | endTime
32 | nextEndTime
33 | nextStartTime
34 | repeat
35 | repeatCount
36 | startTime
37 | timeZone
38 | weeklyRepeatDays
39 | }
40 | status
41 | updatedAt
42 | updatedBy
43 | updatedByUser {
44 | email
45 | gravatar
46 | id
47 | name
48 | }
49 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_Pattern_dashboardCreate.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $accountId: Int!,
3 | $dashboard: DashboardInput!,
4 | ) { dashboardCreate(
5 | accountId: $accountId,
6 | dashboard: $dashboard,
7 | ) {
8 |
9 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_Pattern_dashboardUpdate.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $dashboard: DashboardInput!,
3 | $guid: EntityGuid!,
4 | ) { dashboardUpdate(
5 | dashboard: $dashboard,
6 | guid: $guid,
7 | ) {
8 |
9 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_Pattern_edgeCreateTraceFilterRules.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $accountId: Int!,
3 | $rules: EdgeCreateTraceFilterRulesInput!,
4 | $traceObserverId: Int!,
5 | ) { edgeCreateTraceFilterRules(
6 | accountId: $accountId,
7 | rules: $rules,
8 | traceObserverId: $traceObserverId,
9 | ) {
10 | spanAttributeRules {
11 | errors {
12 | message
13 | type
14 | }
15 | rules {
16 | action
17 | id
18 | key
19 | keyOperator
20 | value
21 | valueOperator
22 | }
23 | }
24 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_Pattern_edgeCreateTraceObserver.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $accountId: Int!,
3 | $traceObserverConfigs: [EdgeCreateTraceObserverInput!]!,
4 | ) { edgeCreateTraceObserver(
5 | accountId: $accountId,
6 | traceObserverConfigs: $traceObserverConfigs,
7 | ) {
8 | responses {
9 | errors {
10 | message
11 | type
12 | }
13 | traceObserver {
14 | complianceTypes
15 | endpoints {
16 | endpointType
17 | status
18 | }
19 | id
20 | monitoringAccountId
21 | name
22 | providerRegion
23 | status
24 | }
25 | }
26 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_Pattern_edgeDeleteTraceFilterRules.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $accountId: Int!,
3 | $rules: EdgeDeleteTraceFilterRulesInput!,
4 | $traceObserverId: Int!,
5 | ) { edgeDeleteTraceFilterRules(
6 | accountId: $accountId,
7 | rules: $rules,
8 | traceObserverId: $traceObserverId,
9 | ) {
10 | spanAttributeRules {
11 | errors {
12 | message
13 | type
14 | }
15 | rule {
16 | action
17 | id
18 | key
19 | keyOperator
20 | value
21 | valueOperator
22 | }
23 | }
24 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_Pattern_edgeDeleteTraceObservers.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $accountId: Int!,
3 | $traceObserverConfigs: [EdgeDeleteTraceObserverInput!]!,
4 | ) { edgeDeleteTraceObservers(
5 | accountId: $accountId,
6 | traceObserverConfigs: $traceObserverConfigs,
7 | ) {
8 | responses {
9 | errors {
10 | message
11 | type
12 | }
13 | traceObserver {
14 | complianceTypes
15 | endpoints {
16 | endpointType
17 | status
18 | }
19 | id
20 | monitoringAccountId
21 | name
22 | providerRegion
23 | status
24 | }
25 | }
26 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_Pattern_edgeUpdateTraceObservers.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $accountId: Int!,
3 | $traceObserverConfigs: [EdgeUpdateTraceObserverInput!]!,
4 | ) { edgeUpdateTraceObservers(
5 | accountId: $accountId,
6 | traceObserverConfigs: $traceObserverConfigs,
7 | ) {
8 | responses {
9 | errors {
10 | message
11 | type
12 | }
13 | traceObserver {
14 | complianceTypes
15 | endpoints {
16 | endpointType
17 | status
18 | }
19 | id
20 | monitoringAccountId
21 | name
22 | providerRegion
23 | status
24 | }
25 | }
26 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_alertsMutingRuleCreate.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $accountId: Int!,
3 | $rule: AlertsMutingRuleInput!,
4 | ) { alertsMutingRuleCreate(
5 | accountId: $accountId,
6 | rule: $rule,
7 | ) {
8 | accountId
9 | condition {
10 | conditions {
11 | attribute
12 | operator
13 | values
14 | }
15 | operator
16 | }
17 | createdAt
18 | createdBy
19 | createdByUser {
20 | email
21 | gravatar
22 | id
23 | name
24 | }
25 | description
26 | enabled
27 | id
28 | name
29 | schedule {
30 | endRepeat
31 | endTime
32 | nextEndTime
33 | nextStartTime
34 | repeat
35 | repeatCount
36 | startTime
37 | timeZone
38 | weeklyRepeatDays
39 | }
40 | status
41 | updatedAt
42 | updatedBy
43 | updatedByUser {
44 | email
45 | gravatar
46 | id
47 | name
48 | }
49 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_apiAccessCreateKeys.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $keys: ApiAccessCreateInput!,
3 | ) { apiAccessCreateKeys(
4 | keys: $keys,
5 | ) {
6 | createdKeys {
7 | __typename
8 | createdAt
9 | id
10 | key
11 | name
12 | type
13 | ... on ApiAccessIngestKey {
14 | __typename
15 | account {
16 | id
17 | name
18 | }
19 | accountId
20 | ingestType
21 | }
22 | ... on ApiAccessUserKey {
23 | __typename
24 | account {
25 | id
26 | name
27 | }
28 | accountId
29 | user {
30 | email
31 | gravatar
32 | id
33 | name
34 | }
35 | userId
36 | }
37 | }
38 | errors {
39 | __typename
40 | message
41 | type
42 | ... on ApiAccessIngestKeyError {
43 | __typename
44 | accountId
45 | errorType
46 | id
47 | ingestType
48 | }
49 | ... on ApiAccessUserKeyError {
50 | __typename
51 | accountId
52 | errorType
53 | id
54 | userId
55 | }
56 | }
57 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestSchema_GetQueryStringForMutation_cloudRenameAccount.txt:
--------------------------------------------------------------------------------
1 | mutation(
2 | $accountId: Int!,
3 | $accounts: [CloudRenameAccountsInput!]!,
4 | ) { cloudRenameAccount(
5 | accountId: $accountId,
6 | accounts: $accounts,
7 | ) {
8 | errors {
9 | linkedAccountId
10 | message
11 | nrAccountId
12 | providerSlug
13 | type
14 | }
15 | linkedAccounts {
16 | authLabel
17 | createdAt
18 | disabled
19 | externalId
20 | id
21 | metricCollectionMode
22 | name
23 | nrAccountId
24 | updatedAt
25 | }
26 | } }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestType_GetQueryFieldsString_AlertsNrqlBaselineCondition.txt:
--------------------------------------------------------------------------------
1 | baselineDirection
2 | description
3 | enabled
4 | entity {
5 | __typename
6 | account {
7 | id
8 | name
9 | reportingEventTypes
10 | }
11 | accountId
12 | alertSeverity
13 | domain
14 | entityType
15 | firstIndexedAt
16 | guid
17 | indexedAt
18 | lastReportingChangeAt
19 | name
20 | permalink
21 | reporting
22 | tags {
23 | key
24 | values
25 | }
26 | type
27 | ... on ApmApplicationEntityOutline {
28 | __typename
29 | applicationId
30 | language
31 | }
32 | ... on ApmDatabaseInstanceEntityOutline {
33 | __typename
34 | host
35 | portOrPath
36 | vendor
37 | }
38 | ... on ApmExternalServiceEntityOutline {
39 | __typename
40 | host
41 | }
42 | ... on BrowserApplicationEntityOutline {
43 | __typename
44 | agentInstallType
45 | applicationId
46 | servingApmApplicationId
47 | }
48 | ... on DashboardEntityOutline {
49 | __typename
50 | createdAt
51 | dashboardParentGuid
52 | permissions
53 | updatedAt
54 | }
55 | ... on ExternalEntityOutline {
56 | __typename
57 | }
58 | ... on GenericEntityOutline {
59 | __typename
60 | }
61 | ... on GenericInfrastructureEntityOutline {
62 | __typename
63 | integrationTypeCode
64 | }
65 | ... on InfrastructureAwsLambdaFunctionEntityOutline {
66 | __typename
67 | integrationTypeCode
68 | runtime
69 | }
70 | ... on InfrastructureHostEntityOutline {
71 | __typename
72 | }
73 | ... on KeyTransactionEntityOutline {
74 | __typename
75 | }
76 | ... on MobileApplicationEntityOutline {
77 | __typename
78 | applicationId
79 | }
80 | ... on SecureCredentialEntityOutline {
81 | __typename
82 | description
83 | secureCredentialId
84 | updatedAt
85 | }
86 | ... on SyntheticMonitorEntityOutline {
87 | __typename
88 | monitorId
89 | monitorType
90 | monitoredUrl
91 | period
92 | }
93 | ... on ThirdPartyServiceEntityOutline {
94 | __typename
95 | }
96 | ... on UnavailableEntityOutline {
97 | __typename
98 | }
99 | ... on WorkloadEntityOutline {
100 | __typename
101 | createdAt
102 | updatedAt
103 | }
104 | }
105 | entityGuid
106 | expiration {
107 | closeViolationsOnExpiration
108 | expirationDuration
109 | openViolationOnExpiration
110 | }
111 | id
112 | name
113 | nrql {
114 | evaluationOffset
115 | query
116 | }
117 | policyId
118 | runbookUrl
119 | signal {
120 | aggregationDelay
121 | aggregationMethod
122 | aggregationTimer
123 | aggregationWindow
124 | evaluationDelay
125 | evaluationOffset
126 | fillOption
127 | fillValue
128 | slideBy
129 | }
130 | terms {
131 | operator
132 | priority
133 | threshold
134 | thresholdDuration
135 | thresholdOccurrences
136 | }
137 | type
138 | violationTimeLimit
139 | violationTimeLimitSeconds
--------------------------------------------------------------------------------
/internal/schema/testdata/TestType_GetQueryFieldsString_AlertsNrqlCondition.txt:
--------------------------------------------------------------------------------
1 | description
2 | enabled
3 | entity {
4 | __typename
5 | account {
6 | id
7 | name
8 | reportingEventTypes
9 | }
10 | accountId
11 | alertSeverity
12 | domain
13 | entityType
14 | firstIndexedAt
15 | guid
16 | indexedAt
17 | lastReportingChangeAt
18 | name
19 | permalink
20 | reporting
21 | tags {
22 | key
23 | values
24 | }
25 | type
26 | ... on ApmApplicationEntityOutline {
27 | __typename
28 | applicationId
29 | language
30 | }
31 | ... on ApmDatabaseInstanceEntityOutline {
32 | __typename
33 | host
34 | portOrPath
35 | vendor
36 | }
37 | ... on ApmExternalServiceEntityOutline {
38 | __typename
39 | host
40 | }
41 | ... on BrowserApplicationEntityOutline {
42 | __typename
43 | agentInstallType
44 | applicationId
45 | servingApmApplicationId
46 | }
47 | ... on DashboardEntityOutline {
48 | __typename
49 | createdAt
50 | dashboardParentGuid
51 | permissions
52 | updatedAt
53 | }
54 | ... on ExternalEntityOutline {
55 | __typename
56 | }
57 | ... on GenericEntityOutline {
58 | __typename
59 | }
60 | ... on GenericInfrastructureEntityOutline {
61 | __typename
62 | integrationTypeCode
63 | }
64 | ... on InfrastructureAwsLambdaFunctionEntityOutline {
65 | __typename
66 | integrationTypeCode
67 | runtime
68 | }
69 | ... on InfrastructureHostEntityOutline {
70 | __typename
71 | }
72 | ... on KeyTransactionEntityOutline {
73 | __typename
74 | }
75 | ... on MobileApplicationEntityOutline {
76 | __typename
77 | applicationId
78 | }
79 | ... on SecureCredentialEntityOutline {
80 | __typename
81 | description
82 | secureCredentialId
83 | updatedAt
84 | }
85 | ... on SyntheticMonitorEntityOutline {
86 | __typename
87 | monitorId
88 | monitorType
89 | monitoredUrl
90 | period
91 | }
92 | ... on ThirdPartyServiceEntityOutline {
93 | __typename
94 | }
95 | ... on UnavailableEntityOutline {
96 | __typename
97 | }
98 | ... on WorkloadEntityOutline {
99 | __typename
100 | createdAt
101 | updatedAt
102 | }
103 | }
104 | entityGuid
105 | expiration {
106 | closeViolationsOnExpiration
107 | expirationDuration
108 | openViolationOnExpiration
109 | }
110 | id
111 | name
112 | nrql {
113 | evaluationOffset
114 | query
115 | }
116 | policyId
117 | runbookUrl
118 | signal {
119 | aggregationDelay
120 | aggregationMethod
121 | aggregationTimer
122 | aggregationWindow
123 | evaluationDelay
124 | evaluationOffset
125 | fillOption
126 | fillValue
127 | slideBy
128 | }
129 | terms {
130 | operator
131 | priority
132 | threshold
133 | thresholdDuration
134 | thresholdOccurrences
135 | }
136 | type
137 | violationTimeLimit
138 | violationTimeLimitSeconds
139 | ... on AlertsNrqlBaselineCondition {
140 | __typename
141 | baselineDirection
142 | entity {
143 | __typename
144 | accountId
145 | alertSeverity
146 | domain
147 | entityType
148 | firstIndexedAt
149 | guid
150 | indexedAt
151 | lastReportingChangeAt
152 | name
153 | permalink
154 | reporting
155 | type
156 | ... on ApmApplicationEntityOutline {
157 | __typename
158 | applicationId
159 | language
160 | }
161 | ... on ApmDatabaseInstanceEntityOutline {
162 | __typename
163 | host
164 | portOrPath
165 | vendor
166 | }
167 | ... on ApmExternalServiceEntityOutline {
168 | __typename
169 | host
170 | }
171 | ... on BrowserApplicationEntityOutline {
172 | __typename
173 | agentInstallType
174 | applicationId
175 | servingApmApplicationId
176 | }
177 | ... on DashboardEntityOutline {
178 | __typename
179 | createdAt
180 | dashboardParentGuid
181 | permissions
182 | updatedAt
183 | }
184 | ... on ExternalEntityOutline {
185 | __typename
186 | }
187 | ... on GenericEntityOutline {
188 | __typename
189 | }
190 | ... on GenericInfrastructureEntityOutline {
191 | __typename
192 | integrationTypeCode
193 | }
194 | ... on InfrastructureAwsLambdaFunctionEntityOutline {
195 | __typename
196 | integrationTypeCode
197 | runtime
198 | }
199 | ... on InfrastructureHostEntityOutline {
200 | __typename
201 | }
202 | ... on KeyTransactionEntityOutline {
203 | __typename
204 | }
205 | ... on MobileApplicationEntityOutline {
206 | __typename
207 | applicationId
208 | }
209 | ... on SecureCredentialEntityOutline {
210 | __typename
211 | description
212 | secureCredentialId
213 | updatedAt
214 | }
215 | ... on SyntheticMonitorEntityOutline {
216 | __typename
217 | monitorId
218 | monitorType
219 | monitoredUrl
220 | period
221 | }
222 | ... on ThirdPartyServiceEntityOutline {
223 | __typename
224 | }
225 | ... on UnavailableEntityOutline {
226 | __typename
227 | }
228 | ... on WorkloadEntityOutline {
229 | __typename
230 | createdAt
231 | updatedAt
232 | }
233 | }
234 | expiration {
235 | closeViolationsOnExpiration
236 | expirationDuration
237 | openViolationOnExpiration
238 | }
239 | nrql {
240 | evaluationOffset
241 | query
242 | }
243 | signal {
244 | aggregationDelay
245 | aggregationMethod
246 | aggregationTimer
247 | aggregationWindow
248 | evaluationDelay
249 | evaluationOffset
250 | fillOption
251 | fillValue
252 | slideBy
253 | }
254 | terms {
255 | operator
256 | priority
257 | threshold
258 | thresholdDuration
259 | thresholdOccurrences
260 | }
261 | }
262 | ... on AlertsNrqlOutlierCondition {
263 | __typename
264 | entity {
265 | __typename
266 | accountId
267 | alertSeverity
268 | domain
269 | entityType
270 | firstIndexedAt
271 | guid
272 | indexedAt
273 | lastReportingChangeAt
274 | name
275 | permalink
276 | reporting
277 | type
278 | ... on ApmApplicationEntityOutline {
279 | __typename
280 | applicationId
281 | language
282 | }
283 | ... on ApmDatabaseInstanceEntityOutline {
284 | __typename
285 | host
286 | portOrPath
287 | vendor
288 | }
289 | ... on ApmExternalServiceEntityOutline {
290 | __typename
291 | host
292 | }
293 | ... on BrowserApplicationEntityOutline {
294 | __typename
295 | agentInstallType
296 | applicationId
297 | servingApmApplicationId
298 | }
299 | ... on DashboardEntityOutline {
300 | __typename
301 | createdAt
302 | dashboardParentGuid
303 | permissions
304 | updatedAt
305 | }
306 | ... on ExternalEntityOutline {
307 | __typename
308 | }
309 | ... on GenericEntityOutline {
310 | __typename
311 | }
312 | ... on GenericInfrastructureEntityOutline {
313 | __typename
314 | integrationTypeCode
315 | }
316 | ... on InfrastructureAwsLambdaFunctionEntityOutline {
317 | __typename
318 | integrationTypeCode
319 | runtime
320 | }
321 | ... on InfrastructureHostEntityOutline {
322 | __typename
323 | }
324 | ... on KeyTransactionEntityOutline {
325 | __typename
326 | }
327 | ... on MobileApplicationEntityOutline {
328 | __typename
329 | applicationId
330 | }
331 | ... on SecureCredentialEntityOutline {
332 | __typename
333 | description
334 | secureCredentialId
335 | updatedAt
336 | }
337 | ... on SyntheticMonitorEntityOutline {
338 | __typename
339 | monitorId
340 | monitorType
341 | monitoredUrl
342 | period
343 | }
344 | ... on ThirdPartyServiceEntityOutline {
345 | __typename
346 | }
347 | ... on UnavailableEntityOutline {
348 | __typename
349 | }
350 | ... on WorkloadEntityOutline {
351 | __typename
352 | createdAt
353 | updatedAt
354 | }
355 | }
356 | expectedGroups
357 | expiration {
358 | closeViolationsOnExpiration
359 | expirationDuration
360 | openViolationOnExpiration
361 | }
362 | nrql {
363 | evaluationOffset
364 | query
365 | }
366 | openViolationOnGroupOverlap
367 | signal {
368 | aggregationDelay
369 | aggregationMethod
370 | aggregationTimer
371 | aggregationWindow
372 | evaluationDelay
373 | evaluationOffset
374 | fillOption
375 | fillValue
376 | slideBy
377 | }
378 | terms {
379 | operator
380 | priority
381 | threshold
382 | thresholdDuration
383 | thresholdOccurrences
384 | }
385 | }
386 | ... on AlertsNrqlStaticCondition {
387 | __typename
388 | entity {
389 | __typename
390 | accountId
391 | alertSeverity
392 | domain
393 | entityType
394 | firstIndexedAt
395 | guid
396 | indexedAt
397 | lastReportingChangeAt
398 | name
399 | permalink
400 | reporting
401 | type
402 | ... on ApmApplicationEntityOutline {
403 | __typename
404 | applicationId
405 | language
406 | }
407 | ... on ApmDatabaseInstanceEntityOutline {
408 | __typename
409 | host
410 | portOrPath
411 | vendor
412 | }
413 | ... on ApmExternalServiceEntityOutline {
414 | __typename
415 | host
416 | }
417 | ... on BrowserApplicationEntityOutline {
418 | __typename
419 | agentInstallType
420 | applicationId
421 | servingApmApplicationId
422 | }
423 | ... on DashboardEntityOutline {
424 | __typename
425 | createdAt
426 | dashboardParentGuid
427 | permissions
428 | updatedAt
429 | }
430 | ... on ExternalEntityOutline {
431 | __typename
432 | }
433 | ... on GenericEntityOutline {
434 | __typename
435 | }
436 | ... on GenericInfrastructureEntityOutline {
437 | __typename
438 | integrationTypeCode
439 | }
440 | ... on InfrastructureAwsLambdaFunctionEntityOutline {
441 | __typename
442 | integrationTypeCode
443 | runtime
444 | }
445 | ... on InfrastructureHostEntityOutline {
446 | __typename
447 | }
448 | ... on KeyTransactionEntityOutline {
449 | __typename
450 | }
451 | ... on MobileApplicationEntityOutline {
452 | __typename
453 | applicationId
454 | }
455 | ... on SecureCredentialEntityOutline {
456 | __typename
457 | description
458 | secureCredentialId
459 | updatedAt
460 | }
461 | ... on SyntheticMonitorEntityOutline {
462 | __typename
463 | monitorId
464 | monitorType
465 | monitoredUrl
466 | period
467 | }
468 | ... on ThirdPartyServiceEntityOutline {
469 | __typename
470 | }
471 | ... on UnavailableEntityOutline {
472 | __typename
473 | }
474 | ... on WorkloadEntityOutline {
475 | __typename
476 | createdAt
477 | updatedAt
478 | }
479 | }
480 | expiration {
481 | closeViolationsOnExpiration
482 | expirationDuration
483 | openViolationOnExpiration
484 | }
485 | nrql {
486 | evaluationOffset
487 | query
488 | }
489 | signal {
490 | aggregationDelay
491 | aggregationMethod
492 | aggregationTimer
493 | aggregationWindow
494 | evaluationDelay
495 | evaluationOffset
496 | fillOption
497 | fillValue
498 | slideBy
499 | }
500 | terms {
501 | operator
502 | priority
503 | threshold
504 | thresholdDuration
505 | thresholdOccurrences
506 | }
507 | valueFunction
508 | }
--------------------------------------------------------------------------------
/internal/schema/testdata/TestType_GetQueryFieldsString_AlertsNrqlOutlierCondition.txt:
--------------------------------------------------------------------------------
1 | description
2 | enabled
3 | entity {
4 | __typename
5 | account {
6 | id
7 | name
8 | reportingEventTypes
9 | }
10 | accountId
11 | alertSeverity
12 | domain
13 | entityType
14 | firstIndexedAt
15 | guid
16 | indexedAt
17 | lastReportingChangeAt
18 | name
19 | permalink
20 | reporting
21 | tags {
22 | key
23 | values
24 | }
25 | type
26 | ... on ApmApplicationEntityOutline {
27 | __typename
28 | applicationId
29 | language
30 | }
31 | ... on ApmDatabaseInstanceEntityOutline {
32 | __typename
33 | host
34 | portOrPath
35 | vendor
36 | }
37 | ... on ApmExternalServiceEntityOutline {
38 | __typename
39 | host
40 | }
41 | ... on BrowserApplicationEntityOutline {
42 | __typename
43 | agentInstallType
44 | applicationId
45 | servingApmApplicationId
46 | }
47 | ... on DashboardEntityOutline {
48 | __typename
49 | createdAt
50 | dashboardParentGuid
51 | permissions
52 | updatedAt
53 | }
54 | ... on ExternalEntityOutline {
55 | __typename
56 | }
57 | ... on GenericEntityOutline {
58 | __typename
59 | }
60 | ... on GenericInfrastructureEntityOutline {
61 | __typename
62 | integrationTypeCode
63 | }
64 | ... on InfrastructureAwsLambdaFunctionEntityOutline {
65 | __typename
66 | integrationTypeCode
67 | runtime
68 | }
69 | ... on InfrastructureHostEntityOutline {
70 | __typename
71 | }
72 | ... on KeyTransactionEntityOutline {
73 | __typename
74 | }
75 | ... on MobileApplicationEntityOutline {
76 | __typename
77 | applicationId
78 | }
79 | ... on SecureCredentialEntityOutline {
80 | __typename
81 | description
82 | secureCredentialId
83 | updatedAt
84 | }
85 | ... on SyntheticMonitorEntityOutline {
86 | __typename
87 | monitorId
88 | monitorType
89 | monitoredUrl
90 | period
91 | }
92 | ... on ThirdPartyServiceEntityOutline {
93 | __typename
94 | }
95 | ... on UnavailableEntityOutline {
96 | __typename
97 | }
98 | ... on WorkloadEntityOutline {
99 | __typename
100 | createdAt
101 | updatedAt
102 | }
103 | }
104 | entityGuid
105 | expectedGroups
106 | expiration {
107 | closeViolationsOnExpiration
108 | expirationDuration
109 | openViolationOnExpiration
110 | }
111 | id
112 | name
113 | nrql {
114 | evaluationOffset
115 | query
116 | }
117 | openViolationOnGroupOverlap
118 | policyId
119 | runbookUrl
120 | signal {
121 | aggregationDelay
122 | aggregationMethod
123 | aggregationTimer
124 | aggregationWindow
125 | evaluationDelay
126 | evaluationOffset
127 | fillOption
128 | fillValue
129 | slideBy
130 | }
131 | terms {
132 | operator
133 | priority
134 | threshold
135 | thresholdDuration
136 | thresholdOccurrences
137 | }
138 | type
139 | violationTimeLimit
140 | violationTimeLimitSeconds
--------------------------------------------------------------------------------
/internal/schema/type.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "sort"
9 | "strings"
10 |
11 | log "github.com/sirupsen/logrus"
12 |
13 | "github.com/newrelic/tutone/internal/util"
14 | )
15 |
16 | // Type defines a specific type within the schema
17 | type Type struct {
18 | Name string `json:"name,omitempty"`
19 | Description string `json:"description,omitempty"`
20 | Kind Kind `json:"kind,omitempty"`
21 |
22 | EnumValues []EnumValue `json:"enumValues,omitempty"`
23 | Fields []Field `json:"fields,omitempty"`
24 | InputFields []Field `json:"inputFields,omitempty"`
25 | Interfaces []TypeRef `json:"interfaces,omitempty"`
26 | PossibleTypes []TypeRef `json:"possibleTypes,omitempty"`
27 | SkipFields []string `json:"skipFields,omitempty"`
28 | }
29 |
30 | // Save writes the schema out to a file
31 | func (t *Type) Save(file string) error {
32 | if file == "" {
33 | return errors.New("unable to save schema, no file specified")
34 | }
35 |
36 | log.WithFields(log.Fields{
37 | "schema_file": file,
38 | }).Debug("saving schema")
39 |
40 | schemaFile, err := json.MarshalIndent(t, "", " ")
41 | if err != nil {
42 | return err
43 | }
44 |
45 | return os.WriteFile(file, schemaFile, 0644)
46 | }
47 |
48 | // GetDescription formats the description into a GoDoc comment.
49 | func (t *Type) GetDescription() string {
50 | if strings.TrimSpace(t.Description) == "" {
51 | return ""
52 | }
53 |
54 | return formatDescription(t.GetName(), t.Description)
55 | }
56 |
57 | // GetName returns the name of a Type, formatted for Go title casing.
58 | func (t *Type) GetName() string {
59 | return formatGoName(t.Name)
60 | }
61 |
62 | // IsGoType is used to determine if a type in NerdGraph is already a native type of Golang.
63 | func (t *Type) IsGoType() bool {
64 | goTypes := []string{
65 | "int",
66 | "string",
67 | "bool",
68 | "boolean",
69 | }
70 |
71 | name := strings.ToLower(t.GetName())
72 |
73 | for _, x := range goTypes {
74 | if x == name {
75 | return true
76 | }
77 | }
78 |
79 | return false
80 | }
81 |
82 | func (t *Type) GetQueryStringFields(s *Schema, depth, maxDepth int, isMutation bool, excludeFields []string) string {
83 | depth++
84 |
85 | var lines []string
86 |
87 | sort.SliceStable(t.Fields, func(i, j int) bool {
88 | return t.Fields[i].Name < t.Fields[j].Name
89 | })
90 |
91 | parentFieldNames := []string{}
92 |
93 | for _, field := range t.Fields {
94 | // If any of the arguments for a given field are required, then we
95 | // currently skip the field in the query since we are not handling the
96 | // parameters necessary to fill that out.
97 | if !isMutation && field.HasRequiredArg() {
98 | log.WithFields(log.Fields{
99 | "depth": depth,
100 | "isMutation": isMutation,
101 | "name": field.Name,
102 | }).Trace("skipping, field has at least one required arg")
103 | continue
104 | }
105 |
106 | // Explicitly skip these via config
107 | if util.StringInStrings(field.Name, excludeFields) {
108 | log.WithFields(log.Fields{
109 | "depth": depth,
110 | "isMutation": isMutation,
111 | "name": field.Name,
112 | }).Trace("skipping, field excluded via configuration")
113 | continue
114 | }
115 |
116 | kinds := field.Type.GetKinds()
117 | lastKind := kinds[len(kinds)-1]
118 |
119 | switch lastKind {
120 | case KindObject, KindInterface:
121 | if depth > maxDepth {
122 | continue
123 | }
124 |
125 | typeName := field.Type.GetTypeName()
126 |
127 | subT, err := s.LookupTypeByName(typeName)
128 | if err != nil {
129 | log.Error(err)
130 | continue
131 | }
132 |
133 | // Recurse first so if we have no children, we skip completely
134 | // and don't end up with `field { }` (invalid)
135 | subTContent := subT.GetQueryStringFields(s, depth, maxDepth, isMutation, excludeFields)
136 | subTLines := strings.Split(subTContent, "\n")
137 | if subTContent == "" || len(subTLines) < 1 {
138 | log.WithFields(log.Fields{
139 | "depth": depth,
140 | "isMutation": isMutation,
141 | "name": field.Name,
142 | }).Trace("skipping, all sub-fields require arguments")
143 | continue
144 | }
145 |
146 | // Add the field
147 | lines = append(lines, field.Name+" {")
148 | if lastKind == KindInterface {
149 | lines = append(lines, "\t__typename")
150 | }
151 |
152 | // Add the sub-fields
153 | for _, b := range subTLines {
154 | lines = append(lines, fmt.Sprintf("\t%s", b))
155 | }
156 |
157 | lines = append(lines, "}")
158 |
159 | default:
160 | lines = append(lines, field.Name)
161 | parentFieldNames = append(parentFieldNames, field.Name)
162 | }
163 | }
164 |
165 | for _, possibleType := range t.PossibleTypes {
166 | possibleT, err := s.LookupTypeByName(possibleType.Name)
167 | if err != nil {
168 | log.Error(err)
169 | // skip appending this query fragment if not found
170 | continue
171 | }
172 |
173 | lines = append(lines, fmt.Sprintf("... on %s {", possibleType.Name))
174 | lines = append(lines, "\t__typename")
175 |
176 | possibleTContent := possibleT.GetQueryStringFields(s, depth, maxDepth, isMutation, excludeFields)
177 |
178 | possibleTLines := strings.Split(possibleTContent, "\n")
179 | for _, b := range possibleTLines {
180 | // Here we skip the fields that are already expressed on the parent type.
181 | // Since we are enumerating the interface types on the type, we want to
182 | // reduce the query complexity, while still retaining all of the data.
183 | // This allows us to rely on the parent types fields and avoid increasing
184 | // the complexity by enumerating all fields on the PossibleTypes as well.
185 | if util.StringInStrings(b, parentFieldNames) {
186 | continue
187 | }
188 |
189 | lines = append(lines, fmt.Sprintf("\t%s", b))
190 | }
191 | lines = append(lines, "}")
192 | }
193 |
194 | return strings.Join(lines, "\n")
195 | }
196 |
197 | func (t *Type) GetField(name string) (*Field, error) {
198 | for _, f := range t.Fields {
199 | if f.Name == name {
200 | return &f, nil
201 | }
202 | }
203 |
204 | return nil, fmt.Errorf("field '%s' not found", name)
205 | }
206 |
--------------------------------------------------------------------------------
/internal/schema/type_test.go:
--------------------------------------------------------------------------------
1 | //go:build unit
2 | // +build unit
3 |
4 | package schema
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func TestType_GetQueryFieldsString(t *testing.T) {
14 | t.Parallel()
15 |
16 | // schema cached by 'make test-prep'
17 | s, err := Load("../../testdata/schema.json")
18 | require.NoError(t, err)
19 |
20 | cases := map[string]struct {
21 | TypeName string
22 | Depth int
23 | Mutation bool
24 | ExcludeFields []string
25 | }{
26 | "AlertsNrqlCondition": {
27 | TypeName: "AlertsNrqlCondition",
28 | Depth: 2,
29 | Mutation: false,
30 | },
31 | "AlertsNrqlBaselineCondition": {
32 | TypeName: "AlertsNrqlBaselineCondition",
33 | Depth: 2,
34 | Mutation: false,
35 | },
36 | "AlertsNrqlOutlierCondition": {
37 | TypeName: "AlertsNrqlOutlierCondition",
38 | Depth: 2,
39 | Mutation: false,
40 | },
41 | "CloudLinkedAccount": {
42 | TypeName: "CloudLinkedAccount",
43 | Depth: 3,
44 | Mutation: false,
45 | },
46 | "CloudDisableIntegrationPayload": {
47 | TypeName: "CloudDisableIntegrationPayload",
48 | Depth: 1,
49 | Mutation: true,
50 | },
51 | }
52 |
53 | for n, tc := range cases {
54 | x, err := s.LookupTypeByName(tc.TypeName)
55 | require.NoError(t, err)
56 |
57 | xx := x.GetQueryStringFields(s, 0, tc.Depth, tc.Mutation, tc.ExcludeFields)
58 | // saveFixture(t, n, xx)
59 | expected := loadFixture(t, n)
60 | assert.Equal(t, expected, xx)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/internal/schema/typeref.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | log "github.com/sirupsen/logrus"
8 | )
9 |
10 | // TypeRef is a GraphQL reference to a Type.
11 | type TypeRef struct {
12 | Name string `json:"name,omitempty"`
13 | Description string `json:"description,omitempty"`
14 | Kind Kind `json:"kind,omitempty"`
15 |
16 | OfType *TypeRef `json:"ofType,omitempty"`
17 | }
18 |
19 | // GetKinds returns an array or the type kind
20 | func (r *TypeRef) GetKinds() []Kind {
21 | tree := []Kind{}
22 |
23 | if r.Kind != "" {
24 | tree = append(tree, r.Kind)
25 | }
26 |
27 | // Recursion FTW
28 | if r.OfType != nil {
29 | tree = append(tree, r.OfType.GetKinds()...)
30 | }
31 |
32 | return tree
33 | }
34 |
35 | // GetName returns a recusive lookup of the type name
36 | func (r *TypeRef) GetName() string {
37 | return formatGoName(r.Name)
38 | }
39 |
40 | // GetTypeName returns the name of the current type, or performs a recursive lookup to determine the name of the nested OfType object's name. In the case that neither are matched, the string "UNKNOWN" is returned. In the GraphQL schema, a non-empty name seems to appear only once in a TypeRef tree, so we want to find the first non-empty.
41 | func (r *TypeRef) GetTypeName() string {
42 | if r != nil {
43 | if r.Name != "" {
44 | return r.Name
45 | }
46 |
47 | // Recursion FTW
48 | if r.OfType != nil {
49 | return r.OfType.GetTypeName()
50 | }
51 | }
52 |
53 | log.Errorf("failed to get name for %#v", *r)
54 | return "UNKNOWN"
55 | }
56 |
57 | // GetType resolves the given SchemaInputField into a field name to use on a go struct.
58 | //
59 | // type, recurse, error
60 | func (r *TypeRef) GetType() (string, bool, error) {
61 | if r == nil {
62 | return "", false, fmt.Errorf("can not get type of nil TypeRef")
63 | }
64 |
65 | switch n := r.GetTypeName(); n {
66 | case "String":
67 | return "string", false, nil
68 | case "Int":
69 | return "int", false, nil
70 | case "Boolean":
71 | return "bool", false, nil
72 | case "Float":
73 | return "float64", false, nil
74 | case "ID":
75 | // ID is a nested object, but behaves like an integer. This may be true of other SCALAR types as well, so logic here could potentially be moved.
76 | return "int", false, nil
77 | case "":
78 | return "", true, fmt.Errorf("empty field name: %+v", r)
79 | default:
80 | return formatGoName(n), true, nil
81 | }
82 | }
83 |
84 | // GetDescription looks for anything in the description before \n\n---\n
85 | // and filters off anything after that (internal messaging that is not useful here)
86 | func (r *TypeRef) GetDescription() string {
87 | if strings.TrimSpace(r.Description) == "" {
88 | return ""
89 | }
90 |
91 | return formatDescription("", r.Description)
92 | }
93 |
94 | func (r *TypeRef) IsInputObject() bool {
95 | kinds := r.GetKinds()
96 |
97 | // Lots of kinds
98 | for _, k := range kinds {
99 | if k == KindInputObject {
100 | return true
101 | }
102 | }
103 |
104 | return false
105 | }
106 |
107 | func (r *TypeRef) IsScalarID() bool {
108 | return r.OfType != nil && r.OfType.Kind == KindScalar && r.GetTypeName() == "ID"
109 | }
110 |
111 | // IsNonNull walks down looking for NON_NULL kind, however that can appear
112 | // multiple times, so this is likely a bit deceiving...
113 | // Example:
114 | //
115 | // {
116 | // "name": "tags",
117 | // "description": "An array of key-values pairs to represent a tag. For example: Team:TeamName.",
118 | // "type": {
119 | // "kind": "NON_NULL",
120 | // "ofType": {
121 | // "kind": "LIST",
122 | // "ofType": {
123 | // "kind": "NON_NULL",
124 | // "ofType": {
125 | // "name": "TaggingTagInput",
126 | // "kind": "INPUT_OBJECT"
127 | // }
128 | // }
129 | // }
130 | // }
131 | // }
132 | // ]
133 | // }
134 | func (r *TypeRef) IsNonNull() bool {
135 | kinds := r.GetKinds()
136 |
137 | // Lots of kinds
138 | for _, k := range kinds {
139 | if k == KindNonNull {
140 | return true
141 | }
142 | }
143 |
144 | return false
145 | }
146 |
147 | // IsList determines if a TypeRef is of a KIND LIST.
148 | func (r *TypeRef) IsList() bool {
149 | kinds := r.GetKinds()
150 |
151 | // Lots of kinds
152 | for _, k := range kinds {
153 | if k == KindList {
154 | return true
155 | }
156 | }
157 |
158 | return false
159 | }
160 |
161 | // IsList determines if a TypeRef is of a KIND INTERFACE.
162 | func (r *TypeRef) IsInterface() bool {
163 | kinds := r.GetKinds()
164 |
165 | // Lots of kinds
166 | for _, k := range kinds {
167 | if k == KindInterface {
168 | return true
169 | }
170 | }
171 |
172 | return false
173 | }
174 |
--------------------------------------------------------------------------------
/internal/util/log.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | log "github.com/sirupsen/logrus"
5 | )
6 |
7 | // LogIfError avoids having to constantly nil check error
8 | func LogIfError(lvl log.Level, err error) {
9 | if err != nil {
10 | log.StandardLogger().Log(lvl, err)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/internal/util/log_test.go:
--------------------------------------------------------------------------------
1 | //go:build unit
2 | // +build unit
3 |
4 | package util
5 |
6 | import (
7 | "errors"
8 | "io"
9 | "testing"
10 |
11 | log "github.com/sirupsen/logrus"
12 | logtest "github.com/sirupsen/logrus/hooks/test"
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func TestLogIfError(t *testing.T) {
17 | t.Parallel()
18 |
19 | cases := []struct {
20 | level log.Level
21 | err error
22 | }{
23 | {log.PanicLevel, nil}, // Should not panic
24 | {log.WarnLevel, errors.New("this should be a warn log")},
25 | {log.InfoLevel, errors.New("this should be an info log")},
26 | {log.ErrorLevel, nil}, // Should not error
27 | }
28 |
29 | // LogIfError uses log.StandardLogger() so we have to override some of it, and add a hook
30 | // to catch the messages
31 | stdLogger := log.StandardLogger()
32 | hook := logtest.NewGlobal()
33 |
34 | oldOut := stdLogger.Out
35 | stdLogger.Out = io.Discard
36 | defer func() { stdLogger.Out = oldOut }()
37 |
38 | for x := range cases {
39 | LogIfError(cases[x].level, cases[x].err)
40 |
41 | if cases[x].err != nil {
42 | assert.Equal(t, cases[x].err.Error(), hook.LastEntry().Message)
43 | hook.Reset()
44 | } else {
45 | assert.Nil(t, hook.LastEntry())
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/internal/util/strings.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "regexp"
5 | "strings"
6 | )
7 |
8 | var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
9 | var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
10 |
11 | // ToSnakeCase returns the string formatted in Snake Case
12 | func ToSnakeCase(str string) string {
13 | snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
14 | snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
15 | return strings.ToLower(snake)
16 | }
17 |
18 | // StringInStrings checks if the string is in the slice of strings
19 | func StringInStrings(s string, ss []string) bool {
20 | for _, sss := range ss {
21 | if s == sss {
22 | return true
23 | }
24 | }
25 |
26 | return false
27 | }
28 |
--------------------------------------------------------------------------------
/internal/util/strings_test.go:
--------------------------------------------------------------------------------
1 | //go:build unit
2 | // +build unit
3 |
4 | package util
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func TestToSnakeCase(t *testing.T) {
14 | t.Parallel()
15 |
16 | cases := []struct {
17 | Input string
18 | Expected string
19 | }{
20 | {"", ""},
21 | {"already_snake", "already_snake"},
22 | {"A", "a"},
23 | {"AA", "aa"},
24 | {"AaAa", "aa_aa"},
25 | {"HTTPRequest", "http_request"},
26 | {"BatteryLifeValue", "battery_life_value"},
27 | {"Id0Value", "id0_value"},
28 | {"ID0Value", "id0_value"},
29 | }
30 | for _, c := range cases {
31 | result := ToSnakeCase(c.Input)
32 |
33 | require.Equal(t, c.Expected, result)
34 | }
35 | }
36 |
37 | func TestStringInStrings(t *testing.T) {
38 | t.Parallel()
39 |
40 | cases := []struct {
41 | Str string
42 | Arry []string
43 | Expected bool
44 | }{
45 | {"foo", []string{}, false},
46 | {"foo", []string{"foo"}, true},
47 | {"foo", []string{"foo", "bar"}, true},
48 | {"bar", []string{"foo", "bar"}, true},
49 | {"baz", []string{"foo", "bar"}, false},
50 | {"", []string{"foo", "bar", "baz"}, false},
51 | }
52 |
53 | for x := range cases {
54 | result := StringInStrings(cases[x].Str, cases[x].Arry)
55 | assert.Equal(t, cases[x].Expected, result)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/internal/util/template_funcs.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "reflect"
5 | "text/template"
6 |
7 | "github.com/Masterminds/sprig/v3"
8 | )
9 |
10 | func GetTemplateFuncs() template.FuncMap {
11 | funcs := sprig.TxtFuncMap()
12 |
13 | // Custom funcs
14 | funcs["hasField"] = hasField
15 |
16 | return funcs
17 | }
18 |
19 | func hasField(v interface{}, name string) bool {
20 | rv := reflect.ValueOf(v)
21 | if rv.Kind() == reflect.Ptr {
22 | rv = rv.Elem()
23 | }
24 | if rv.Kind() != reflect.Struct {
25 | return false
26 | }
27 | return rv.FieldByName(name).IsValid()
28 | }
29 |
--------------------------------------------------------------------------------
/internal/util/template_funcs_test.go:
--------------------------------------------------------------------------------
1 | //go:build unit
2 | // +build unit
3 |
4 | package util
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestGetTemplateFuncs(t *testing.T) {
13 | t.Parallel()
14 |
15 | // List of custom funcs we add
16 | customFuncs := []string{
17 | "hasField",
18 | }
19 |
20 | tf := GetTemplateFuncs()
21 |
22 | for _, x := range customFuncs {
23 | assert.Contains(t, tf, x)
24 | }
25 | }
26 |
27 | func TestHasField(t *testing.T) {
28 | t.Parallel()
29 |
30 | // Make an object with fields
31 | testStruct := struct {
32 | name string
33 | }{
34 | "some name",
35 | }
36 |
37 | // Use that object here
38 | cases := []struct {
39 | object interface{}
40 | fieldName string
41 | result bool
42 | }{
43 | {testStruct, "name", true},
44 | {testStruct, "foo", false},
45 | // With pointers
46 | {&testStruct, "name", true},
47 | {&testStruct, "foo", false},
48 | // Non-structs
49 | {"string", "name", false},
50 | {42, "name", false},
51 | {false, "name", false},
52 | }
53 |
54 | for x := range cases {
55 | ret := hasField(cases[x].object, cases[x].fieldName)
56 | assert.Equal(t, cases[x].result, ret)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/internal/version/version.go:
--------------------------------------------------------------------------------
1 | // Code generated by `make release` DO NOT EDIT.
2 | package version
3 |
4 | // Version of this library
5 | const Version string = "0.12.1"
6 |
--------------------------------------------------------------------------------
/internal/version/version_test.go:
--------------------------------------------------------------------------------
1 | //go:build unit
2 | // +build unit
3 |
4 | package version
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | // TestVersion simply ensures that Version is not empty
13 | func TestVersion(t *testing.T) {
14 | t.Parallel()
15 |
16 | assert.NotEmpty(t, Version)
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newrelic/tutone/11d56607628a3619e58f4a7ce39fc144ed2b8bb9/pkg/.keep
--------------------------------------------------------------------------------
/pkg/accountmanagement/types.go:
--------------------------------------------------------------------------------
1 | // Code generated by tutone: DO NOT EDIT
2 | package accountmanagementingest
3 |
4 | // AccountManagementCreateInput - Attributes for creating an account.
5 | type AccountManagementCreateInput struct {
6 | // The name of the account.
7 | Name string `json:"name"`
8 | // The id of the managed organization where the account will be created.
9 | OrganizationId int `json:"organizationId,omitempty"`
10 | // The data center region for the account
11 | RegionCode string `json:"regionCode,omitempty"`
12 | }
13 |
14 | // AccountManagementCreateResponse - The return object for a create-account mutation.
15 | type AccountManagementCreateResponse struct {
16 | // Information about the newly created account.
17 | ManagedAccount AccountManagementManagedAccount `json:"managedAccount,omitempty"`
18 | }
19 |
20 | // AccountManagementManagedAccount - Account data view for administration tasks.
21 | type AccountManagementManagedAccount struct {
22 | // The account ID.
23 | ID int `json:"id"`
24 | // True if account is canceled
25 | IsCanceled bool `json:"isCanceled"`
26 | // The name of the account.
27 | Name string `json:"name"`
28 | // The data center region for the account (US or EU).
29 | RegionCode string `json:"regionCode"`
30 | }
31 |
32 | // AccountManagementOrganizationStitchedFields - The field type for stitching into the NerdGraph schema.
33 | type AccountManagementOrganizationStitchedFields struct {
34 | // Admin-level info about the accounts in an organization.
35 | ManagedAccounts []AccountManagementManagedAccount `json:"managedAccounts"`
36 | }
37 |
38 | // AccountManagementUpdateInput - The attributes for updating an account.
39 | type AccountManagementUpdateInput struct {
40 | // The ID for the account being updated.
41 | ID int `json:"id"`
42 | // The new account name.
43 | Name string `json:"name"`
44 | }
45 |
46 | // AccountManagementUpdateResponse - The return object for an update-account mutation.
47 | type AccountManagementUpdateResponse struct {
48 | // Information about an updated account
49 | ManagedAccount AccountManagementManagedAccount `json:"managedAccount,omitempty"`
50 | }
51 |
52 | // Actor - The `Actor` object contains fields that are scoped to the API user's access level.
53 | type Actor struct {
54 | // The `organization` field is the entry point into data that is scoped to the user's organization.
55 | Organization Organization `json:"organization,omitempty"`
56 | }
57 |
58 | // Organization - The `Organization` object provides basic data about an organization.
59 | type Organization struct {
60 | // This field provides access to AccountManagement data.
61 | AccountManagement AccountManagementOrganizationStitchedFields `json:"accountManagement,omitempty"`
62 | // The customer id for the organization.
63 | CustomerId string `json:"customerId,omitempty"`
64 | // The ID of the organization.
65 | ID int `json:"id,omitempty"`
66 | // The name of the organization.
67 | Name string `json:"name,omitempty"`
68 | // The telemetry id for the organization
69 | TelemetryId string `json:"telemetryId,omitempty"`
70 | }
71 |
72 | type managedAccountsResponse struct {
73 | Actor Actor `json:"actor"`
74 | }
75 |
76 | // ID - The `ID` scalar type represents a unique identifier, often used to
77 | // refetch an object or as key for a cache. The ID type appears in a JSON
78 | // response as a String; however, it is not intended to be human-readable.
79 | // When expected as an input type, any string (such as `"4"`) or integer
80 | // (such as `4`) input value will be accepted as an ID.
81 | type ID string
82 |
--------------------------------------------------------------------------------
/pkg/fetch/command.go:
--------------------------------------------------------------------------------
1 | package fetch
2 |
3 | import (
4 | "os"
5 |
6 | log "github.com/sirupsen/logrus"
7 | "github.com/spf13/cobra"
8 | "github.com/spf13/viper"
9 |
10 | "github.com/newrelic/tutone/internal/util"
11 | )
12 |
13 | const (
14 | DefaultAPIKeyEnv = "TUTONE_API_KEY"
15 | DefaultSchemaCacheFile = "schema.json"
16 | )
17 |
18 | var refetch bool
19 |
20 | var Command = &cobra.Command{
21 | Use: "fetch",
22 | Short: "Fetch GraphQL Schema",
23 | Long: `Fetch GraphQL Schema
24 |
25 | Query the GraphQL server for schema and write it to a file.
26 | `,
27 | Example: "tutone fetch --config configs/tutone.yaml",
28 | Run: func(cmd *cobra.Command, args []string) {
29 | Fetch(
30 | viper.GetString("endpoint"),
31 | viper.GetBool("auth.disable"),
32 | viper.GetString("auth.header"),
33 | viper.GetString("auth.api_key_env_var"),
34 | viper.GetString("cache.schema_file"),
35 | refetch,
36 | )
37 | },
38 | }
39 |
40 | func Fetch(
41 | endpoint string,
42 | disableAuth bool,
43 | authHeader string,
44 | authEnvVariableName string,
45 | schemaFile string,
46 | refetch bool,
47 | ) {
48 | e := NewEndpoint()
49 | e.URL = endpoint
50 | e.Auth.Disable = disableAuth
51 | e.Auth.Header = authHeader
52 | e.Auth.APIKey = os.Getenv(authEnvVariableName)
53 |
54 | _, err := os.Stat(schemaFile)
55 |
56 | if os.IsNotExist(err) || refetch {
57 | schema, err := e.Fetch()
58 | if err != nil {
59 | log.Fatal(err)
60 | }
61 |
62 | if schemaFile != "" {
63 | util.LogIfError(log.ErrorLevel, schema.Save(schemaFile))
64 | }
65 |
66 | log.WithFields(log.Fields{
67 | "endpoint": endpoint,
68 | "schema_file": schemaFile,
69 | }).Info("successfully fetched schema")
70 | }
71 | }
72 |
73 | func init() {
74 | Command.Flags().StringP("endpoint", "e", "", "GraphQL Endpoint")
75 | util.LogIfError(log.ErrorLevel, viper.BindPFlag("endpoint", Command.Flags().Lookup("endpoint")))
76 |
77 | Command.Flags().String("header", DefaultAuthHeader, "Header name set for Authentication")
78 | util.LogIfError(log.ErrorLevel, viper.BindPFlag("auth.header", Command.Flags().Lookup("header")))
79 |
80 | Command.Flags().String("api-key-env", DefaultAPIKeyEnv, "Environment variable to read API key from")
81 | util.LogIfError(log.ErrorLevel, viper.BindPFlag("auth.api_key_env_var", Command.Flags().Lookup("api-key-env")))
82 |
83 | Command.Flags().StringP("schema", "s", DefaultSchemaCacheFile, "Output file for the schema")
84 | util.LogIfError(log.ErrorLevel, viper.BindPFlag("cache.schema_file", Command.Flags().Lookup("schema")))
85 |
86 | Command.Flags().BoolVar(&refetch, "refetch", false, "Force a refetch of your GraphQL schema to ensure the generated types are up to date.")
87 | }
88 |
--------------------------------------------------------------------------------
/pkg/fetch/http.go:
--------------------------------------------------------------------------------
1 | package fetch
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "net/http"
10 | "time"
11 |
12 | log "github.com/sirupsen/logrus"
13 |
14 | "github.com/newrelic/tutone/internal/schema"
15 | )
16 |
17 | const (
18 | DefaultHTTPTimeout = 30 * time.Second
19 | DefaultAuthHeader = "Api-Key"
20 | )
21 |
22 | type GraphqlQuery struct {
23 | Query string `json:"query"`
24 | Variables interface{} `json:"variables"` // map[string]interface really...
25 | }
26 |
27 | type Endpoint struct {
28 | URL string
29 | Auth AuthConfig
30 | HTTP HTTPConfig
31 | }
32 |
33 | type AuthConfig struct {
34 | Disable bool
35 | Header string
36 | APIKey string
37 | }
38 |
39 | type HTTPConfig struct {
40 | Timeout time.Duration
41 | }
42 |
43 | func NewEndpoint() *Endpoint {
44 | e := &Endpoint{
45 | Auth: AuthConfig{
46 | Header: DefaultAuthHeader,
47 | },
48 | HTTP: HTTPConfig{
49 | Timeout: DefaultHTTPTimeout,
50 | },
51 | }
52 |
53 | return e
54 | }
55 |
56 | // Fetch returns everything we know how to get about the schema of
57 | // an endpoint
58 | func (e *Endpoint) Fetch() (*schema.Schema, error) {
59 | s, err := e.FetchSchema()
60 | if err != nil {
61 | return nil, err
62 | }
63 |
64 | // Grab the root mutation name and fetch that type
65 | if s.MutationType != nil {
66 | m, mErr := e.FetchType(s.MutationType.Name)
67 | if mErr != nil {
68 | return nil, mErr
69 | }
70 | s.MutationType = m
71 | }
72 |
73 | if s.QueryType != nil {
74 | m, mErr := e.FetchType(s.QueryType.Name)
75 | if mErr != nil {
76 | return nil, mErr
77 | }
78 | s.QueryType = m
79 | }
80 |
81 | // Fetch all of the other data types
82 | t, err := e.FetchSchemaTypes()
83 | if err != nil {
84 | return nil, err
85 | }
86 | s.Types = t
87 |
88 | return s, nil
89 | }
90 |
91 | // FetchSchema returns basic info about the schema
92 | func (e *Endpoint) FetchSchema() (*schema.Schema, error) {
93 | log.Infof("fetching schema from endpoint: %s", e.URL)
94 | query := GraphqlQuery{
95 | Query: schema.QuerySchema,
96 | }
97 |
98 | resp, err := e.fetch(query)
99 | if err != nil {
100 | return nil, err
101 | }
102 |
103 | return &resp.Data.Schema, nil
104 | }
105 |
106 | // FetchTypes gathers all of the data types in the schema
107 | func (e *Endpoint) FetchSchemaTypes() ([]*schema.Type, error) {
108 | log.Info("fetching schema types")
109 | query := GraphqlQuery{
110 | Query: schema.QuerySchemaTypes,
111 | }
112 |
113 | resp, err := e.fetch(query)
114 | if err != nil {
115 | return nil, err
116 | }
117 |
118 | return resp.Data.Schema.Types, nil
119 | }
120 |
121 | func (e *Endpoint) FetchType(name string) (*schema.Type, error) {
122 | if name == "" {
123 | return nil, errors.New("can not fetch type without a name")
124 | }
125 |
126 | log.WithFields(log.Fields{
127 | "type": name,
128 | }).Info("fetching type")
129 |
130 | query := GraphqlQuery{
131 | Query: schema.QueryType,
132 | Variables: schema.QueryTypeVars{
133 | Name: name,
134 | },
135 | }
136 |
137 | resp, err := e.fetch(query)
138 | if err != nil {
139 | return nil, err
140 | }
141 |
142 | return &resp.Data.Type, nil
143 | }
144 |
145 | // fetch does the heavy lifting to return the schema data
146 | func (e *Endpoint) fetch(query GraphqlQuery) (*schema.QueryResponse, error) {
147 | if e.URL == "" {
148 | return nil, errors.New("unable to fetch from empty URL")
149 | }
150 |
151 | j, err := json.Marshal(query)
152 | if err != nil {
153 | return nil, err
154 | }
155 | log.WithFields(log.Fields{
156 | "query": string(j),
157 | }).Trace("using query")
158 | reqBody := bytes.NewBuffer(j)
159 |
160 | req, err := http.NewRequestWithContext(context.Background(), "POST", e.URL, reqBody)
161 | if err != nil {
162 | return nil, err
163 | }
164 | req.Header.Set("Content-Type", "application/json")
165 |
166 | if !e.Auth.Disable {
167 | if e.Auth.APIKey != "" {
168 | log.WithFields(log.Fields{
169 | "header": req.Header,
170 | }).Trace("setting API Key header")
171 | req.Header.Set(e.Auth.Header, e.Auth.APIKey)
172 | }
173 | }
174 |
175 | log.Trace("creating HTTP client")
176 | tr := &http.Transport{
177 | MaxIdleConns: 10,
178 | IdleConnTimeout: e.HTTP.Timeout,
179 | ResponseHeaderTimeout: e.HTTP.Timeout,
180 | }
181 |
182 | client := &http.Client{Transport: tr}
183 |
184 | log.WithFields(log.Fields{
185 | "header": e.Auth.Header,
186 | "endpoint": e.URL,
187 | "method": req.Method,
188 | }).Debug("sending request")
189 |
190 | resp, err := client.Do(req)
191 | if err != nil {
192 | return nil, err
193 | }
194 | defer resp.Body.Close()
195 |
196 | log.WithFields(log.Fields{
197 | "status_code": resp.StatusCode,
198 | }).Debug("request completed")
199 | if resp.StatusCode != 200 {
200 | return nil, fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode)
201 | }
202 |
203 | return schema.ParseResponse(resp)
204 | }
205 |
--------------------------------------------------------------------------------
/pkg/generate/command.go:
--------------------------------------------------------------------------------
1 | package generate
2 |
3 | import (
4 | log "github.com/sirupsen/logrus"
5 | "github.com/spf13/cobra"
6 | "github.com/spf13/viper"
7 |
8 | "github.com/newrelic/tutone/internal/util"
9 | "github.com/newrelic/tutone/pkg/fetch"
10 | )
11 |
12 | const (
13 | DefaultGenerateOutputFile = "types.go"
14 | )
15 |
16 | var (
17 | packageName string
18 | packageNames []string
19 | refetch bool
20 | includeIntegrationTest bool
21 | )
22 |
23 | var Command = &cobra.Command{
24 | Use: "generate",
25 | Short: "Generate code from GraphQL Schema",
26 | Long: `Generate code from GraphQL Schema
27 |
28 | The generate command will generate code based on the
29 | configured types in your .tutone.yml configuration file.
30 | Use the --refetch flag when new types have been added to
31 | your upstream GraphQL schema to ensure your generated code
32 | is up to date with your configured GraphQL API.
33 | `,
34 | Example: "tutone generate --config .tutone.yml",
35 | Run: func(cmd *cobra.Command, args []string) {
36 | util.LogIfError(log.ErrorLevel, Generate(GeneratorOptions{
37 | PackageName: packageName,
38 | PackageNames: packageNames,
39 | Refetch: refetch,
40 | IncludeIntegrationTest: includeIntegrationTest,
41 | }))
42 | },
43 | }
44 |
45 | func init() {
46 | Command.Flags().StringVarP(&packageName, "package", "p", "", "Go package name for the generated files")
47 | Command.Flags().StringSliceVar(&packageNames, "packages", []string{}, "List of package names to generate. Example: --packages=package1,package2")
48 |
49 | Command.Flags().StringP("schema", "s", fetch.DefaultSchemaCacheFile, "Schema file to read from")
50 | util.LogIfError(log.ErrorLevel, viper.BindPFlag("schema_file", Command.Flags().Lookup("schema")))
51 |
52 | Command.Flags().String("types", DefaultGenerateOutputFile, "Output file for generated types")
53 | util.LogIfError(log.ErrorLevel, viper.BindPFlag("generate.type_file", Command.Flags().Lookup("types")))
54 |
55 | Command.Flags().BoolVar(&refetch, "refetch", false, "Force a refetch of your GraphQL schema to ensure the generated types are up to date.")
56 | Command.Flags().BoolVar(&includeIntegrationTest, "include-integration-test", false, "Generate a basic scaffolded integration test file for the associated package.")
57 | }
58 |
--------------------------------------------------------------------------------
/pkg/generate/generate.go:
--------------------------------------------------------------------------------
1 | package generate
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | log "github.com/sirupsen/logrus"
8 | "github.com/spf13/viper"
9 |
10 | "github.com/newrelic/tutone/generators/command"
11 | "github.com/newrelic/tutone/generators/nerdgraphclient"
12 | "github.com/newrelic/tutone/generators/typegen"
13 | "github.com/newrelic/tutone/internal/codegen"
14 | "github.com/newrelic/tutone/internal/config"
15 | "github.com/newrelic/tutone/internal/schema"
16 | "github.com/newrelic/tutone/pkg/fetch"
17 | )
18 |
19 | type GeneratorOptions struct {
20 | PackageName string
21 | PackageNames []string
22 | Refetch bool
23 | IncludeIntegrationTest bool
24 | }
25 |
26 | // Generate reads the configuration file and executes generators relevant to a particular package.
27 | func Generate(options GeneratorOptions) error {
28 | schemaFile := viper.GetString("cache.schema_file")
29 |
30 | _, err := os.Stat(schemaFile)
31 |
32 | // Fetch a new schema if it doesn't exist or if --refetch flag has been provided.
33 | if os.IsNotExist(err) || options.Refetch {
34 | fetch.Fetch(
35 | viper.GetString("endpoint"),
36 | viper.GetBool("auth.disable"),
37 | viper.GetString("auth.header"),
38 | viper.GetString("auth.api_key_env_var"),
39 | schemaFile,
40 | options.Refetch,
41 | )
42 | }
43 |
44 | log.WithFields(log.Fields{
45 | "schema_file": schemaFile,
46 | }).Info("Loading generation config")
47 |
48 | // load the config
49 | cfg, err := config.LoadConfig(viper.ConfigFileUsed())
50 | if err != nil {
51 | return err
52 | }
53 |
54 | log.Debugf("config: %+v", cfg)
55 |
56 | // package is required
57 | if len(cfg.Packages) == 0 {
58 | return fmt.Errorf("an array of packages is required")
59 | }
60 |
61 | // Load the schema
62 | s, err := schema.Load(schemaFile)
63 | if err != nil {
64 | return err
65 | }
66 |
67 | log.WithFields(log.Fields{
68 | "count_packages": len(cfg.Packages),
69 | "count_generators": len(cfg.Generators),
70 | // "count_mutation": len(cfg.Mutations),
71 | // "count_query": len(cfg.Queries),
72 | // "count_subscription": len(cfg.Subscriptions),
73 | }).Info("starting code generation")
74 |
75 | // Generate for a specific package
76 | if options.PackageName != "" {
77 | return generateForPackage(options.PackageName, cfg, s, options.IncludeIntegrationTest)
78 | }
79 |
80 | if options.PackageNames != nil {
81 | for _, packageName := range options.PackageNames {
82 | if err := generateForPackage(packageName, cfg, s, options.IncludeIntegrationTest); err != nil {
83 | return err
84 | }
85 | }
86 |
87 | return nil
88 | }
89 |
90 | // Generate for all configured packages
91 | for _, pkgConfig := range cfg.Packages {
92 | if err := generateForPackage(pkgConfig.Name, cfg, s, options.IncludeIntegrationTest); err != nil {
93 | return err
94 | }
95 | }
96 |
97 | return nil
98 | }
99 |
100 | func findPackageConfigByName(name string, packages []config.PackageConfig) *config.PackageConfig {
101 | for _, p := range packages {
102 | if p.Name == name {
103 | return &p
104 | }
105 | }
106 |
107 | return nil
108 | }
109 |
110 | func generatePkgTypes(pkgConfig *config.PackageConfig, cfg *config.Config, s *schema.Schema) error {
111 | allGenerators := map[string]codegen.Generator{
112 | // &terraform.Generator{},
113 | "typegen": &typegen.Generator{},
114 | "nerdgraphclient": &nerdgraphclient.Generator{},
115 | "command": &command.Generator{},
116 | }
117 |
118 | log.WithFields(log.Fields{
119 | "name": pkgConfig.Name,
120 | "generators": pkgConfig.Generators,
121 | "count_type": len(pkgConfig.Types),
122 | "count_imports": len(pkgConfig.Imports),
123 | }).Info("generating package")
124 |
125 | for _, generatorName := range pkgConfig.Generators {
126 | ggg, err := getGeneratorByName(generatorName, allGenerators)
127 | if err != nil {
128 | log.Error(err)
129 | continue
130 | }
131 |
132 | genConfig, err := getGeneratorConfigByName(generatorName, cfg.Generators)
133 | if err != nil {
134 | log.Error(err)
135 | continue
136 | }
137 |
138 | if ggg != nil && genConfig != nil {
139 | g := *ggg
140 |
141 | log.WithFields(log.Fields{
142 | "generator": generatorName,
143 | }).Info("starting generator")
144 |
145 | err = g.Generate(s, genConfig, pkgConfig)
146 | if err != nil {
147 | return fmt.Errorf("failed to call Generate() for provider %T: %s", generatorName, err)
148 | }
149 |
150 | err = g.Execute(genConfig, pkgConfig)
151 | if err != nil {
152 | return fmt.Errorf("failed to call Execute() for provider %T: %s", generatorName, err)
153 | }
154 | }
155 | }
156 |
157 | return nil
158 | }
159 |
160 | func generateForPackage(packageName string, cfg *config.Config, schema *schema.Schema, includeIntegrationTest bool) error {
161 | pkg := findPackageConfigByName(packageName, cfg.Packages)
162 | if pkg == nil {
163 | return fmt.Errorf("[Error] package %s not found", packageName)
164 | }
165 |
166 | pkg.IncludeIntegrationTest = includeIntegrationTest
167 |
168 | return generatePkgTypes(pkg, cfg, schema)
169 | }
170 |
171 | // getGeneratorConfigByName retrieve the *config.GeneratorConfig from the given set or errros.
172 | func getGeneratorConfigByName(name string, matchSet []config.GeneratorConfig) (*config.GeneratorConfig, error) {
173 | for _, g := range matchSet {
174 | if g.Name == name {
175 | return &g, nil
176 | }
177 | }
178 |
179 | return nil, fmt.Errorf("no generatorConfig with name %s found", name)
180 | }
181 |
182 | // getGeneratorByName retrieve the *generator.Generator from the given set or errros.
183 | func getGeneratorByName(name string, matchSet map[string]codegen.Generator) (*codegen.Generator, error) {
184 | for n, g := range matchSet {
185 | if n == name {
186 | return &g, nil
187 | }
188 | }
189 |
190 | return nil, fmt.Errorf("no generator named %s found", name)
191 | }
192 |
--------------------------------------------------------------------------------
/pkg/lang/command.go:
--------------------------------------------------------------------------------
1 | package lang
2 |
3 | // TODO: Move CommandGenerator and its friends to a proper home
4 | type CommandGenerator struct {
5 | PackageName string
6 | Imports []string
7 | Commands []Command
8 | }
9 |
10 | type InputObject struct {
11 | Name string
12 | GoType string
13 | }
14 |
15 | type Command struct {
16 | Name string
17 | CmdVariableName string
18 | ShortDescription string
19 | LongDescription string
20 | Example string
21 | InputType string
22 | ClientMethod string
23 | ClientMethodArgs []string
24 | InputObjects []InputObject
25 | Flags []CommandFlag
26 | Subcommands []Command
27 |
28 | GraphQLPath []string // Should mutations also use this? Probably
29 | }
30 |
31 | type CommandFlag struct {
32 | Name string
33 | Type string
34 | FlagMethodName string
35 | DefaultValue string
36 | Description string
37 | VariableName string
38 | VariableType string
39 | ClientType string
40 | Required bool
41 | IsInputType bool
42 | IsEnumType bool
43 | }
44 |
45 | type CommandExampleData struct {
46 | CLIName string
47 | PackageName string
48 | Command string
49 | Subcommand string
50 | Flags []CommandFlag
51 | }
52 |
--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | DEFAULT_BRANCH='main'
4 | CURRENT_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
5 |
6 | SRCDIR=${SRCDIR:-"."}
7 | GOBIN=$(go env GOPATH)/bin
8 | VER_PACKAGE="internal/version"
9 | VER_CMD=${GOBIN}/svu
10 | VER_BUMP=${GOBIN}/gobump
11 | CHANGELOG_CMD=${GOBIN}/git-chglog
12 | REL_CMD=${GOBIN}/goreleaser
13 | RELEASE_NOTES_FILE=${SRCDIR}/tmp/relnotes.md
14 |
15 | if [ $CURRENT_GIT_BRANCH != ${DEFAULT_BRANCH} ]; then
16 | echo "Not on ${DEFAULT_BRANCH}, skipping"
17 | exit 0
18 | fi
19 |
20 | # Compare versions
21 | VER_CURR=$(${VER_CMD} current --strip-prefix)
22 | VER_NEXT=$(${VER_CMD} next --strip-prefix)
23 |
24 | echo " "
25 | echo "Comparing tag versions..."
26 | echo "Current version: ${VER_CURR}"
27 | echo "Next version: ${VER_NEXT}"
28 | echo " "
29 |
30 | if [ "${VER_CURR}" = "${VER_NEXT}" ]; then
31 | VER_NEXT=$(${VER_CMD} patch --strip-prefix)
32 |
33 | printf "Bumping current version ${VER_CURR} to version ${VER_NEXT} for release."
34 | fi
35 |
36 | GIT_USER=$(git config user.name)
37 | GIT_EMAIL=$(git config user.email)
38 |
39 | if [ -z "${GIT_USER}" ]; then
40 | echo "git user.name not set"
41 | exit 1
42 | fi
43 |
44 | if [ -z "${GIT_EMAIL}" ]; then
45 | echo "git user.email not set"
46 | exit 1
47 | fi
48 |
49 | echo "Generating release for v${VER_NEXT} with git user ${GIT_USER}"
50 |
51 | # Update package version in version.go file using svu
52 | ${VER_BUMP} set ${VER_NEXT} -r -w ${VER_PACKAGE}
53 |
54 | # Auto-generate CHANGELOG updates
55 | ${CHANGELOG_CMD} --next-tag v${VER_NEXT} -o CHANGELOG.md
56 |
57 | # Commit CHANGELOG updates
58 | git add CHANGELOG.md ${VER_PACKAGE}
59 |
60 | git commit --no-verify -m "chore(release): release v${VER_NEXT}"
61 | git push --no-verify origin HEAD:${DEFAULT_BRANCH}
62 |
63 | if [ $? -ne 0 ]; then
64 | echo "Failed to push branch updates, exiting"
65 | exit 1
66 | fi
67 |
68 | # Tag and push
69 | git tag v${VER_NEXT}
70 | git push --no-verify origin ${CURRENT_GIT_BRANCH}:${DEFAULT_BRANCH} --tags
71 |
72 | if [ $? -ne 0 ]; then
73 | echo "Failed to push tag, exiting"
74 | exit 1
75 | fi
76 |
77 | # Make release notes
78 | ${CHANGELOG_CMD} --silent -o ${RELEASE_NOTES_FILE} v${VER_NEXT}
79 |
80 | # Publish the release
81 | ${REL_CMD} release --release-notes=${RELEASE_NOTES_FILE}
82 |
--------------------------------------------------------------------------------
/templates/clientgo/types.go.tmpl:
--------------------------------------------------------------------------------
1 | package {{.PackageName}}
2 |
3 | {{range .Enums}}
4 | {{ .Description }}
5 | type {{.Name}} string
6 | {{$typeName := .Name}}
7 |
8 | var {{.Name}}Types = struct {
9 | {{- range .Values}}
10 | {{ .Description }}
11 | {{.Name}} {{$typeName}}
12 | {{- end}}
13 | }{
14 | {{- range .Values}}
15 | {{ .Description }}
16 | {{.Name}}: "{{.Name}}",
17 | {{- end}}
18 | }
19 | {{ end}}
20 |
21 | {{range .Types}}
22 | {{ .Description }}
23 | type {{.Name}} struct {
24 | {{- range .Fields}}
25 | {{.Name}} {{.Type}} {{.Tags}}
26 | {{- end}}
27 | }
28 | {{ end}}
29 |
--------------------------------------------------------------------------------
/templates/command/command.go.tmpl:
--------------------------------------------------------------------------------
1 | {{- $packageName := .PackageName -}}
2 | package {{ $packageName }}
3 |
4 | {{- if gt (len .Imports) 0 }}
5 | import (
6 | {{- range .Imports }}
7 | {{ . }}
8 | {{- end }}
9 | )
10 | {{- end }}
11 |
12 | {{ range .Commands }}
13 |
14 | var {{ .CmdVariableName }} = &cobra.Command{
15 | Use: {{ .Name | quote }},
16 | Short: {{ .ShortDescription | quote }},
17 | Long: {{ .LongDescription | quote }},
18 | Example: "newrelic {{ $packageName }} {{ .Name }} --help",
19 | }
20 |
21 | {{ range .Subcommands }}
22 | {{- $cmdVarName := .CmdVariableName -}}
23 |
24 | {{ range .Flags }}
25 | var {{ .VariableName }} {{ .VariableType }}
26 | {{- end }}
27 |
28 | var {{ $cmdVarName }} = &cobra.Command{
29 | Use: {{ .Name | quote }},
30 | Short: {{ .ShortDescription | quote }},
31 | Long: {{ .LongDescription | quote }},
32 | Example: {{ .Example | quote }},
33 | Run: func(cmd *cobra.Command, args []string) {
34 | client.WithClient(func(nrClient *newrelic.NewRelic) {
35 | {{- range .Flags }}
36 | {{- if .IsInputType }}
37 | var {{ .Name }} {{ .ClientType }}
38 |
39 | err := json.Unmarshal([]byte({{ .VariableName }}), &{{ .Name }})
40 | utils.LogIfFatal(err)
41 | {{- end -}}
42 | {{ end }}
43 |
44 | resp, err := {{ .ClientMethod }}({{ .ClientMethodArgs | join ", " }})
45 | utils.LogIfFatal(err)
46 |
47 | utils.LogIfFatal(output.Print(resp))
48 | })
49 | },
50 | }
51 | {{ end }}
52 | {{- end }}
53 |
54 | func init() {
55 | {{ range .Commands }}
56 | {{- $parentCmdVarName := .CmdVariableName -}}
57 |
58 | {{ range .Subcommands}}
59 | {{- $cmdVarName := .CmdVariableName -}}
60 |
61 | {{ $parentCmdVarName }}.AddCommand({{ $cmdVarName }})
62 | {{ range .Flags}}
63 | {{- $defaultVal := .DefaultValue | quote -}}
64 | {{- if (eq .Type "int") -}}{{- $defaultVal = 0 -}}{{- end -}}
65 | {{ $cmdVarName }}.Flags().{{- .FlagMethodName -}}(&{{ .VariableName }}, {{ .Name | quote }}, {{ $defaultVal }}, {{ .Description | quote }})
66 |
67 | {{- if .Required }}
68 | utils.LogIfError({{- $cmdVarName -}}.MarkFlagRequired({{ .Name | quote }}))
69 | {{ end }}
70 |
71 | {{ end }}
72 | {{ end }}
73 | {{- end }}
74 | }
75 |
--------------------------------------------------------------------------------
/templates/nerdgraphclient/client.go.tmpl:
--------------------------------------------------------------------------------
1 | // Code generated by tutone: DO NOT EDIT
2 | package {{.PackageName | lower}}
3 | {{$packageName := .PackageName}}
4 |
5 | {{- if gt (len .Imports) 0 }}
6 | import(
7 | {{- range .Imports}}
8 | "{{.}}"
9 | {{- end}}
10 | )
11 | {{- end}}
12 |
13 | {{range .Mutations}}
14 | {{/*
15 | // TODO The name of the method here could use some love. Perhaps we allow an
16 | // override from the user at config time, so that we are able to replace what
17 | // exists? Perhps too this is an opporunity for us to use the method prefix as
18 | // some kind of indicator for which package this method should belong to,
19 | // perhaps.
20 | */}}
21 | {{ .Description }}
22 | func (a *{{$packageName|title}}) {{.Name | title}}(
23 | {{- range .Signature.Input}}
24 | {{.Name | untitle}} {{.Type}},
25 | {{- end}}
26 | ) (*{{ .Signature.Return | join ", "}}) {
27 |
28 | resp := {{.Name}}QueryResponse{}
29 | vars := map[string]interface{}{
30 | {{- range .QueryVars}}
31 | "{{.Key}}": {{.Value | untitle}},
32 | {{- end}}
33 | }
34 |
35 | if err := a.client.NerdGraphQuery({{.Name}}Mutation, vars, &resp); err != nil {
36 | return nil, err
37 | }
38 |
39 | return &resp.{{first .Signature.Return}}, nil
40 | }
41 |
42 | {{ if gt (len .QueryVars) 0 }}
43 | type {{.Name}}QueryResponse struct {
44 | {{first .Signature.Return}} {{first .Signature.Return}} `json:"{{.Name}}"`
45 | }
46 | {{ end}}
47 |
48 | const {{.Name}}Mutation = `{{ .QueryString }}`
49 |
50 | {{ end}}
51 |
52 | {{ range .Queries}}
53 | {{ .Description }}
54 | func (a *{{$packageName|title}}) Get{{.Name | title}}(
55 | {{- range .Signature.Input}}
56 | {{.Name | untitle}} {{.Type}},
57 | {{- end}}
58 | ) (*{{ .Signature.Return | join ", "}}) {
59 |
60 | resp := {{.ResponseObjectType}}{}
61 | vars := map[string]interface{}{
62 | {{- range .QueryVars}}
63 | "{{.Key}}": {{.Value | untitle}},
64 | {{- end}}
65 | }
66 |
67 | if err := a.client.NerdGraphQuery(get{{.Name}}Query, vars, &resp); err != nil {
68 | return nil, err
69 | }
70 |
71 | {{ if .Signature.ReturnSlice}}
72 | if len(resp.{{.Signature.ReturnPath | join "."}}.{{.Name}}) == 0 {
73 | return nil, errors.NewNotFound("")
74 | }
75 | {{- end}}
76 |
77 | return &resp.{{.Signature.ReturnPath | join "."}}.{{.Name}}, nil
78 | }
79 |
80 | const get{{.Name}}Query = `{{ .QueryString }}`
81 |
82 | {{ end}}
83 |
--------------------------------------------------------------------------------
/templates/nerdgraphclient/integration_test.go.tmpl:
--------------------------------------------------------------------------------
1 | //go:build integration
2 |
3 | package {{.PackageName | lower}}
4 | {{$packageName := .PackageName}}
5 |
6 | import(
7 | "testing"
8 |
9 | "github.com/stretchr/testify/require"
10 |
11 | "github.com/newrelic/newrelic-client-go/v2/pkg/testhelpers"
12 | )
13 |
14 | {{range $index, $_ := .Mutations}}
15 | func Test{{.Name | title}}(t *testing.T) {
16 | t.Parallel()
17 | t.Error("This test was generated but is incomplete. Please add the necessary code and additional test cases to cover the proper scenarios.")
18 | client := new{{$packageName | title}}IntegrationTestClient(t)
19 | require.NotNil(t, client)
20 |
21 | {{range .Signature.Input -}}
22 | {{.Name | untitle}} {{.Type}} := {{.Type}}{
23 | // TODO: Add required variables to initialize instance of this input type
24 | }
25 | {{- end}}
26 |
27 | result, err {{if (eq $index 0)}}:{{end}}= client.{{.Name | title}}(
28 | {{- range .Signature.Input}}
29 | {{.Name | untitle}},
30 | {{- end}}
31 | )
32 |
33 | require.NoError(t, err)
34 | require.NotNil(t, result)
35 | }
36 | {{end}}
37 |
38 | func new{{$packageName | title}}IntegrationTestClient(t *testing.T) AgentApplications {
39 | tc := testhelpers.NewIntegrationTestConfig(t)
40 |
41 | return New(tc)
42 | }
43 |
--------------------------------------------------------------------------------
/templates/typegen/types.go.tmpl:
--------------------------------------------------------------------------------
1 | // Code generated by tutone: DO NOT EDIT
2 | package {{.PackageName | lower}}
3 |
4 | {{- if gt (len .Imports) 0 }}
5 | import(
6 | {{- range .Imports}}
7 | "{{.}}"
8 | {{- end}}
9 | )
10 | {{- end}}
11 |
12 | {{- range .Enums }}
13 | {{ .Description }}
14 | type {{ .Name }} string
15 | {{ $typeName := .Name }}
16 |
17 | var {{.Name}}Types = struct {
18 | {{- range .Values }}
19 | {{- if ne .Description "" }}
20 | {{ .Description }}
21 | {{- end }}
22 | {{ .Name }} {{ $typeName }}
23 | {{- end}}
24 | }{
25 | {{- range .Values }}
26 | {{- if ne .Description "" }}
27 | {{ .Description }}
28 | {{- end }}
29 | {{ .Name }}: "{{ .Name }}",
30 | {{- end}}
31 | }
32 | {{- end }}
33 |
34 | {{- range .Types }}
35 | {{ .Description }}
36 | {{- $typeName := .Name }}
37 | type {{.Name}} struct {
38 | {{- range .Fields }}
39 | {{- if ne .Description "" }}
40 | {{ .Description }}
41 | {{- end }}
42 | {{ .Name }} {{ .Type }} {{ .Tags }}
43 | {{- end}}
44 | }
45 | {{- if .GenerateGetters }}
46 | {{- range .Fields }}
47 | // Get{{ .Name }} returns a pointer to the value of {{ .Name }} from {{ $typeName }}
48 | func (x {{ $typeName }}) Get{{ .Name}}() {{ .Type }} {
49 | return x.{{ .Name }}
50 | }
51 | {{- end }}
52 | {{- end }}
53 |
54 | {{ range .Implements }}
55 | func (x *{{ $typeName }}) Implements{{ . }}() {}
56 | {{ end }}
57 |
58 | {{- if .SpecialUnmarshal }}
59 | // special
60 | func (x *{{ $typeName }}) UnmarshalJSON(b []byte) error {
61 | var objMap map[string]*json.RawMessage
62 | err := json.Unmarshal(b, &objMap)
63 | if err != nil {
64 | return err
65 | }
66 |
67 | for k, v := range objMap {
68 | if v == nil {
69 | continue
70 | }
71 |
72 | switch k {
73 | {{- range .Fields }}
74 | {{- $field := . }}
75 | case "{{ .TagKey }}":
76 | {{- if .IsInterface }}
77 | if v == nil {
78 | continue
79 | }
80 | {{- if .IsList }}
81 | var rawMessage{{ .Name }} []*json.RawMessage
82 | err = json.Unmarshal(*v, &rawMessage{{ .Name }})
83 | if err != nil {
84 | return err
85 | }
86 |
87 | for _, m := range rawMessage{{ .Name }} {
88 | {{- if contains "." $field.TypeName }}
89 | {{- $m := split "." $field.TypeName }}
90 | xxx, err := {{ $m._0 }}.Unmarshal{{ $m._1 }}Interface(*m)
91 | {{- else }}
92 | xxx, err := Unmarshal{{ $field.TypeName }}Interface(*m)
93 | {{- end }}
94 | if err != nil {
95 | return err
96 | }
97 |
98 | if xxx != nil {
99 | x.{{ $field.Name }} = append(x.{{ $field.Name }}, *xxx)
100 | }
101 | }
102 |
103 |
104 | {{- else }}
105 | {{- if contains "." $field.TypeName }}
106 | {{- $m := split "." $field.TypeName }}
107 | xxx, err := {{ $m._0 }}.Unmarshal{{ $m._1 }}Interface(*v)
108 | {{- else }}
109 | xxx, err := Unmarshal{{ $field.TypeName }}Interface(*v)
110 | {{- end }}
111 | if err != nil {
112 | return err
113 | }
114 |
115 | if xxx != nil {
116 | x.{{ $field.Name }} = *xxx
117 | }
118 | {{- end }}
119 | {{- else }}
120 | err = json.Unmarshal(*v, &x.{{ .Name }})
121 | if err != nil {
122 | return err
123 | }
124 | {{- end }}
125 | {{- end }}
126 | }
127 | }
128 |
129 | return nil
130 | }
131 | {{ end}}
132 | {{ end }}
133 |
134 | {{- range .Scalars }}
135 | {{- if ne .Description "" }}
136 | {{ .Description }}
137 | {{- end }}
138 | type {{.Name}} {{.Type}}
139 | {{- end }}
140 |
141 | {{- range .Interfaces }}
142 | {{- $interfaceType := . }}
143 | {{- if ne .Description "" }}
144 | {{ .Description }}
145 | {{- end }}
146 | type {{ $interfaceType.Name }}Interface interface{
147 | {{- range $m := .Methods }}
148 | {{ $m }}
149 | {{- end }}
150 | }
151 |
152 | // Unmarshal{{ $interfaceType.Name }}Interface unmarshals the interface into the correct type
153 | // based on __typename provided by GraphQL
154 | func Unmarshal{{ $interfaceType.Name }}Interface(b []byte) (*{{ $interfaceType.Name }}Interface, error) {
155 | var err error
156 |
157 | var rawMessage{{ $interfaceType.Name }} map[string]*json.RawMessage
158 | err = json.Unmarshal(b, &rawMessage{{ $interfaceType.Name }})
159 | if err != nil {
160 | return nil, err
161 | }
162 |
163 | // Nothing to unmarshal
164 | if len(rawMessage{{ $interfaceType.Name }}) < 1 {
165 | return nil, nil
166 | }
167 |
168 | var typeName string
169 |
170 | if rawTypeName, ok := rawMessage{{ $interfaceType.Name }}["__typename"]; ok {
171 | err = json.Unmarshal(*rawTypeName, &typeName)
172 | if err != nil {
173 | return nil, err
174 | }
175 |
176 | switch typeName {
177 | {{- range .PossibleTypes }}
178 | case "{{ .GraphQLName }}":
179 | var interfaceType {{ .GoName }}
180 | err = json.Unmarshal(b, &interfaceType)
181 | if err != nil {
182 | return nil, err
183 | }
184 |
185 | var xxx {{ $interfaceType.Name }}Interface = &interfaceType
186 |
187 | return &xxx, nil
188 | {{- end }}
189 | }
190 | } else {
191 | keys := []string{}
192 | for k := range rawMessage{{ $interfaceType.Name }} {
193 | keys = append(keys, k)
194 | }
195 | return nil, fmt.Errorf("interface {{ $interfaceType.Name }} did not include a __typename field for inspection: %s", keys)
196 | }
197 |
198 | return nil, fmt.Errorf("interface {{ $interfaceType.Name }} was not matched against all PossibleTypes: %s", typeName)
199 | }
200 | {{- end }}
201 |
--------------------------------------------------------------------------------
/testdata/goodConfig_fixture.yml:
--------------------------------------------------------------------------------
1 | ---
2 | log_level: trace
3 | cache:
4 | schema_file: testing.schema.json
5 | endpoint: https://api222.newrelic.com/graphql
6 | auth:
7 | header: Api-Key
8 | api_key_env_var: NEW_RELIC_API_KEY
9 | packages:
10 | - name: alerts
11 | path: pkg/alerts
12 | import_path: "github.com/newrelic/newrelic-client-go/pkg/alerts"
13 | generators:
14 | - typegen
15 | mutations:
16 | - name: cloudConfigureIntegration
17 | max_query_field_depth: 1
18 | - name: cloudLinkAccount
19 | max_query_field_depth: 1
20 | argument_type_overrides:
21 | accountId: "Int!"
22 | accounts: "CloudLinkCloudAccountsInput!"
23 | exclude_fields:
24 | - "updatedAt"
25 | queries:
26 | - path: ["actor", "cloud"]
27 | endpoints:
28 | - name: linkedAccounts
29 | max_query_field_depth: 2
30 | include_arguments:
31 | - "provider"
32 | exclude_fields:
33 | - "updatedAt"
34 | types:
35 | - name: AlertsMutingRuleConditionInput
36 | - name: AlertsPolicy
37 | generate_struct_getters: true
38 | - name: ID
39 | field_type_override: string
40 | skip_type_create: true
41 | - name: InterfaceImplementation
42 | interface_methods:
43 | - "Get() string"
44 |
45 | generators:
46 | - name: typegen
47 | fileName: "types.go"
48 |
--------------------------------------------------------------------------------
/tools/tools.go:
--------------------------------------------------------------------------------
1 | //go:build tools
2 | // +build tools
3 |
4 | package tools
5 |
6 | import (
7 | // build/test.mk
8 | _ "github.com/stretchr/testify/assert"
9 | _ "gotest.tools/gotestsum"
10 |
11 | // build/lint.mk
12 | _ "github.com/client9/misspell/cmd/misspell"
13 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint"
14 | _ "github.com/psampaz/go-mod-outdated"
15 | _ "golang.org/x/tools/cmd/goimports"
16 |
17 | // build/document.mk
18 | _ "github.com/git-chglog/git-chglog/cmd/git-chglog"
19 | _ "golang.org/x/tools/cmd/godoc"
20 |
21 | // build/release.mk
22 | _ "github.com/caarlos0/svu"
23 | _ "github.com/goreleaser/goreleaser"
24 | _ "github.com/x-motemen/gobump/cmd/gobump"
25 | )
26 |
--------------------------------------------------------------------------------