graphql_tag_rbs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +178 -0
- data/Rakefile +8 -0
- data/VERSION +1 -0
- data/lib/graphql_tag_rbs/config.rb +14 -0
- data/lib/graphql_tag_rbs/generator.rb +402 -0
- data/lib/graphql_tag_rbs/railtie.rb +13 -0
- data/lib/graphql_tag_rbs/rake_tasks.rb +12 -0
- data/lib/graphql_tag_rbs/schema_loader.rb +19 -0
- data/lib/graphql_tag_rbs/tasks/generate.rake +10 -0
- data/lib/graphql_tag_rbs/types.rb +65 -0
- data/lib/graphql_tag_rbs/version.rb +6 -0
- data/lib/graphql_tag_rbs/writer.rb +34 -0
- data/lib/graphql_tag_rbs.rb +58 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 91736967e60bb08acadda5c3394bcd2f572358030fc1e44a831c0223cd600203
|
|
4
|
+
data.tar.gz: 854c435820049688a2b720277ffadda627d653d0481b0c52ed563ce189016f57
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8dc02cfe6152943af815460d4c55e364ef4d22f35fd2c5529cc4935dc29f5d17013359ebe399cd05a1b81377aa490e7cf8db9a172b67d2f609f915eea8c9f59e
|
|
7
|
+
data.tar.gz: 388c4aff5e4e83ca00a2d21a56c3d9bb98cd0e168ad9e50817cd911110d3f23a85058214650bd4e126b30cf1203a3e1344b0b2abf877b4d925d6e87e5f6afc2e
|
data/.rspec
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-11-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Generate RBS `variables`, `data`, and `response` types for each operation, and
|
|
15
|
+
a `data` type for each fragment, from a graphql_tag_pluck (0.3.0 or later)
|
|
16
|
+
operation list and a schema in SDL or introspection JSON.
|
|
17
|
+
- One `.rbs` file per source file, plus a shared file with the GraphQL error
|
|
18
|
+
type and the enums and input objects the operations use.
|
|
19
|
+
- Validation of operations against the schema, with errors naming the
|
|
20
|
+
operation's location.
|
|
21
|
+
- Options for the namespace, string or symbol keys, and custom scalar types.
|
|
22
|
+
- The `graphql_tag_rbs:generate` rake task, for Rails and other projects.
|
data/Gemfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
# Specify your gem's dependencies in graphql_tag_rbs.gemspec
|
|
6
|
+
gemspec
|
|
7
|
+
|
|
8
|
+
gem "rake", "~> 13.0"
|
|
9
|
+
gem "rspec", "~> 3.0"
|
|
10
|
+
|
|
11
|
+
# Used by the specs to check that generated signatures are valid and usable.
|
|
12
|
+
gem "rbs", ">= 3.4"
|
|
13
|
+
gem "steep", ">= 1.9"
|
|
14
|
+
|
|
15
|
+
# Used by the end-to-end spec that plucks operations from Ruby files.
|
|
16
|
+
gem "graphql_tag_pluck", ">= 0.3.0"
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 indigolain
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# GraphqlTagRbs
|
|
2
|
+
|
|
3
|
+
Generates [RBS](https://github.com/ruby/rbs) types for the GraphQL operations
|
|
4
|
+
in your Ruby code, so [Steep](https://github.com/soutaro/steep) can check how
|
|
5
|
+
you build variables and read responses.
|
|
6
|
+
|
|
7
|
+
It reads the operation list exported by
|
|
8
|
+
[graphql_tag_pluck](https://github.com/indigolain/graphql_tag_pluck) (0.3.0 or
|
|
9
|
+
later) and your GraphQL schema, and writes one `.rbs` file per Ruby source file.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Given a heredoc in `app/clients/user_client.rb`:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
module Clients
|
|
17
|
+
class UserClient
|
|
18
|
+
USER_QUERY = <<~GRAPHQL
|
|
19
|
+
query UserQuery($id: ID!) {
|
|
20
|
+
user(id: $id) {
|
|
21
|
+
id
|
|
22
|
+
name
|
|
23
|
+
role
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
GRAPHQL
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
run
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bundle exec rake graphql_tag_pluck:generate_graphql_operation_list graphql_tag_rbs:generate
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
to get `sig/graphql/app/clients/user_client.rbs`:
|
|
38
|
+
|
|
39
|
+
```rbs
|
|
40
|
+
# Generated by graphql_tag_rbs from app/clients/user_client.rb. Do not edit.
|
|
41
|
+
|
|
42
|
+
module GraphQLTypes
|
|
43
|
+
# query UserQuery at app/clients/user_client.rb:4 (Clients::UserClient::USER_QUERY)
|
|
44
|
+
module UserQuery
|
|
45
|
+
type variables = {
|
|
46
|
+
"id" => ::String | ::Integer
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type data = {
|
|
50
|
+
"user" => {
|
|
51
|
+
"id" => ::String,
|
|
52
|
+
"name" => ::String?,
|
|
53
|
+
"role" => ::GraphQLTypes::Enums::role
|
|
54
|
+
}?
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type response = {
|
|
58
|
+
?"data" => data?,
|
|
59
|
+
?"errors" => ::Array[::GraphQLTypes::error],
|
|
60
|
+
?"extensions" => ::Hash[::String, untyped]
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
and `sig/graphql/_shared.rbs` with the error type, enums (`type role = "ADMIN" | "MEMBER"`)
|
|
67
|
+
and input objects the operations use.
|
|
68
|
+
|
|
69
|
+
The types describe the parsed JSON response, so annotate where you parse it:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# @type var response: GraphQLTypes::UserQuery::response
|
|
73
|
+
response = JSON.parse(http_response.body)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
or give your client method a signature that takes `GraphQLTypes::UserQuery::variables`
|
|
77
|
+
and returns `GraphQLTypes::UserQuery::response`.
|
|
78
|
+
|
|
79
|
+
### Loading the rake task
|
|
80
|
+
|
|
81
|
+
In a Rails app the task is available automatically. In any other project, add this to your `Rakefile`:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
require "graphql_tag_rbs/rake_tasks"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## How GraphQL maps to RBS
|
|
88
|
+
|
|
89
|
+
| GraphQL | RBS |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `String`, `ID` | `::String` |
|
|
92
|
+
| `Int` | `::Integer` |
|
|
93
|
+
| `Float` | `::Float` |
|
|
94
|
+
| `Boolean` | `bool` |
|
|
95
|
+
| custom scalar | `untyped`, unless mapped with the `scalars` option |
|
|
96
|
+
| enum | a literal union in `Enums`, e.g. `"ADMIN" \| "MEMBER"` |
|
|
97
|
+
| nullable field `T` | `T?` |
|
|
98
|
+
| list `[T!]!` | `::Array[T]` |
|
|
99
|
+
| field under `@include` / `@skip` | optional key, `?"field" => T` |
|
|
100
|
+
| union / interface | a record per distinct shape, with `"__typename"` as a literal type |
|
|
101
|
+
| fragment | inlined into each operation, plus a `data` type of its own |
|
|
102
|
+
|
|
103
|
+
Variables accept what GraphQL input coercion accepts: `ID` also takes `::Integer`,
|
|
104
|
+
`Float` also takes `::Integer`. Nullable variables and variables with a default value
|
|
105
|
+
are optional keys. Input objects become records in `Inputs`; `@oneOf` input objects
|
|
106
|
+
become a union of single-key records.
|
|
107
|
+
|
|
108
|
+
When a selection on a union or interface picks different fields for different
|
|
109
|
+
possible types, select `__typename` there. Generation fails otherwise, since
|
|
110
|
+
the result could not be told apart.
|
|
111
|
+
|
|
112
|
+
Each definition gets a module named after it (`query getUser` becomes `GetUser`).
|
|
113
|
+
Unnamed operations are skipped with a warning.
|
|
114
|
+
|
|
115
|
+
Operations are validated against the schema first, and errors name the operation
|
|
116
|
+
and where it is defined:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
GraphQL validation failed:
|
|
120
|
+
- UserQuery (app/clients/user_client.rb:4): Field 'nmae' doesn't exist on type 'User'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Steep caveat
|
|
124
|
+
|
|
125
|
+
Steep reports calling methods on nullable fields and missing or mistyped variables,
|
|
126
|
+
but it does not report reading a key a record does not have: `user["nmae"]` is typed
|
|
127
|
+
as the union of the record's value types instead of an error.
|
|
128
|
+
|
|
129
|
+
## Configuration
|
|
130
|
+
|
|
131
|
+
Create `.graphqltagrbsconfig.yaml` to change any of these options:
|
|
132
|
+
|
|
133
|
+
- schema_path
|
|
134
|
+
- The GraphQL schema, as SDL (`.graphql`, `.graphqls`) or an introspection result (`.json`)
|
|
135
|
+
- default: `"schema.graphql"`
|
|
136
|
+
- input_path
|
|
137
|
+
- The operation list exported by graphql_tag_pluck
|
|
138
|
+
- default: `"__generated__/graphql_operation_list.json"`
|
|
139
|
+
- output_dir
|
|
140
|
+
- Where to write the `.rbs` files. Generated files that are no longer produced are removed; files without the generated header are left alone.
|
|
141
|
+
- default: `"sig/graphql"`
|
|
142
|
+
- namespace
|
|
143
|
+
- The module the types are defined in. May be nested, e.g. `"MyApp::GraphQL"`.
|
|
144
|
+
- default: `"GraphQLTypes"`
|
|
145
|
+
- key_style
|
|
146
|
+
- `"string"` for `JSON.parse(body)`, `"symbol"` for `JSON.parse(body, symbolize_names: true)`
|
|
147
|
+
- default: `"string"`
|
|
148
|
+
- scalars
|
|
149
|
+
- RBS types for custom scalars, as they arrive in JSON
|
|
150
|
+
- default: `{}`, e.g. `{ DateTime: "::String", JSON: "untyped" }`
|
|
151
|
+
|
|
152
|
+
Add the output directory to your `Steepfile`:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
target :app do
|
|
156
|
+
signature "sig"
|
|
157
|
+
check "app"
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Installation
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
bundle add graphql_tag_pluck graphql_tag_rbs --group development
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Releasing
|
|
168
|
+
|
|
169
|
+
Releases are automated by the [Release workflow](.github/workflows/release.yml):
|
|
170
|
+
|
|
171
|
+
1. Bump the version in the `VERSION` file at the repository root in a pull request.
|
|
172
|
+
2. Merge it into `main`.
|
|
173
|
+
|
|
174
|
+
The workflow then tags the merge commit as `vX.Y.Z`, runs the specs and publishes the gem to RubyGems.org. If publishing fails, re-run it from **Actions → Release → Run workflow**; the existing tag is reused.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphqlTagRbs
|
|
4
|
+
# Loads options from .graphqltagrbsconfig.yaml in the current directory.
|
|
5
|
+
class Config
|
|
6
|
+
CONFIG_FILE = File.expand_path(".graphqltagrbsconfig.yaml", Dir.pwd)
|
|
7
|
+
|
|
8
|
+
def self.load(path = CONFIG_FILE)
|
|
9
|
+
return {} unless File.exist?(path)
|
|
10
|
+
|
|
11
|
+
(YAML.load_file(path) || {}).to_h { |key, value| [key.to_sym, value] }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphqlTagRbs
|
|
4
|
+
# Turns a graphql_tag_pluck operation list into RBS files.
|
|
5
|
+
#
|
|
6
|
+
# Generator.new(schema: schema, operations: operations).generate
|
|
7
|
+
# # => { "app/clients/user_client.rbs" => "...", "_shared.rbs" => "..." }
|
|
8
|
+
#
|
|
9
|
+
# Every operation gets a module with `variables`, `data`, and `response`
|
|
10
|
+
# type aliases, and every fragment a module with a `data` alias. Enums,
|
|
11
|
+
# input objects, and the GraphQL error type go into the shared file.
|
|
12
|
+
class Generator
|
|
13
|
+
include Types
|
|
14
|
+
|
|
15
|
+
HEADER = "# Generated by graphql_tag_rbs"
|
|
16
|
+
SHARED_FILE = "_shared.rbs"
|
|
17
|
+
KEY_STYLES = %i[string symbol].freeze
|
|
18
|
+
|
|
19
|
+
Nodes = GraphQL::Language::Nodes
|
|
20
|
+
|
|
21
|
+
Definition = Struct.new(:name, :module_name, :kind, :node, :file, :line, :constant, keyword_init: true) do
|
|
22
|
+
def label = "#{name} (#{file}:#{line})"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# All occurrences of one response key within a selection set.
|
|
26
|
+
FieldEntry = Struct.new(:name, :nodes, :conditional)
|
|
27
|
+
|
|
28
|
+
def initialize(schema:, operations:, namespace: "GraphQLTypes", key_style: :string, scalars: {})
|
|
29
|
+
@schema = schema
|
|
30
|
+
@namespace = namespace.to_s.split("::")
|
|
31
|
+
@key_style = key_style.to_sym
|
|
32
|
+
raise Error, "key_style must be one of #{KEY_STYLES.join(", ")}, got #{key_style.inspect}" unless KEY_STYLES.include?(@key_style)
|
|
33
|
+
|
|
34
|
+
@scalars = scalars.to_h { |name, type| [name.to_s, type.to_s] }
|
|
35
|
+
@definitions = build_definitions(operations)
|
|
36
|
+
@fragments = @definitions.select { |d| d.kind == "fragment" }.to_h { |d| [d.name, d.node] }
|
|
37
|
+
@enums = Set.new
|
|
38
|
+
@inputs = Set.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def generate
|
|
42
|
+
validate!
|
|
43
|
+
|
|
44
|
+
files = @definitions.group_by(&:file).sort.to_h do |file, definitions|
|
|
45
|
+
[output_path(file), render_file(file, definitions.sort_by(&:line))]
|
|
46
|
+
end
|
|
47
|
+
raise Error, "#{SHARED_FILE} clashes with a file generated for a source file; rename that file" if files.key?(SHARED_FILE)
|
|
48
|
+
|
|
49
|
+
# Rendered last: rendering the operations collects the enums and inputs it needs.
|
|
50
|
+
files[SHARED_FILE] = render_shared
|
|
51
|
+
files
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# --- Input ------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def build_definitions(operations)
|
|
59
|
+
definitions = operations.each_value.filter_map do |entry|
|
|
60
|
+
entry = entry.to_h { |key, value| [key.to_s, value] }
|
|
61
|
+
unless entry.key?("file") && entry.key?("line")
|
|
62
|
+
raise Error, "#{entry["name"] || "An operation"} has no file / line. " \
|
|
63
|
+
"Regenerate the operation list with graphql_tag_pluck 0.3.0 or later."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if entry["name"].nil?
|
|
67
|
+
warn "graphql_tag_rbs: skipping unnamed operation at #{entry["file"]}:#{entry["line"]}; name it to generate types"
|
|
68
|
+
next
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Definition.new(
|
|
72
|
+
name: entry["name"],
|
|
73
|
+
module_name: module_name(entry["name"]),
|
|
74
|
+
kind: entry["type"],
|
|
75
|
+
node: GraphQL.parse(entry["source"]).definitions.first,
|
|
76
|
+
file: entry["file"],
|
|
77
|
+
line: entry["line"],
|
|
78
|
+
constant: entry["constant"]
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
definitions.group_by(&:module_name).each do |module_name, same|
|
|
83
|
+
next if same.size == 1
|
|
84
|
+
|
|
85
|
+
raise Error, "#{same.map(&:label).join(" and ")} would both generate module #{module_name}"
|
|
86
|
+
end
|
|
87
|
+
definitions
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate!
|
|
91
|
+
document = Nodes::Document.new(definitions: @definitions.map(&:node))
|
|
92
|
+
errors = @schema.validate(document).reject do |error|
|
|
93
|
+
# Fragments get their own types, so a fragment no operation spreads is fine.
|
|
94
|
+
error.is_a?(GraphQL::StaticValidation::FragmentsAreUsedError) && error.message.end_with?("was defined, but not used")
|
|
95
|
+
end
|
|
96
|
+
return if errors.empty?
|
|
97
|
+
|
|
98
|
+
by_name = @definitions.to_h { |d| [d.name, d] }
|
|
99
|
+
messages = errors.map do |error|
|
|
100
|
+
definition = by_name[error.path&.first.to_s.split(" ").last]
|
|
101
|
+
definition ? "#{definition.label}: #{error.message}" : error.message
|
|
102
|
+
end
|
|
103
|
+
raise Error, "GraphQL validation failed:\n#{messages.map { |m| " - #{m}" }.join("\n")}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# --- Files ------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def output_path(file)
|
|
109
|
+
"#{file.delete_prefix("/").delete_suffix(".rb")}.rbs"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def render_file(file, definitions)
|
|
113
|
+
body = definitions.flat_map.with_index do |definition, index|
|
|
114
|
+
(index.zero? ? [] : [""]) + render_definition(definition)
|
|
115
|
+
end
|
|
116
|
+
"#{HEADER} from #{file}. Do not edit.\n\n#{wrap_in_namespace(body)}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def render_definition(definition)
|
|
120
|
+
@current = definition
|
|
121
|
+
comment = "# #{definition.kind} #{definition.name} at #{definition.file}:#{definition.line}"
|
|
122
|
+
comment += " (#{definition.constant})" if definition.constant
|
|
123
|
+
|
|
124
|
+
aliases =
|
|
125
|
+
if definition.kind == "fragment"
|
|
126
|
+
{ "data" => fragment_type(definition.node) }
|
|
127
|
+
else
|
|
128
|
+
{ "variables" => variables_type(definition.node), "data" => data_type(definition.node), "response" => response_type }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
lines = [comment, "module #{definition.module_name}"]
|
|
132
|
+
aliases.each_with_index do |(name, type), index|
|
|
133
|
+
lines << "" unless index.zero?
|
|
134
|
+
lines.concat(indent_lines("type #{name} = #{type.render("")}"))
|
|
135
|
+
end
|
|
136
|
+
lines << "end"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def render_shared
|
|
140
|
+
inputs = {}
|
|
141
|
+
# Input objects can reference further input objects and enums.
|
|
142
|
+
until (pending = @inputs.to_a.sort - inputs.keys).empty?
|
|
143
|
+
pending.each { |name| inputs[name] = input_object_type(@schema.get_type(name)) }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
body = ["type error = #{error_type.render("")}"].flat_map { |line| line.lines(chomp: true) }
|
|
147
|
+
body += shared_module("Enums", @enums.to_a.sort.to_h { |name| [name, enum_type(@schema.get_type(name))] })
|
|
148
|
+
body += shared_module("Inputs", inputs.sort.to_h)
|
|
149
|
+
|
|
150
|
+
"#{HEADER}. Do not edit.\n\n#{wrap_in_namespace(body)}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def shared_module(module_name, types)
|
|
154
|
+
return [] if types.empty?
|
|
155
|
+
|
|
156
|
+
lines = ["", "module #{module_name}"]
|
|
157
|
+
types.each_with_index do |(name, type), index|
|
|
158
|
+
lines << "" unless index.zero?
|
|
159
|
+
lines.concat(indent_lines("type #{alias_name(name)} = #{type.render("")}"))
|
|
160
|
+
end
|
|
161
|
+
lines << "end"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def wrap_in_namespace(lines)
|
|
165
|
+
@namespace.reverse_each do |name|
|
|
166
|
+
lines = ["module #{name}", *indent_lines(lines), "end"]
|
|
167
|
+
end
|
|
168
|
+
"#{lines.join("\n")}\n"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def indent_lines(text)
|
|
172
|
+
Array(text)
|
|
173
|
+
.flat_map { |chunk| chunk.empty? ? [""] : chunk.lines(chomp: true) }
|
|
174
|
+
.map { |line| line.empty? ? line : INDENT + line }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# --- Operation types ----------------------------------------------------
|
|
178
|
+
|
|
179
|
+
def data_type(operation)
|
|
180
|
+
root = @schema.root_type_for_operation(operation.operation_type)
|
|
181
|
+
raise Error, "#{@current.label}: the schema has no #{operation.operation_type} type" if root.nil?
|
|
182
|
+
|
|
183
|
+
selection_set_type(root, operation.selections, [])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def fragment_type(fragment)
|
|
187
|
+
selection_set_type(@schema.get_type(fragment.type.name), fragment.selections, [])
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def variables_type(operation)
|
|
191
|
+
fields = operation.variables.map do |variable|
|
|
192
|
+
required = variable.type.is_a?(Nodes::NonNullType) && variable.default_value.nil?
|
|
193
|
+
Field.new(variable.name, input_ast_type(variable.type), !required)
|
|
194
|
+
end
|
|
195
|
+
Record.new(fields, @key_style)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def response_type
|
|
199
|
+
Record.new([
|
|
200
|
+
Field.new("data", Optional.new(Named.new("data")), true),
|
|
201
|
+
Field.new("errors", ArrayOf.new(Named.new("#{namespace_ref}::error")), true),
|
|
202
|
+
Field.new("extensions", hash_type, true)
|
|
203
|
+
], @key_style)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def error_type
|
|
207
|
+
location = Record.new([Field.new("line", Named.new("::Integer"), false), Field.new("column", Named.new("::Integer"), false)], @key_style)
|
|
208
|
+
Record.new([
|
|
209
|
+
Field.new("message", Named.new("::String"), false),
|
|
210
|
+
Field.new("locations", ArrayOf.new(location), true),
|
|
211
|
+
Field.new("path", ArrayOf.new(Union.of([Named.new("::String"), Named.new("::Integer")])), true),
|
|
212
|
+
Field.new("extensions", hash_type, true)
|
|
213
|
+
], @key_style)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def hash_type
|
|
217
|
+
Named.new(@key_style == :symbol ? "::Hash[::Symbol, untyped]" : "::Hash[::String, untyped]")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# --- Selections ---------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
# The type of a selection set on `parent_type`. For abstract types, possible
|
|
223
|
+
# types that select the same fields share one record, distinguished by a
|
|
224
|
+
# `__typename` literal union; differing shapes become a union of records.
|
|
225
|
+
def selection_set_type(parent_type, selections, path)
|
|
226
|
+
possible = @schema.possible_types(parent_type).sort_by(&:graphql_name)
|
|
227
|
+
return UNTYPED if possible.empty?
|
|
228
|
+
|
|
229
|
+
shapes = possible.map do |type|
|
|
230
|
+
entries = {}
|
|
231
|
+
collect_fields(type, selections, false, entries)
|
|
232
|
+
[type, entries.map { |key, entry| field_for(type, key, entry, path) }]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
groups = shapes.group_by { |_type, fields| fields }.transform_values { |members| members.map(&:first) }
|
|
236
|
+
if groups.size > 1 && !groups.keys.all? { |fields| fields.any? { |field| field.type == :typename } }
|
|
237
|
+
location = path.empty? ? "the root selection" : "`#{path.join(".")}`"
|
|
238
|
+
raise Error, "#{@current.label}: #{location} selects different fields depending on whether the " \
|
|
239
|
+
"#{parent_type.graphql_name} is a #{possible.map(&:graphql_name).join(", ")}; " \
|
|
240
|
+
"select `__typename` there so the result can be told apart"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
Union.of(groups.map do |fields, types|
|
|
244
|
+
typename = Union.of(types.map { |type| Literal.new(type.graphql_name) })
|
|
245
|
+
Record.new(fields.map { |field| field.type == :typename ? Field.new(field.key, typename, field.optional) : field }, @key_style)
|
|
246
|
+
end)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Collects the fields `type` gets from `selections`, following inline
|
|
250
|
+
# fragments and fragment spreads that apply to it. Fields under
|
|
251
|
+
# @include / @skip may be missing from the response, so they are marked
|
|
252
|
+
# conditional unless the same key is also selected unconditionally.
|
|
253
|
+
def collect_fields(type, selections, conditional, entries)
|
|
254
|
+
selections.each do |selection|
|
|
255
|
+
selection_conditional = conditional || selection.directives.any? { |d| %w[include skip].include?(d.name) }
|
|
256
|
+
|
|
257
|
+
case selection
|
|
258
|
+
when Nodes::Field
|
|
259
|
+
key = selection.alias || selection.name
|
|
260
|
+
if (entry = entries[key])
|
|
261
|
+
entry.nodes << selection
|
|
262
|
+
entry.conditional &&= selection_conditional
|
|
263
|
+
else
|
|
264
|
+
entries[key] = FieldEntry.new(selection.name, [selection], selection_conditional)
|
|
265
|
+
end
|
|
266
|
+
when Nodes::InlineFragment
|
|
267
|
+
next if selection.type && !applies_to?(type, selection.type.name)
|
|
268
|
+
|
|
269
|
+
collect_fields(type, selection.selections, selection_conditional, entries)
|
|
270
|
+
when Nodes::FragmentSpread
|
|
271
|
+
fragment = @fragments.fetch(selection.name)
|
|
272
|
+
next unless applies_to?(type, fragment.type.name)
|
|
273
|
+
|
|
274
|
+
collect_fields(type, fragment.selections, selection_conditional, entries)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def applies_to?(type, type_condition)
|
|
280
|
+
condition = @schema.get_type(type_condition)
|
|
281
|
+
condition == type || (condition.kind.abstract? && @schema.possible_types(condition).include?(type))
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def field_for(type, key, entry, path)
|
|
285
|
+
# A placeholder, replaced with the possible type names once types are grouped.
|
|
286
|
+
return Field.new(key, :typename, entry.conditional) if entry.name == "__typename"
|
|
287
|
+
|
|
288
|
+
definition = @schema.get_field(type, entry.name)
|
|
289
|
+
field_type = output_type(definition.type, entry.nodes.flat_map(&:selections), path + [key])
|
|
290
|
+
Field.new(key, field_type, entry.conditional)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def output_type(type, selections, path)
|
|
294
|
+
return bare_output_type(type.of_type, selections, path) if type.non_null?
|
|
295
|
+
|
|
296
|
+
Optional.new(bare_output_type(type, selections, path))
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def bare_output_type(type, selections, path)
|
|
300
|
+
return ArrayOf.new(output_type(type.of_type, selections, path)) if type.list?
|
|
301
|
+
|
|
302
|
+
case type.kind.name
|
|
303
|
+
when "SCALAR" then scalar_type(type.graphql_name, :output)
|
|
304
|
+
when "ENUM" then enum_ref(type)
|
|
305
|
+
else selection_set_type(type, selections, path)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# --- Inputs ---------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
def input_ast_type(ast_type)
|
|
312
|
+
return bare_input_ast_type(ast_type.of_type) if ast_type.is_a?(Nodes::NonNullType)
|
|
313
|
+
|
|
314
|
+
Optional.new(bare_input_ast_type(ast_type))
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def bare_input_ast_type(ast_type)
|
|
318
|
+
return ArrayOf.new(input_ast_type(ast_type.of_type)) if ast_type.is_a?(Nodes::ListType)
|
|
319
|
+
|
|
320
|
+
named_input_type(@schema.get_type(ast_type.name))
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def input_type(type)
|
|
324
|
+
return bare_input_type(type.of_type) if type.non_null?
|
|
325
|
+
|
|
326
|
+
Optional.new(bare_input_type(type))
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def bare_input_type(type)
|
|
330
|
+
return ArrayOf.new(input_type(type.of_type)) if type.list?
|
|
331
|
+
|
|
332
|
+
named_input_type(type)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def named_input_type(type)
|
|
336
|
+
case type.kind.name
|
|
337
|
+
when "SCALAR" then scalar_type(type.graphql_name, :input)
|
|
338
|
+
when "ENUM" then enum_ref(type)
|
|
339
|
+
else
|
|
340
|
+
@inputs << type.graphql_name
|
|
341
|
+
Named.new("#{namespace_ref}::Inputs::#{alias_name(type.graphql_name)}")
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def input_object_type(type)
|
|
346
|
+
arguments = type.arguments.values
|
|
347
|
+
|
|
348
|
+
# @oneOf input objects take exactly one of their fields, and it must not be null.
|
|
349
|
+
if type.respond_to?(:one_of?) && type.one_of?
|
|
350
|
+
return Union.of(arguments.map do |argument|
|
|
351
|
+
value_type = argument.type.non_null? ? argument.type.of_type : argument.type
|
|
352
|
+
Record.new([Field.new(argument.graphql_name, bare_input_type(value_type), false)], @key_style)
|
|
353
|
+
end)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
Record.new(arguments.map do |argument|
|
|
357
|
+
required = argument.type.non_null? && !argument.default_value?
|
|
358
|
+
Field.new(argument.graphql_name, input_type(argument.type), !required)
|
|
359
|
+
end, @key_style)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# --- Leaves ---------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
BUILT_IN_SCALARS = {
|
|
365
|
+
output: { "ID" => "::String", "String" => "::String", "Int" => "::Integer", "Float" => "::Float", "Boolean" => "bool" },
|
|
366
|
+
# Input coercion also accepts integers for ID and Float.
|
|
367
|
+
input: { "ID" => "::String | ::Integer", "String" => "::String", "Int" => "::Integer", "Float" => "::Float | ::Integer", "Boolean" => "bool" }
|
|
368
|
+
}.freeze
|
|
369
|
+
|
|
370
|
+
def scalar_type(name, direction)
|
|
371
|
+
Named.new(@scalars.fetch(name) { BUILT_IN_SCALARS[direction].fetch(name, "untyped") })
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def enum_ref(type)
|
|
375
|
+
@enums << type.graphql_name
|
|
376
|
+
Named.new("#{namespace_ref}::Enums::#{alias_name(type.graphql_name)}")
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def enum_type(type)
|
|
380
|
+
Union.of(type.values.keys.map { |value| Literal.new(value) })
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# --- Naming ---------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def namespace_ref = "::#{@namespace.join("::")}"
|
|
386
|
+
|
|
387
|
+
# GraphQL names may start with a lowercase letter or underscores; RBS module names may not.
|
|
388
|
+
def module_name(name)
|
|
389
|
+
stripped = name.sub(/\A_+/, "")
|
|
390
|
+
module_name = stripped[0].to_s.upcase + stripped[1..].to_s
|
|
391
|
+
raise Error, "Cannot turn GraphQL name #{name.inspect} into an RBS module name" unless module_name.match?(/\A[A-Z]\w*\z/)
|
|
392
|
+
raise Error, "#{name} clashes with the generated #{module_name} module; rename it" if %w[Enums Inputs].include?(module_name)
|
|
393
|
+
|
|
394
|
+
module_name
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# RBS type alias names start with a lowercase letter: UserRole => user_role.
|
|
398
|
+
def alias_name(name)
|
|
399
|
+
name.sub(/\A_+/, "").gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
|
|
5
|
+
module GraphqlTagRbs
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
railtie_name :graphql_tag_rbs
|
|
8
|
+
|
|
9
|
+
rake_tasks do
|
|
10
|
+
Dir.glob(File.expand_path("tasks/**/*.rake", __dir__)).each { |f| load f }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Defines the graphql_tag_rbs rake tasks outside Rails. Add this to your
|
|
4
|
+
# Rakefile:
|
|
5
|
+
#
|
|
6
|
+
# require "graphql_tag_rbs/rake_tasks"
|
|
7
|
+
#
|
|
8
|
+
# Rails apps get the tasks through GraphqlTagRbs::Railtie and should not
|
|
9
|
+
# require this file, or the tasks will run twice.
|
|
10
|
+
require "graphql_tag_rbs"
|
|
11
|
+
|
|
12
|
+
Dir.glob(File.expand_path("tasks/**/*.rake", __dir__)).each { |f| load f }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphqlTagRbs
|
|
4
|
+
# Loads a schema from SDL (.graphql / .graphqls) or an introspection result (.json).
|
|
5
|
+
module SchemaLoader
|
|
6
|
+
def self.load(path)
|
|
7
|
+
raise Error, "GraphQL schema not found at #{path}" unless File.exist?(path)
|
|
8
|
+
|
|
9
|
+
if File.extname(path) == ".json"
|
|
10
|
+
introspection = JSON.parse(File.read(path))
|
|
11
|
+
# Accept both a raw `{ "__schema": ... }` and a full `{ "data": { "__schema": ... } }` response.
|
|
12
|
+
introspection = { "data" => introspection } if introspection.key?("__schema")
|
|
13
|
+
GraphQL::Schema.from_introspection(introspection)
|
|
14
|
+
else
|
|
15
|
+
GraphQL::Schema.from_definition(File.read(path))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :graphql_tag_rbs do
|
|
4
|
+
desc "Generate RBS types for the GraphQL operations plucked by graphql_tag_pluck"
|
|
5
|
+
task :generate do
|
|
6
|
+
result = GraphqlTagRbs.generate
|
|
7
|
+
puts "graphql_tag_rbs: wrote #{result.written.size} file(s) to #{GraphqlTagRbs.options[:output_dir]}"
|
|
8
|
+
result.deleted.each { |path| puts "graphql_tag_rbs: removed #{path}" }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphqlTagRbs
|
|
4
|
+
# A small RBS type AST. Every node renders itself given the indentation of
|
|
5
|
+
# the line it starts on; records span several lines.
|
|
6
|
+
module Types
|
|
7
|
+
INDENT = " "
|
|
8
|
+
|
|
9
|
+
# A type written as-is, e.g. `::String`, `bool`, `untyped`, or a type alias reference.
|
|
10
|
+
Named = Struct.new(:name) do
|
|
11
|
+
def render(_indent) = name
|
|
12
|
+
|
|
13
|
+
def compound? = name.match?(/[|&]/)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A string literal type, e.g. `"User"`.
|
|
17
|
+
Literal = Struct.new(:value) do
|
|
18
|
+
def render(_indent) = value.dump
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Optional = Struct.new(:type) do
|
|
22
|
+
def render(indent)
|
|
23
|
+
return type.render(indent) if type == UNTYPED
|
|
24
|
+
|
|
25
|
+
inner = type.render(indent)
|
|
26
|
+
needs_parens = type.is_a?(Union) || (type.is_a?(Named) && type.compound?)
|
|
27
|
+
needs_parens ? "(#{inner})?" : "#{inner}?"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
ArrayOf = Struct.new(:type) do
|
|
32
|
+
def render(indent) = "::Array[#{type.render(indent)}]"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Union = Struct.new(:types) do
|
|
36
|
+
def self.of(types)
|
|
37
|
+
unique = types.uniq
|
|
38
|
+
unique.size == 1 ? unique.first : new(unique)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render(indent) = types.map { |type| type.render(indent) }.join(" | ")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Field = Struct.new(:key, :type, :optional)
|
|
45
|
+
|
|
46
|
+
Record = Struct.new(:fields, :key_style) do
|
|
47
|
+
def render(indent)
|
|
48
|
+
return "{}" if fields.empty?
|
|
49
|
+
|
|
50
|
+
inner = indent + INDENT
|
|
51
|
+
lines = fields.map { |field| "#{inner}#{key(field)}#{field.type.render(inner)}" }
|
|
52
|
+
"{\n#{lines.join(",\n")}\n#{indent}}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def key(field)
|
|
58
|
+
prefix = field.optional ? "?" : ""
|
|
59
|
+
key_style == :symbol ? "#{prefix}#{field.key}: " : "#{prefix}#{field.key.dump} => "
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
UNTYPED = Named.new("untyped")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module GraphqlTagRbs
|
|
6
|
+
# Writes generated files into the output directory and removes generated
|
|
7
|
+
# files that are no longer produced, such as those of a deleted source file.
|
|
8
|
+
# Files without the generated header are never touched.
|
|
9
|
+
class Writer
|
|
10
|
+
Result = Struct.new(:written, :deleted, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
def initialize(output_dir)
|
|
13
|
+
@output_dir = output_dir
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write(files)
|
|
17
|
+
written = files.map do |relative_path, content|
|
|
18
|
+
path = File.join(@output_dir, relative_path)
|
|
19
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
20
|
+
File.write(path, content) unless File.exist?(path) && File.read(path) == content
|
|
21
|
+
path
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
deleted = (Dir.glob(File.join(@output_dir, "**/*.rbs")) - written).select do |path|
|
|
25
|
+
next false unless File.foreach(path).first.to_s.start_with?(Generator::HEADER)
|
|
26
|
+
|
|
27
|
+
File.delete(path)
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Result.new(written: written, deleted: deleted)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "graphql"
|
|
4
|
+
require "json"
|
|
5
|
+
require "set"
|
|
6
|
+
require "yaml"
|
|
7
|
+
|
|
8
|
+
require_relative "graphql_tag_rbs/version"
|
|
9
|
+
|
|
10
|
+
module GraphqlTagRbs
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require_relative "graphql_tag_rbs/config"
|
|
15
|
+
require_relative "graphql_tag_rbs/types"
|
|
16
|
+
require_relative "graphql_tag_rbs/schema_loader"
|
|
17
|
+
require_relative "graphql_tag_rbs/generator"
|
|
18
|
+
require_relative "graphql_tag_rbs/writer"
|
|
19
|
+
require_relative "graphql_tag_rbs/railtie" if defined?(Rails::Railtie)
|
|
20
|
+
|
|
21
|
+
module GraphqlTagRbs
|
|
22
|
+
class << self
|
|
23
|
+
attr_accessor :options
|
|
24
|
+
|
|
25
|
+
def default_options
|
|
26
|
+
{
|
|
27
|
+
schema_path: "schema.graphql",
|
|
28
|
+
input_path: "__generated__/graphql_operation_list.json",
|
|
29
|
+
output_dir: "sig/graphql",
|
|
30
|
+
namespace: "GraphQLTypes",
|
|
31
|
+
key_style: "string",
|
|
32
|
+
scalars: {}
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Reads the schema and the graphql_tag_pluck operation list, and writes
|
|
37
|
+
# RBS files into output_dir. Returns the Writer::Result.
|
|
38
|
+
def generate(options = self.options)
|
|
39
|
+
input_path = options[:input_path]
|
|
40
|
+
unless File.exist?(input_path)
|
|
41
|
+
raise Error, "Operation list not found at #{input_path}. " \
|
|
42
|
+
"Run `rake graphql_tag_pluck:generate_graphql_operation_list` first."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
files = Generator.new(
|
|
46
|
+
schema: SchemaLoader.load(options[:schema_path]),
|
|
47
|
+
operations: JSON.parse(File.read(input_path)),
|
|
48
|
+
namespace: options[:namespace],
|
|
49
|
+
key_style: options[:key_style],
|
|
50
|
+
scalars: options[:scalars] || {}
|
|
51
|
+
).generate
|
|
52
|
+
|
|
53
|
+
Writer.new(options[:output_dir]).write(files)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
self.options = default_options.merge(Config.load)
|
|
58
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: graphql_tag_rbs
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- indigolain
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: graphql
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
description: Reads the operation list exported by graphql_tag_pluck and a GraphQL
|
|
27
|
+
schema, and writes RBS type aliases for each operation's variables, data, and response.
|
|
28
|
+
email:
|
|
29
|
+
- indigo.lain@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- ".rspec"
|
|
35
|
+
- ".ruby-version"
|
|
36
|
+
- CHANGELOG.md
|
|
37
|
+
- Gemfile
|
|
38
|
+
- LICENSE.txt
|
|
39
|
+
- README.md
|
|
40
|
+
- Rakefile
|
|
41
|
+
- VERSION
|
|
42
|
+
- lib/graphql_tag_rbs.rb
|
|
43
|
+
- lib/graphql_tag_rbs/config.rb
|
|
44
|
+
- lib/graphql_tag_rbs/generator.rb
|
|
45
|
+
- lib/graphql_tag_rbs/railtie.rb
|
|
46
|
+
- lib/graphql_tag_rbs/rake_tasks.rb
|
|
47
|
+
- lib/graphql_tag_rbs/schema_loader.rb
|
|
48
|
+
- lib/graphql_tag_rbs/tasks/generate.rake
|
|
49
|
+
- lib/graphql_tag_rbs/types.rb
|
|
50
|
+
- lib/graphql_tag_rbs/version.rb
|
|
51
|
+
- lib/graphql_tag_rbs/writer.rb
|
|
52
|
+
homepage: https://github.com/indigolain/graphql_tag_rbs
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
homepage_uri: https://github.com/indigolain/graphql_tag_rbs
|
|
57
|
+
source_code_uri: https://github.com/indigolain/graphql_tag_rbs
|
|
58
|
+
changelog_uri: https://github.com/indigolain/graphql_tag_rbs/blob/main/CHANGELOG.md
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: 3.2.0
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 4.0.3
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: Generates RBS types for GraphQL operations plucked by graphql_tag_pluck
|
|
76
|
+
test_files: []
|