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)
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/graph_weaver/schema_loader.rb', line 205 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.
256 257 258 259 |
# File 'lib/graph_weaver/schema_loader.rb', line 256 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.
251 252 253 |
# File 'lib/graph_weaver/schema_loader.rb', line 251 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.
263 264 265 266 267 268 269 270 |
# File 'lib/graph_weaver/schema_loader.rb', line 263 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.
276 277 278 279 280 281 |
# File 'lib/graph_weaver/schema_loader.rb', line 276 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.
74 75 76 77 78 79 80 |
# File 'lib/graph_weaver/schema_loader.rb', line 74 def self.strip_federation(sdl) doc = GraphQL.parse(sdl) defs = remove_inaccessible(doc.definitions) .reject { |defn| defn.respond_to?(:name) && federation_name?(defn.name) } .map { |defn| strip_federation_directives(defn) } GraphQL::Language::Nodes::Document.new(definitions: defs).to_query_string end |