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
- CACHE_EXTENSIONS =
%w[.json .graphql .gql].freeze
Class Method Summary collapse
-
.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.
Class Method Details
.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)
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/graph_weaver/schema_loader.rb', line 73 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 GraphQL::Schema.from_definition(source) else # a file path case File.extname(source) when ".json" GraphQL::Schema.from_introspection(JSON.parse(File.read(source))) when ".graphql", ".gql" GraphQL::Schema.from_definition(File.read(source)) else raise ArgumentError, "unsupported schema format: #{source}" end end end |
.locate(path = GraphWeaver.schema_path) ⇒ Object
locate_path, loaded.
124 125 126 127 |
# File 'lib/graph_weaver/schema_loader.rb', line 124 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.
119 120 121 |
# File 'lib/graph_weaver/schema_loader.rb', line 119 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.
131 132 133 134 135 136 137 138 |
# File 'lib/graph_weaver/schema_loader.rb', line 131 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.
144 145 146 147 148 149 |
# File 'lib/graph_weaver/schema_loader.rb', line 144 def self.stale?(path, transport: nil) transport ||= source_transport(path) fresh = introspect(transport) fresh.to_definition != load(path).to_definition end |