Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
- { ruby: 3.1 }
- { ruby: 3.2 }
- { ruby: 3.3 }
- { ruby: 3.4 }
- { ruby: 4.0 }
- { ruby: "ruby-head", ignore: true }
- { ruby: "jruby-9.4.8.0", ignore: true }
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
.byebug_history
.history/
.DS_Store
.solargraph.yml
.vscode/
.idea/

# Used by dotenv library to load environment variables.
# .env

Expand Down Expand Up @@ -50,3 +54,6 @@ Gemfile.lock

# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
.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!

10 changes: 4 additions & 6 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
inherit_from: .rubocop_todo.yml

AllCops:
TargetRubyVersion: 2.3

Lint/SplatKeywordArguments:
Enabled: false
NewCops: disable

Style/FrozenStringLiteralComment:
Enabled: false
Expand All @@ -13,7 +13,7 @@ Style/Documentation:
Style/SafeNavigation:
Enabled: false

Metrics/LineLength:
Layout/LineLength:
Enabled: false

Metrics/MethodLength:
Expand All @@ -24,5 +24,3 @@ Metrics/BlockLength:

Metrics/AbcSize:
Enabled: false

inherit_from: .rubocop_todo.yml
68 changes: 46 additions & 22 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,48 +1,72 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2018-08-14 17:57:21 -0400 using RuboCop version 0.56.0.
# on 2026-06-11 10:55:02 UTC using RuboCop version 1.87.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
Gemspec/RequiredRubyVersion:
Exclude:
- 'graphlient.gemspec'

# Offense count: 1
# Configuration parameters: AllowedMethods.
# AllowedMethods: enums
Lint/ConstantDefinitionInBlock:
Exclude:
- 'spec/graphlient/static_client_query_spec.rb'

# Offense count: 1
Lint/MixedRegexpCaptureTypes:
Exclude:
- 'lib/graphlient/query.rb'

# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Lint/NonDeterministicRequireOrder:
Exclude:
- 'spec/spec_helper.rb'

# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
# NotImplementedExceptions: NotImplementedError
Lint/UnusedMethodArgument:
Exclude:
- 'lib/graphlient/adapters/http/http_adapter.rb'

# Offense count: 2
Metrics/CyclomaticComplexity:
Max: 8
# Offense count: 1
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
Max: 104

# Offense count: 1
Metrics/PerceivedComplexity:
Max: 8
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/CyclomaticComplexity:
Max: 9

# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle.
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules.
# SupportedStyles: nested, compact
# SupportedStylesForClasses: ~, nested, compact
# SupportedStylesForModules: ~, nested, compact
Style/ClassAndModuleChildren:
Exclude:
- 'spec/graphlient/static_client_query_spec.rb'

# Offense count: 2
Style/MethodMissingSuper:
Exclude:
- 'lib/graphlient/extensions/query.rb'
- 'lib/graphlient/query.rb'

# Offense count: 5
# Offense count: 8
Style/MultilineBlockChain:
Exclude:
- 'spec/graphlient/client_query_spec.rb'
- 'spec/graphlient/client_schema_spec.rb'

