graph_weaver 0.2.0 → 0.2.1

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.
data/lib/graph_weaver.rb CHANGED
@@ -4,6 +4,7 @@ require "sorbet-runtime"
4
4
  require_relative "graph_weaver/logging"
5
5
  require_relative "graph_weaver/errors"
6
6
  require_relative "graph_weaver/hints"
7
+ require_relative "graph_weaver/input_struct"
7
8
  require_relative "graph_weaver/response"
8
9
  require_relative "graph_weaver/inflect"
9
10
  require_relative "graph_weaver/codegen"
@@ -56,15 +57,58 @@ module GraphWeaver
56
57
  target.is_a?(Client) ? target.transport! : target
57
58
  end
58
59
 
59
- # Conventional locations, factory_bot-style: used as defaults by
60
- # generate!, verify_generated!, load_generated!, and the rake tasks;
61
- # override the accessors or pass paths.
62
- attr_writer :queries_path, :generated_path, :schema_path
60
+ # Conventional locations, factory_bot-style LISTS, so extra
61
+ # locations (a test-only dir, an engine's) can be appended and every
62
+ # loader walks them all:
63
+ #
64
+ # # e.g. in spec/support/graph_weaver.rb
65
+ # GraphWeaver.generated_paths << "spec/support/graphql/generated"
66
+ # GraphWeaver.queries_paths << "spec/support/graphql/queries"
67
+ #
68
+ # The singular accessors read the first entry (the default target
69
+ # for generate! and the rake tasks); assigning one replaces the list.
70
+ attr_writer :queries_paths, :generated_paths, :schema_path
71
+
72
+ # Entries may be glob patterns — the generated default also matches
73
+ # per-schema layouts (app/graphql/github/generated). Queries stay
74
+ # single-schema: load_queries! parses everything against one client.
75
+ def queries_paths = @queries_paths ||= ["app/graphql/queries"]
76
+ def generated_paths = @generated_paths ||= ["app/graphql/generated", "app/graphql/*/generated"]
77
+
78
+ def queries_path = queries_paths.first
79
+ def generated_path = generated_paths.first
80
+
81
+ def queries_path=(path)
82
+ @queries_paths = path.nil? ? nil : [path]
83
+ end
84
+
85
+ def generated_path=(path)
86
+ @generated_paths = path.nil? ? nil : [path]
87
+ end
63
88
 
64
- def queries_path = @queries_path || "app/graphql/queries"
65
- def generated_path = @generated_path || "app/graphql/generated"
66
89
  def schema_path = @schema_path || "app/graphql/schema.json"
67
90
 
91
+ # The shared-inputs module name: set it globally, pass inputs_module:
92
+ # per generate!, or let it derive from the output path — the
93
+ # directory above generated/ names the schema in multi-schema
94
+ # layouts (app/graphql/github/generated => GithubInputs); the
95
+ # conventional layout (and anything unrecognizable) stays
96
+ # GraphQLInputs.
97
+ attr_writer :inputs_module
98
+
99
+ def inputs_module(output = generated_path)
100
+ return @inputs_module if @inputs_module
101
+
102
+ segments = File.expand_path(output.to_s).split(File::SEPARATOR)
103
+ segments.pop if segments.last == "generated"
104
+ parent = segments.last.to_s
105
+ if parent.match?(/\A[a-zA-Z]\w*\z/) && !%w[graphql app lib spec support test].include?(parent)
106
+ "#{Inflect.camelize(parent)}Inputs"
107
+ else
108
+ "GraphQLInputs"
109
+ end
110
+ end
111
+
68
112
  # Generate every .graphql query in a directory into checked-in Ruby
69
113
  # files. Paths default to the conventions above; schema: defaults to
70
114
  # the dump at schema_path (any supported extension):
@@ -73,16 +117,28 @@ module GraphWeaver
73
117
  #
74
118
  # person.graphql => person_query.rb defining PersonQuery. Returns the
75
119
  # written paths. Pair with a freshness spec (docs/generated_modules.md).
76
- def generate!(schema: nil, queries: queries_path, output: generated_path, client: nil)
120
+ def generate!(schema: nil, queries: queries_path, output: generated_path, client: nil,
121
+ shared_inputs: true, inputs_module: nil)
77
122
  schema ||= locate_schema!
78
- FileUtils.mkdir_p(output)
123
+ inputs_module ||= self.inputs_module(output)
79
124
 
80
- each_query(queries, schema:, client:).map do |base, source|
81
- target = File.join(output, "#{base}_query.rb")
125
+ plan = generation_plan(queries:, schema:, client:, shared_inputs:, inputs_module:)
126
+ written = plan.map do |filename, source|
127
+ target = File.join(output, filename)
128
+ FileUtils.mkdir_p(File.dirname(target))
82
129
  File.write(target, source)
83
130
  log(:info) { "generated #{target}" }
84
131
  target
85
132
  end
