Module: GraphWeaver::SchemaLoader
- Defined in:
- lib/graph_weaver/schema_loader.rb
Overview
Load a schema for codegen from either format a remote service can hand you — introspection JSON or SDL, as a file path or the content itself — or fetch one straight from a live endpoint via introspect.
Constant Summary collapse
- FEDERATION_PREFIXES =
%w[join__ link__ core__].freeze
- FEDERATION_DIRECTIVES =
%w[link core inaccessible].to_set.freeze
- CACHE_EXTENSIONS =
%w[.json .graphql .gql].freeze
Class Method Summary collapse
-
.federation_sdl?(sdl) ⇒ Boolean
A composed Fed2 supergraph is marked by @join__* directives (every merged type carries them); a plain schema has none.
-
.introspect(transport, cache: nil, ttl: nil) ⇒ Object
Run the standard introspection query through a transport and build a schema from the result:.
-
.load(source) ⇒ Object
Accepts, and detects: - a Hash (a parsed introspection result) - a file path — .json (introspection) or .graphql/.gql (SDL) - raw content — introspection JSON (starts with "{") or SDL so a cache round-trip is symmetrical with introspect: SchemaLoader.load(cached_json) # from Rails.cache/redis/....
-
.locate(path = GraphWeaver.schema_path) ⇒ Object
locate_path, loaded.
-
.locate_path(path = GraphWeaver.schema_path) ⇒ Object
The conventional schema dump, whatever its format: schema_path or the first sibling extension that exists.
-
.provenance(path) ⇒ Object
The provenance recorded in a dump (=> ..., "introspected_at" => ...), whichever format holds it; nil for local/unannotated dumps.
-
.stale?(path, transport: nil) ⇒ Boolean
Re-introspect a dump's source and compare — true when the server has drifted from what's on disk.
-
.strip_federation(sdl) ⇒ Object
Drop the composition machinery from supergraph SDL: the synthetic join__/link__ type and directive definitions, and every @join__*/@link application on the types that remain.
Class Method Details
.federation_sdl?(sdl) ⇒ Boolean
A composed Fed2 supergraph is marked by @join__* directives (every merged type carries them); a plain schema has none.
54 55 56 |
# File 'lib/graph_weaver/schema_loader.rb', line 54 def self.federation_sdl?(sdl) sdl.match?(/@join__\w/) end |
.introspect(transport, cache: nil, ttl: nil) ⇒ Object
Run the standard introspection query through a transport and build a schema from the result:
transport = GraphWeaver::Transport::HTTP.new(url, headers: { ... })
schema = GraphWeaver::SchemaLoader.introspect(transport)
Introspecting a large API takes seconds, so cache: dumps the schema to a file and reuses it until ttl: seconds elapse (no ttl = until the file is deleted). cache: takes
- true — GraphWeaver.schema_path, the file the generation workflow reads (its extension picks the format)
- a path — the extension picks the format: .json is the verbatim introspection result, .graphql/.gql is SDL (human-readable, PR-reviewable diffs); both load back identically
- :json / :graphql / :gql — GraphWeaver.schema_path's location, in that format Reading is format-agnostic: any fresh sibling dump counts, whatever its format — an existing schema.graphql is reused rather than re-introspecting to write schema.json. GraphQL has no standard schema-version signal to invalidate on — a stale cache surfaces as server-side validation errors (see QueryError#schema_stale?), so pick a ttl that matches how fast the API moves, or delete the file.
To cache anywhere else (Rails.cache, redis, ...), serialize the schema itself — schemas round-trip through their introspection JSON:
json = Rails.cache.fetch("gh_schema", expires_in: 12.hours) do
GraphWeaver::SchemaLoader.introspect(transport).to_json
end
schema = GraphWeaver::SchemaLoader.load(json)
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/graph_weaver/schema_loader.rb', line 233 def self.introspect(transport, cache: nil, ttl: nil) cache = cache_path(cache) if cache # reuse whatever fresh dump is present, regardless of format — # don't re-introspect to write schema.json when a usable # schema.graphql already sits there existing = cache_candidates(cache).find { |candidate| fresh?(candidate, ttl) } if existing GraphWeaver.log(:info) { "schema cache hit: #{existing}#{" (ttl #{ttl}s)" if ttl}" } return load(existing) end GraphWeaver.log(:info) { "schema cache miss: #{cache}" } end result = GraphWeaver.log_timed(:info, "introspected #{transport.respond_to?(:url) ? transport.url : transport.class}") do transport.execute(GraphQL::Introspection.query, variables: {}).to_h end if (errors = result["errors"]) raise GraphWeaver::Error, "introspection failed: #{errors.inspect}" end schema = GraphQL::Schema.from_introspection(result) if cache FileUtils.mkdir_p(File.dirname(cache)) # the extension picks the format: .json is the verbatim wire # artifact; .graphql/.gql is SDL — human-readable, PR-reviewable # diffs (both generate byte-identical code) = stamp(transport) content = if cache.end_with?(".json") JSON.generate( ? result.merge("graph_weaver" => ) : result) else header = && "# graph_weaver: #{JSON.generate()}\n\n" "#{header}#{schema.to_definition}" end File.write(cache, content) GraphWeaver.log(:info) { "wrote schema cache: #{cache} (#{content.bytesize} bytes)" } end schema end |
.load(source) ⇒ Object
Accepts, and detects:
- a Hash (a parsed introspection result)
- a file path — .json (introspection) or .graphql/.gql (SDL)
- raw content — introspection JSON (starts with "{") or SDL
so a cache round-trip is symmetrical with introspect: SchemaLoader.load(cached_json) # from Rails.cache/redis/...
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/graph_weaver/schema_loader.rb', line 19 def self.load(source) return GraphQL::Schema.from_introspection(source) if source.is_a?(Hash) if source.lstrip.start_with?("{") # introspection JSON content GraphQL::Schema.from_introspection(JSON.parse(source)) elsif source.include?("\n") # multi-line: SDL content unless source.match?(/^\s*(schema|type|interface|union|enum|scalar|directive|input|")/) raise ArgumentError, "unsupported schema content: #{source.lstrip[0, 80].inspect}" end build_sdl(source) else # a file path case File.extname(source) when ".json" GraphQL::Schema.from_introspection(JSON.parse(File.read(source))) when ".graphql", ".gql" build_sdl(File.read(source)) else raise ArgumentError, "unsupported schema format: #{source}" end end end |
.locate(path = GraphWeaver.schema_path) ⇒ Object
locate_path, loaded.
284 285 286 287 |
# File 'lib/graph_weaver/schema_loader.rb', line 284 def self.locate(path = GraphWeaver.schema_path) found = locate_path(path) found && load(found) end |
.locate_path(path = GraphWeaver.schema_path) ⇒ Object
The conventional schema dump, whatever its format: schema_path or the first sibling extension that exists. nil when none is on disk.
279 280 281 |
# File 'lib/graph_weaver/schema_loader.rb', line 279 def self.locate_path(path = GraphWeaver.schema_path) cache_candidates(path).find { |candidate| File.exist?(candidate) } end |
.provenance(path) ⇒ Object
The provenance recorded in a dump (=> ..., "introspected_at" => ...), whichever format holds it; nil for local/unannotated dumps.
291 292 293 294 295 296 297 298 |
# File 'lib/graph_weaver/schema_loader.rb', line 291 def self.provenance(path) content = File.read(path) if path.end_with?(".json") JSON.parse(content)["graph_weaver"] elsif ( = content[/\A# graph_weaver: (\{.*\})$/, 1]) JSON.parse() end end |
.stale?(path, transport: nil) ⇒ Boolean
Re-introspect a dump's source and compare — true when the server has
drifted from what's on disk. transport: overrides the transport (auth
etc); by default one is built from the dump's recorded url. Wired up
as rake graph_weaver:schema:verify / :refresh.
304 305 306 307 308 309 |
# File 'lib/graph_weaver/schema_loader.rb', line 304 def self.stale?(path, transport: nil) transport ||= source_transport(path) fresh = introspect(transport) fresh.to_definition != load(path).to_definition end |
.strip_federation(sdl) ⇒ Object
Drop the composition machinery from supergraph SDL: the synthetic join__/link__ type and directive definitions, and every @join__/@link application on the types that remain. What's left is the merged graph's ordinary type shapes — exactly what codegen reads. Parsing is lenient (it's schema building that rejects the join directives), so we parse, filter the AST, and reprint clean SDL for from_definition — no graphql-ruby monkeypatch and no join__ leaking into schema.types.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/graph_weaver/schema_loader.rb', line 81 def self.strip_federation(sdl) doc = GraphQL.parse(sdl) defs = remove_inaccessible(doc.definitions) .reject { |defn| federation_definition?(defn) } .map { |defn| strip_federation_directives(defn) } if defs.none? { |d| d.is_a?(GraphQL::Language::Nodes::ObjectTypeDefinition) } raise GraphWeaver::Error, "supergraph has no object types left after stripping the federation machinery — " \ "is the whole schema behind @inaccessible?" end GraphQL::Language::Nodes::Document.new(definitions: defs).to_query_string end |