Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cc010ad
feat: add Client#to_query_string and Query#spread
rellampec Jun 4, 2026
8a56299
docs: move changelog entries to (Next) section, remove org-name header
rellampec Jun 4, 2026
896c73d
fix: resolve CI failures — schema_path + rubocop offenses
rellampec Jun 9, 2026
d81578e
fix: make TCP error spec cross-platform (Windows/Linux)
rellampec Jun 9, 2026
c0225fc
chore: gitignore .vscode/ and .idea/ editor directories
rellampec Jun 9, 2026
9a665d4
wip: directive DSL spec — pending implementation
rellampec Jun 9, 2026
964b659
docs+spec: spread/fragment/directive README + mark directive spec pen…
rellampec Jun 9, 2026
341f9fc
feat: directive DSL, inline fragments, fragment definitions, custom s…
rellampec Jun 9, 2026
4bb4e74
docs+specs: update README + add client-level DSL coverage
rellampec Jun 9, 2026
d1ad529
chore: untrack CLAUDE.md + add to .gitignore
rellampec Jun 9, 2026
2ce05e5
chore: merge .gitignore CLAUDE.md fix from feature branch
rellampec Jun 9, 2026
1b6b000
fix: use WebMock to_rack for Ruby 3.1 CI compatibility
rellampec Jun 9, 2026
ad0f14c
fix: rubocop AsciiComments + Ruby 3.4 mutex_m + add 3.4 to CI matrix
rellampec Jun 10, 2026
5712a3f
docs: document to_query_string and spread in README
rellampec Jun 10, 2026
c9fc14e
chore: merge feature/dsl-mode-only-option into directives-dsl-support
rellampec Jun 10, 2026
3a30761
docs: update CHANGELOG for directives-dsl-support
rellampec Jun 10, 2026
62b5aca
docs: fix CHANGELOG continuation line indentation (4 spaces)
rellampec Jun 10, 2026
7e938a2
docs: fix CHANGELOG continuation line indentation (4 spaces)
rellampec Jun 10, 2026
112a99d
docs: add PR #115 reference to CHANGELOG entries
rellampec Jun 10, 2026
a7628c3
chore: sync with feature/dsl-mode-only-option (PR #115 CHANGELOG links)
rellampec Jun 10, 2026
34267ba
docs: clean up CHANGELOG for PR #116
rellampec Jun 10, 2026
98a8812
don't change changelog
rellampec Jun 10, 2026
836fa42
fix: close unclosed code block in Directives section of README
rellampec Jun 10, 2026
ee3c55d
style: fix rubocop 0.56 offenses on feature/directives-dsl-support
rellampec Jun 10, 2026
1bb7e26
chore: replace inline rubocop:disable with auto-gen-config, bump rubo…
rellampec Jun 11, 2026
3129ba2
Merge branch 'master' into feature/dsl-mode-only-option
rellampec Jun 11, 2026
1e9d77b
Update CHANGELOG.md
rellampec Jun 11, 2026
f6109ae
Merge branch 'master' into feature/directives-dsl-support
rellampec Jun 11, 2026
65707b2
Merge branch 'feature/dsl-mode-only-option' into feature/directives-d…
rellampec Jun 11, 2026
edae1d8
correct order in change kig
rellampec Jun 11, 2026
fe45f97
fix: rubocop offenses after merge of feature/dsl-mode-only-option
rellampec Jun 11, 2026
517edc0
Merge branch 'master' into feature/directives-dsl-support
rellampec Jun 11, 2026
f42c699
chore: pin rubocop to 1.87.0 (fixed version, no pessimistic constraint)
rellampec Jun 11, 2026
efa67cd
docs: clarify inline vs external-constant fragment field access
rellampec Jun 12, 2026
688264f
feat: pass allow_named_fragment_access through to graphql-client
rellampec Jun 12, 2026
e2f0b48
revert: remove allow_named_fragment_access passthrough (flag removed …
rellampec Jun 13, 2026
c3ec984
refactor: unify inline fragments under spread(on:) per #116 review
rellampec Jun 15, 2026
192bb21
test: require ostruct + mutex_m before loading graphlient
rellampec Jun 15, 2026
b621655
style: satisfy rubocop in unified spread
rellampec Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
### (Next)
* Your contribution here.
* [#117](https://github.com/ashkan18/graphlient/pull/117): Migrate Danger to use `danger-pr-comment` reusable workflow, add Ruby 4.0 to CI, fix tests on Ruby 3.4+ and 4.0 - [@dblock](https://github.com/dblock).
* [#116](https://github.com/ashkan18/graphlient/pull/116): Directive DSL — `_skip(if: :x)` → `@skip(if: $x)`, `_include(if: :x)` → `@include(if: $x)`. Any `_name` method is treated as a `@name` directive; applies to fields, spreads, and inline fragments - [@rellampec](https://github.com/rellampec).
* [#116](https://github.com/ashkan18/graphlient/pull/116): `spread(on: :Type, *directives, &block)`: inline fragment DSL — `spread(on: :PaidInvoice) { amountPaid }` → `... on PaidInvoice { amountPaid }`. `spread` is the single verb for both named fragment spreads (`spread :Name`) and inline fragments (`spread on: :Type`), using the same `on:` keyword as `fragment(name, on:)` - [@rellampec](https://github.com/rellampec).
* [#116](https://github.com/ashkan18/graphlient/pull/116): `fragment(:Name, on: :Type, &block)`: inline fragment definition appended to the query document automatically; scoped to the query call, no global registry - [@rellampec](https://github.com/rellampec).
* [#116](https://github.com/ashkan18/graphlient/pull/116): Custom scalar registration — `c.scalar :date, 'Date'` extends the built-in scalar set (`:int`, `:float`, `:string`, `:boolean`) with API-specific types - [@rellampec](https://github.com/rellampec).
* [#116](https://github.com/ashkan18/graphlient/pull/116): `Query` refactored into `Query::Serializer` composed from concern modules (`Scalars`, `Arguments`, `Evaluator`, `Fragments`, `Directives`). Public API unchanged - [@rellampec](https://github.com/rellampec).
* [#116](https://github.com/ashkan18/graphlient/pull/116): CI: Ruby 3.4 added to test matrix; Ruby 3.1 WebMock `to_rack` fix; `mutex_m` gem added for Ruby 3.4 stdlib removal - [@rellampec](https://github.com/rellampec).
* [#115](https://github.com/ashkan18/graphlient/pull/115): `Client#to_query_string(**kargs, &block)`: builds the full GraphQL query document from the DSL block and returns a `String` without touching the schema or Faraday HTTP adapter. Enables DSL-only mode where HTTP dispatch is handled externally - [@rellampec](https://github.com/rellampec).
* [#115](https://github.com/ashkan18/graphlient/pull/115): `Query#spread(fragment_name)`: emits `...FragmentName` into the query string as a named fragment spread. Alternative to the `___Const` convention for use cases where `graphql-client` constant parsing is not available - [@rellampec](https://github.com/rellampec).
* [#115](https://github.com/ashkan18/graphlient/pull/115): `spec/spec_helper.rb`: rescue `LoadError` on `byebug` require so specs run on platforms where byebug is not installable (e.g. Windows with Ruby 3.2) - [@rellampec](https://github.com/rellampec).
* [#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 DSL block and returns a `String` without touching the schema or Faraday HTTP adapter. Enables DSL-only mode where HTTP dispatch is handled externally - [@rellampec](https://github.com/rellampec).
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ end

group :development do
gem 'byebug', platform: :ruby
gem 'rubocop', '~> 1.0'
gem 'rubocop', '1.87.0'
end

group :test do
Expand Down
219 changes: 201 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ A friendlier Ruby client for consuming GraphQL-based APIs. Built on top of your
- [Build Query Strings without Validation](#build-query-strings-without-validation)
- [Dynamic vs. Static Queries](#dynamic-vs-static-queries)
- [Generate Queries with Graphlient::Query](#generate-queries-with-graphlientquery)
- [Fragment Spreads](#fragment-spreads)
- [Fragment Spreads and Definitions in the DSL](#fragment-spreads-and-definitions-in-the-dsl)
- [Inline Fragments in the DSL](#inline-fragments-in-the-dsl)
- [Directives in the DSL](#directives-in-the-dsl)
- [Custom Scalar Types](#custom-scalar-types)
- [Create API Client Classes with Graphlient::Extension::Query](#create-api-client-classes-with-graphlientextensionquery)
- [Swapping the HTTP Stack](#swapping-the-http-stack)
- [Testing with Graphlient and RSpec](#testing-with-graphlient-and-rspec)
Expand Down Expand Up @@ -390,7 +393,87 @@ query.to_s
# "\nquery {\n invoice(id: 10){\n line_items\n }\n }\n"
```

### Use of Fragments
### Fragment Spreads and Definitions in the DSL

Use `spread` to insert a named fragment spread (`...FragmentName`) in a DSL block:

```ruby
client.query do
query do
invoice(id: 10) do
id
spread :InvoiceFields # → ...InvoiceFields
end
end
end
```

Define the fragment body inline with `fragment` — graphlient assembles the complete
query string automatically, no external tooling needed:

```ruby
client.query do
fragment(:InvoiceFields, on: :Invoice) do
id
feeInCents
end

query do
invoice(id: 10) do
spread :InvoiceFields
end
end
end
```

Produces and sends:

```graphql
query {
invoice(id: 10) {
...InvoiceFields
}
}

fragment InvoiceFields on Invoice {
id
feeInCents
}
```

Because the fragment is defined inline in the same query block, graphql-client treats it
as part of the same document — all fragment fields are directly accessible on the response
wrapper with no extra wrapping step:

```ruby
response = client.query do
fragment(:InvoiceFields, on: :Invoice) do
id
feeInCents
end

query do
invoice(id: 10) do
spread :InvoiceFields
end
end
end

response.data.invoice.id # 10
response.data.invoice.fee_in_cents # 20000
```

Multiple fragments are supported. Fragments are scoped to the query call — no global
registry, no cross-contamination between requests.

You can also apply a directive to a spread (see [Directives in the DSL](#directives-in-the-dsl)):

```ruby
spread :InvoiceFields, _skip(if: :skip_invoice)
# → ...InvoiceFields @skip(if: $skip_invoice)
```

### Use of Fragments (graphql-client style)

[Fragments](https://github.com/github-community-projects/graphql-client#defining-queries) should be referred by constant:

Expand Down Expand Up @@ -423,7 +506,12 @@ end
```

The wrapped response only allows access to fields that have been explicitly asked for.
In this example, while `id` has been referenced directly in the main query, `feeInCents` has been spread via fragment and trying to access it in the original wrapped response will throw [`GraphQL::Client::ImplicitlyFetchedFieldError`](https://github.com/github-community-projects/graphql-client/blob/master/guides/implicitly-fetched-field-error.md) (to prevent data leaks between components).
In this example, while `id` has been referenced directly in the main query, `feeInCents`
has been spread via an **external fragment constant** and trying to access it in the
original wrapped response will throw
[`GraphQL::Client::ImplicitlyFetchedFieldError`](https://github.com/github-community-projects/graphql-client/blob/master/guides/implicitly-fetched-field-error.md).
This is graphql-client's component-isolation mechanism: each fragment constant "owns" the
fields it declares, preventing accidental data access across component boundaries.

```ruby
response = client.execute(invoice_query)
Expand All @@ -446,39 +534,134 @@ invoice.fee_in_cents
# 20000
```

### Fragment Spreads
> **Note:** This component-isolation behaviour only applies to external fragment constants
> (the `___` / `__` pattern). Fragments defined inline via the `fragment` DSL in the same
> query block are not subject to this restriction — their fields are accessible directly
> on the operation response (see [Fragment Spreads and Definitions in the DSL](#fragment-spreads-and-definitions-in-the-dsl)).

### Inline Fragments in the DSL

Use `spread` to emit a named fragment spread (`...FragmentName`) inside a DSL block.
This is a cleaner alternative to the `___Const__Name` triple-underscore convention.
Use `spread(on: :TypeName)` for inline fragments (`... on Type { }`), useful for union
types and interface implementations. It's the same `spread` verb as named fragment
spreads, and the same `on:` keyword as `fragment(name, on:)`:

```ruby
query_str = client.to_query_string do
client.query do
query do
invoice(id: 10) do
spread(on: :PaidInvoice) do
amountPaid
end
spread(on: :UnpaidInvoice) do
amountDue
end
end
end
end
```

Produces:

```graphql
query {
invoice(id: 10) {
... on PaidInvoice {
amountPaid
}
... on UnpaidInvoice {
amountDue
}
}
}
```

Directives can be applied to inline fragments too (see [Directives in the DSL](#directives-in-the-dsl)):

```ruby
spread(_skip(if: :skip_drafts), on: :DraftInvoice) { draftId }
# → ... on DraftInvoice @skip(if: $skip_drafts) { draftId }
```

### Directives in the DSL

Apply GraphQL directives to fields, spreads, and inline fragments using the `_name`
convention — any method starting with `_` followed by a lowercase letter is treated
as a directive (`_skip` → `@skip`, `_include` → `@include`, `_myDirective` → `@myDirective`).

**On a field:**

```ruby
client.query(some_id: :int, skip_fee: :boolean!) do
query(some_id: :int, skip_fee: :boolean!) do
invoice(id: :some_id) do
id
spread :InvoiceFields # → ...InvoiceFields
feeInCents _skip(if: :skip_fee) # → feeInCents @skip(if: $skip_fee)
end
end
end
```

Multiple spreads at the same level are supported:
**On a fragment spread:**

```ruby
client.to_query_string do
query do
invoice(id: 10) do
spread :CoreFields
spread :AuditFields
spread :InvoiceFields, _skip(if: :skip_invoice)
# → ...InvoiceFields @skip(if: $skip_invoice)
```

**On an inline fragment:**

```ruby
spread(_skip(if: :skip_drafts), on: :DraftInvoice) { draftId }
# → ... on DraftInvoice @skip(if: $skip_drafts) { draftId }
```

**Multiple directives on one field:**

```ruby
feeInCents _skip(if: :skip_fee), _include(if: :show_cents)
# → feeInCents @skip(if: $skip_fee) @include(if: $show_cents)
```

**No-argument directive:**

```ruby
legacyField _deprecated
# → legacyField @deprecated
```

The directive value is a plain Ruby method call that returns a `Directive` object
before the field method runs, ensuring the directive appears in the correct position
in the output string regardless of Ruby's evaluation order.

### Custom Scalar Types

By default, graphlient maps `:int → Int`, `:float → Float`, `:string → String`, and
`:boolean → Boolean` for variable type declarations. Register additional scalar types
in the client initialiser block:

```ruby
client = Graphlient::Client.new('https://example.com/graphql') do |c|
c.scalar :date, 'Date'
c.scalar :uuid, 'UUID'
c.scalar :decimal, 'Decimal'
end
```

Use the registered symbol in variable declarations:

```ruby
client.query(created_after: Date.today.iso8601, order_id: :uuid) do
query(created_after: :date, order_id: :uuid!) do
orders(created_after: :created_after, id: :order_id) do
id
total
end
end
end
# → query($created_after: Date, $order_id: UUID!) { ... }
```

`spread` works in both `to_query_string` and the standard `parse`/`query` execution
paths. The caller is responsible for appending the fragment definition to the query
string before sending it to the server, or for using a framework layer that handles
fragment assembly automatically.
Non-null variants work with `!`: `:date!` → `Date!`.

### Create API Client Classes with Graphlient::Extension::Query

Expand Down
11 changes: 11 additions & 0 deletions lib/graphlient/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ def http(&block)
@http ||= http_adapter_class.new(@url, adapter_options, &block)
end

# Register a custom scalar type for use in variable declarations.
# Call inside the client initialiser block:
# client = Graphlient::Client.new(url) do |c|
# c.scalar :date, 'Date'
# c.scalar :uuid, 'UUID'
# c.scalar :decimal, 'Decimal'
# end
def scalar(sym, graphql_type)
Query.scalar(sym, graphql_type)
end

def schema
@schema ||= options[:schema] || Graphlient::Schema.new(http, schema_path)
end
Expand Down
Loading
Loading