Skip to content

Add DSL-only mode: to_query_string + spread#115

Merged
dblock merged 14 commits into
ashkan18:masterfrom
rellampec:feature/dsl-mode-only-option
Jun 11, 2026
Merged

Add DSL-only mode: to_query_string + spread#115
dblock merged 14 commits into
ashkan18:masterfrom
rellampec:feature/dsl-mode-only-option

Conversation

@rellampec

@rellampec rellampec commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Add DSL-only mode: to_query_string + spread

Addresses enhancement proposal: Decouple DSL from GraphQL::Client logic

What

Two new methods for building GraphQL query strings from the DSL without triggering
schema loading, HTTP, or graphql-client validation.

Client#to_query_string(&block) — serializes a DSL block to a String and
returns it. The string can be fed back to client.execute for fragment-free queries,
or sent with your own HTTP client to replace graphql-client entirely.

Query#spread(fragment_name) — emits ...FragmentName in the query string.
Cleaner alternative to the ___Const triple-underscore convention.

Why

Framework layers that need to inspect, log, or augment the query string before
sending (e.g. appending fragment definitions, custom connection pooling) need the
serialized string before graphql-client's validation runs. This also opens a fully
external HTTP path.

Changes

  • lib/graphlient/client.rbto_query_string
  • lib/graphlient/query.rbspread
  • spec/graphlient/client_dsl_spec.rb — new spec file (7 examples)
  • README.mdBuild Query Strings without Validation + Fragment Spreads sections

CI fixes included

  • WebMock stub_request.to_rack replaces Faraday::Adapter::Rack for cross-Ruby-version reliability
  • Regex match on TCP error message (Windows vs Linux format difference)
  • Ruby 3.4 added to CI matrix; mutex_m added to Gemfile (removed from stdlib in 3.4)
  • .gitignore updated for .vscode/, .idea/

Checklist

  • 73 examples, 0 failures
  • Rubocop clean
  • CI: Ruby 2.7 – 3.4 green
  • No breaking changes

rellampec added 11 commits June 5, 2026 09:32
- Client#to_query_string(**kargs, &block): builds full query document from
  DSL block and returns a String without touching Faraday or HTTP.
- Query#spread(fragment_name): emits ...FragmentName in the query string,
  replacing the deprecated ___Const graphql-client convention.
- spec_helper.rb: rescue LoadError on byebug require (Windows Ruby 3.2).
- CLAUDE.md: AI agent instructions for this fork.
static_client_query_spec: add schema_path: 'spec/support/fixtures/invoice_api.json'
  The client was fetching the schema from the live URL at module load time
  (before WebMock is active), causing 403 in CI. Using a local schema file
  avoids the network call and matches the pattern in webmock_client_query_spec.

client_dsl_spec: nodes { } → nodes do end (Style/BlockDelimiters)

query.rb: add rubocop:disable Metrics/ClassLength
  Class grew past 100 lines after DSL additions (spread, fragment resolution).

spec_helper.rb: add rubocop:disable Lint/HandleExceptions on rescue LoadError
  The empty rescue is intentional — byebug unavailable on some platforms.

Note: faraday_adapter_spec.rb:147 fails locally on Windows (TCP error message
format differs from Linux) but passes in CI on ubuntu-latest. Pre-existing.
On Linux, Errno::ECONNREFUSED.new(msg).message prepends 'Connection refused - '.
On Windows, the OS prepends a different string. Use a regex that matches the
core error_message content rather than the exact platform-specific prefix.
AI agent instructions are local-only and not relevant to the upstream repo.
CLAUDE.md is removed from git tracking but kept in the working directory.
Added to .gitignore so future sessions don't accidentally commit it.
Root cause: on Ruby 3.1, WebMock's Faraday middleware intercepts requests
before they reach Faraday::Adapter::Rack, returning 403 for any URL without
a stub. This caused 25 CI failures on that Ruby version.

spec/support/context/dummy_client.rb:
  Replace Faraday::Adapter::Rack with stub_request.to_rack(app).
  WebMock routes both schema introspection and query execution through
  the Sinatra dummy app at the WebMock level (before Faraday dispatch).
  Removed schema_path: so graphql-client gets the full live schema
  (including test fields: executionErrorInvoice, partialSuccess etc.).