133
+
134
+ # a type dropped from the schema must not linger as a stale file —
135
+ # inputs/ is wholly generated, so pruning is safe
136
+ (Dir[File.join(output, "inputs", "*.rb")] - written).each do |orphan|
137
+ File.delete(orphan)
138
+ log(:info) { "pruned #{orphan}" }
139
+ end
140
+
141
+ written
86
142
  end
87
143
 
88
144
  # The freshness guard: raise unless every generated file matches what
@@ -92,12 +148,17 @@ module GraphWeaver
92
148
  # it "generated queries are current" do
93
149
  # GraphWeaver.verify_generated!
94
150
  # end
95
- def verify_generated!(schema: nil, queries: queries_path, output: generated_path, client: nil)
151
+ def verify_generated!(schema: nil, queries: queries_path, output: generated_path, client: nil,
152
+ shared_inputs: true, inputs_module: nil)
96
153
  schema ||= locate_schema!
97
- stale = each_query(queries, schema:, client:).filter_map do |base, source|
98
- target = File.join(output, "#{base}_query.rb")
154
+ inputs_module ||= self.inputs_module(output)
155
+ plan = generation_plan(queries:, schema:, client:, shared_inputs:, inputs_module:)
156
+ stale = plan.filter_map do |filename, source|
157
+ target = File.join(output, filename)
99
158
  target unless File.exist?(target) && File.read(target) == source
100
159
  end
160
+ # strays: a type file the current schema no longer produces
161
+ stale += Dir[File.join(output, "inputs", "*.rb")] - plan.map { |f, _| File.join(output, f) }
101
162
 
102
163
  unless stale.empty?
103
164
  raise Error, "stale generated queries — regenerate (rake graph_weaver:generate): #{stale.join(", ")}"
@@ -116,10 +177,11 @@ module GraphWeaver
116
177
  # Generated::PersonQuery from generated/person_query.rb, and
117
178
  # generated code only changes on regeneration anyway (restart, like
118
179
  # a schema migration).
119
- def load_generated!(path = generated_path)
120
- files = Dir[File.join(path, "**/*.rb")].sort
180
+ def load_generated!(path = nil)
181
+ paths = path ? [path] : generated_paths
182
+ files = paths.flat_map { |dir| Dir[File.join(dir, "**/*.rb")].sort }.uniq
121
183
  files.each { |file| require File.expand_path(file) }
122
- log(:info) { "loaded #{files.size} generated module(s) from #{path}" }
184
+ log(:info) { "loaded #{files.size} generated module(s) from #{paths.join(", ")}" }
123
185
  files
124
186
  end
125
187
 
@@ -130,20 +192,39 @@ module GraphWeaver
130
192
  end
131
193
  private :locate_schema!
132
194
 
133
- # (base, generated_source) per .graphql file in a directory
134
- def each_query(queries, schema:, client:)
135
- Dir[File.join(queries, "*.graphql")].sort.map do |path|
195
+ # (filename, source) per artifact: shared_inputs (the default) emits
196
+ # every variable type once into inputs.rb, with query modules
197
+ # aliasing what they use — the difference between hundreds of
198
+ # duplicated bool_exp structs and one copy per schema.
199
+ def generation_plan(queries:, schema:, client:, shared_inputs:, inputs_module: self.inputs_module)
200
+ namespace = shared_inputs ? inputs_module : nil
201
+ used = { inputs: [], enums: [], mapped: [] }
202
+
203
+ plan = Dir[File.join(queries, "*.graphql")].sort.map do |path|
136
204
  base = File.basename(path, ".graphql")
137
- source = Codegen.generate(
205
+ codegen = Codegen.new(
138
206
  schema:,
139
207
  query: File.read(path),
140
208
  module_name: "#{Inflect.camelize(base)}Query",
141
209
  client:,
210
+ inputs_namespace: namespace,
142
211
  )
143
- [base, source]
212
+ source = codegen.generate
213
+ codegen.variable_type_names.each { |kind, names| used[kind] |= names }
214
+ ["#{base}_query.rb", source]
144
215
  end
216
+
217
+ if namespace && used.values.any?(&:any?)
218
+ shared = Codegen.generate_inputs(
219
+ schema:, module_name: namespace,
220
+ input_types: used[:inputs], enum_types: used[:enums] + used[:mapped],
221
+ )
222
+ plan = shared.to_a + plan
223
+ end
224
+
225
+ plan
145
226
  end
146
- private :each_query
227
+ private :generation_plan
147
228
 
148
229
  # Default input coercion for scalars that don't say coerce: themselves,
149
230
  # resolved lazily at generation time (so set it any time before you
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graph_weaver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
@@ -255,6 +255,7 @@ files:
255
255
  - lib/graph_weaver/errors.rb
256
256
  - lib/graph_weaver/hints.rb
257
257
  - lib/graph_weaver/inflect.rb
258
+ - lib/graph_weaver/input_struct.rb
258
259
  - lib/graph_weaver/logging.rb
259
260
  - lib/graph_weaver/railtie.rb
260
261
  - lib/graph_weaver/response.rb