# Offense count: 39
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 181
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: literals, strict
Style/MutableConstant:
Exclude:
- 'lib/graphlient/query.rb'
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
* [#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).
* [#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).
* [#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).

### 0.8.0 (2024/01/06)
* [#110](https://github.com/ashkan18/graphlient/pull/110): Ensure correct Faraday JSON response body parsing with invalid response header - [@taylorthurlow](https://github.com/taylorthurlow).
Expand Down
4 changes: 1 addition & 3 deletions Dangerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ warn("There're library changes, but not tests. That's OK as long as you're refac
# --------------------------------------------------------------------------------------------------------------------
# You've made changes to specs, but no library code has changed?
# --------------------------------------------------------------------------------------------------------------------
if !has_app_changes && has_spec_changes
message('We really appreciate pull requests that demonstrate issues, even without a fix. That said, the next step is to try and fix the failing tests!', sticky: false)
end
message('We really appreciate pull requests that demonstrate issues, even without a fix. That said, the next step is to try and fix the failing tests!', sticky: false) if !has_app_changes && has_spec_changes

# --------------------------------------------------------------------------------------------------------------------
# Have you updated CHANGELOG.md?
Expand Down
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ gem 'rake'

group :development, :test do
gem 'activesupport', '< 6'
gem 'mutex_m'
gem 'mutex_m' # activesupport 5.x depends on mutex_m, removed from stdlib in Ruby 3.4
gem 'ostruct'
end

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.

end

group :test do
Expand Down
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ A friendlier Ruby client for consuming GraphQL-based APIs. Built on top of your
- [Error Handling](#error-handling)
- [Executing Parameterized Queries and Mutations](#executing-parameterized-queries-and-mutations)
- [Parse and Execute Queries Separately](#parse-and-execute-queries-separately)
- [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)
- [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 @@ -278,6 +280,53 @@ GRAPHQL
client.execute query, ids: [42]
```

### Build Query Strings without Validation

`Client#to_query_string` serializes a DSL block into a GraphQL query string and
returns it as a plain `String`. It uses the same DSL serializer (`Graphlient::Query`)
that powers `client.query` and `client.parse`, but it stops there — no schema is
loaded, no graphql-client validation runs, no HTTP call is made.

```ruby
client = Graphlient::Client.new('https://example.com/graphql',
headers: { 'Authorization' => 'Bearer 123' }
)

query_str = client.to_query_string do
query(id: :int) do
invoice(id: :id) do
id
feeInCents
end
end
end

# => "query($id: Int){\n invoice(id: $id){\n id\n feeInCents\n }\n }"
```

For fragment-free queries the string can be fed back to `client.execute`:

```ruby
client.execute(query_str, id: 42)
```

Queries containing fragment spreads (`spread :Name` → `...Name`) cannot go back
through the gem — graphql-client requires fragments to be pre-registered module
constants, not named strings. Pass those directly to your own HTTP client instead.

This is also the escape hatch if you want to replace the graphql-client dependency
entirely. `to_query_string` gives you a standard GraphQL document — from there you
own the transport: Faraday, Net::HTTP, anything else. You get full control over
headers, retries, connection pooling, and middleware without any graphql-client
overhead.

```ruby
conn = Faraday.new('https://example.com/graphql',
headers: { 'Authorization' => 'Bearer 123', 'Content-Type' => 'application/json' }
)
response = conn.post('/', { query: query_str, variables: { id: 42 } }.to_json)
```

### Dynamic vs. Static Queries

Graphlient uses [graphql-client](https://github.com/github-community-projects/graphql-client), which [recommends](https://github.com/github-community-projects/graphql-client/blob/master/guides/dynamic-query-error.md) building queries as static module members along with dynamic variables during execution. This can be accomplished with graphlient the same way.
Expand Down Expand Up @@ -397,6 +446,40 @@ invoice.fee_in_cents
# 20000
```

### Fragment Spreads

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.

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

Multiple spreads at the same level are supported:

```ruby
client.to_query_string do
query do
invoice(id: 10) do
spread :CoreFields
spread :AuditFields
end
end
end
```

`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.

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

You can include `Graphlient::Extensions::Query` in your class. This will add a new `method_missing` method to your context which will be used to generate GraphQL queries.
Expand Down
1 change: 1 addition & 0 deletions lib/graphlient/adapters/http/http_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def execute(document:, operation_name: nil, variables: {}, context: {})

response = connection.request(request)
raise Graphlient::Errors::HttpServerError, response unless response.is_a?(Net::HTTPOK)

JSON.parse(response.body)
end

Expand Down
9 changes: 8 additions & 1 deletion lib/graphlient/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ def parse(query_str = nil, &block)
raise Graphlient::Errors::ClientError, e.message
end

def to_query_string(**_kargs, &block)
Graphlient::Query.new do
instance_eval(&block)
end.to_s
end

def execute(query, variables = nil)
query_params = {}
query_params[:context] = @options if @options
Expand All @@ -30,6 +36,7 @@ def execute(query, variables = nil)
# see https://github.com/github-community-projects/graphql-client/pull/132
# see https://github.com/exAspArk/graphql-errors/issues/2
raise Graphlient::Errors::ExecutionError, rc if errors_in_result?(rc)

rc
rescue GraphQL::Client::Error => e
raise Graphlient::Errors::ClientError, e.message
Expand Down Expand Up @@ -64,7 +71,7 @@ def raise_error_if_invalid_configuration!
end

def schema_path
return options[:schema_path].to_s if options[:schema_path]
options[:schema_path].to_s if options[:schema_path]
end

def client
Expand Down
1 change: 1 addition & 0 deletions lib/graphlient/errors/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Graphlient
module Errors
class Error < StandardError
attr_reader :inner_exception

def initialize(message, inner_exception = nil)
super(message)

Expand Down
1 change: 1 addition & 0 deletions lib/graphlient/errors/graphql_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Graphlient
module Errors
class GraphQLError < Error
attr_reader :response

def initialize(response)
super('the server responded with a GraphQL error')
@response = response
Expand Down
8 changes: 8 additions & 0 deletions lib/graphlient/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ def to_s
query_str.strip
end

# Appends a named fragment spread (`...fragment_name`) to the query string.
# Use instead of the deprecated `___Const` convention.
def spread(fragment_name)
@query_str << "\n#{indent}...#{fragment_name}"
@query_str << "\n#{indent}"
end

private

def evaluate(&block)
Expand All @@ -49,6 +56,7 @@ def evaluate(&block)

def resolve_fragment_constant(value)
return nil unless (match = value.to_s.match(FRAGMENT_DEFITION))

raw_const = match[:const].gsub('__', '::')
@context[@last_block].eval(raw_const).tap do |const|
msg = "Expected constant #{raw_const} to be GraphQL::Client::FragmentDefinition. Given #{const.class}"
Expand Down
1 change: 1 addition & 0 deletions lib/graphlient/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def initialize(http, path)

def dump!
raise MissingConfigurationError, PATH_ERROR_MESSAGE unless path

GraphQL::Client.dump_schema(http, path)
end
end
Expand Down
9 changes: 7 additions & 2 deletions spec/graphlient/adapters/http/faraday_adapter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,13 @@
end

specify do
expected_error_message = "Connection refused - #{error_message}"
expect { client.schema }.to raise_error(Graphlient::Errors::ConnectionFailedError, expected_error_message)
# Use a regex so the test passes on both Linux ("Connection refused - ...")
# and Windows (where Errno::ECONNREFUSED prepends a different OS message).
# Both platforms include the core error_message string in the result.
expect { client.schema }.to raise_error(
Graphlient::Errors::ConnectionFailedError,
/#{Regexp.escape(error_message)}/
)
end
end

Expand Down
Loading
Loading