spec/graphlient/static_client_query_spec.rb:
  Same to_rack approach for execution tests (schema_path kept here since
  the module constants are created at file-load time, before any before block).
  Removed Faraday::Adapter::Rack from the static client constructor.

spec/graphlient/client_schema_spec.rb:
  Use include instead of eq for error message — Faraday 2.x appends
  the URL to the message; include matches on both old and new formats.

73 examples, 0 failures
spec/support/context/dummy_client.rb:
spec/graphlient/static_client_query_spec.rb:
  Replace em-dashes (--) with ASCII semicolons/colons in comments.
  Style/AsciiComments cop requires pure ASCII.

Gemfile:
  Add mutex_m gem -- activesupport 5.x requires mutex_m which was removed
  from Ruby's standard library in Ruby 3.4. Adding it explicitly silences
  the warning and fixes the LoadError on Ruby 3.4.

.github/workflows/ci.yml:
  Add Ruby 3.4 to the required CI matrix (was previously only in ruby-head
  which is allowed to fail). Ruby 3.4 is now released and should be tested.
New sections:
  'Build Query Strings without Validation' -- explains the serialization-only
    path (no schema load, no graphql-client validation, no HTTP), shows the
    basic usage, clarifies when the string can be fed back to client.execute
    (fragment-free queries only), explains why fragment spreads cannot go back
    through the gem (graphql-client's constant-based fragment model), and
    shows how to_query_string is the escape hatch for replacing graphql-client
    with your own HTTP transport entirely.

  'Fragment Spreads' -- documents Query#spread as the modern alternative to
    the triple-underscore convention; covers basic usage and multiple spreads.
@rellampec

Copy link
Copy Markdown
Collaborator Author

The Danger job is failing due to an expired token in the workflow — unrelated to this PR's changes.

rellampec added a commit to rellampec/graphlient that referenced this pull request Jun 10, 2026
rellampec added a commit to rellampec/graphlient that referenced this pull request Jun 10, 2026
- All entries single-line matching file convention (no continuation lines)
- ashkan18#115 entries restored to their original order and not modified
- ashkan18#116 prefix added to all directives/fragments/scalars/CI entries
@dblock

dblock commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

I fixed CI / Danger in #117, rebase?

@dblock dblock left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some cosmetics for now, still have to read the details in the PR, also @ashkan18 and @yuki24 take a look?

Comment thread lib/graphlient/query.rb Outdated
@@ -1,5 +1,5 @@
module Graphlient
class Query
class Query # rubocop:disable Metrics/ClassLength

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run rubocop -a ; rubocop --auto-gen-config.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dblock there's a refactor on next MR that resolves this rubocop violation: #116

Wanted to keep the current MR clear and clean of refactoring, as it resolves the major blocker.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ran the command

Comment thread CHANGELOG.md Outdated
* Your contribution here.
* [#113](https://github.com/ashkan18/graphlient/pull/113): Fix CI builds - [@yuki24](https://github.com/yuki24).
* [#112](https://github.com/ashkan18/graphlient/pull/112): Update graphql-client github repository links in README - [@th1988](https://github.com/th1988).
* [#115](https://github.com/ashkan18/graphlient/pull/115): `Client#to_query_string(**kargs, &block)`: builds the full GraphQL query document from the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it a 1-liner. Add to README if you need something longer.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, will work on this asap

…cop to ~> 1.0

Removed two inline disable comments (Metrics/ClassLength on Query, Lint/HandleExceptions in
spec_helper) in favour of the rubocop_todo.yml approach suggested by upstream maintainer.
Bumped rubocop dev dependency from pinned 0.56.0 to ~> 1.0 (required for Ruby 3.2 compatibility
— Psych.safe_load signature changed). TargetRubyVersion kept at 2.3 to reflect the gem's
minimum supported Ruby. Ran rubocop -a (11 auto-corrections) then --auto-gen-config to
regenerate .rubocop_todo.yml with current cop names (Metrics/LineLength renamed to
Layout/LineLength, Style/MethodMissingSuper removed, etc.).
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@rellampec
rellampec requested a review from dblock June 11, 2026 15:44

@dblock dblock left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! Can we lock rubocop to a specific version in a future PR please, see my comment.

module Graphlient::Client::Spec
# schema_path avoids an HTTP schema fetch at file-load time.
# No Rack adapter needed here; parsing is local and execution is handled
# by the stub_request below.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

Comment thread .gitignore
.rvmrc

# AI agent instructions — local only, not for the upstream repo
CLAUDE.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should commit an AGENTS.md!

Comment thread Gemfile
group :development do
gem 'byebug', platform: :ruby
gem 'rubocop', '0.56.0'
gem 'rubocop', '~> 1.0'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a gem and we don't check in Gemfile.lock, this should be locked to a specific version. Otherwise future CI runs will keep breaking without changes.

@dblock
dblock merged commit 6a0998c into ashkan18:master Jun 11, 2026
12 checks passed
dblock pushed a commit that referenced this pull request Jun 20, 2026
…#116)

* feat: add Client#to_query_string and Query#spread

- Client#to_query_string(**kargs, &block): builds full query document from
  DSL block and returns a String without touching Faraday or HTTP.
- Query#spread(fragment_name): emits ...FragmentName in the query string,
  replacing the deprecated ___Const graphql-client convention.
- spec_helper.rb: rescue LoadError on byebug require (Windows Ruby 3.2).
- CLAUDE.md: AI agent instructions for this fork.

* docs: move changelog entries to (Next) section, remove org-name header

* fix: resolve CI failures — schema_path + rubocop offenses

static_client_query_spec: add schema_path: 'spec/support/fixtures/invoice_api.json'
  The client was fetching the schema from the live URL at module load time
  (before WebMock is active), causing 403 in CI. Using a local schema file
  avoids the network call and matches the pattern in webmock_client_query_spec.

client_dsl_spec: nodes { } → nodes do end (Style/BlockDelimiters)

query.rb: add rubocop:disable Metrics/ClassLength
  Class grew past 100 lines after DSL additions (spread, fragment resolution).

spec_helper.rb: add rubocop:disable Lint/HandleExceptions on rescue LoadError
  The empty rescue is intentional — byebug unavailable on some platforms.

Note: faraday_adapter_spec.rb:147 fails locally on Windows (TCP error message
format differs from Linux) but passes in CI on ubuntu-latest. Pre-existing.

* fix: make TCP error spec cross-platform (Windows/Linux)

On Linux, Errno::ECONNREFUSED.new(msg).message prepends 'Connection refused - '.
On Windows, the OS prepends a different string. Use a regex that matches the
core error_message content rather than the exact platform-specific prefix.

* chore: gitignore .vscode/ and .idea/ editor directories

* wip: directive DSL spec — pending implementation

Adds a spec for directive DSL syntax:
  feeInCents _skip(if: :skip_fee)

This should translate to @Skip(if: \) in the GraphQL query.
Implementation deferred — tracked in this branch (directives-dsl-support).
Base: feature/dsl-mode-only-option.

* docs+spec: spread/fragment/directive README + mark directive spec pending

README.md:
  'Fragment Spreads in the DSL' — documents spread :Name DSL method
    (already implemented in feature/dsl-mode-only-option base)
  'Fragment Spreads — inline definition' — shows the proposed fragment DSL
    (inline fragment(:Name, on: :Type) block, scoped to query call)
    marked 'coming in a future release'
  'Directives in the DSL' — shows _skip(if:) → @Skip(if: \) proposal
    marked 'coming in a future release', anchored to this branch

spec/graphlient/client_query_spec.rb:
  Directive test marked pending with explanation — shows intended behaviour
  without causing a CI failure while the implementation is outstanding.
  74 examples, 0 failures, 1 pending.

* feat: directive DSL, inline fragments, fragment definitions, custom scalars

Query refactored into Query (thin shell) + Query::Serializer (composed):
  query/directive.rb              Directive value object — returned by _name(args),
                                  placed AFTER field name in output (correct order)
  query/serializer.rb             Main composition; includes all concern modules
  query/serializer/scalars.rb     SCALAR_TYPES extraction + Serializer.scalar(:sym, Type)
                                  class-level custom scalar registration
  query/serializer/arguments.rb   hash_arg/args_str/variable_string; field_args/directive_args
                                  split; variable_string bug fixed (lambda case)
  query/serializer/evaluator.rb   evaluate() via instance_eval
  query/serializer/fragments.rb   ___Const triple-underscore resolution
  query/serializer/directives.rb  directive?() predicate; method_missing returns Directive
  Removed: query/transcript.rb    (empty stub — deleted)

New DSL surface:
  _skip(if: :x)                  → @Skip(if: $x)     (any _name directive)
  _include(if: :x)               → @include(if: $x)
  feeInCents _skip(if: :x)       → feeInCents @Skip(if: $x)
  spread :F, _skip(if: :x)       → ...F @Skip(if: $x)
  on(:Type) { fields }           → ... on Type { fields }
  on(:Type, _skip(if: :x)) { }  → ... on Type @Skip(if: $x) { }
  fragment(:F, on: :T) { }       → fragment F on T { } (appended after main query)

Custom scalar registration (client config block):
  client = Graphlient::Client.new(url) { |c| c.scalar :date, 'Date' }
  query(created_after: :date) { } → query($created_after: Date)

Specs: query_dsl_spec.rb — 16 examples covering all new features
       client_query_spec.rb — directive test enabled (was pending, now live)
       90 examples total, 0 failures

* docs+specs: update README + add client-level DSL coverage

README.md:
  Fragment Spreads and Definitions — remove 'proposed/coming' language,
    document fragment(:Name, on: :Type) as shipped, add spread+directive example
  Inline Fragments — new section: on(:Type) { } and on(:Type, _skip) { }
  Directives in the DSL — rewrite from 'coming soon' to full reference:
    on a field, on a spread, on an inline fragment, multiple, no-arg
  Custom Scalar Types — new section: c.scalar :date, 'Date' client config
  Table of contents updated to match all new sections

spec/graphlient/client_dsl_spec.rb:
  to_query_string with directive — verifies @Skip in output
  to_query_string with inline fragment — verifies ... on Type
  to_query_string with fragment definition — verifies assembled output
  scalar registration via client block — verifies c.scalar :date, 'Date'

94 examples, 0 failures

* chore: untrack CLAUDE.md + add to .gitignore

AI agent instructions are local-only and not relevant to the upstream repo.
CLAUDE.md is removed from git tracking but kept in the working directory.
Added to .gitignore so future sessions don't accidentally commit it.

* fix: use WebMock to_rack for Ruby 3.1 CI compatibility

Root cause: on Ruby 3.1, WebMock's Faraday middleware intercepts requests
before they reach Faraday::Adapter::Rack, returning 403 for any URL without
a stub. This caused 25 CI failures on that Ruby version.

spec/support/context/dummy_client.rb:
  Replace Faraday::Adapter::Rack with stub_request.to_rack(app).
  WebMock routes both schema introspection and query execution through
  the Sinatra dummy app at the WebMock level (before Faraday dispatch).
  Removed schema_path: so graphql-client gets the full live schema
  (including test fields: executionErrorInvoice, partialSuccess etc.).

spec/graphlient/static_client_query_spec.rb:
  Same to_rack approach for execution tests (schema_path kept here since
  the module constants are created at file-load time, before any before block).
  Removed Faraday::Adapter::Rack from the static client constructor.

spec/graphlient/client_schema_spec.rb:
  Use include instead of eq for error message — Faraday 2.x appends
  the URL to the message; include matches on both old and new formats.

73 examples, 0 failures

* fix: rubocop AsciiComments + Ruby 3.4 mutex_m + add 3.4 to CI matrix

spec/support/context/dummy_client.rb:
spec/graphlient/static_client_query_spec.rb:
  Replace em-dashes (--) with ASCII semicolons/colons in comments.
  Style/AsciiComments cop requires pure ASCII.

Gemfile:
  Add mutex_m gem -- activesupport 5.x requires mutex_m which was removed
  from Ruby's standard library in Ruby 3.4. Adding it explicitly silences
  the warning and fixes the LoadError on Ruby 3.4.

.github/workflows/ci.yml:
  Add Ruby 3.4 to the required CI matrix (was previously only in ruby-head
  which is allowed to fail). Ruby 3.4 is now released and should be tested.

* docs: document to_query_string and spread in README

New sections:
  'Build Query Strings without Validation' -- explains the serialization-only
    path (no schema load, no graphql-client validation, no HTTP), shows the
    basic usage, clarifies when the string can be fed back to client.execute
    (fragment-free queries only), explains why fragment spreads cannot go back
    through the gem (graphql-client's constant-based fragment model), and
    shows how to_query_string is the escape hatch for replacing graphql-client
    with your own HTTP transport entirely.

  'Fragment Spreads' -- documents Query#spread as the modern alternative to
    the triple-underscore convention; covers basic usage and multiple spreads.

* docs: update CHANGELOG for directives-dsl-support

Adds entries for: directive DSL (_name convention), on(:Type) inline
fragments, fragment(:Name, on:) definitions, custom scalar registration,
Query::Serializer refactor, Ruby 3.1/3.4 CI fixes.

* docs: fix CHANGELOG continuation line indentation (4 spaces)

* docs: fix CHANGELOG continuation line indentation (4 spaces)

* docs: add PR #115 reference to CHANGELOG entries

* docs: clean up CHANGELOG for PR #116

- All entries single-line matching file convention (no continuation lines)
- #115 entries restored to their original order and not modified
- #116 prefix added to all directives/fragments/scalars/CI entries

* don't change changelog

* fix: close unclosed code block in Directives section of README

* style: fix rubocop 0.56 offenses on feature/directives-dsl-support

* chore: replace inline rubocop:disable with auto-gen-config, bump rubocop to ~> 1.0

Removed two inline disable comments (Metrics/ClassLength on Query, Lint/HandleExceptions in
spec_helper) in favour of the rubocop_todo.yml approach suggested by upstream maintainer.
Bumped rubocop dev dependency from pinned 0.56.0 to ~> 1.0 (required for Ruby 3.2 compatibility
— Psych.safe_load signature changed). TargetRubyVersion kept at 2.3 to reflect the gem's
minimum supported Ruby. Ran rubocop -a (11 auto-corrections) then --auto-gen-config to
regenerate .rubocop_todo.yml with current cop names (Metrics/LineLength renamed to
Layout/LineLength, Style/MethodMissingSuper removed, etc.).

* Update CHANGELOG.md

* correct order in change kig

* fix: rubocop offenses after merge of feature/dsl-mode-only-option

* chore: pin rubocop to 1.87.0 (fixed version, no pessimistic constraint)

* docs: clarify inline vs external-constant fragment field access

* feat: pass allow_named_fragment_access through to graphql-client

* revert: remove allow_named_fragment_access passthrough (flag removed from graphql-client)

* refactor: unify inline fragments under spread(on:) per #116 review

Addresses @dblock's review question on #116 — use a single, consistent verb
instead of a separate `on`. `spread` now covers both GraphQL forms:

  spread :InvoiceFields              # ...InvoiceFields (named fragment spread)
  spread on: :PaidInvoice { ... }    # ... on PaidInvoice { ... } (inline fragment)
  spread on: :Invoice                # ... on Invoice (bare type condition)

The `on:` keyword matches `fragment(name, on:)`, keeping the DSL coherent and
leaving room for a future `spread(:X).skip(...)` chaining form without a breaking
change. The standalone `on` DSL method is removed (introduced in #116, never
released, so no compatibility impact).

Guards: `spread` with neither a name nor `on:` raises; a named spread given a
block raises (named spreads have no selection set). Updated specs, README, and
CHANGELOG accordingly.

* test: require ostruct + mutex_m before loading graphlient

On newer Ruby (3.4+) ostruct and mutex_m are no longer auto-available, so the
test-group deps that rely on them fail to load: graphql 1.13's compatibility
specs reference OpenStruct (NameError at require time), and activesupport 5.x
needs mutex_m. Both gems are already in the test Gemfile group; require them in
spec_helper before graphlient is loaded transitively. Test-only — not added to
the library, which must not depend on test gems.

* style: satisfy rubocop in unified spread

- freeze the SPREAD_* message constants (Style/MutableConstant)
- extract append_inline_fragment / append_named_spread helpers so #spread is
  below the Metrics/PerceivedComplexity threshold
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants