├── .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 | [![Community Project header](https://github.com/newrelic/open-source-office/raw/master/examples/categories/images/Community_Project.png)](https://github.com/newrelic/open-source-office/blob/master/examples/categories/index.md#category-community-project) 2 | 3 | # Tutone 4 | 5 | [![Testing](https://github.com/newrelic/tutone/workflows/Testing/badge.svg)](https://github.com/newrelic/tutone/actions) 6 | [![Security Scan](https://github.com/newrelic/tutone/workflows/Security%20Scan/badge.svg)](https://github.com/newrelic/tutone/actions) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/newrelic/tutone?style=flat-square)](https://goreportcard.com/report/github.com/newrelic/tutone) 8 | [![GoDoc](https://godoc.org/github.com/newrelic/tutone?status.svg)](https://godoc.org/github.com/newrelic/tutone) 9 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/newrelic/tutone/blob/main/LICENSE) 10 | [![CLA assistant](https://cla-assistant.io/readme/badge/newrelic/tutone)](https://cla-assistant.io/newrelic/tutone) 11 | [![Release](https://img.shields.io/github/release/newrelic/tutone/all.svg)](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 | --------------------------------------------------------------------------------