Class: GraphWeaver::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/graph_weaver/client.rb

Overview

One object tying the whole flow together — transport, schema, and generation:

 github = GraphWeaver.new("https://api.github.com/graphql", auth: token, cache: true)
 github.register_scalar("DateTime", Time, serialize: :iso8601, requires: "time")

 RepoQuery = github.parse("queries/repo.graphql")   # implicit schema + transport
 github.execute!("query { viewer { login } }")      # one-shot

The first argument is a url (a transport is built; the schema comes from introspection on first use, cached per cache:/ttl:) or a schema source — a live schema class (which also executes in-process), or a path/SDL/introspection dump via SchemaLoader. Pass transport: to bring your own transport for a schema source.

Clients are independent: each has its own transport, schema, and scalar registrations, so one app can talk to several GraphQL servers — even ones that disagree about what a "DateTime" is.

Constant Summary collapse

URL =
%r{\Ahttps?://}i

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, auth: nil, headers: {}, retries: false, transport: nil, cache: nil, ttl: nil, &middleware) ⇒ Client

Returns a new instance of Client.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/graph_weaver/client.rb', line 32

def initialize(source, auth: nil, headers: {}, retries: false, transport: nil, cache: nil, ttl: nil, &middleware)
  if source.is_a?(String) && source.match?(URL)
    raise ArgumentError, "pass a url or transport:, not both" if transport

    @transport = wrap_retries(build_transport(source, auth:, headers:, &middleware), retries)
  else
    if auth || middleware || retries != false
      raise ArgumentError, "auth:/retries:/middleware apply to a url — got a schema source"
    end
    if cache || ttl
      # a schema source never introspects, so a cache would silently no-op
      raise ArgumentError, "cache:/ttl: apply to url introspection — got a schema source"
    end

    # a live schema class doubles as an in-process transport; a loaded
    # dump has no resolvers, so it is type information only
    @schema = source.is_a?(Module) ? source : GraphWeaver::SchemaLoader.load(source)
    @transport = transport || (source if source.is_a?(Module))
  end

  @cache = cache
  @ttl = ttl
  @scalars = {}
  @enums = {}
  @types = {}
end

Instance Attribute Details

#transportObject (readonly)

The transport queries run through: a url-built transport, an explicit transport:, or the live schema class executing in-process. Clients are self-contained — the app default never leaks in; nil for schema-dump clients (type information only).



63
64
65
# File 'lib/graph_weaver/client.rb', line 63

def transport
  @transport
end

Instance Method Details

#execute(query, **variables) ⇒ Object

One-shot dynamic execution — parse + execute, returning the typed Response envelope (execute! returns the result or raises). Variables are plain kwargs, exactly as on a generated module; graphql-cased string keys work too.



144
145
146
147
148
# File 'lib/graph_weaver/client.rb', line 144

def execute(query, **variables)
  mod = parse(query)
  kwargs = variables.to_h { |key, value| [GraphWeaver::Inflect.underscore(key.to_s).to_sym, value] }
  mod.execute(transport!, **kwargs)
end

#execute!(query, **variables) ⇒ Object



150
151
152
# File 'lib/graph_weaver/client.rb', line 150

def execute!(query, **variables)
  execute(query, **variables).data!
end

#load_queries!(dir = nil, namespace: Object) ⇒ Object

Parse every .graphql query in a directory into typed modules, named like generation would name them — the no-build-step analog of generate! + load_generated!:

 github.load_queries!                        # queries/person.graphql => ::PersonQuery
 github.load_queries!(namespace: Github)     # => Github::PersonQuery

Reloadable (constants are replaced), so it suits consoles and dev. Returns the modules.



130
131
132
133
134
135
136
137
138
# File 'lib/graph_weaver/client.rb', line 130

def load_queries!(dir = nil, namespace: Object)
  dirs = dir ? [dir] : GraphWeaver.queries_paths
  dirs.flat_map { |d| Dir[File.join(d, "*.graphql")].sort }.map do |path|
    name = "#{GraphWeaver::Inflect.camelize(File.basename(path, ".graphql"))}Query"
    namespace.send(:remove_const, name) if namespace.const_defined?(name, false)
    GraphWeaver.log(:info) { "loaded #{name} from #{path}" }
    namespace.const_set(name, parse(path))
  end
end

#parse(query, name: nil) ⇒ Object

Parse a query (a .graphql path or raw string) into a typed module bound to this client's schema, scalars, enums, helpers, and transport (including a live schema class executing in-process — the module came from this client, so it runs against it; pass a client per call to override, e.g. with a fake).



116
117
118
119
# File 'lib/graph_weaver/client.rb', line 116

def parse(query, name: nil)
  GraphWeaver.parse(schema:, query:, name:, client: transport,
    scalars: @scalars, enums: @enums, types: @types)
end

#register_enum(graphql_name, type, map: nil, fallback: nil, requires: nil) ⇒ Object

Client-scoped enum mapping: this client's generated code speaks your T::Enum for the named GraphQL enum (see Codegen::EnumType — inference by name, map: for renames, fallback: to absorb unknown wire values).



90
91
92
93
94
# File 'lib/graph_weaver/client.rb', line 90

def register_enum(graphql_name, type, map: nil, fallback: nil, requires: nil)
  validate_registration!("enum", graphql_name.to_s)
  @enums[graphql_name.to_s] =
    GraphWeaver::Codegen::EnumType.new(graphql_name, type, map:, fallback:, requires:)
end

#register_enums(mappings) ⇒ Object

Bulk, inference-only form: register_enums("Species" => PetKind, ...)



97
98
99
# File 'lib/graph_weaver/client.rb', line 97

def register_enums(mappings)
  mappings.each { |graphql_name, type| register_enum(graphql_name, type) }
end

#register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil) ⇒ Object

Client-scoped scalar registration: consulted before the global registry when this client generates code, so two clients can map the same scalar name onto different Ruby types. Same signature as GraphWeaver.register_scalar.



81
82
83
84
85
# File 'lib/graph_weaver/client.rb', line 81

def register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil)
  validate_registration!("scalar", graphql_name.to_s)
  @scalars[graphql_name.to_s] =
    GraphWeaver::Codegen::ScalarType.new(graphql_name, type, cast:, serialize:, requires:, coerce:)
end

#register_type(graphql_name, *mixins, requires: nil, &block) ⇒ Object

Client-scoped type helpers: include app-owned modules into every struct this client generates from the named GraphQL type — pass modules, or a block to build one inline. Additive with global registrations (see GraphWeaver.register_type).



105
106
107
108
109
# File 'lib/graph_weaver/client.rb', line 105

def register_type(graphql_name, *mixins, requires: nil, &block)
  validate_registration!("type", graphql_name.to_s)
  entry = @types[graphql_name.to_s] ||= { mixins: [], requires: [] }
  GraphWeaver::Codegen.add_type_helpers(entry, graphql_name, mixins, requires, block)
end

#schemaObject

The schema, introspecting through the transport on first use (cached per the client's cache:/ttl:) unless one was given up front.



73
74
75
# File 'lib/graph_weaver/client.rb', line 73

def schema
  @schema ||= GraphWeaver::SchemaLoader.introspect(transport!, cache: @cache, ttl: @ttl)
end

#transport!Object

transport, when this client must be able to execute



66
67
68
69
# File 'lib/graph_weaver/client.rb', line 66

def transport!
  transport or raise GraphWeaver::Error,
    "this client has no transport (built from a schema dump) — pass a url or transport:"
end