Showing preview only (5,224K chars total). Download the full file or copy to clipboard to get everything.
Repository: yuki24/artemis
Branch: main
Commit: 8b3d76a11bf7
Files: 72
Total size: 5.0 MB
Directory structure:
gitextract_iw48rug4/
├── .github/
│ └── workflows/
│ └── ruby.yml
├── .gitignore
├── .rspec
├── Appraisals
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── artemis.gemspec
├── bin/
│ ├── console
│ └── setup
├── gemfiles/
│ ├── .bundle/
│ │ └── config
│ ├── graphql_2_0.gemfile
│ ├── rails_70.gemfile
│ ├── rails_71.gemfile
│ ├── rails_72.gemfile
│ ├── rails_80.gemfile
│ ├── rails_81.gemfile
│ └── rails_edge.gemfile
├── lib/
│ ├── artemis/
│ │ ├── adapters/
│ │ │ ├── abstract_adapter.rb
│ │ │ ├── curb_adapter.rb
│ │ │ ├── multi_domain_adapter.rb
│ │ │ ├── net_http_adapter.rb
│ │ │ ├── net_http_persistent_adapter.rb
│ │ │ └── test_adapter.rb
│ │ ├── adapters.rb
│ │ ├── client.rb
│ │ ├── exceptions.rb
│ │ ├── graphql_endpoint.rb
│ │ ├── railtie.rb
│ │ ├── rspec.rb
│ │ ├── test_helper.rb
│ │ └── version.rb
│ ├── artemis.rb
│ ├── generators/
│ │ └── artemis/
│ │ ├── install/
│ │ │ ├── USAGE
│ │ │ ├── install_generator.rb
│ │ │ └── templates/
│ │ │ ├── client.rb
│ │ │ └── graphql.yml
│ │ ├── mutation/
│ │ │ ├── USAGE
│ │ │ ├── mutation_generator.rb
│ │ │ └── templates/
│ │ │ └── mutation.graphql
│ │ └── query/
│ │ ├── USAGE
│ │ ├── query_generator.rb
│ │ └── templates/
│ │ ├── fixture.yml
│ │ └── query.graphql
│ └── tasks/
│ └── artemis.rake
└── test/
├── adapters_test.rb
├── autoloading_test.rb
├── backport/
│ └── method_call_assertions.rb
├── callbacks_test.rb
├── client_test.rb
├── endpoint_test.rb
├── fixtures/
│ ├── github/
│ │ ├── _repository_fields.graphql
│ │ ├── repository.graphql
│ │ ├── schema.json
│ │ ├── user.graphql
│ │ └── user_repositories.graphql
│ ├── github.rb
│ └── responses/
│ ├── github/
│ │ ├── repository.yml
│ │ └── user.json
│ └── spotify_client/
│ └── artist.yml
├── generators/
│ ├── install_generator_test.rb
│ ├── mutation_generator_test.rb
│ └── query_generator_test.rb
├── helpers/
│ ├── fake_server.rb
│ ├── isolated_test_helper.rb
│ └── test_helper.rb
├── railtie_test.rb
├── rake_tasks_test.rb
└── test_helper_test.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ruby.yml
================================================
name: build
on:
push:
pull_request:
jobs:
mri:
strategy:
fail-fast: false
matrix:
ruby_version:
- '3.4'
- '3.3'
- '3.2'
- '3.1'
- '3.0'
- '2.7'
gemfile:
- gemfiles/rails_81.gemfile
- gemfiles/rails_80.gemfile
- gemfiles/rails_72.gemfile
- gemfiles/rails_71.gemfile
- gemfiles/rails_70.gemfile
- gemfiles/graphql_2_0.gemfile
exclude:
- ruby_version: '3.4'
gemfile: gemfiles/rails_71.gemfile
- ruby_version: '3.4'
gemfile: gemfiles/rails_70.gemfile
- ruby_version: '3.1'
gemfile: gemfiles/rails_81.gemfile
- ruby_version: '3.1'
gemfile: gemfiles/rails_80.gemfile
- ruby_version: '3.0'
gemfile: gemfiles/rails_81.gemfile
- ruby_version: '3.0'
gemfile: gemfiles/rails_80.gemfile
- ruby_version: '3.0'
gemfile: gemfiles/rails_72.gemfile
- ruby_version: '3.0'
gemfile: gemfiles/graphql_2_0.gemfile
- ruby_version: '2.7'
gemfile: gemfiles/rails_81.gemfile
- ruby_version: '2.7'
gemfile: gemfiles/rails_80.gemfile
- ruby_version: '2.7'
gemfile: gemfiles/rails_72.gemfile
- ruby_version: '2.7'
gemfile: gemfiles/rails_71.gemfile
- ruby_version: '2.7'
gemfile: gemfiles/graphql_2_0.gemfile
runs-on: ubuntu-22.04
env:
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
steps:
- uses: actions/checkout@v4
- name: Install curl
run: sudo apt-get install curl libcurl4-openssl-dev
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby_version }}
bundler-cache: true
- run: bundle exec rake
rails_edge:
needs:
- mri
runs-on: ubuntu-22.04
env:
BUNDLE_GEMFILE: gemfiles/rails_edge.gemfile
steps:
- uses: actions/checkout@v4
- name: Install curl
run: sudo apt-get install curl libcurl4-openssl-dev
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4
bundler-cache: true
- run: bundle exec rake || echo "Rails edge test is done."
ruby_edge:
needs:
- mri
strategy:
matrix:
ruby_version:
- 'ruby-head'
gemfile:
- gemfiles/rails_edge.gemfile
- gemfiles/rails_80.gemfile
runs-on: ubuntu-22.04
env:
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
steps:
- uses: actions/checkout@v4
- name: Install curl
run: sudo apt-get install curl libcurl4-openssl-dev
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby_version }}
bundler-cache: true
- run: bundle exec rake || echo "Ruby edge test is done."
# The curb gem does not work well with JRuby, so skipping for now...
# jruby:
# needs:
# - mri
# strategy:
# matrix:
# ruby_version:
# - 'jruby-9.4'
# - 'jruby-head'
# gemfile:
# - gemfiles/rails_70.gemfile
# runs-on: ubuntu-22.04
# env:
# BUNDLE_GEMFILE: ${{ matrix.gemfile }}
# steps:
# - uses: actions/checkout@v4
# - name: Set up Ruby
# uses: ruby/setup-ruby@v1
# with:
# ruby-version: ${{ matrix.ruby_version }}
# bundler-cache: true
# - run: bundle exec rake || echo "JRuby test is done."
================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
Gemfile.lock
gemfiles/*.lock
.byebug_history
================================================
FILE: .rspec
================================================
--require spec_helper
--colour
================================================
FILE: Appraisals
================================================
appraise "rails_edge" do
git 'https://github.com/rails/rails.git' do
gem "rails"
gem "railties"
gem "activesupport"
end
gem "rackup"
end
appraise "graphql_2_0" do
gem "rails", '~> 7.1.0'
gem "railties", '~> 7.1.0'
gem "activesupport", '~> 7.1.0'
gem "rackup"
gem "graphql", "< 2.1"
end
appraise "rails_80" do
gem "rails", '~> 8.0.0'
gem "railties", '~> 8.0.0'
gem "activesupport", '~> 8.0.0'
gem "rackup"
end
appraise "rails_72" do
gem "rails", '~> 7.2.0'
gem "railties", '~> 7.2.0'
gem "activesupport", '~> 7.2.0'
gem "rackup"
end
appraise "rails_71" do
gem "rails", '~> 7.1.0'
gem "railties", '~> 7.1.0'
gem "activesupport", '~> 7.1.0'
gem "rackup"
end
appraise "rails_70" do
gem "rails", '~> 7.0.0'
gem "railties", '~> 7.0.0'
gem "activesupport", '~> 7.0.0'
end
appraise "rails_61" do
gem "rails", '~> 6.1.0'
gem "railties", '~> 6.1.0'
gem "activesupport", '~> 6.1.0'
end
appraise "rails_60" do
gem "rails", '~> 6.0.0'
gem "railties", '~> 6.0.0'
gem "activesupport", '~> 6.0.0'
end
appraise "rails_52" do
gem "rails", '~> 5.2.0'
gem "railties", '~> 5.2.0'
gem "activesupport", '~> 5.2.0'
end
appraise "rails_51" do
gem "rails", '~> 5.1.0'
gem "railties", '~> 5.1.0'
gem "activesupport", '~> 5.1.0'
end
appraise "rails_50" do
gem "rails", '~> 5.0.0'
gem "railties", '~> 5.0.0'
gem "activesupport", '~> 5.0.0'
gem "minitest", '5.10.3'
end
================================================
FILE: CHANGELOG.md
================================================
## Unreleased
#### 🚨 Breaking Changes
- No changes.
#### ⭐️ New Features
- Add support for Rails 8.0 ([<tt>#96</tt>](https://github.com/yuki24/artemis/pull/96))
#### 🐞 Bug Fixes
- No changes.
## [v1.1.0](https://github.com/yuki24/artemis/tree/v1.1.0)
_<sup>released at 2024-08-16 05:38:33 UTC</sup>_
#### ⭐️ New Features
- Add support for Ruby 3.3. ([<tt>e057567</tt>](https://github.com/yuki24/artemis/commit/e05756768c1535babccfca71f32d5218dd4da626))
## [v1.0.2](https://github.com/yuki24/artemis/tree/v1.0.2)
_<sup>released at 2024-05-02 02:41:10 UTC</sup>_
#### 🐞 Bug Fixes
- Fixes a bug where abstract client classes are not loaded correctly ([#93](https://github.com/yuki24/artemis/issues/93), `494d30b`)
## [v1.0.1](https://github.com/yuki24/artemis/tree/v1.0.1)
_<sup>released at 2024-05-02 02:40:54 UTC</sup>_
> Yanked due to inconsistent commit history.
## [v1.0.0](https://github.com/yuki24/artemis/tree/v1.0.0)
_<sup>released at 2024-02-05 06:16:35 UTC</sup>_
#### 🚨 Breaking Changes
- Drop support for Ruby 2.6. For those of you looking to use Artemis on Ruby 2.6, please use the `artemis` version
`0.9.0` and the `graphql-client` version `0.17.0`. ([#90](https://github.com/yuki24/artemis/pull/90))
#### ⭐️ New Features
- Add support for Ruby 3.3. ([#91](https://github.com/yuki24/artemis/pull/91))
- Add support for the latest versions of the `graphql` gem. ([#92](https://github.com/yuki24/artemis/pull/92))
#### 🐞 Bug Fixes
- No bug fixes.
## [v0.9.0](https://github.com/yuki24/artemis/tree/v0.9.0)
_<sup>released at 2023-09-18 01:08:34 UTC</sup>_
#### New Features
- Rails 7.1.0.beta1 is now officially supported ([<tt>f25ba29</tt>](https://github.com/yuki24/artemis/commit/f25ba296f15b26ffba7e4ec0f5b4cbeb061c97a1))
#### Fixes
- Fixes an issue where `graphql` gem `2.1.0` may not work with `graphql-client` ([<tt>b144ee2</tt>](https://github.com/yuki24/artemis/commit/b144ee2fbca2c23b4aaed8236f6fc07f65d8239d))
## [v0.8.0](https://github.com/yuki24/artemis/tree/v0.8.0)
_<sup>released at 2023-01-05 05:29:37 UTC</sup>_
#### New Features
- Ruby 3.2 is now officially supported
## [v0.7.0](https://github.com/yuki24/artemis/tree/v0.7.0)
_<sup>released at 2022-03-05 08:24:45 UTC</sup>_
#### Features
- Add support for Ruby 3.1 and Rails 7.0
- Add support for [the Multiplex query](https://graphql-ruby.org/queries/multiplex.html)
- Do not allow the usage of the `multi_domain` adapter to be nested ([<tt>9b7b520</tt>](https://github.com/yuki24/artemis/commit/9b7b5202c9fbe424d4ca22f05dc9c9759b5202c3))
- Do not require fragment files to end with `_fragment.graphql` ([<tt>3c6c0fa</tt>](https://github.com/yuki24/artemis/commit/3c6c0fa))
- Allow for overriding the namespace used for resolving graphql file paths ([<tt>bd18762</tt>](https://github.com/yuki24/artemis/commit/bd18762))
## [v0.6.0](https://github.com/yuki24/artemis/tree/v0.6.0)
_<sup>released at 2021-09-03 04:17:55 UTC</sup>_
#### Features
- Add support for Ruby 3.0 and Rails 6.0, 6.1
- Add the multi domain adapter ([<tt>744b8ea</tt>](https://github.com/yuki24/artemis/commit/744b8ea35795b4e6cc4fdc1ebb63dd9a4e9819f0))
#### Fixes
- Address warnings from Ruby 2.7 ([<tt>408adcb</tt>](https://github.com/yuki24/artemis/commit/408adcb3f39912f7afb7b3690a52f1d593662b7b))
- Avoid crashing when config/graphql.yml does not exist ([@dlackty](https://github.com/dlackty), [#76](https://github.com/yuki24/artemis/pull/76))
## [v0.5.2](https://github.com/yuki24/artemis/tree/v0.5.2)
_<sup>released at 2019-07-26 03:21:43 UTC</sup>_
#### Fixes
- Fixes an issue where fixtures can not be looked up properly when the client has two or more words in its name ([@JanStevens](https://github.com/JanStevens), issue: [#72](https://github.com/yuki24/artemis/issues/72), PR: [#73](https://github.com/yuki24/artemis/pull/73))
## [v0.5.1](https://github.com/yuki24/artemis/tree/v0.5.1)
_<sup>released at 2019-07-01 14:25:35 UTC</sup>_
#### Fixes
- Fixes an issue where callbacks are shared across all clients ([@JanStevens](https://github.com/JanStevens), issue: [#69](https://github.com/yuki24/artemis/issues/69), PR: [#70](https://github.com/yuki24/artemis/pull/70))
- Fixes an issue where fixtures with the same name cause a conflict ([@JanStevens](https://github.com/JanStevens), Issue: [#68](https://github.com/yuki24/artemis/issues/68), commit: [<tt>e1f57f4</tt>](https://github.com/yuki24/artemis/commit/e1f57f49ebb032553d7a6f70e48422fc9825c119))
## [v0.5.0](https://github.com/yuki24/artemis/tree/v0.5.0)
_<sup>released at 2019-06-02 22:01:57 UTC</sup>_
#### Features
- Add support for Rails 6.0, 4.1, and 4.0
- [<tt>6701b54</tt>](https://github.com/yuki24/artemis/commit/6701b546a143c22109c7ab30018acf96d67067d1), [#62](https://github.com/yuki24/artemis/issues/62): Allow to dynamically call the operation ([@JanStevens](https://github.com/JanStevens))
#### Fixes
- [#67](https://github.com/yuki24/artemis/pull/67): Fix the wrong test version constraints in `Appraisals` ([@daemonsy](https://github.com/daemonsy))
- [#60](https://github.com/yuki24/artemis/pull/60): Fix an issue where not all adapters send required HTTP headers
## [v0.4.0](https://github.com/yuki24/artemis/tree/v0.4.0)
_<sup>released at 2019-01-30 03:42:14 UTC</sup>_
#### Features
- [<tt>48d052e</tt>](https://github.com/yuki24/artemis/commit/48d052e9819703f1cefa95fbdb431bd03928f4ed): Add an easy way to set up Rspec integration
- [<tt>0f7cd12</tt>](https://github.com/yuki24/artemis/commit/0f7cd120594a0dd2a4af2b2e5cf990891dd8de16): Make Artemis' Railtie configurable
- [<tt>6bd15e2</tt>](https://github.com/yuki24/artemis/commit/6bd15e20779e5a6f898e1aacf8237c94c8c46aba): Add the ability to use ERB in test fixtures
- [#49](https://github.com/yuki24/artemis/pull/49): Expose the TestAdapter as a public API
#### Bug fixes
- [<tt>b7ad4a4</tt>](https://github.com/yuki24/artemis/commit/b7ad4a481a43cadd9193076c0e44938e05e6d44b): Require `graphl >= 1.8` to fix a bug in the generator
- [#48](https://github.com/yuki24/artemis/pull/48): Do not transform keys of query variables ([@erikdstock](https://github.com/erikdstock))
- [#47](https://github.com/yuki24/artemis/pull/47): Fixes an issue where errors thrown from `config/graphql.yml` get swallowed
## [v0.2.0: New generators and small improvements](https://github.com/yuki24/artemis/tree/v0.2.0)
_<sup>released at 2018-10-30 02:09:59 UTC</sup>_
#### Features
- [#43](https://github.com/yuki24/artemis/pull/43): Keep persistent connections open for 30 minutes
- [#42](https://github.com/yuki24/artemis/pull/42): Allow for setting up the test adapter without `url`
- [#41](https://github.com/yuki24/artemis/pull/41): Add a mutation generator
- [#40](https://github.com/yuki24/artemis/pull/40): Add a query generator
- [#39](https://github.com/yuki24/artemis/pull/39): Installer now adds a new service if `config/graphql.yml` is present
## [v0.1.0: First release!](https://github.com/yuki24/artemis/tree/v0.1.0)
_<sup>released at 2018-10-16 20:57:51 UTC</sup>_
First release of Artemis! 🎉
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at yk.nishijima@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: Gemfile
================================================
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
# Specify your gem's dependencies in artemis.gemspec
gemspec
gem "bigdecimal"
gem "drb"
gem "irb"
gem "logger"
gem "mutex_m"
gem "reline"
gem "curb", ">= 0.9.6"
gem "minitest"
gem "webrick"
================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)
Copyright (c) 2018 Yuki Nishijima
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Artemis [](https://github.com/yuki24/artemis/actions/workflows/ruby.yml) [](https://rubygems.org/gems/artemis)
Artemis is a GraphQL client that is designed to fit well on Rails.
* **Convention over Configuration**: You'll never have to make trivial decisions or spend time on boring setup. Start
making a GraphQL request in literally 30s.
* **Performant by default**: You can't do wrong when it comes to performance. All GraphQL files are pre-loaded only
once in production and it'll never affect runtime performance. Comes with options that enable persistent connections
and even HTTP/2, the next-gen high-performance protocol.
* **First-class support for testing**: Testing and stubbing GraphQL requests couldn't be simpler. No need to add
external dependencies to test well.
<img width="24" height="24" src="https://avatars1.githubusercontent.com/u/541332?s=48&v=4"> Battle-tested at [Artsy](https://www.artsy.net)

## Getting started
Add this line to your application's Gemfile:
```ruby
gem 'artemis'
```
Once you run `bundle install` on your Rails app, run the install command:
```sh
$ rails g artemis:install artsy https://metaphysics-production.artsy.net/
# or if you need to specify the `--authorization` header:
$ rails g artemis:install github https://api.github.com/graphql --authorization 'token ...'
```
### Generating your first query
Artemis comes with a query generator. For exmaple, you could use the query generator to generate a query stub for `artist`:
```sh
$ rails g artemis:query artist
```
Then this will generate:
```graphql
# app/operations/artist.graphql
query($id: String!) {
artist(id: $id) {
# Add fields here...
}
}
```
Then you could call the class method that has the matching name `artist`:
```ruby
Artsy.artist(id: "pablo-picasso")
# => makes a GraphQL query that's in app/operations/artist.graphql
```
You can also specify a file name:
```sh
$ rails g artemis:query artist artist_details_on_artwork
# => generates app/operations/artist_details_on_artwork.graphql
```
Then you can make a query in `artist_details_on_artwork.graphql` with:
```ruby
Artsy.artist_details_on_artwork(id: "pablo-picasso")
```
## The convention
Artemis assumes that the files related to GraphQL are organized in a certain way. For example, a service that talks to Artsy's GraphQL API could have the following structure:
```
├──app/operations
│ ├── artsy
│ │ ├── _artist_fragment.graphql
│ │ ├── artwork.graphql
│ │ ├── artist.graphql
│ │ └── artists.graphql
│ └── artsy.rb
├──config/graphql.yml
├──test/fixtures/graphql
│ └── artsy
│ ├── artwork.yml
│ ├── artist.yml
│ └── artists.yml
└──vendor/graphql/schema/artsy.json
```
### Fragments
Fragments are defined in defined in a standard way in a file named `_artwork_fragment.graphql` with the standard convention:
```graphql
fragment on Artwork {
id,
name,
artist_id
# other artwork fields here
}
```
The way of calling an Artemis fragment on other queries or models is with a **Rails convention**. Let us suppose we have the Artist model and its corresponding artwork. The way of nesting or calling the artwork on the artist model would look like this:
```graphql
fragment on Artist {
id,
name,
artworks {
...Artsy::ArtworkFragment
}
}
```
Where `Artsy` is the name of the folder/module.
## Callbacks
You can use the `before_execute` callback to intercept outgoing requests and the `after_execute` callback to observe the
response. A common operation that's done in the `before_execute` hook is assigning a token to the header:
```ruby
class Artsy < Artemis::Client
before_execute do |document, operation_name, variables, context|
context[:headers] = {
Authorization: "token ..."
}
end
end
```
Here the `:headers` key is a special context type. The hash object assigned to the `context[:headers]` will be sent as
the HTTP headers of the request.
Another common thing when receiving a response is to check if there's any error in the response and throw and error
accordingly:
```ruby
class Artsy < Artemis::Client
after_execute do |data, errors, extensions|
raise "GraphQL error: #{errors.to_json}" if errors.present?
end
end
```
## Multi domain support
Services like Shopify provide
[a different endpoint per customer](https://shopify.dev/api/admin/graphql/reference#graphql-endpoint) (e.g.
`https://{shop}.myshopify.com`). In order to switch the endpoint on a per-request basis, you will have to use the
`:multi_domain` adapter. This is a wrapper adapter that relies on an actual HTTP adapter such as `:net_http` and
`:curb` so that e.g. it can maintain multiple connections for each endpoint if necessary. This could be configured
as shown below:
```yaml
default: &default
# Specify the :multi_domain adapter:
adapter: :multi_domain
# Other configurations such as `timeout` and `pool_size` are passed down to the underlying adapter:
timeout: 10
pool_size: 25
# Additional adapter-specific configurations could be configured as `adapter_options`:
adapter_options:
# Here you can configure the actual adapter to use. By default, it is set to :net_http. Available adapters are
# :net_http, :net_http_persistent, :curb, and :test. You can not nest the use of the `:multi_domain` adapter.
adapter: :net_http
development:
shopify:
<<: *default
...
```
Upon making a request you will also have to specify the `url` option:
```ruby
# Makes a request to https://myawesomeshop.myshopify.com:
Shopify.with_context(url: "https://myawesomeshop.myshopify.com").product(id: "...")
```
## Configuration
You can configure the GraphQL client using the following options. Those configurations are found in the
`config/graphql.yml`.
| Name | Required? | Default | Description |
| ------------- | --------- | ------------| ----------- |
| `adapter` | No | `:net_http` | The underlying client library that actually makes an HTTP request. See Adapters for available options.
| `pool_size` | No | 25 | The number of keep-alive connections. The `:net_http` adapter will ignore this option.
| `schema_path` | No | See above | The path to the GrapQL schema. Setting an empty value to this will force the client to download the schema upon the first request.
| `timeout` | No | 10 | HTTP timeout set for the adapter in seconds. This will be set to both `read_timeout` and `write_timeout` and there is no way to configure them with a different value as of writing (PRs welcome!)
| `url` | Yes | N/A | The URL for the GraphQL endpoint.
### Adapters
There are four adapter options available. Choose the adapter that best fits on your use case.
| Adapter | Protocol | Keep-alive | Performance | Dependencies |
| ---------------------- | ------------------------ | ----------- | ----------- | ------------ |
| `:curb` | HTTP/1.1, **HTTP/2** | **Yes** | **Fastest** | [`curb 0.9.6+`][curb]<br>[`libcurl 7.64.0+`][curl]<br>[`nghttp2 1.0.0+`][nghttp]
| `:net_http` (default) | HTTP/1.1 only | No | Slow | **None**
| `:net_http_persistent` | HTTP/1.1 only | **Yes** | **Fast** | [`net-http-persistent 3.0.0+`][nhp]
| `:multi_domain` | See [multi domain support](#multi-domain-support)
| `:test` | See [Testing](#testing)
#### Third-party adapters
This is a comminuty-maintained adapter. Want to add yours? Send us a pull request!
| Adapter | Description |
| ---------------------- | ------------|
| [`:net_http_hmac`](https://github.com/JanStevens/artemis-api-auth/tree/master) | provides a new Adapter for the Artemis GraphQL ruby client to support HMAC Authentication using [ApiAuth](https://github.com/mgomes/api_auth). |
#### Writing your own adapter
When the built-in adapters do not satisfy your needs, you may want to implement your own adapter. You could do so by following the steps below. Let's implement the [`:net_http_hmac`](https://github.com/JanStevens/artemis-api-auth/tree/master) adapter as an example.
1. Define `NetHttpHmacAdapter` under the `Artemis::Adapters` namespace and implement [the `#execute` method](https://github.com/github/graphql-client/blob/master/guides/remote-queries.md):
```ruby
# lib/artemis/adapters/net_http_hmac_adapter.rb
module Artemis::Adapters
class NetHttpHmacAdapter
def execute(document:, operation_name: nil, variables: {}, context: {})
# Makes an HTTP request for GraphQL query.
end
end
end
```
2. Load the adapter in `config/initializers/artemis.rb` (or any place that gets loaded before Rails runs initializers):
```ruby
require 'artemis/adapters/net_http_hmac_adapter'
```
3. Specify the adapter name in `config/graphql.yml`:
```yml
default: &default
adapter: :net_http_hmac
```
## Rake tasks
Artemis also adds a useful `rake graphql:schema:update` rake task that downloads the GraphQL schema using the
`Introspection` query.
### `graphql:schema:update`
Downloads and saves the GraphQL schema.
| Option Name | Description |
| ------------------ | ------------|
| `SERVICE` | Service name the schema is downloaded from.|
| `AUTHORIZATION` | HTTP `Authorization` header value used to download the schema with.|
#### Examples
```
$ rake graphql:schema:update
# => downloads schema from the service. fails if there are multiple services in config/graphql.yml.
$ rake graphql:schema:update SERVICE=github AUTHORIZATION="token ..."
# => downloads schema from the `github` service using the HTTP header "AUTHORIZATION: token ..."
```
## Testing
Given that you have `app/operations/artsy/artist.graphql` and fixture file for the `artist.yml`:
```yml
# test/fixtures/graphql/artist.yml:
leonardo_da_vinci:
data:
artist:
name: Leonardo da Vinci
birthday: 1452/04/15
yayoi_kusama:
data:
artist:
name: Yayoi Kusama
birthday: 1929/03/22
```
Then you can stub the request with the `stub_graphql` DSL:
```ruby
stub_graphql(Artsy, :artist, id: "yayoi-kusama").to_return(:yayoi_kusama)
stub_graphql(Artsy, :artist, id: "leonardo-da-vinci").to_return(:leonardo_da_vinci)
yayoi_kusama = Artsy.artist(id: "yayoi-kusama")
yayoi_kusama.data.artist.name # => "Yayoi Kusama"
yayoi_kusama.data.artist.birthday # => "1452/04/15"
da_vinci = Artsy.artist(id: "leonardo-da-vinci")
da_vinci.data.artist.name # => "Leonardo da Vinci"
da_vinci.data.artist.birthday # => "1452/04/15"
```
You can also use JSON instead of YAML. See [example fixtures](https://github.com/yuki24/artemis/tree/master/spec/fixtures/responses)
and [test cases](https://github.com/yuki24/artemis/blob/master/spec/test_helper_spec.rb#L16-L51).
### MiniTest
Setting up the test helper with Artemis is very easy and simple. Just add the following code to the
`test/test_helper.rb` in your app:
```ruby
# spec/test_helper.rb
require 'artemis/test_helper'
class ActiveSupport::TestCase
setup do
graphql_requests.clear
graphql_responses.clear
end
end
```
### RSpec
Artemis also comes with a script that wires up helper methods on Rspec. Because it is more common to use the `spec/`
directory to organize spec files in RSpec, the `config.artemis.fixture_path` config needs to point to
`spec/fixtures/graphql`. Other than that, it is very straightforward to set it up:
```ruby
# config/application.rb
config.artemis.fixture_path = 'spec/fixtures/graphql'
```
```ruby
# Add this to your spec/rails_helper.rb or spec_helper.rb if you don't have rails_helper.rb
require 'artemis/rspec'
```
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/yuki24/artemis. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## Code of Conduct
Everyone interacting in the Artemis project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/artemis/blob/master/CODE_OF_CONDUCT.md).
[curb]: https://rubygems.org/gems/curb
[curl]: https://curl.haxx.se/docs/http2.html
[nghttp]: https://nghttp2.org/
[nhp]: https://rubygems.org/gems/net-http-persistent
================================================
FILE: Rakefile
================================================
require "bundler/gem_tasks"
require "rake/testtask"
TESTS_IN_ISOLATION = ['test/railtie_test.rb', 'test/rake_tasks_test.rb']
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.test_files = FileList['test/**/*_test.rb'] - TESTS_IN_ISOLATION
t.warning = false
end
Rake::TestTask.new('test:isolated') do |t|
t.libs << "test"
t.test_files = TESTS_IN_ISOLATION
t.warning = false
end
task default: ['test', 'test:isolated']
================================================
FILE: artemis.gemspec
================================================
lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "artemis/version"
Gem::Specification.new do |spec|
spec.name = "artemis"
spec.version = Artemis::VERSION
spec.authors = ["Jon Allured", "Yuki Nishijima"]
spec.email = ["jon.allured@gmail.com", "yk.nishijima@gmail.com"]
spec.summary = %q{GraphQL on Rails}
spec.description = %q{GraphQL client on Rails + Convention over Configuration = ❤️}
spec.homepage = "https://github.com/yuki24/artemis"
spec.license = "MIT"
spec.files = `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^(test)/}) }
spec.require_paths = ["lib"]
spec.add_dependency "activesupport", ">= 4.0.0"
spec.add_dependency "railties", ">= 4.0.0"
spec.add_dependency "graphql"
spec.add_dependency "graphql-client", ">= 0.13.0"
spec.add_development_dependency "appraisal", ">= 2.2"
spec.add_development_dependency "bundler", ">= 1.16"
spec.add_development_dependency "net-http-persistent", ">= 3.0"
spec.add_development_dependency "rake", ">= 10.0"
end
================================================
FILE: bin/console
================================================
#!/usr/bin/env ruby
require "bundler/setup"
require "artemis"
Artemis::Client.query_paths = [File.join(__dir__, '../spec/fixtures')]
Artemis::GraphQLEndpoint.register!(:github, adapter: :test, url: '', schema_path: 'spec/fixtures/github/schema.json')
Artemis::GraphQLEndpoint.lookup(:github).load_schema!
require_relative '../spec/fixtures/github'
require "pry"
Pry.start
================================================
FILE: bin/setup
================================================
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here
================================================
FILE: gemfiles/.bundle/config
================================================
---
BUNDLE_RETRY: "1"
================================================
FILE: gemfiles/graphql_2_0.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "bigdecimal"
gem "drb"
gem "irb"
gem "logger"
gem "mutex_m"
gem "reline"
gem "curb", ">= 0.9.6"
gem "minitest"
gem "webrick"
gem "rails", "~> 7.1.0"
gem "railties", "~> 7.1.0"
gem "activesupport", "~> 7.1.0"
gem "rackup"
gem "graphql", "< 2.1"
gemspec path: "../"
================================================
FILE: gemfiles/rails_70.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "bigdecimal"
gem "drb"
gem "irb"
gem "logger"
gem "mutex_m"
gem "reline"
gem "curb", ">= 0.9.6"
gem "minitest"
gem "webrick"
gem "rails", "~> 7.0.0"
gem "railties", "~> 7.0.0"
gem "activesupport", "~> 7.0.0"
gemspec path: "../"
================================================
FILE: gemfiles/rails_71.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "bigdecimal"
gem "drb"
gem "irb"
gem "logger"
gem "mutex_m"
gem "reline"
gem "curb", ">= 0.9.6"
gem "minitest"
gem "webrick"
gem "rails", "~> 7.1.0"
gem "railties", "~> 7.1.0"
gem "activesupport", "~> 7.1.0"
gem "rackup"
gemspec path: "../"
================================================
FILE: gemfiles/rails_72.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "bigdecimal"
gem "drb"
gem "irb"
gem "logger"
gem "mutex_m"
gem "reline"
gem "curb", ">= 0.9.6"
gem "minitest"
gem "webrick"
gem "rails", "~> 7.2.0"
gem "railties", "~> 7.2.0"
gem "activesupport", "~> 7.2.0"
gem "rackup"
gemspec path: "../"
================================================
FILE: gemfiles/rails_80.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "bigdecimal"
gem "drb"
gem "irb"
gem "logger"
gem "mutex_m"
gem "reline"
gem "curb", ">= 0.9.6"
gem "minitest"
gem "webrick"
gem "rails", "~> 8.0.0"
gem "railties", "~> 8.0.0"
gem "activesupport", "~> 8.0.0"
gem "rackup"
gemspec path: "../"
================================================
FILE: gemfiles/rails_81.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "pry"
gem "pry-byebug", platforms: :mri
gem "curb", ">= 0.9.6"
gem "webrick"
gem "minitest", "< 5.25.0"
gem "rails", "~> 8.1.0"
gem "railties", "~> 8.1.0"
gem "activesupport", "~> 8.1.0"
gem "rackup"
gemspec path: "../"
================================================
FILE: gemfiles/rails_edge.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
git "https://github.com/rails/rails.git" do
gem "rails"
gem "railties"
gem "activesupport"
end
gem "bigdecimal"
gem "drb"
gem "irb"
gem "logger"
gem "mutex_m"
gem "reline"
gem "curb", ">= 0.9.6"
gem "minitest"
gem "webrick"
gem "rackup"
gemspec path: "../"
================================================
FILE: lib/artemis/adapters/abstract_adapter.rb
================================================
# frozen_string_literal: true
require 'active_support/core_ext/object/blank'
require 'graphql/client/http'
module Artemis
module Adapters
class AbstractAdapter < ::GraphQL::Client::HTTP
attr_reader :service_name, :timeout, :pool_size
EMPTY_HEADERS = {}.freeze
DEFAULT_HEADERS = {
"Accept" => "application/json",
"Content-Type" => "application/json"
}.freeze
def initialize(uri, service_name: , timeout: , pool_size: , adapter_options: {})
raise ArgumentError, "url is required (given `#{uri.inspect}')" if uri.blank?
super(uri) # Do not pass in the block to avoid getting #headers and #connection overridden.
@service_name = service_name.to_s
@timeout = timeout
@pool_size = pool_size
end
# Public: Extension point for subclasses to set custom request headers.
#
# Returns Hash of String header names and values.
def headers(_context)
_context[:headers] || EMPTY_HEADERS
end
# Public: Make an HTTP request for GraphQL query.
#
# A subclass that inherits from +AbstractAdapter+ can override this method if it needs more flexibility for how
# it makes a request.
#
# For more details, see +GraphQL::Client::HTTP#execute+.
def execute(*)
super
end
# Public: Extension point for subclasses to customize the Net:HTTP client
#
# A subclass that inherits from +AbstractAdapter+ should returns a Net::HTTP object or an object that responds
# to +request+ that is given a Net::HTTP request object.
def connection
raise "AbstractAdapter is an abstract class that can not be instantiated!"
end
end
end
end
================================================
FILE: lib/artemis/adapters/curb_adapter.rb
================================================
# frozen_string_literal: true
require 'delegate'
require 'json'
require 'curb'
require 'artemis/adapters/abstract_adapter'
require 'artemis/exceptions'
module Artemis
module Adapters
class CurbAdapter < AbstractAdapter
attr_reader :multi
def initialize(uri, service_name: , timeout: , pool_size: , adapter_options: {})
super
@multi = Curl::Multi.new
@multi.pipeline = Curl::CURLPIPE_MULTIPLEX if defined?(Curl::CURLPIPE_MULTIPLEX)
end
def multiplex(queries, context: {})
make_request({ _json: queries }, context)
end
def execute(document:, operation_name: nil, variables: {}, context: {})
body = {}
body["query"] = document.to_query_string
body["variables"] = variables if variables.any?
body["operationName"] = operation_name if operation_name
make_request(body, context)
end
private
def make_request(body, context)
easy = Curl::Easy.new(uri.to_s)
easy.timeout = timeout
easy.multi = multi
easy.headers = DEFAULT_HEADERS.merge(headers(context))
easy.post_body = JSON.generate(body)
if defined?(Curl::CURLPIPE_MULTIPLEX)
# This ensures libcurl waits for the connection to reveal if it is
# possible to pipeline/multiplex on before it continues.
easy.setopt(Curl::CURLOPT_PIPEWAIT, 1)
easy.version = Curl::HTTP_2_0
end
easy.http_post
case easy.response_code
when 200, 400
JSON.parse(easy.body)
when 500..599
raise Artemis::GraphQLServerError, "Received server error status #{easy.response_code}: #{easy.body}"
else
{ "errors" => [{ "message" => "#{easy.response_code} #{easy.body}" }] }
end
end
end
end
end
================================================
FILE: lib/artemis/adapters/multi_domain_adapter.rb
================================================
# frozen_string_literal: true
require 'artemis/adapters/abstract_adapter'
module Artemis
module Adapters
class MultiDomainAdapter < AbstractAdapter
attr_reader :adapter
def initialize(_uri, service_name: , timeout: , pool_size: , adapter_options: {})
if adapter_options[:adapter] == :multi_domain
raise ArgumentError, "You can not use the :multi_domain adapter with the :multi_domain adapter."
end
@connection_by_url = {}
@service_name = service_name.to_s
@timeout = timeout
@pool_size = pool_size
@adapter = adapter_options[:adapter] || :net_http
@mutex_for_connection = Mutex.new
end
def multiplex(queries, context: {})
url = context[:url]
if url.nil?
raise ArgumentError, 'The MultiDomain adapter requires a url on every request. Please specify a url with a context: ' \
'Client.multiplex(url: "https://awesomeshop.domain.conm") { ... }'
end
connection_for_url(url).multiplex(queries, context: context)
end
# Makes an HTTP request for GraphQL query.
def execute(document:, operation_name: nil, variables: {}, context: {})
url = context[:url]
if url.nil?
raise ArgumentError, 'The MultiDomain adapter requires a url on every request. Please specify a url with a context: ' \
'Client.with_context(url: "https://awesomeshop.domain.conm")'
end
connection_for_url(url).execute(document: document, operation_name: operation_name, variables: variables, context: context)
end
def connection
raise NotImplementedError, "Calling the #connection method without a URI is not supported. Please use the " \
"#connection_for_url(uri) instead."
end
def connection_for_url(url)
@connection_by_url[url.to_s] || @mutex_for_connection.synchronize do
@connection_by_url[url.to_s] ||= ::Artemis::Adapters.lookup(adapter).new(url, service_name: service_name, timeout: timeout, pool_size: pool_size)
end
end
end
end
end
================================================
FILE: lib/artemis/adapters/net_http_adapter.rb
================================================
# frozen_string_literal: true
require 'json'
require 'net/http'
require 'artemis/adapters/abstract_adapter'
require 'artemis/exceptions'
module Artemis
module Adapters
class NetHttpAdapter < AbstractAdapter
def multiplex(queries, context: {})
make_request({ _json: queries }, context)
end
# Makes an HTTP request for GraphQL query.
def execute(document:, operation_name: nil, variables: {}, context: {})
body = {}
body["query"] = document.to_query_string
body["variables"] = variables if variables.any?
body["operationName"] = operation_name if operation_name
make_request(body, context)
end
# Returns a fresh Net::HTTP object that creates a new connection.
def connection
Net::HTTP.new(uri.host, uri.port).tap do |client|
client.use_ssl = uri.scheme == "https"
client.open_timeout = timeout
client.read_timeout = timeout
client.write_timeout = timeout if client.respond_to?(:write_timeout=)
end
end
private
def make_request(body, context)
request = Net::HTTP::Post.new(uri.request_uri)
request.basic_auth(uri.user, uri.password) if uri.user || uri.password
request.body = JSON.generate(body)
DEFAULT_HEADERS.merge(headers(context)).each { |name, value| request[name] = value }
response = connection.request(request)
case response.code.to_i
when 200, 400
JSON.parse(response.body)
when 500..599
raise Artemis::GraphQLServerError, "Received server error status #{response.code}: #{response.body}"
else
{ "errors" => [{ "message" => "#{response.code} #{response.message}" }] }
end
end
end
end
end
================================================
FILE: lib/artemis/adapters/net_http_persistent_adapter.rb
================================================
# frozen_string_literal: true
require 'delegate'
begin
require "active_support/isolated_execution_state"
rescue LoadError
# no-op... Rails 7.0 requires this.
end
begin
require 'active_support/deprecation'
require 'active_support/deprecator'
rescue LoadError
end
require 'active_support/core_ext/numeric/time'
require 'net/http/persistent'
require 'artemis/adapters/net_http_adapter'
module Artemis
module Adapters
class NetHttpPersistentAdapter < NetHttpAdapter
attr_reader :_connection, :raw_connection
def initialize(uri, service_name: , timeout: , pool_size: , adapter_options: {})
super
@raw_connection = Net::HTTP::Persistent.new(name: service_name, pool_size: pool_size)
@raw_connection.open_timeout = timeout
@raw_connection.read_timeout = timeout
@raw_connection.idle_timeout = 30.minutes.to_i # TODO: Make it configurable
@_connection = ConnectionWrapper.new(@raw_connection, uri)
end
# Public: Extension point for subclasses to customize the Net:HTTP client
#
# Returns a Net::HTTP object
def connection
_connection
end
class ConnectionWrapper < SimpleDelegator #:nodoc:
def initialize(obj, url)
super(obj)
@url = url
end
def request(req)
__getobj__.request(@url, req)
end
end
private_constant :ConnectionWrapper
end
end
end
================================================
FILE: lib/artemis/adapters/test_adapter.rb
================================================
# frozen_string_literal: true
require 'active_support/core_ext/module/attribute_accessors'
module Artemis
module Adapters
class TestAdapter
cattr_accessor :requests
self.requests = []
cattr_accessor :responses
self.responses = []
Request = Struct.new(:document, :operation_name, :variables, :context)
Multiplex = Struct.new(:queries, :context)
private_constant :Request, :Multiplex
def initialize(*)
end
def multiplex(queries, context: {})
self.requests << Multiplex.new(queries, context)
queries.map do |query|
result = responses.detect do |mock|
query[:operationName] == mock.operation_name && (mock.arguments == :__unspecified__ || query[:variables] == mock.arguments)
end
result&.data || fake_response
end
end
def execute(**arguments)
self.requests << Request.new(*arguments.values_at(:document, :operation_name, :variables, :context))
response = responses.detect do |mock|
arguments[:operation_name] == mock.operation_name && (mock.arguments == :__unspecified__ || arguments[:variables] == mock.arguments)
end
response&.data || fake_response
end
private
def fake_response
{
'data' => { 'test' => 'data' },
'errors' => [],
'extensions' => {}
}
end
end
end
end
================================================
FILE: lib/artemis/adapters.rb
================================================
# frozen-string-literal: true
require 'active_support/dependencies/autoload'
module Artemis
module Adapters
extend ActiveSupport::Autoload
autoload :CurbAdapter
autoload :MultiDomainAdapter
autoload :NetHttpAdapter
autoload :NetHttpPersistentAdapter
autoload :TestAdapter
class << self
##
# Returns the constant for the specified adapter name.
#
# Artemis::Adapters.lookup(:net_http)
# # => Artemis::Adapters::NetHttpAdapter
def lookup(name)
const_get("#{name.to_s.camelize}Adapter")
end
end
end
end
================================================
FILE: lib/artemis/client.rb
================================================
# frozen_string_literal: true
require 'delegate'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/inflections'
require 'artemis/graphql_endpoint'
require 'artemis/exceptions'
module Artemis
class Client
# The paths in which the Artemis client looks for files that have the +.graphql+ extension.
# In a rails app, this value will be set to +["app/operations"]+ by Artemis' +Artemis::Railtie+.
cattr_accessor :query_paths
# Returns a plain +GraphQL::Client+ object. For more details please refer to the official documentation for
# {the +graphql-client+ gem}[https://github.com/github/graphql-client].
attr_reader :client
# Default context that is appended to every GraphQL request for the client.
class_attribute :default_context, default: {}
# List of before callbacks that get invoked in every +execute+ call.
#
# @api private
class_attribute :before_callbacks, default: []
# List of after callbacks that get invoked in every +execute+ call.
#
# @api private
class_attribute :after_callbacks, default: []
# Creates a new instance of the GraphQL client for the service.
#
# # app/operations/github/user.graphql
# query($id: String!) {
# user(login: $id) {
# name
# }
# }
#
# # app/operations/github.rb
# class Github < Artemis::Client
# end
#
# github = Github.new
# github.user(id: 'yuki24').data.user.name # => "Yuki Nishijima"
#
# github = Github.new(context: { headers: { Authorization: "bearer ..." } })
# github.user(id: 'yuki24').data.user.name # => "Yuki Nishijima"
#
def initialize(context = {})
@client = self.class.instantiate_client(context)
end
class << self
# Creates a new instance of the GraphQL client for the service.
#
# # app/operations/github/user.graphql
# query($id: String!) {
# user(login: $id) {
# name
# }
# }
#
# # app/operations/github.rb
# class Github < Artemis::Client
# end
#
# github = Github.new
# github.user(id: 'yuki24').data.user.name # => "Yuki Nishijima"
#
# github = Github.new(context: { headers: { Authorization: "bearer ..." } })
# github.user(id: 'yuki24').data.user.name # => "Yuki Nishijima"
#
alias with_context new
# Returns the registered meta data (generally present in +config/graphql.yml+) for the client.
#
# # config/graphql.yml
# development:
# github:
# url: https://api.github.com/graphql
# adapter: :net_http
#
# # app/operations/github.rb
# class Github < Artemis::Client
# end
#
# Github.endpoint.url # => "https://api.github.com/graphql"
# Github.endpoint.adapter # => :net_http
#
def endpoint
Artemis::GraphQLEndpoint.lookup(name)
end
# Instantiates a new instance of +GraphQL::Client+ for the service.
#
# # app/operations/github/user.graphql
# query($id: String!) {
# user(login: $id) {
# name
# }
# }
#
# # app/operations/github.rb
# class Github < Artemis::Client
# end
#
# client = Github.instantiate_client
# client.query(Github::User, arguments: { id: 'yuki24' }) # makes a Graphql request
#
# client = Github.instantiate_client(context: { headers: { Authorization: "bearer ..." } })
# client.query(Github::User, arguments: { id: 'yuki24' }) # makes a Graphql request with Authorization header
#
def instantiate_client(context = {})
::GraphQL::Client.new(schema: endpoint.schema, execute: connection(context))
end
# Defines a callback that will get called right before the
# client's execute method is executed.
#
# class Github < Artemis::Client
#
# before_execute do |document, operation_name, variables, context|
# Analytics.log(operation_name, variables, context[:user_id])
# end
#
# ...
# end
#
def before_execute(&block)
self.before_callbacks += [block]
end
# Defines a callback that will get called right after the
# client's execute method has finished.
#
# class Github < Artemis::Client
#
# after_execute do |data, errors, extensions|
# if errors.present?
# Rails.logger.error(errors.to_json)
# end
# end
#
# ...
# end
#
def after_execute(&block)
self.after_callbacks += [block]
end
def resolve_graphql_file_path(filename, fragment: false)
filename = filename.to_s.underscore
graphql_file_paths.detect do |path|
path.end_with?("#{namespace}/#{filename}.graphql") || (fragment && path.end_with?("#{namespace}/_#{filename}.graphql"))
end
end
def graphql_file_paths
@graphql_file_paths ||= query_paths.flat_map {|path| Dir["#{path}/#{namespace}/*.graphql"] }
end
def namespace
name.underscore
end
def preload!
graphql_file_paths.each do |path|
load_constant(File.basename(path, File.extname(path)).camelize)
end
end
# Looks up the GraphQL file that matches the given +const_name+ and sets it to a constant.
#
# # app/operations/github.rb
# class Github < Artemis::Client
# end
#
# defined?(Github::User) # => nil
# Github.load_constant(:User) # => loads an operation definition from app/operations/github/user.graphql
# defined?(Github::User) # => 'constant'
#
# Github.load_constant(:None) # => nil
#
def load_constant(const_name)
graphql_file = resolve_graphql_file_path(const_name.to_s.underscore, fragment: true)
if graphql_file
graphql = File.open(graphql_file).read
ast = instantiate_client.parse(graphql)
const_set(const_name, ast)
end
end
alias load_query load_constant
def connection(context = {})
Executor.new(endpoint.connection, callbacks, default_context.deep_merge(context))
end
def execute(query, context: {}, **arguments)
new(default_context).execute(query, context: context, **arguments)
end
def multiplex(**context, &block)
queue = MultiplexQueue.new
wrapped_executor = Executor.new(queue, callbacks, default_context.deep_merge(context))
api_client = ::GraphQL::Client.new(schema: endpoint.schema, execute: wrapped_executor)
service_client = new
service_client.instance_variable_set(:@client, api_client)
block.call(service_client)
connection.multiplex(queue.queries, context: context)
end
private
# Looks up the GraphQL file that matches the given +const_name+ and sets it to a constant. If the files it not
# found it will raise an +NameError+.
#
# # app/operations/github.rb
# class Github < Artemis::Client
# end
#
# defined?(Github::User) # => nil
# Github::User # => loads an operation definition from app/operations/github/user.graphql
# defined?(Github::User) # => 'constant'
#
# Github::DoesNotExist # => raises an NameError
#
# @api private
def const_missing(const_name)
load_constant(const_name) || super
end
# Delegates a class method call to an instance method call, which in turn looks up the GraphQL file that matches
# the given +method_name+ and delegates the call to it.
#
# # app/operations/github.rb
# class Github < Artemis::Client
# end
#
# Github.user # => delegates to Github.new(default_context).user
#
# @api private
def method_missing(method_name, **arguments, &block)
if resolve_graphql_file_path(method_name)
new(default_context).public_send(method_name, **arguments, &block)
else
super
end
end
def respond_to_missing?(method_name, *_, &block) #:nodoc:
resolve_graphql_file_path(method_name) || super
end
# Returns a +Callbacks+ collection object that implements the interface for the +Executor+ object.
#
# @api private
def callbacks
Callbacks.new(before_callbacks, after_callbacks)
end
end
# Executes a given query, raises if we didn't define the operation
#
# @param [String] operation
# @param [Hash] context
# @param [Hash] arguments
#
# @return [GraphQL::Client::Response]
def execute(query, context: {}, **arguments)
if self.class.resolve_graphql_file_path(query)
const_name = query.to_s.camelize
# This check will be unnecessary once we drop support for Ruby 2.4 and earlier
if !self.class.const_get(const_name).is_a?(GraphQL::Client::OperationDefinition)
self.class.load_constant(const_name)
end
client.query(self.class.const_get(const_name), variables: arguments, context: context)
else
raise GraphQLFileNotFound.new("Query #{query}.graphql not found in: #{query_paths.join(", ")}")
end
end
private
# Delegates a method call to a GraphQL call.
#
# # app/operations/github.rb
# class Github < Artemis::Client
# end
#
# github = Github.new
# github.user # => delegates to app/operations/github/user.graphql
#
# @api private
def method_missing(method_name, context: {}, **arguments)
execute(method_name, context: context, **arguments)
rescue GraphQLFileNotFound
super
end
def respond_to_missing?(method_name, *_, &block) #:nodoc:
self.class.resolve_graphql_file_path(method_name) || super
end
# Internal collection object that holds references to the callback blocks.
#
# @api private
Callbacks = Struct.new(:before_callbacks, :after_callbacks) #:nodoc:
# Wrapper object around the adapter that wires up callbacks.
#
# @api private
class Executor < SimpleDelegator
def initialize(connection, callbacks, default_context) #:nodoc:
super(connection)
@callbacks = callbacks
@default_context = default_context
end
def execute(document:, operation_name: nil, variables: {}, context: {}) #:nodoc:
_context = @default_context.deep_merge(context)
@callbacks.before_callbacks.each do |callback|
callback.call(document, operation_name, variables, _context)
end
response = __getobj__.execute(document: document, operation_name: operation_name, variables: variables, context: _context)
@callbacks.after_callbacks.each do |callback|
callback.call(response['data'], response['errors'], response['extensions'])
end
response
end
end
class MultiplexQueue
attr_reader :queries
def initialize
@queries = []
end
def execute(document:, operation_name: nil, variables: {}, context: {}) #:nodoc:
@queries << {
query: document.to_query_string,
variables: variables.presence || {},
operationName: operation_name,
context: context
}
{}
end
end
private_constant :Callbacks, :Executor, :MultiplexQueue
end
end
================================================
FILE: lib/artemis/exceptions.rb
================================================
module Artemis
class Error < StandardError
end
class EndpointNotFound < Error
end
class ConfigurationError < Error
end
class GraphQLFileNotFound < Error
end
class FixtureNotFound < Error
end
class GraphQLError < Error
end
class GraphQLServerError < GraphQLError
end
end
================================================
FILE: lib/artemis/graphql_endpoint.rb
================================================
# frozen_string_literal: true
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/inflections'
require 'graphql/client'
require 'artemis/adapters'
require 'artemis/exceptions'
module Artemis
class GraphQLEndpoint
# Whether or not to suppress warnings on schema load. Use it with caution.
#
# @private
cattr_accessor :suppress_warnings_on_schema_load
self.suppress_warnings_on_schema_load = false
# Hash object that holds references to adapter instances.
ENDPOINT_INSTANCES = {}
private_constant :ENDPOINT_INSTANCES
class << self
##
# Provides an endpoint instance specified in the +configuration+. If the endpoint is not found in
# +ENDPOINT_INSTANCES+, it'll raise an exception.
def lookup(service_name)
ENDPOINT_INSTANCES[service_name.to_s.underscore] || raise(Artemis::EndpointNotFound, "Service `#{service_name}' not registered.")
end
def register!(service_name, configurations)
ENDPOINT_INSTANCES[service_name.to_s.underscore] = new(service_name.to_s, **configurations.symbolize_keys)
end
##
# Returns the registered services as an array.
#
def registered_services
ENDPOINT_INSTANCES.keys
end
end
attr_reader :name, :url, :adapter, :timeout, :schema_path, :pool_size, :adapter_options
def initialize(name, url: nil, adapter: :net_http, timeout: 10, schema_path: nil, pool_size: 25, adapter_options: {})
@name = name.to_s
@url = url
@adapter = adapter
@timeout = timeout
@schema_path = schema_path
@pool_size = pool_size
@adapter_options = adapter_options
@mutex_for_schema = Mutex.new
@mutex_for_connection = Mutex.new
end
def schema
org, $stderr = $stderr, File.new("/dev/null", "w") if self.class.suppress_warnings_on_schema_load
@schema || @mutex_for_schema.synchronize do
@schema ||= ::GraphQL::Client.load_schema(schema_path.presence || connection)
end
ensure
$stderr = org if self.class.suppress_warnings_on_schema_load
end
alias load_schema! schema
def connection
@connection || @mutex_for_connection.synchronize do
@connection ||= ::Artemis::Adapters.lookup(adapter).new(url, service_name: name, timeout: timeout, pool_size: pool_size, adapter_options: adapter_options)
end
end
end
end
================================================
FILE: lib/artemis/railtie.rb
================================================
require 'active_support/file_update_checker'
module Artemis
class Railtie < ::Rails::Railtie #:nodoc:
config.artemis = ActiveSupport::OrderedOptions.new
config.artemis.query_path = "app/operations"
config.artemis.fixture_path = "test/fixtures/graphql"
config.artemis.schema_path = "vendor/graphql/schema"
config.artemis.graphql_extentions = ["graphql"]
initializer 'graphql.client.attach_log_subscriber' do
if !defined?(GraphQL::Client::LogSubscriber)
require "graphql/client/log_subscriber"
GraphQL::Client::LogSubscriber.attach_to :graphql
end
end
initializer 'graphql.client.set_query_paths' do |app|
query_path = config.artemis.query_path
app.paths.add query_path
Artemis::Client.query_paths = app.paths[query_path].existent
end
initializer 'graphql.test_helper' do |app|
if !Rails.env.production?
require 'artemis/test_helper'
Artemis::TestHelper.__graphql_fixture_path__ = app.root.join(config.artemis.fixture_path)
end
end
initializer 'graphql.client.set_reloader', after: 'graphql.client.set_query_paths' do |app|
not_on_zeitwerk = !defined?(Zeitwerk) || (app.config.respond_to?(:autoloader) && app.config.autoloader != :zeitwerk)
if not_on_zeitwerk
files_to_watch = Artemis::Client.query_paths.map {|path| [path, config.artemis.graphql_extentions] }.to_h
app.reloaders << ActiveSupport::FileUpdateChecker.new([], files_to_watch) do
Artemis.config_for_graphql(app).each_key do |endpoint_name|
Artemis::Client.query_paths.each do |path|
FileUtils.touch("#{path}/#{endpoint_name}.rb") if File.exist?("#{path}/#{endpoint_name}.rb")
end
end
end
end
end
initializer 'graphql.client.load_config' do |app|
if Pathname.new("#{app.paths["config"].existent.first}/graphql.yml").exist?
Artemis.config_for_graphql(app).each do |endpoint_name, options|
Artemis::GraphQLEndpoint
.register!(
endpoint_name,
schema_path: app.root.join(config.artemis.schema_path, "#{endpoint_name}.json").to_s,
**options.symbolize_keys
)
end
end
end
initializer 'graphql.client.preload', after: 'graphql.client.load_config' do |app|
not_on_zeitwerk = !defined?(Zeitwerk) || (app.config.respond_to?(:autoloader) && app.config.autoloader != :zeitwerk)
if app.config.eager_load && app.config.cache_classes && not_on_zeitwerk
Artemis::GraphQLEndpoint.registered_services.each do |endpoint_name|
endpoint_name.camelize.constantize.preload!
end
end
end
rake_tasks do
load "tasks/artemis.rake"
end
end
end
================================================
FILE: lib/artemis/rspec.rb
================================================
require 'artemis/test_helper'
RSpec.configure do |config|
config.include ::Artemis::TestHelper
config.before :each do
graphql_requests.clear
graphql_responses.clear
end
end
================================================
FILE: lib/artemis/test_helper.rb
================================================
# frozen_string_literal: true
require 'erb'
require 'yaml'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/inflections'
require 'artemis/exceptions'
module Artemis
# TODO: Write documentation for +TestHelper+
module TestHelper
mattr_accessor :__graphql_fixture_path__
# Creates an object that stubs a GraphQL request for the given +service+. No mock response is registered until the
# +to_return+ method.
#
# # test/fixtures/graphql/metaphysics/artist.yml
# leonardo_da_vinci:
# data:
# artist:
# name: Leonardo da Vinci
# birthday: 1452/04/15
#
# # In a test:
# stub_graphql(Metaphysics, :artist).to_return(:leonardo_da_vinci)
#
# response = Metaphysics.artist(id: "leonardo-da-vinci")
#
# response.data.artist.name # => "Leonardo da Vinci"
# response.data.artist.birthday # => "1452/04/15"
#
# Test responses could also be parameterized by specifying the +arguments+ argument for the query name.
#
# stub_graphql(Metaphysics, :artist, id: "pablo-picasso").to_return(:pablo_picasso)
# stub_graphql(Metaphysics, :artist, id: "leonardo-da-vinci").to_return(:leonardo_da_vinci)
#
# pablo_picasso = Metaphysics.artist(id: "pablo-picasso")
# da_vinci = Metaphysics.artist(id: "leonardo-da-vinci")
#
# pablo_picasso.data.artist.name # => "Pablo Picasso"
# da_vinci.data.artist.name # => "Leonardo da Vinci"
#
def stub_graphql(service, query_name, arguments = :__unspecified__)
StubbingDSL.new(service.to_s, query_name, graphql_fixture_files, arguments)
end
# Returns out-going GraphQL requests.
#
def graphql_requests
Artemis::Adapters::TestAdapter.requests
end
private
def graphql_responses #:nodoc:
Artemis::Adapters::TestAdapter.responses
end
def graphql_fixture_path #:nodoc:
__graphql_fixture_path__ || raise(Artemis::ConfigurationError, "GraphQL fixture path is unset")
end
def graphql_fixture_files #:nodoc:
@graphql_fixture_sets ||= Dir["#{graphql_fixture_path}/{**,*}/*.{yml,json}"]
.uniq
.select {|file| ::File.file?(file) }
.map {|file| GraphQLFixture.new(File.basename(file, File.extname(file)), file, read_erb_yaml(file)) }
end
def read_erb_yaml(path) #:nodoc:
if YAML.method(:load).parameters.any? { |_arg_type, arg_name| arg_name == :aliases }
YAML.load(ERB.new(File.read(path)).result, aliases: true)
else
YAML.load(ERB.new(File.read(path)).result)
end
end
class StubbingDSL #:nodoc:
attr_reader :service_name, :query_name, :fixture_sets, :arguments
def initialize(service_name, query_name, fixture_sets, arguments) #:nodoc:
@service_name, @query_name, @fixture_sets, @arguments = service_name, query_name, fixture_sets, arguments
end
def get(fixture_key)
fixture_set = find_fixture_set
fixture = fixture_set.data[fixture_key.to_s]
if fixture.nil?
raise Artemis::FixtureNotFound, "Fixture `#{fixture_key}' not found in #{fixture_set.path}"
end
fixture
end
def to_return(fixture_key) #:nodoc:
fixture_set = find_fixture_set
fixture = fixture_set.data[fixture_key.to_s]
if fixture.nil?
raise Artemis::FixtureNotFound, "Fixture `#{fixture_key}' not found in #{fixture_set.path}"
end
Artemis::Adapters::TestAdapter.responses <<
TestResponse.new(
"#{service_name}__#{fixture_set.name.to_s.camelcase}",
arguments.respond_to?(:deep_stringify_keys) ? arguments.deep_stringify_keys : arguments,
fixture
)
end
private
def find_fixture_set
fixture_set = fixture_sets
.detect { |fixture| %r{#{service_name.underscore}/#{query_name}\.(yml|json)\z} =~ fixture.path }
fixture_set ||= fixture_sets.detect { |fixture| fixture.name == query_name.to_s }
if fixture_set.nil?
raise Artemis::FixtureNotFound, "Fixture file `#{query_name}.{yml,json}' not found"
end
fixture_set
end
end
TestResponse = Struct.new(:operation_name, :arguments, :data) #:nodoc:
GraphQLFixture = Struct.new(:name, :path, :data) #:nodoc
private_constant :GraphQLFixture, :StubbingDSL, :TestResponse
end
end
================================================
FILE: lib/artemis/version.rb
================================================
module Artemis
VERSION = "1.1.0"
end
================================================
FILE: lib/artemis.rb
================================================
require "artemis/version"
require "artemis/client"
require "artemis/railtie" if defined?(Rails)
module Artemis
def self.config_for_graphql(app)
if app.respond_to?(:config_for)
app.config_for(:graphql)
else
config_for(:graphql, app: app)
end
end
# backported from https://github.com/rails/rails/blob/b9ca94ca/railties/lib/rails/application.rb#L226
# TODO: Remove once dropping Rails <= 4.1 support
def self.config_for(name, app:, env: Rails.env)
if name.is_a?(Pathname)
yaml = name
else
yaml = Pathname.new("#{app.paths["config"].existent.first}/#{name}.yml")
end
if yaml.exist?
require "erb"
(YAML.load(ERB.new(yaml.read).result) || {})[env] || {}
else
raise "Could not load configuration. No such file - #{yaml}"
end
rescue Psych::SyntaxError => e
raise "YAML syntax error occurred while parsing #{yaml}. " \
"Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
"Error: #{e.message}"
end
end
================================================
FILE: lib/generators/artemis/install/USAGE
================================================
Description:
Generates a client stub and config files and downloads the schema from the given endpoint.
Example:
rails generate artemis:install Artsy https://metaphysics-production.artsy.net
This will create:
app/operations/artsy.rb
app/operations/artsy/.gitkeep
config/graphql.yml
vendor/graphql/schema/artsy.json
================================================
FILE: lib/generators/artemis/install/install_generator.rb
================================================
require 'graphql/client'
require 'graphql/client/http'
class Artemis::InstallGenerator < Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)
argument :endpoint_url, type: :string, banner: "The endpoint URL for a GraphQL service"
class_option :authorization, type: :string, default: nil, aliases: "-A"
def generate_client
template "client.rb", client_file_name
create_file query_dir_gitkeep, ""
end
def generate_config
in_root do
if behavior == :invoke && !File.exist?(config_file_name)
template "graphql.yml", config_file_name
else
inject_into_file config_file_name, <<-YAML, after: "development:\n"
#{file_name}:
<<: *default
url: #{endpoint_url}\n
YAML
inject_into_file config_file_name, <<-YAML, after: "test:\n", force: true
#{file_name}:
<<: *default
url: #{endpoint_url}\n
YAML
inject_into_file config_file_name, <<-YAML, after: "production:\n", force: true
#{file_name}:
<<: *default
url: #{endpoint_url}\n
YAML
end
end
end
def download_schema
say " downloading GraphQL schema from #{endpoint_url}..."
if options['authorization'].present?
rake "graphql:schema:update SERVICE=#{file_name} AUTHORIZATION='#{options['authorization']}'"
else
rake "graphql:schema:update SERVICE=#{file_name}"
end
end
private
def file_name # :doc:
@_file_name ||= super.underscore
end
def client_file_name
if respond_to?(:mountable_engine?) && mountable_engine?
"app/operations/#{namespaced_path}/#{file_name}.rb"
else
"app/operations/#{file_name}.rb"
end
end
def query_dir_gitkeep
if respond_to?(:mountable_engine?) && mountable_engine?
"app/operations/#{namespaced_path}/#{file_name}/.gitkeep"
else
"app/operations/#{file_name}/.gitkeep"
end
end
def config_file_name
"config/graphql.yml"
end
end
================================================
FILE: lib/generators/artemis/install/templates/client.rb
================================================
<% module_namespacing do -%>
class <%= class_name %> < Artemis::Client
# If an access token needs to be assigned to every request:
# self.default_context = {
# headers: {
# Authorization: "token ..."
# }
# }
end
<% end %>
================================================
FILE: lib/generators/artemis/install/templates/graphql.yml
================================================
default: &default
# The underlying client library that actually makes an HTTP request.
# Available adapters are :net_http, :net_http_persistent, :curb, and :test.
#
# It is set to :net_http by default.
adapter: :net_http
# HTTP timeout set for the adapter in seconds. This will be set to both
# `read_timeout` and `write_timeout` and there is no way to configure them
# with a different value as of writing (PRs welcome!)
#
# It is set to 10 by default.
timeout: 10
# The number of keep-alive connections. The `:net_http` adapter will ignore
# this option.
#
# It is set to 25 by default.
pool_size: 25
development:
<%= file_name %>:
<<: *default
url: <%= endpoint_url %>
test:
<%= file_name %>:
<<: *default
url: <%= endpoint_url %>
production:
<%= file_name %>:
<<: *default
url: <%= endpoint_url %>
================================================
FILE: lib/generators/artemis/mutation/USAGE
================================================
Description:
Generates a mutation stub.
rails g artemis:mutation MUTATION_TYPE [FILE_NAME]
Example:
rails g artemis:mutation followArtist
This will create:
app/operations/artsy.rb
app/operations/artsy/follow_artist.graphql
The GraphQL file name could be specified by giving the third argument:
rails g artemis:mutation followArtist follow_artist_on_artwork
This will create:
app/operations/artsy.rb
app/operations/artsy/follow_artist_on_artwork.graphql
If there are multiple services registered, the service could be specified with +--service+ option:
rails g artemis:mutation createProject --service github
This will create:
app/operations/github.rb
app/operations/github/create_project.graphql
================================================
FILE: lib/generators/artemis/mutation/mutation_generator.rb
================================================
# frozen_string_literal: true
require 'graphql/schema/finder'
class Artemis::MutationGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__)
argument :mutation_type, type: :string, required: true, banner: "Mutation type"
argument :graphql_file_name, type: :string, required: false, default: nil, banner: "The name of the GraphQL file to be generated"
class_option :service, type: :string, default: nil, aliases: "-A"
def generate_mutation_file
template "mutation.graphql", graphql_file_path
end
private
def mutation_name
mutation_type.underscore
end
def graphql_file_path
"app/operations/#{service_name.underscore}/#{graphql_file_name.presence || mutation_name}.graphql"
end
def arguments
target_mutation.arguments
end
def target_mutation
schema.find("Mutation").fields[mutation_type] ||
raise(GraphQL::Schema::Finder::MemberNotFoundError, "Could not find type `#{mutation_type}` in schema.")
end
def schema
service_name.camelize.constantize.endpoint.schema
end
def service_name
options['service'].presence || begin
services = Artemis::GraphQLEndpoint.registered_services
if services.size == 1
services.first
else
fail "Please specify a service name (available services: #{services.join(", ")}):\n\n" \
" rails g artemis:mutation #{mutation_type} #{graphql_file_name} --service SERVICE"
end
end
end
end
================================================
FILE: lib/generators/artemis/mutation/templates/mutation.graphql
================================================
mutation<%= arguments.present? && "(#{ arguments.map {|name, type| "$#{name}: #{type.type.to_type_signature}" }.join(", ") })" %> {
<%= target_mutation.name %><%= arguments.present? && "(#{ arguments.map {|name, type| "#{name}: $#{name}" }.join(", ") })" %> {
# Add fields here...
}
}
================================================
FILE: lib/generators/artemis/query/USAGE
================================================
Description:
Generates a query stub.
rails g artemis:query QUERY_TYPE [FILE_NAME]
Example:
rails g artemis:query artist
This will create:
app/operations/artsy.rb
app/operations/artsy/artist.graphql
The GraphQL file name could be specified by giving the third argument:
rails g artemis:query artist artist_in_tooltip
This will create:
app/operations/artsy.rb
app/operations/artsy/artist_in_tooltip.graphql
If there are multiple services registered, the service could be specified with +--service+ option:
rails g artemis:query repository --service github
This will create:
app/operations/github.rb
app/operations/github/repository.graphql
================================================
FILE: lib/generators/artemis/query/query_generator.rb
================================================
# frozen_string_literal: true
require 'graphql/schema/finder'
class Artemis::QueryGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__)
argument :query_type, type: :string, required: true, banner: "Query type"
argument :graphql_file_name, type: :string, required: false, default: nil, banner: "The name of the GraphQL file to be generated"
class_option :service, type: :string, default: nil, aliases: "-A"
def generate_query_file
template "query.graphql", graphql_file_path
end
# def generate_text_fixture_file
# template "fixture.yml", text_fixture_path
# end
private
def query_name
query_type.underscore
end
def graphql_file_path
"app/operations/#{service_name.underscore}/#{qualified_name}.graphql"
end
def text_fixture_path
File.join(Artemis::Railtie.config.artemis.fixture_path, service_name.underscore, "#{qualified_name}.yml")
end
def arguments
target_query.arguments
end
def target_query
schema.query.fields[query_type] ||
raise(GraphQL::Schema::Finder::MemberNotFoundError, "Could not find type `#{query_type}` in schema.")
end
def schema
service_name.camelize.constantize.endpoint.schema
end
def service_name
options['service'].presence || begin
services = Artemis::GraphQLEndpoint.registered_services
if services.size == 1
services.first
else
fail "Please specify a service name (available services: #{services.join(", ")}):\n\n" \
" rails g artemis:query #{query_type} #{graphql_file_name} --service SERVICE"
end
end
end
def qualified_name
graphql_file_name.presence || query_name
end
end
================================================
FILE: lib/generators/artemis/query/templates/fixture.yml
================================================
# You can stub GraphQL queries by calling the `stub_graphql' method in test:
#
# stub_graphql(<%= service_name.camelize %>, :<%= qualified_name.underscore %>).to_return(:<%= target_query.name %>_1)
#
# Or with a arguments matcher:
#
# stub_graphql(<%= service_name.camelize %>, :<%= qualified_name.underscore %>, <%= arguments.map {|name, _| "#{name}: \"...\"" }.join(", ") %>).to_return(:<%= target_query.name %>_2)
#
<%= target_query.name %>_1:
data:
<% target_query.type.fields.values.each do |field| -%>
<%= field.name %>: # type: <%= field.type.to_type_signature %>
<% end %>
<%= target_query.name %>_2:
data:
<% target_query.type.fields.values.each do |field| -%>
<%= field.name %>: # type: <%= field.type.to_type_signature %>
<% end %>
================================================
FILE: lib/generators/artemis/query/templates/query.graphql
================================================
query<%= arguments.present? ? "(#{ arguments.map {|name, type| "$#{name}: #{type.type.to_type_signature}" }.join(", ") })" : "" %> {
<%= target_query.name %><%= arguments.present? ? "(#{ arguments.map {|name, type| "#{name}: $#{name}" }.join(", ") })" : "" %> {
# Add fields here...
}
}
================================================
FILE: lib/tasks/artemis.rake
================================================
# frozen_string_literal: true
require 'json'
require 'active_support/core_ext/string/inflections'
require 'graphql/client'
namespace :graphql do
namespace :schema do
desc "Downloads and saves the GraphQL schema (options: SERVICE=service_name AUTHORIZATION='token ...')"
task update: :environment do
service = if ENV['SERVICE']
ENV['SERVICE']
else
services = Artemis.config_for_graphql(Rails.application).keys
if services.size == 1
services.first
else
raise "Please specify a service name (available services: #{services.join(", ")}): rake graphql:schema:update SERVICE=service"
end
end
headers = ENV['AUTHORIZATION'] ? { Authorization: ENV['AUTHORIZATION'] } : {}
service_class = service.to_s.camelize.constantize
schema_path = service_class.endpoint.schema_path
schema = service_class.connection
.execute(
document: GraphQL::Client::IntrospectionDocument,
operation_name: "IntrospectionQuery",
variables: {},
context: { headers: headers }
).to_h
if schema['errors'].nil? || schema['errors'].empty?
FileUtils.mkdir_p(File.dirname(schema_path))
File.open(schema_path, 'w') do |file|
file.write(JSON.pretty_generate(schema))
end
puts "saved schema to: #{schema_path.gsub("#{Dir.pwd}/", '')}"
else
raise "received error from server: #{schema}\n\n"
end
end
end
end
================================================
FILE: test/adapters_test.rb
================================================
require_relative 'helpers/test_helper'
require 'artemis/adapters/abstract_adapter'
class AdaptersTest < ActiveSupport::TestCase
Artemis::Adapters::AbstractAdapter.send(:attr_writer, :uri, :timeout)
test "NetHttpAdapter behaves like an adapter" do
adapter = Artemis::Adapters::NetHttpAdapter.new("http://localhost:8000", service_name: nil, timeout: 0.5, pool_size: 5)
assert_adapter adapter, Net::ReadTimeout
end
# Using Net::HTTP::Persistent some times gets the CI build stuck, so avoiding it in CI for now."
# unless ENV["CI"]
# test "NetHttpPersistentAdapter behaves like an adapter" do
# adapter = Artemis::Adapters::NetHttpPersistentAdapter.new("http://localhost:8000", service_name: nil, timeout: 0.5, pool_size: 5)
#
# assert_adapter adapter, Net::ReadTimeout
# ensure
# # Make sure the connection is closed otherwise the webrick server wouldn't be able to shut down.
# adapter.instance_variable_get(:@raw_connection)&.shutdown
# end
# end
# if RUBY_ENGINE == 'ruby'
# test "CurbAdapter behaves like an adapter" do
# adapter = Artemis::Adapters::CurbAdapter.new("http://localhost:8000", service_name: nil, timeout: 2, pool_size: 5)
#
# assert_adapter adapter, Curl::Err::TimeoutError
# end
# end
test 'MultiDomainAdapter makes an actual HTTP request' do
adapter = Artemis::Adapters::MultiDomainAdapter.new('ignored', service_name: nil, timeout: 0.5, pool_size: 5, adapter_options: { adapter: :net_http })
response = adapter.execute(document: GraphQL::Client::IntrospectionDocument, context: { url: "http://localhost:8000/test_multi_domain" })
assert_equal "Endpoint switched.", response['data']['body']
assert_equal [], response['errors']
assert_equal({}, response['extensions'])
end
test 'MultiDomainAdapter can make a multiplex (the graphql feature, not HTTP/2) request' do
adapter = Artemis::Adapters::MultiDomainAdapter.new('ignored', service_name: nil, timeout: 0.5, pool_size: 5, adapter_options: { adapter: :net_http })
response = adapter.multiplex(
[
{
query: GraphQL::Client::IntrospectionDocument.to_query_string,
operationName: 'IntrospectionQuery',
variables: {
id: 'yayoi-kusama'
},
},
],
context: {
url: "http://localhost:8000/test_multi_domain"
}
)
assert_equal "Endpoint switched.", response['data']['body']
assert_equal [], response['errors']
assert_equal({}, response['extensions'])
end
test 'MultiDomainAdapter can make a multiplex request with custom HTTP headers' do
adapter = Artemis::Adapters::MultiDomainAdapter.new('ignored', service_name: nil, timeout: 0.5, pool_size: 5, adapter_options: { adapter: :net_http })
response = adapter.multiplex(
[
{
query: GraphQL::Client::IntrospectionDocument.to_query_string,
operationName: 'IntrospectionQuery',
},
],
context: {
headers: {
Authorization: "Token token",
},
url: "http://localhost:8000/test_multi_domain"
}
)
assert_equal "token token", response['data']['headers']['AUTHORIZATION']
end
test 'MultiDomainAdapter raises an error when adapter_options.adapter is set to :multi domain' do
assert_raises(ArgumentError, "You can not use the :multi_domain adapter with the :multi_domain adapter.") do
Artemis::Adapters::MultiDomainAdapter.new('ignored', service_name: nil, timeout: 0.5, pool_size: 5, adapter_options: { adapter: :multi_domain })
end
end
test 'MultiDomainAdapter raises an error when context.url is not specified' do
adapter = Artemis::Adapters::MultiDomainAdapter.new('ignored', service_name: nil, timeout: 0.5, pool_size: 5, adapter_options: { adapter: :net_http })
message = 'The MultiDomain adapter requires a url on every request. Please specify a ' \
'url with a context: Client.with_context(url: "https://awesomeshop.domain.conm")'
assert_raises(ArgumentError, message) do
adapter.execute(document: GraphQL::Client::IntrospectionDocument)
end
end
test 'MultiDomainAdapter raises an error when it receives a server error' do
adapter = Artemis::Adapters::MultiDomainAdapter.new('ignored', service_name: nil, timeout: 0.5, pool_size: 5, adapter_options: { adapter: :net_http })
assert_raises(Artemis::GraphQLServerError, "Received server error status 500: Server error") do
adapter.execute(document: GraphQL::Client::IntrospectionDocument, context: { url: "http://localhost:8000/500" })
end
end
test 'MultiDomainAdapter allows for overriding timeout' do
adapter = Artemis::Adapters::MultiDomainAdapter.new('ignored', service_name: nil, timeout: 0.5, pool_size: 5, adapter_options: { adapter: :net_http })
assert_raises(Net::ReadTimeout) do
adapter.execute(document: GraphQL::Client::IntrospectionDocument, context: { url: "http://localhost:8000/slow_server" })
end
end
private
def assert_adapter(adapter, timeout_error)
assert_adapter_initialization adapter
assert_adapter_execution adapter
assert_adapter_server_error adapter
assert_adapter_timeout adapter, timeout_error
# assert_adapter_multiplex adapter
assert_adapter_multiplex_server_error adapter
assert_adapter_multiplex_timeout adapter, timeout_error
end
def assert_adapter_initialization(adapter)
assert_raises(ArgumentError, "url is required (given `nil`)") do
adapter.class.new(nil, service_name: nil, timeout: 2, pool_size: 5)
end
end
def assert_adapter_execution(adapter)
response = adapter.execute(
document: GraphQL::Client::IntrospectionDocument,
operation_name: 'IntrospectionQuery',
variables: { id: 'yayoi-kusama' },
context: { user_id: 1 }
)
assert_equal GraphQL::Client::IntrospectionDocument.to_query_string, response['data']['body']['query']
assert_equal({ 'id' => 'yayoi-kusama' }, response['data']['body']['variables'])
assert_equal 'IntrospectionQuery', response['data']['body']['operationName']
assert_equal 'application/json', response['data']['headers']['CONTENT_TYPE']
assert_equal 'application/json', response['data']['headers']['ACCEPT']
assert_equal [], response['errors']
assert_equal({}, response['extensions'])
end
def assert_adapter_server_error(adapter)
adapter.uri = URI.parse("http://localhost:8000/500")
assert_raises(Artemis::GraphQLServerError, "Received server error status 500: Server error") do
adapter.execute(document: GraphQL::Client::IntrospectionDocument, operation_name: 'IntrospectionQuery')
end
end
def assert_adapter_timeout(adapter, timeout_error)
adapter.uri = URI.parse("http://localhost:8000/slow_server")
assert_raises(timeout_error) do
adapter.execute(document: GraphQL::Client::IntrospectionDocument, operation_name: 'IntrospectionQuery')
end
end
def assert_adapter_multiplex(adapter)
response = adapter.multiplex(
[
{
query: GraphQL::Client::IntrospectionDocument.to_query_string,
operationName: 'IntrospectionQuery',
variables: {
id: 'yayoi-kusama'
},
},
],
context: {
user_id: 1
}
)
introspection_query = response[0]
assert_equal GraphQL::Client::IntrospectionDocument.to_query_string, introspection_query['data']['body']['query']
assert_equal({ 'id' => 'yayoi-kusama' }, introspection_query['data']['body']['variables'])
assert_equal 'IntrospectionQuery', introspection_query['data']['body']['operationName']
assert_equal 'application/json', introspection_query['data']['headers']['CONTENT_TYPE']
assert_equal 'application/json', introspection_query['data']['headers']['ACCEPT']
assert_equal [], introspection_query['errors']
assert_equal({}, introspection_query['extensions'])
end
def assert_adapter_multiplex_server_error(adapter)
adapter.uri = URI.parse("http://localhost:8000/500")
assert_raises(Artemis::GraphQLServerError, "Received server error status 500: Server error") do
adapter.multiplex([])
end
end
def assert_adapter_multiplex_timeout(adapter, timeout_error)
adapter.uri = URI.parse("http://localhost:8000/slow_server")
assert_raises(timeout_error) do
adapter.multiplex([])
end
end
end
================================================
FILE: test/autoloading_test.rb
================================================
require_relative 'helpers/test_helper'
class AutoLoadingTest < ActiveSupport::TestCase
test ".load_constant loads the specified constant if there is a matching graphql file" do
Github.send(:remove_const, :User) if Github.constants.include?(:User)
Github.load_constant(:User)
assert_equal 'constant', defined?(Github::User)
end
test ".load_constant does nothing and returns nil if there is no matching file" do
assert_nil Github.load_constant(:DoesNotExist)
end
test ".preload! preloads all the graphQL files in the query paths" do
%i(User UserRepositories Repository RepositoryFields)
.select {|const_name| Github.constants.include?(const_name) }
.each {|const_name| Github.send(:remove_const, const_name) }
Github.preload!
assert_equal 'constant', defined?(Github::User)
assert_equal 'constant', defined?(Github::Repository)
end
test "dynamically loads the matching GraphQL query and sets it to a constant" do
Github.send(:remove_const, :User) if Github.constants.include?(:User)
query = Github::User
assert_equal <<~GRAPHQL.strip, query.document.to_query_string
query Github__User {
user(login: "yuki24") {
id
name
}
}
GRAPHQL
end
test "dynamically loads the matching GraphQL fragment and sets it to a constant" do
Github.send(:remove_const, :RepositoryFields) if Github.constants.include?(:RepositoryFields)
query = Github::RepositoryFields
assert_equal <<~GRAPHQL.strip, query.document.to_query_string
fragment Github__RepositoryFields on Repository {
name
nameWithOwner
url
updatedAt
languages(first: 1) {
nodes {
name
color
}
}
}
GRAPHQL
end
test "correctly loads the matching GraphQL query even when the top-level constant with the same name exists" do
# In Ruby <= 2.4 top-level constants can be looked up through a namespace, which turned out to be a bad practice.
# This has been removed in 2.5, but in earlier versions still suffer from this behaviour.
Github.send(:remove_const, :User) if Github.constants.include?(:User)
Object.send(:remove_const, :User) if Object.constants.include?(:User)
begin
Object.send(:const_set, :User, 1)
Github.user
ensure
Object.send(:remove_const, :User)
end
query = Github::User
assert_equal <<~GRAPHQL.strip, query.document.to_query_string
query Github__User {
user(login: "yuki24") {
id
name
}
}
GRAPHQL
end
test "raises an exception when the path was resolved but the file does not exist" do
begin
Github.graphql_file_paths << "github/removed.graphql"
assert_raises(Errno::ENOENT) { Github::Removed }
ensure
Github.graphql_file_paths.delete("github/removed.graphql")
end
end
test "raises an NameError when there is no graphql file that matches the const name" do
assert_raises(NameError) { Github::DoesNotExist }
end
test "defines the query method when the matching class method gets called for the first time" do
skip
Github.undef_method(:user) if Github.public_instance_methods.include?(:user)
Github.user
expect(Github.public_instance_methods).to include(:user)
end
test "raises an NameError when there is no graphql file that matches the class method name" do
assert_raises(NameError) { Github.does_not_exist }
end
test "raises an NameError when the class method name matches a fragment name" do
assert_raises(NameError) { Github.repository_fields_fragment }
end
test "responds to a class method that has a matching graphQL file" do
assert_respond_to Github, :user
end
test "does not respond to class methods that do not have a matching graphQL file" do
assert_not_respond_to Github, :does_not_exist
end
test "defines the query method when the matching instance method gets called for the first time" do
skip
Github.undef_method(:user) if Github.public_instance_methods.include?(:user)
Github.new.user
expect(Github.public_instance_methods).to include(:user)
end
test "raises an NameError when there is no graphql file that matches the instance method name" do
assert_raises(NameError) { Github.new.does_not_exist }
end
test "raises an NameError when the instance method name matches a fragment name" do
assert_raises(NameError) { Github.new.repository_fields_fragment }
end
test "responds to the method that has a matching graphQL file" do
assert_respond_to Github.new, :user
end
test "does not respond to methods that do not have a matching graphQL file" do
assert_not_respond_to Github.new, :does_not_exist
end
end
================================================
FILE: test/backport/method_call_assertions.rb
================================================
require "minitest/mock"
module MethodCallAssertions # :nodoc:
private
def assert_called(object, method_name, message = nil, times: 1, returns: nil)
times_called = 0
object.stub(method_name, proc { times_called += 1; returns }) { yield }
error = "Expected #{method_name} to be called #{times} times, " \
"but was called #{times_called} times"
error = "#{message}.\n#{error}" if message
assert_equal times, times_called, error
end
def assert_called_with(object, method_name, args = [], returns: nil)
mock = Minitest::Mock.new
if args.all? { |arg| arg.is_a?(Array) }
args.each { |arg| mock.expect(:call, returns, arg) }
else
mock.expect(:call, returns, args)
end
object.stub(method_name, mock) { yield }
mock.verify
end
def assert_not_called(object, method_name, message = nil, &block)
assert_called(object, method_name, message, times: 0, &block)
end
def stub_any_instance(klass, instance: klass.new)
klass.stub(:new, instance) { yield instance }
end
end
================================================
FILE: test/callbacks_test.rb
================================================
require_relative 'helpers/test_helper'
require 'active_support/core_ext/module/attribute_accessors'
class CallbacksTest < ActiveSupport::TestCase
Client = Class.new(Artemis::Client) do
def self.name
'Github'
end
mattr_accessor :before_callback, :after_callback
self.before_callback = nil
self.after_callback = nil
before_execute do |document, operation_name, variables, context|
self.before_callback = document, operation_name, variables, context
end
after_execute do |data, errors, extensions|
self.after_callback = data, errors, extensions
end
end
Spotify = Class.new(Artemis::Client) do
def self.name
'Spotify'
end
before_execute do
raise "this callback should not get invoked"
end
after_execute do
raise "this callback should not get invoked"
end
end
test ".before_execute gets invoked before executing" do
Client.repository(owner: "yuki24", name: "artemis", context: { user_id: 'yuki24' })
document, operation_name, variables, context = Client.before_callback
assert_equal Client::Repository.document, document
assert_equal 'CallbacksTest__Client__Repository', operation_name
assert_equal({ "name" => "artemis", "owner" => "yuki24" }, variables)
assert_equal({ user_id: 'yuki24' }, context)
end
test ".after_execute gets invoked after executing" do
Client.user
data, errors, extensions = Client.after_callback
assert_equal({ "test" => "data" }, data)
assert_equal [], errors
assert_equal({}, extensions)
end
end
================================================
FILE: test/client_test.rb
================================================
require 'helpers/test_helper'
class ClientTest < ActiveSupport::TestCase
setup do
requests.clear
end
test ".lookup_graphql_file returns the path to the matching graph file" do
assert_equal "#{PROJECT_DIR}/test/fixtures/github/user.graphql", Github.resolve_graphql_file_path("user")
end
test ".lookup_graphql_file returns nil if the file is missing" do
assert_nil Github.resolve_graphql_file_path("does_not_exist")
end
test ".graphql_file_paths returns a list of GraphQL files (*.graphql) in the query_paths" do
Github.instance_variable_set :@graphql_file_paths, nil
original = Github.query_paths
Github.query_paths = [File.join(PROJECT_DIR, 'tmp')]
begin
FileUtils.mkdir "./tmp/github" if !Dir.exist?("./tmp/github")
with_files "./tmp/github/text.txt", "./tmp/github/sale.graphql" do
assert_equal ["#{PROJECT_DIR}/tmp/github/sale.graphql"], Github.graphql_file_paths
end
ensure
Github.instance_variable_set :@graphql_file_paths, nil
Github.query_paths = original
end
end
test "can make a GraphQL request without variables" do
Github.user
request = requests[0]
assert_equal 'Github__User', request.operation_name
assert_empty request.variables
assert_equal({}, request.context)
assert_equal <<~GRAPHQL.strip, request.document.to_query_string
query Github__User {
user(login: "yuki24") {
id
name
}
}
GRAPHQL
end
test "can make a GraphQL request with variables" do
Github.repository(owner: "yuki24", name: "artemis")
request = requests[0]
assert_equal 'Github__Repository', request.operation_name
assert_equal({ "owner" => "yuki24", "name" => "artemis" }, request.variables)
assert_equal({}, request.context)
assert_equal <<~GRAPHQL.strip, request.document.to_query_string
query Github__Repository($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
name
nameWithOwner
}
}
GRAPHQL
end
test "can make a GraphQL request with a query that contains fragments" do
Github.user_repositories(login: "yuki24", size: 10)
request = requests[0]
assert_equal 'Github__UserRepositories', request.operation_name
assert_equal({ 'login' => 'yuki24', 'size' => 10 }, request.variables)
assert_equal({}, request.context)
assert_equal <<~GRAPHQL.strip, request.document.to_query_string
query Github__UserRepositories($login: String!, $size: Int!) {
user(login: $login) {
id
name
repositories(first: $size) {
nodes {
name
description
...Github__RepositoryFields
}
}
}
}
fragment Github__RepositoryFields on Repository {
name
nameWithOwner
url
updatedAt
languages(first: 1) {
nodes {
name
color
}
}
}
GRAPHQL
end
test "can make a GraphQL request with #execute" do
Github.execute(:repository, owner: "yuki24", name: "artemis")
request = requests[0]
assert_equal 'Github__Repository', request.operation_name
assert_equal({ "owner" => "yuki24", "name" => "artemis" }, request.variables)
assert_equal({}, request.context)
assert_equal <<~GRAPHQL.strip, request.document.to_query_string
query Github__Repository($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
name
nameWithOwner
}
}
GRAPHQL
end
test "raises an error when the specified graphql file does not exist" do
assert_raises Artemis::GraphQLFileNotFound, match: /Query does_not_exist\.graphql not found/ do
Github.execute(:does_not_exist)
end
end
test "assigns context to the request when provided as an argument" do
context = { headers: { Authorization: 'bearer ...' } }
Github.repository(owner: "yuki24", name: "artemis", context: context)
assert_equal context, requests[0].context
end
test "can create a client that always assigns the provided context to the request" do
context = { headers: { Authorization: 'bearer ...' } }
client = Github.with_context(context)
client.repository(owner: "yuki24", name: "artemis")
client.repository(owner: "yuki24", name: "artemis")
assert_equal context, requests[0].context
assert_equal context, requests[1].context
end
test "assigns the default context to a GraphQL request if present" do
begin
Github.default_context = { headers: { Authorization: 'bearer ...' } }
Github.repository(owner: "yuki24", name: "artemis")
assert_equal({ headers: { Authorization: 'bearer ...' } }, requests[0].context)
ensure
Github.default_context = { }
end
end
test "can make a GraphQL request with all of .default_context, with_context(...) and the :context argument" do
begin
Github.default_context = { headers: { 'User-Agent': 'Artemis', 'X-key': 'value', Authorization: 'token ...' } }
Github
.with_context({ headers: { 'X-key': 'overridden' } })
.repository(owner: "yuki24", name: "artemis", context: { headers: { Authorization: 'bearer ...' } })
expected = {
headers: {
'User-Agent': 'Artemis',
'X-key': 'overridden',
Authorization: 'bearer ...',
}
}
assert_equal expected, requests[0].context
ensure
Github.default_context = { }
end
end
test "can batch multiple requests using Multiplex" do
Github.multiplex do |queue|
queue.repository(owner: "yuki24", name: "artemis", context: { headers: { Authorization: 'bearer ...' } })
queue.user
end
repository_query, user_query = requests[0].queries
assert_equal 'Github__Repository', repository_query[:operationName]
assert_equal({ "owner" => "yuki24", "name" => "artemis" }, repository_query[:variables])
assert_equal({ headers: { Authorization: 'bearer ...' } }, repository_query[:context])
assert_equal <<~GRAPHQL.strip, repository_query[:query]
query Github__Repository($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
name
nameWithOwner
}
}
GRAPHQL
assert_equal 'Github__User', user_query[:operationName]
assert_empty user_query[:variables]
assert_equal({}, user_query[:context])
assert_equal <<~GRAPHQL.strip, user_query[:query]
query Github__User {
user(login: "yuki24") {
id
name
}
}
GRAPHQL
end
private
def requests
Artemis::Adapters::TestAdapter.requests
end
def with_files(*files)
files.each {|file| FileUtils.touch(file) }
yield
ensure
files.each {|file| File.delete(file) }
end
end
================================================
FILE: test/endpoint_test.rb
================================================
require_relative 'helpers/test_helper'
class GraphQLEndpointTest < ActiveSupport::TestCase
teardown do
Artemis::GraphQLEndpoint.const_get(:ENDPOINT_INSTANCES).delete("gitlab")
end
test ".lookup raises an exception when the service is missing" do
assert_raises Artemis::EndpointNotFound do
Artemis::GraphQLEndpoint.lookup(:does_not_exit)
end
end
test "can register an endpoint" do
endpoint = Artemis::GraphQLEndpoint.register!(:gitlab, url: "https://api.gitlab.com/graphql")
assert_equal "https://api.gitlab.com/graphql", endpoint.url
assert_instance_of Artemis::Adapters::NetHttpAdapter, endpoint.connection
end
test "can look up a registered endpoint" do
Artemis::GraphQLEndpoint.register!(:gitlab, url: "https://api.gitlab.com/graphql")
endpoint = Artemis::GraphQLEndpoint.lookup(:gitlab)
assert_equal "https://api.gitlab.com/graphql", endpoint.url
assert_instance_of Artemis::Adapters::NetHttpAdapter, endpoint.connection
# FIXME: This #schema method makes a network call.
# assert_equal ..., endpoint.schema
end
test "can register an endpoint with options" do
options = {
adapter: :test,
timeout: 10,
# schema_path: nil,
pool_size: 25,
}
endpoint = Artemis::GraphQLEndpoint.register!(:gitlab, url: "https://api.gitlab.com/graphql", **options)
assert_equal "https://api.gitlab.com/graphql", endpoint.url
assert_equal 10, endpoint.timeout
assert_equal 25, endpoint.pool_size
assert_instance_of Artemis::Adapters::TestAdapter, endpoint.connection
# FIXME: needs an example schema (and specify the :schema_path option) to test this.
# assert_equal ..., endpoint.schema
end
end
================================================
FILE: test/fixtures/github/_repository_fields.graphql
================================================
fragment on Repository {
name
nameWithOwner
url
updatedAt
languages(first: 1) {
nodes {
name
color
}
}
}
================================================
FILE: test/fixtures/github/repository.graphql
================================================
query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
name
nameWithOwner
}
}
================================================
FILE: test/fixtures/github/schema.json
================================================
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
},
"mutationType": {
"name": "Mutation"
},
"subscriptionType": null,
"types": [
{
"kind": "INPUT_OBJECT",
"name": "AbortQueuedMigrationsInput",
"description": "Autogenerated input type of AbortQueuedMigrations",
"fields": null,
"inputFields": [
{
"name": "ownerId",
"description": "The ID of the organization that is running the migrations.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AbortQueuedMigrationsPayload",
"description": "Autogenerated return type of AbortQueuedMigrations",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "success",
"description": "Did the operation succeed?",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AcceptEnterpriseAdministratorInvitationInput",
"description": "Autogenerated input type of AcceptEnterpriseAdministratorInvitation",
"fields": null,
"inputFields": [
{
"name": "invitationId",
"description": "The id of the invitation being accepted",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AcceptEnterpriseAdministratorInvitationPayload",
"description": "Autogenerated return type of AcceptEnterpriseAdministratorInvitation",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "invitation",
"description": "The invitation that was accepted.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "EnterpriseAdministratorInvitation",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "message",
"description": "A message confirming the result of accepting an administrator invitation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AcceptTopicSuggestionInput",
"description": "Autogenerated input type of AcceptTopicSuggestion",
"fields": null,
"inputFields": [
{
"name": "repositoryId",
"description": "The Node ID of the repository.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "name",
"description": "The name of the suggested topic.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AcceptTopicSuggestionPayload",
"description": "Autogenerated return type of AcceptTopicSuggestion",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "topic",
"description": "The accepted topic.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Topic",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INTERFACE",
"name": "Actor",
"description": "Represents an object which can take actions on GitHub. Typically a User or Bot.",
"fields": [
{
"name": "avatarUrl",
"description": "A URL pointing to the actor's public avatar.",
"args": [
{
"name": "size",
"description": "The size of the resulting square image.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "URI",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "login",
"description": "The username of the actor.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "resourcePath",
"description": "The HTTP path for this actor.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "URI",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "The HTTP URL for this actor.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "URI",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "Bot",
"ofType": null
},
{
"kind": "OBJECT",
"name": "EnterpriseUserAccount",
"ofType": null
},
{
"kind": "OBJECT",
"name": "Mannequin",
"ofType": null
},
{
"kind": "OBJECT",
"name": "Organization",
"ofType": null
},
{
"kind": "OBJECT",
"name": "User",
"ofType": null
}
]
},
{
"kind": "OBJECT",
"name": "ActorLocation",
"description": "Location information for an actor",
"fields": [
{
"name": "city",
"description": "City",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "country",
"description": "Country name",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "countryCode",
"description": "Country code",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "region",
"description": "Region name",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "regionCode",
"description": "Region or state code",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "ActorType",
"description": "The actor's type.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "USER",
"description": "Indicates a user actor.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "TEAM",
"description": "Indicates a team actor.",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddAssigneesToAssignableInput",
"description": "Autogenerated input type of AddAssigneesToAssignable",
"fields": null,
"inputFields": [
{
"name": "assignableId",
"description": "The id of the assignable object to add assignees to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "assigneeIds",
"description": "The id of users to add as assignees.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddAssigneesToAssignablePayload",
"description": "Autogenerated return type of AddAssigneesToAssignable",
"fields": [
{
"name": "assignable",
"description": "The item that was assigned.",
"args": [
],
"type": {
"kind": "INTERFACE",
"name": "Assignable",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddCommentInput",
"description": "Autogenerated input type of AddComment",
"fields": null,
"inputFields": [
{
"name": "subjectId",
"description": "The Node ID of the subject to modify.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "body",
"description": "The contents of the comment.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddCommentPayload",
"description": "Autogenerated return type of AddComment",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "commentEdge",
"description": "The edge from the subject's comment connection.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "IssueCommentEdge",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "subject",
"description": "The subject",
"args": [
],
"type": {
"kind": "INTERFACE",
"name": "Node",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "timelineEdge",
"description": "The edge from the subject's timeline connection.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "IssueTimelineItemEdge",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddDiscussionCommentInput",
"description": "Autogenerated input type of AddDiscussionComment",
"fields": null,
"inputFields": [
{
"name": "discussionId",
"description": "The Node ID of the discussion to comment on.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "replyToId",
"description": "The Node ID of the discussion comment within this discussion to reply to.",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "body",
"description": "The contents of the comment.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddDiscussionCommentPayload",
"description": "Autogenerated return type of AddDiscussionComment",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "comment",
"description": "The newly created discussion comment.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DiscussionComment",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddDiscussionPollVoteInput",
"description": "Autogenerated input type of AddDiscussionPollVote",
"fields": null,
"inputFields": [
{
"name": "pollOptionId",
"description": "The Node ID of the discussion poll option to vote for.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddDiscussionPollVotePayload",
"description": "Autogenerated return type of AddDiscussionPollVote",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pollOption",
"description": "The poll option that a vote was added to.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DiscussionPollOption",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddEnterpriseOrganizationMemberInput",
"description": "Autogenerated input type of AddEnterpriseOrganizationMember",
"fields": null,
"inputFields": [
{
"name": "enterpriseId",
"description": "The ID of the enterprise which owns the organization.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "organizationId",
"description": "The ID of the organization the users will be added to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "userIds",
"description": "The IDs of the enterprise members to add.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
}
},
"defaultValue": null
},
{
"name": "role",
"description": "The role to assign the users in the organization",
"type": {
"kind": "ENUM",
"name": "OrganizationMemberRole",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddEnterpriseOrganizationMemberPayload",
"description": "Autogenerated return type of AddEnterpriseOrganizationMember",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "users",
"description": "The users who were added to the organization.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "User",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddEnterpriseSupportEntitlementInput",
"description": "Autogenerated input type of AddEnterpriseSupportEntitlement",
"fields": null,
"inputFields": [
{
"name": "enterpriseId",
"description": "The ID of the Enterprise which the admin belongs to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "login",
"description": "The login of a member who will receive the support entitlement.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddEnterpriseSupportEntitlementPayload",
"description": "Autogenerated return type of AddEnterpriseSupportEntitlement",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "message",
"description": "A message confirming the result of adding the support entitlement.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddLabelsToLabelableInput",
"description": "Autogenerated input type of AddLabelsToLabelable",
"fields": null,
"inputFields": [
{
"name": "labelableId",
"description": "The id of the labelable object to add labels to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "labelIds",
"description": "The ids of the labels to add.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddLabelsToLabelablePayload",
"description": "Autogenerated return type of AddLabelsToLabelable",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "labelable",
"description": "The item that was labeled.",
"args": [
],
"type": {
"kind": "INTERFACE",
"name": "Labelable",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddProjectCardInput",
"description": "Autogenerated input type of AddProjectCard",
"fields": null,
"inputFields": [
{
"name": "projectColumnId",
"description": "The Node ID of the ProjectColumn.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "contentId",
"description": "The content of the card. Must be a member of the ProjectCardItem union",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "note",
"description": "The note on the card.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddProjectCardPayload",
"description": "Autogenerated return type of AddProjectCard",
"fields": [
{
"name": "cardEdge",
"description": "The edge from the ProjectColumn's card connection.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ProjectCardEdge",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "projectColumn",
"description": "The ProjectColumn",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ProjectColumn",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddProjectColumnInput",
"description": "Autogenerated input type of AddProjectColumn",
"fields": null,
"inputFields": [
{
"name": "projectId",
"description": "The Node ID of the project.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "name",
"description": "The name of the column.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddProjectColumnPayload",
"description": "Autogenerated return type of AddProjectColumn",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "columnEdge",
"description": "The edge from the project's column connection.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ProjectColumnEdge",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "project",
"description": "The project",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Project",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddProjectV2DraftIssueInput",
"description": "Autogenerated input type of AddProjectV2DraftIssue",
"fields": null,
"inputFields": [
{
"name": "projectId",
"description": "The ID of the Project to add the draft issue to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "title",
"description": "The title of the draft issue. A project item can also be created by providing the URL of an Issue or Pull Request if you have access.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "body",
"description": "The body of the draft issue.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "assigneeIds",
"description": "The IDs of the assignees of the draft issue.",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddProjectV2DraftIssuePayload",
"description": "Autogenerated return type of AddProjectV2DraftIssue",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "projectItem",
"description": "The draft issue added to the project.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ProjectV2Item",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddProjectV2ItemByIdInput",
"description": "Autogenerated input type of AddProjectV2ItemById",
"fields": null,
"inputFields": [
{
"name": "projectId",
"description": "The ID of the Project to add the item to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "contentId",
"description": "The id of the Issue or Pull Request to add.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddProjectV2ItemByIdPayload",
"description": "Autogenerated return type of AddProjectV2ItemById",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "item",
"description": "The item added to the project.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ProjectV2Item",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddPullRequestReviewCommentInput",
"description": "Autogenerated input type of AddPullRequestReviewComment",
"fields": null,
"inputFields": [
{
"name": "pullRequestId",
"description": "The node ID of the pull request reviewing\n\n**Upcoming Change on 2023-10-01 UTC**\n**Description:** `pullRequestId` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead\n**Reason:** We are deprecating the addPullRequestReviewComment mutation\n",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "pullRequestReviewId",
"description": "The Node ID of the review to modify.\n\n**Upcoming Change on 2023-10-01 UTC**\n**Description:** `pullRequestReviewId` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead\n**Reason:** We are deprecating the addPullRequestReviewComment mutation\n",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "commitOID",
"description": "The SHA of the commit to comment on.\n\n**Upcoming Change on 2023-10-01 UTC**\n**Description:** `commitOID` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead\n**Reason:** We are deprecating the addPullRequestReviewComment mutation\n",
"type": {
"kind": "SCALAR",
"name": "GitObjectID",
"ofType": null
},
"defaultValue": null
},
{
"name": "body",
"description": "The text of the comment. This field is required\n\n**Upcoming Change on 2023-10-01 UTC**\n**Description:** `body` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead\n**Reason:** We are deprecating the addPullRequestReviewComment mutation\n",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "path",
"description": "The relative path of the file to comment on.\n\n**Upcoming Change on 2023-10-01 UTC**\n**Description:** `path` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead\n**Reason:** We are deprecating the addPullRequestReviewComment mutation\n",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "position",
"description": "The line index in the diff to comment on.\n\n**Upcoming Change on 2023-10-01 UTC**\n**Description:** `position` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead\n**Reason:** We are deprecating the addPullRequestReviewComment mutation\n",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "inReplyTo",
"description": "The comment id to reply to.\n\n**Upcoming Change on 2023-10-01 UTC**\n**Description:** `inReplyTo` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead\n**Reason:** We are deprecating the addPullRequestReviewComment mutation\n",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddPullRequestReviewCommentPayload",
"description": "Autogenerated return type of AddPullRequestReviewComment",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "comment",
"description": "The newly created comment.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PullRequestReviewComment",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "commentEdge",
"description": "The edge from the review's comment connection.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PullRequestReviewCommentEdge",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddPullRequestReviewInput",
"description": "Autogenerated input type of AddPullRequestReview",
"fields": null,
"inputFields": [
{
"name": "pullRequestId",
"description": "The Node ID of the pull request to modify.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "commitOID",
"description": "The commit OID the review pertains to.",
"type": {
"kind": "SCALAR",
"name": "GitObjectID",
"ofType": null
},
"defaultValue": null
},
{
"name": "body",
"description": "The contents of the review body comment.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "event",
"description": "The event to perform on the pull request review.",
"type": {
"kind": "ENUM",
"name": "PullRequestReviewEvent",
"ofType": null
},
"defaultValue": null
},
{
"name": "comments",
"description": "The review line comments.\n\n**Upcoming Change on 2023-10-01 UTC**\n**Description:** `comments` will be removed. use the `threads` argument instead\n**Reason:** We are deprecating comment fields that use diff-relative positioning\n",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DraftPullRequestReviewComment",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "threads",
"description": "The review line comment threads.",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DraftPullRequestReviewThread",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddPullRequestReviewPayload",
"description": "Autogenerated return type of AddPullRequestReview",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pullRequestReview",
"description": "The newly created pull request review.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PullRequestReview",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "reviewEdge",
"description": "The edge from the pull request's review connection.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PullRequestReviewEdge",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddPullRequestReviewThreadInput",
"description": "Autogenerated input type of AddPullRequestReviewThread",
"fields": null,
"inputFields": [
{
"name": "path",
"description": "Path to the file being commented on.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "body",
"description": "Body of the thread's first comment.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "pullRequestId",
"description": "The node ID of the pull request reviewing",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "pullRequestReviewId",
"description": "The Node ID of the review to modify.",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "line",
"description": "The line of the blob to which the thread refers, required for line-level threads. The end of the line range for multi-line comments.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "side",
"description": "The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range.",
"type": {
"kind": "ENUM",
"name": "DiffSide",
"ofType": null
},
"defaultValue": "RIGHT"
},
{
"name": "startLine",
"description": "The first line of the range to which the comment refers.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "startSide",
"description": "The side of the diff on which the start line resides.",
"type": {
"kind": "ENUM",
"name": "DiffSide",
"ofType": null
},
"defaultValue": "RIGHT"
},
{
"name": "subjectType",
"description": "The level at which the comments in the corresponding thread are targeted, can be a diff line or a file",
"type": {
"kind": "ENUM",
"name": "PullRequestReviewThreadSubjectType",
"ofType": null
},
"defaultValue": "LINE"
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddPullRequestReviewThreadPayload",
"description": "Autogenerated return type of AddPullRequestReviewThread",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "thread",
"description": "The newly created thread.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PullRequestReviewThread",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddPullRequestReviewThreadReplyInput",
"description": "Autogenerated input type of AddPullRequestReviewThreadReply",
"fields": null,
"inputFields": [
{
"name": "pullRequestReviewId",
"description": "The Node ID of the pending review to which the reply will belong.",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "pullRequestReviewThreadId",
"description": "The Node ID of the thread to which this reply is being written.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "body",
"description": "The text of the reply.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddPullRequestReviewThreadReplyPayload",
"description": "Autogenerated return type of AddPullRequestReviewThreadReply",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "comment",
"description": "The newly created reply.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PullRequestReviewComment",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddReactionInput",
"description": "Autogenerated input type of AddReaction",
"fields": null,
"inputFields": [
{
"name": "subjectId",
"description": "The Node ID of the subject to modify.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "content",
"description": "The name of the emoji to react with.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "ReactionContent",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddReactionPayload",
"description": "Autogenerated return type of AddReaction",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "reaction",
"description": "The reaction object.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Reaction",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "reactionGroups",
"description": "The reaction groups for the subject.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ReactionGroup",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "subject",
"description": "The reactable subject.",
"args": [
],
"type": {
"kind": "INTERFACE",
"name": "Reactable",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddStarInput",
"description": "Autogenerated input type of AddStar",
"fields": null,
"inputFields": [
{
"name": "starrableId",
"description": "The Starrable ID to star.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddStarPayload",
"description": "Autogenerated return type of AddStar",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "starrable",
"description": "The starrable.",
"args": [
],
"type": {
"kind": "INTERFACE",
"name": "Starrable",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddUpvoteInput",
"description": "Autogenerated input type of AddUpvote",
"fields": null,
"inputFields": [
{
"name": "subjectId",
"description": "The Node ID of the discussion or comment to upvote.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddUpvotePayload",
"description": "Autogenerated return type of AddUpvote",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "subject",
"description": "The votable subject.",
"args": [
],
"type": {
"kind": "INTERFACE",
"name": "Votable",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AddVerifiableDomainInput",
"description": "Autogenerated input type of AddVerifiableDomain",
"fields": null,
"inputFields": [
{
"name": "ownerId",
"description": "The ID of the owner to add the domain to",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "domain",
"description": "The URL of the domain",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "URI",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddVerifiableDomainPayload",
"description": "Autogenerated return type of AddVerifiableDomain",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "domain",
"description": "The verifiable domain that was added.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "VerifiableDomain",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddedToMergeQueueEvent",
"description": "Represents an 'added_to_merge_queue' event on a given pull request.",
"fields": [
{
"name": "actor",
"description": "Identifies the actor who performed the event.",
"args": [
],
"type": {
"kind": "INTERFACE",
"name": "Actor",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "Identifies the date and time when the object was created.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "enqueuer",
"description": "The user who added this Pull Request to the merge queue",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "User",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeQueue",
"description": "The merge queue where this pull request was added to.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "MergeQueue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pullRequest",
"description": "PullRequest referenced by event.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PullRequest",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
{
"kind": "INTERFACE",
"name": "Node",
"ofType": null
}
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddedToProjectEvent",
"description": "Represents a 'added_to_project' event on a given issue or pull request.",
"fields": [
{
"name": "actor",
"description": "Identifies the actor who performed the event.",
"args": [
],
"type": {
"kind": "INTERFACE",
"name": "Actor",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "Identifies the date and time when the object was created.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "databaseId",
"description": "Identifies the primary key from the database.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
{
"kind": "INTERFACE",
"name": "Node",
"ofType": null
}
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INTERFACE",
"name": "AnnouncementBanner",
"description": "Represents an announcement banner.",
"fields": [
{
"name": "announcement",
"description": "The text of the announcement",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "announcementExpiresAt",
"description": "The expiration date of the announcement, if any",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "announcementUserDismissible",
"description": "Whether the announcement can be dismissed by the user",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "Enterprise",
"ofType": null
},
{
"kind": "OBJECT",
"name": "Organization",
"ofType": null
}
]
},
{
"kind": "OBJECT",
"name": "App",
"description": "A GitHub App.",
"fields": [
{
"name": "createdAt",
"description": "Identifies the date and time when the object was created.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "databaseId",
"description": "Identifies the primary key from the database.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "The description of the app.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDe
gitextract_iw48rug4/
├── .github/
│ └── workflows/
│ └── ruby.yml
├── .gitignore
├── .rspec
├── Appraisals
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── artemis.gemspec
├── bin/
│ ├── console
│ └── setup
├── gemfiles/
│ ├── .bundle/
│ │ └── config
│ ├── graphql_2_0.gemfile
│ ├── rails_70.gemfile
│ ├── rails_71.gemfile
│ ├── rails_72.gemfile
│ ├── rails_80.gemfile
│ ├── rails_81.gemfile
│ └── rails_edge.gemfile
├── lib/
│ ├── artemis/
│ │ ├── adapters/
│ │ │ ├── abstract_adapter.rb
│ │ │ ├── curb_adapter.rb
│ │ │ ├── multi_domain_adapter.rb
│ │ │ ├── net_http_adapter.rb
│ │ │ ├── net_http_persistent_adapter.rb
│ │ │ └── test_adapter.rb
│ │ ├── adapters.rb
│ │ ├── client.rb
│ │ ├── exceptions.rb
│ │ ├── graphql_endpoint.rb
│ │ ├── railtie.rb
│ │ ├── rspec.rb
│ │ ├── test_helper.rb
│ │ └── version.rb
│ ├── artemis.rb
│ ├── generators/
│ │ └── artemis/
│ │ ├── install/
│ │ │ ├── USAGE
│ │ │ ├── install_generator.rb
│ │ │ └── templates/
│ │ │ ├── client.rb
│ │ │ └── graphql.yml
│ │ ├── mutation/
│ │ │ ├── USAGE
│ │ │ ├── mutation_generator.rb
│ │ │ └── templates/
│ │ │ └── mutation.graphql
│ │ └── query/
│ │ ├── USAGE
│ │ ├── query_generator.rb
│ │ └── templates/
│ │ ├── fixture.yml
│ │ └── query.graphql
│ └── tasks/
│ └── artemis.rake
└── test/
├── adapters_test.rb
├── autoloading_test.rb
├── backport/
│ └── method_call_assertions.rb
├── callbacks_test.rb
├── client_test.rb
├── endpoint_test.rb
├── fixtures/
│ ├── github/
│ │ ├── _repository_fields.graphql
│ │ ├── repository.graphql
│ │ ├── schema.json
│ │ ├── user.graphql
│ │ └── user_repositories.graphql
│ ├── github.rb
│ └── responses/
│ ├── github/
│ │ ├── repository.yml
│ │ └── user.json
│ └── spotify_client/
│ └── artist.yml
├── generators/
│ ├── install_generator_test.rb
│ ├── mutation_generator_test.rb
│ └── query_generator_test.rb
├── helpers/
│ ├── fake_server.rb
│ ├── isolated_test_helper.rb
│ └── test_helper.rb
├── railtie_test.rb
├── rake_tasks_test.rb
└── test_helper_test.rb
SYMBOL INDEX (183 symbols across 32 files)
FILE: lib/artemis.rb
type Artemis (line 5) | module Artemis
function config_for_graphql (line 6) | def self.config_for_graphql(app)
function config_for (line 16) | def self.config_for(name, app:, env: Rails.env)
FILE: lib/artemis/adapters.rb
type Artemis (line 5) | module Artemis
type Adapters (line 6) | module Adapters
function lookup (line 21) | def lookup(name)
FILE: lib/artemis/adapters/abstract_adapter.rb
type Artemis (line 6) | module Artemis
type Adapters (line 7) | module Adapters
class AbstractAdapter (line 8) | class AbstractAdapter < ::GraphQL::Client::HTTP
method initialize (line 18) | def initialize(uri, service_name: , timeout: , pool_size: , adapte...
method headers (line 31) | def headers(_context)
method execute (line 41) | def execute(*)
method connection (line 49) | def connection
FILE: lib/artemis/adapters/curb_adapter.rb
type Artemis (line 11) | module Artemis
type Adapters (line 12) | module Adapters
class CurbAdapter (line 13) | class CurbAdapter < AbstractAdapter
method initialize (line 16) | def initialize(uri, service_name: , timeout: , pool_size: , adapte...
method multiplex (line 23) | def multiplex(queries, context: {})
method execute (line 27) | def execute(document:, operation_name: nil, variables: {}, context...
method make_request (line 38) | def make_request(body, context)
FILE: lib/artemis/adapters/multi_domain_adapter.rb
type Artemis (line 5) | module Artemis
type Adapters (line 6) | module Adapters
class MultiDomainAdapter (line 7) | class MultiDomainAdapter < AbstractAdapter
method initialize (line 10) | def initialize(_uri, service_name: , timeout: , pool_size: , adapt...
method multiplex (line 23) | def multiplex(queries, context: {})
method execute (line 35) | def execute(document:, operation_name: nil, variables: {}, context...
method connection (line 46) | def connection
method connection_for_url (line 51) | def connection_for_url(url)
FILE: lib/artemis/adapters/net_http_adapter.rb
type Artemis (line 9) | module Artemis
type Adapters (line 10) | module Adapters
class NetHttpAdapter (line 11) | class NetHttpAdapter < AbstractAdapter
method multiplex (line 12) | def multiplex(queries, context: {})
method execute (line 17) | def execute(document:, operation_name: nil, variables: {}, context...
method connection (line 27) | def connection
method make_request (line 38) | def make_request(body, context)
FILE: lib/artemis/adapters/net_http_persistent_adapter.rb
type Artemis (line 22) | module Artemis
type Adapters (line 23) | module Adapters
class NetHttpPersistentAdapter (line 24) | class NetHttpPersistentAdapter < NetHttpAdapter
method initialize (line 27) | def initialize(uri, service_name: , timeout: , pool_size: , adapte...
method connection (line 41) | def connection
class ConnectionWrapper (line 45) | class ConnectionWrapper < SimpleDelegator #:nodoc:
method initialize (line 46) | def initialize(obj, url)
method request (line 52) | def request(req)
FILE: lib/artemis/adapters/test_adapter.rb
type Artemis (line 5) | module Artemis
type Adapters (line 6) | module Adapters
class TestAdapter (line 7) | class TestAdapter
method initialize (line 19) | def initialize(*)
method multiplex (line 22) | def multiplex(queries, context: {})
method execute (line 34) | def execute(**arguments)
method fake_response (line 46) | def fake_response
FILE: lib/artemis/client.rb
type Artemis (line 13) | module Artemis
class Client (line 14) | class Client
method initialize (line 54) | def initialize(context = {})
method endpoint (line 95) | def endpoint
method instantiate_client (line 118) | def instantiate_client(context = {})
method before_execute (line 134) | def before_execute(&block)
method after_execute (line 152) | def after_execute(&block)
method resolve_graphql_file_path (line 156) | def resolve_graphql_file_path(filename, fragment: false)
method graphql_file_paths (line 164) | def graphql_file_paths
method namespace (line 168) | def namespace
method preload! (line 172) | def preload!
method load_constant (line 190) | def load_constant(const_name)
method connection (line 202) | def connection(context = {})
method execute (line 206) | def execute(query, context: {}, **arguments)
method multiplex (line 210) | def multiplex(**context, &block)
method const_missing (line 239) | def const_missing(const_name)
method method_missing (line 253) | def method_missing(method_name, **arguments, &block)
method respond_to_missing? (line 261) | def respond_to_missing?(method_name, *_, &block) #:nodoc:
method callbacks (line 268) | def callbacks
method execute (line 280) | def execute(query, context: {}, **arguments)
method method_missing (line 307) | def method_missing(method_name, context: {}, **arguments)
method respond_to_missing? (line 313) | def respond_to_missing?(method_name, *_, &block) #:nodoc:
class Executor (line 325) | class Executor < SimpleDelegator
method initialize (line 326) | def initialize(connection, callbacks, default_context) #:nodoc:
method execute (line 333) | def execute(document:, operation_name: nil, variables: {}, context...
class MultiplexQueue (line 350) | class MultiplexQueue
method initialize (line 353) | def initialize
method execute (line 357) | def execute(document:, operation_name: nil, variables: {}, context...
FILE: lib/artemis/exceptions.rb
type Artemis (line 1) | module Artemis
class Error (line 2) | class Error < StandardError
class EndpointNotFound (line 5) | class EndpointNotFound < Error
class ConfigurationError (line 8) | class ConfigurationError < Error
class GraphQLFileNotFound (line 11) | class GraphQLFileNotFound < Error
class FixtureNotFound (line 14) | class FixtureNotFound < Error
class GraphQLError (line 17) | class GraphQLError < Error
class GraphQLServerError (line 20) | class GraphQLServerError < GraphQLError
FILE: lib/artemis/graphql_endpoint.rb
type Artemis (line 13) | module Artemis
class GraphQLEndpoint (line 14) | class GraphQLEndpoint
method lookup (line 31) | def lookup(service_name)
method register! (line 35) | def register!(service_name, configurations)
method registered_services (line 42) | def registered_services
method initialize (line 49) | def initialize(name, url: nil, adapter: :net_http, timeout: 10, sche...
method schema (line 62) | def schema
method connection (line 73) | def connection
FILE: lib/artemis/railtie.rb
type Artemis (line 3) | module Artemis
class Railtie (line 4) | class Railtie < ::Rails::Railtie #:nodoc:
FILE: lib/artemis/test_helper.rb
type Artemis (line 11) | module Artemis
type TestHelper (line 13) | module TestHelper
function stub_graphql (line 45) | def stub_graphql(service, query_name, arguments = :__unspecified__)
function graphql_requests (line 51) | def graphql_requests
function graphql_responses (line 57) | def graphql_responses #:nodoc:
function graphql_fixture_path (line 61) | def graphql_fixture_path #:nodoc:
function graphql_fixture_files (line 65) | def graphql_fixture_files #:nodoc:
function read_erb_yaml (line 72) | def read_erb_yaml(path) #:nodoc:
class StubbingDSL (line 80) | class StubbingDSL #:nodoc:
method initialize (line 83) | def initialize(service_name, query_name, fixture_sets, arguments) ...
method get (line 87) | def get(fixture_key)
method to_return (line 98) | def to_return(fixture_key) #:nodoc:
method find_fixture_set (line 116) | def find_fixture_set
FILE: lib/artemis/version.rb
type Artemis (line 1) | module Artemis
FILE: lib/generators/artemis/install/install_generator.rb
class Artemis::InstallGenerator (line 4) | class Artemis::InstallGenerator < Rails::Generators::NamedBase
method generate_client (line 11) | def generate_client
method generate_config (line 16) | def generate_config
method download_schema (line 42) | def download_schema
method file_name (line 54) | def file_name # :doc:
method client_file_name (line 58) | def client_file_name
method query_dir_gitkeep (line 66) | def query_dir_gitkeep
method config_file_name (line 74) | def config_file_name
FILE: lib/generators/artemis/mutation/mutation_generator.rb
class Artemis::MutationGenerator (line 5) | class Artemis::MutationGenerator < Rails::Generators::Base
method generate_mutation_file (line 13) | def generate_mutation_file
method mutation_name (line 19) | def mutation_name
method graphql_file_path (line 23) | def graphql_file_path
method arguments (line 27) | def arguments
method target_mutation (line 31) | def target_mutation
method schema (line 36) | def schema
method service_name (line 40) | def service_name
FILE: lib/generators/artemis/query/query_generator.rb
class Artemis::QueryGenerator (line 5) | class Artemis::QueryGenerator < Rails::Generators::Base
method generate_query_file (line 13) | def generate_query_file
method query_name (line 23) | def query_name
method graphql_file_path (line 27) | def graphql_file_path
method text_fixture_path (line 31) | def text_fixture_path
method arguments (line 35) | def arguments
method target_query (line 39) | def target_query
method schema (line 44) | def schema
method service_name (line 48) | def service_name
method qualified_name (line 61) | def qualified_name
FILE: test/adapters_test.rb
class AdaptersTest (line 5) | class AdaptersTest < ActiveSupport::TestCase
method assert_adapter (line 122) | def assert_adapter(adapter, timeout_error)
method assert_adapter_initialization (line 132) | def assert_adapter_initialization(adapter)
method assert_adapter_execution (line 138) | def assert_adapter_execution(adapter)
method assert_adapter_server_error (line 155) | def assert_adapter_server_error(adapter)
method assert_adapter_timeout (line 163) | def assert_adapter_timeout(adapter, timeout_error)
method assert_adapter_multiplex (line 171) | def assert_adapter_multiplex(adapter)
method assert_adapter_multiplex_server_error (line 198) | def assert_adapter_multiplex_server_error(adapter)
method assert_adapter_multiplex_timeout (line 206) | def assert_adapter_multiplex_timeout(adapter, timeout_error)
FILE: test/autoloading_test.rb
class AutoLoadingTest (line 3) | class AutoLoadingTest < ActiveSupport::TestCase
FILE: test/backport/method_call_assertions.rb
type MethodCallAssertions (line 3) | module MethodCallAssertions # :nodoc:
function assert_called (line 5) | def assert_called(object, method_name, message = nil, times: 1, return...
function assert_called_with (line 16) | def assert_called_with(object, method_name, args = [], returns: nil)
function assert_not_called (line 30) | def assert_not_called(object, method_name, message = nil, &block)
function stub_any_instance (line 34) | def stub_any_instance(klass, instance: klass.new)
FILE: test/callbacks_test.rb
class CallbacksTest (line 5) | class CallbacksTest < ActiveSupport::TestCase
method name (line 7) | def self.name
method name (line 25) | def self.name
FILE: test/client_test.rb
class ClientTest (line 3) | class ClientTest < ActiveSupport::TestCase
method requests (line 218) | def requests
method with_files (line 222) | def with_files(*files)
FILE: test/endpoint_test.rb
class GraphQLEndpointTest (line 3) | class GraphQLEndpointTest < ActiveSupport::TestCase
FILE: test/fixtures/github.rb
class Github (line 1) | class Github < Artemis::Client
FILE: test/generators/install_generator_test.rb
class InstallGeneratorTest (line 6) | class InstallGeneratorTest < Rails::Generators::TestCase
method assert_mock (line 107) | def assert_mock(mock)
FILE: test/generators/mutation_generator_test.rb
class MutationGeneratorTest (line 6) | class MutationGeneratorTest < Rails::Generators::TestCase
FILE: test/generators/query_generator_test.rb
class QueryGeneratorTest (line 6) | class QueryGeneratorTest < Rails::Generators::TestCase
FILE: test/helpers/fake_server.rb
function start_server (line 67) | def start_server
function teardown_server (line 84) | def teardown_server(server_thread)
FILE: test/helpers/isolated_test_helper.rb
type Paths (line 25) | module Paths
function app_template_path (line 26) | def app_template_path
function tmp_path (line 30) | def tmp_path(*args)
function app_path (line 35) | def app_path(*args)
type Generation (line 40) | module Generation
function build_app (line 45) | def build_app(options = {})
function teardown_app (line 70) | def teardown_app
function add_to_config (line 75) | def add_to_config(str)
function initialize_app (line 84) | def self.initialize_app
FILE: test/railtie_test.rb
class RailtieTest (line 6) | class RailtieTest < ActiveSupport::TestCase
method app (line 217) | def app
method boot_rails (line 221) | def boot_rails
FILE: test/rake_tasks_test.rb
type ApplicationTests (line 5) | module ApplicationTests
type RakeTests (line 6) | module RakeTests
class RakeRoutesTest (line 7) | class RakeRoutesTest < ActiveSupport::TestCase
method run_rake (line 137) | def run_rake(task)
FILE: test/test_helper_test.rb
class TestHelperTest (line 4) | class TestHelperTest < ActiveSupport::TestCase
method graphql_fixture_path (line 7) | def graphql_fixture_path
Condensed preview — 72 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,710K chars).
[
{
"path": ".github/workflows/ruby.yml",
"chars": 3631,
"preview": "name: build\n\non:\n push:\n pull_request:\n\njobs:\n mri:\n strategy:\n fail-fast: false\n matrix:\n ruby_v"
},
{
"path": ".gitignore",
"chars": 118,
"preview": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\nGemfile.lock\ngemfiles/*.lock\n.byebug_history\n"
},
{
"path": ".rspec",
"chars": 30,
"preview": "--require spec_helper\n--colour"
},
{
"path": "Appraisals",
"chars": 1442,
"preview": "appraise \"rails_edge\" do\n git 'https://github.com/rails/rails.git' do\n gem \"rails\"\n gem \"railties\"\n gem \"activ"
},
{
"path": "CHANGELOG.md",
"chars": 7055,
"preview": "## Unreleased\n\n#### 🚨 Breaking Changes\n\n- No changes.\n\n#### ⭐️ New Features\n\n- Add support for Rails 8.0 ([<tt>#96</tt>]"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3230,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "Gemfile",
"chars": 292,
"preview": "source \"https://rubygems.org\"\n\ngit_source(:github) {|repo_name| \"https://github.com/#{repo_name}\" }\n\n# Specify your gem'"
},
{
"path": "LICENSE.txt",
"chars": 1081,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2018 Yuki Nishijima\n\nPermission is hereby granted, free of charge, to any person ob"
},
{
"path": "README.md",
"chars": 13374,
"preview": "# Artemis [](https://github.com/yuki24/a"
},
{
"path": "Rakefile",
"chars": 441,
"preview": "require \"bundler/gem_tasks\"\nrequire \"rake/testtask\"\n\nTESTS_IN_ISOLATION = ['test/railtie_test.rb', 'test/rake_tasks_test"
},
{
"path": "artemis.gemspec",
"chars": 1120,
"preview": "\nlib = File.expand_path(\"../lib\", __FILE__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire \"artemis/ver"
},
{
"path": "bin/console",
"chars": 375,
"preview": "#!/usr/bin/env ruby\n\nrequire \"bundler/setup\"\nrequire \"artemis\"\n\nArtemis::Client.query_paths = [File.join(__dir__, '../sp"
},
{
"path": "bin/setup",
"chars": 131,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\nbundle install\n\n# Do any other automated setup that you need "
},
{
"path": "gemfiles/.bundle/config",
"chars": 22,
"preview": "---\nBUNDLE_RETRY: \"1\"\n"
},
{
"path": "gemfiles/graphql_2_0.gemfile",
"chars": 340,
"preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"bigdecimal\"\ngem \"drb\"\ngem \"irb\"\ngem \"logger\""
},
{
"path": "gemfiles/rails_70.gemfile",
"chars": 304,
"preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"bigdecimal\"\ngem \"drb\"\ngem \"irb\"\ngem \"logger\""
},
{
"path": "gemfiles/rails_71.gemfile",
"chars": 317,
"preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"bigdecimal\"\ngem \"drb\"\ngem \"irb\"\ngem \"logger\""
},
{
"path": "gemfiles/rails_72.gemfile",
"chars": 317,
"preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"bigdecimal\"\ngem \"drb\"\ngem \"irb\"\ngem \"logger\""
},
{
"path": "gemfiles/rails_80.gemfile",
"chars": 317,
"preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"bigdecimal\"\ngem \"drb\"\ngem \"irb\"\ngem \"logger\""
},
{
"path": "gemfiles/rails_81.gemfile",
"chars": 296,
"preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"pry\"\ngem \"pry-byebug\", platforms: :mri\ngem \""
},
{
"path": "gemfiles/rails_edge.gemfile",
"chars": 336,
"preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngit \"https://github.com/rails/rails.git\" do\n gem"
},
{
"path": "lib/artemis/adapters/abstract_adapter.rb",
"chars": 1755,
"preview": "# frozen_string_literal: true\n\nrequire 'active_support/core_ext/object/blank'\nrequire 'graphql/client/http'\n\nmodule Arte"
},
{
"path": "lib/artemis/adapters/curb_adapter.rb",
"chars": 1847,
"preview": "# frozen_string_literal: true\n\nrequire 'delegate'\nrequire 'json'\n\nrequire 'curb'\n\nrequire 'artemis/adapters/abstract_ada"
},
{
"path": "lib/artemis/adapters/multi_domain_adapter.rb",
"chars": 2232,
"preview": "# frozen_string_literal: true\n\nrequire 'artemis/adapters/abstract_adapter'\n\nmodule Artemis\n module Adapters\n class M"
},
{
"path": "lib/artemis/adapters/net_http_adapter.rb",
"chars": 1806,
"preview": "# frozen_string_literal: true\n\nrequire 'json'\nrequire 'net/http'\n\nrequire 'artemis/adapters/abstract_adapter'\nrequire 'a"
},
{
"path": "lib/artemis/adapters/net_http_persistent_adapter.rb",
"chars": 1453,
"preview": "# frozen_string_literal: true\n\nrequire 'delegate'\n\nbegin\n require \"active_support/isolated_execution_state\"\nrescue Load"
},
{
"path": "lib/artemis/adapters/test_adapter.rb",
"chars": 1440,
"preview": "# frozen_string_literal: true\n\nrequire 'active_support/core_ext/module/attribute_accessors'\n\nmodule Artemis\n module Ada"
},
{
"path": "lib/artemis/adapters.rb",
"chars": 594,
"preview": "# frozen-string-literal: true\n\nrequire 'active_support/dependencies/autoload'\n\nmodule Artemis\n module Adapters\n exte"
},
{
"path": "lib/artemis/client.rb",
"chars": 11908,
"preview": "# frozen_string_literal: true\n\nrequire 'delegate'\nrequire 'active_support/core_ext/class/attribute'\nrequire 'active_supp"
},
{
"path": "lib/artemis/exceptions.rb",
"chars": 303,
"preview": "module Artemis\n class Error < StandardError\n end\n\n class EndpointNotFound < Error\n end\n\n class ConfigurationError <"
},
{
"path": "lib/artemis/graphql_endpoint.rb",
"chars": 2638,
"preview": "# frozen_string_literal: true\n\nrequire 'active_support/core_ext/hash/deep_merge'\nrequire 'active_support/core_ext/hash/k"
},
{
"path": "lib/artemis/railtie.rb",
"chars": 2816,
"preview": "require 'active_support/file_update_checker'\n\nmodule Artemis\n class Railtie < ::Rails::Railtie #:nodoc:\n config.arte"
},
{
"path": "lib/artemis/rspec.rb",
"chars": 189,
"preview": "require 'artemis/test_helper'\n\nRSpec.configure do |config|\n config.include ::Artemis::TestHelper\n\n config.before :each"
},
{
"path": "lib/artemis/test_helper.rb",
"chars": 4606,
"preview": "# frozen_string_literal: true\n\nrequire 'erb'\nrequire 'yaml'\n\nrequire 'active_support/core_ext/module/attribute_accessors"
},
{
"path": "lib/artemis/version.rb",
"chars": 39,
"preview": "module Artemis\n VERSION = \"1.1.0\"\nend\n"
},
{
"path": "lib/artemis.rb",
"chars": 1044,
"preview": "require \"artemis/version\"\nrequire \"artemis/client\"\nrequire \"artemis/railtie\" if defined?(Rails)\n\nmodule Artemis\n def se"
},
{
"path": "lib/generators/artemis/install/USAGE",
"chars": 361,
"preview": "Description:\n Generates a client stub and config files and downloads the schema from the given endpoint.\n\nExample:\n "
},
{
"path": "lib/generators/artemis/install/install_generator.rb",
"chars": 1950,
"preview": "require 'graphql/client'\nrequire 'graphql/client/http'\n\nclass Artemis::InstallGenerator < Rails::Generators::NamedBase\n "
},
{
"path": "lib/generators/artemis/install/templates/client.rb",
"chars": 241,
"preview": "<% module_namespacing do -%>\nclass <%= class_name %> < Artemis::Client\n # If an access token needs to be assigned to ev"
},
{
"path": "lib/generators/artemis/install/templates/graphql.yml",
"chars": 869,
"preview": "default: &default\n # The underlying client library that actually makes an HTTP request.\n # Available adapters are :net"
},
{
"path": "lib/generators/artemis/mutation/USAGE",
"chars": 797,
"preview": "Description:\n Generates a mutation stub.\n\n rails g artemis:mutation MUTATION_TYPE [FILE_NAME]\n\nExample:\n rails "
},
{
"path": "lib/generators/artemis/mutation/mutation_generator.rb",
"chars": 1500,
"preview": "# frozen_string_literal: true\n\nrequire 'graphql/schema/finder'\n\nclass Artemis::MutationGenerator < Rails::Generators::Ba"
},
{
"path": "lib/generators/artemis/mutation/templates/mutation.graphql",
"chars": 292,
"preview": "mutation<%= arguments.present? && \"(#{ arguments.map {|name, type| \"$#{name}: #{type.type.to_type_signature}\" }.join(\", "
},
{
"path": "lib/generators/artemis/query/USAGE",
"chars": 739,
"preview": "Description:\n Generates a query stub.\n\n rails g artemis:query QUERY_TYPE [FILE_NAME]\n\nExample:\n rails g artemis"
},
{
"path": "lib/generators/artemis/query/query_generator.rb",
"chars": 1730,
"preview": "# frozen_string_literal: true\n\nrequire 'graphql/schema/finder'\n\nclass Artemis::QueryGenerator < Rails::Generators::Base\n"
},
{
"path": "lib/generators/artemis/query/templates/fixture.yml",
"chars": 764,
"preview": "# You can stub GraphQL queries by calling the `stub_graphql' method in test:\n#\n# stub_graphql(<%= service_name.cameliz"
},
{
"path": "lib/generators/artemis/query/templates/query.graphql",
"chars": 294,
"preview": "query<%= arguments.present? ? \"(#{ arguments.map {|name, type| \"$#{name}: #{type.type.to_type_signature}\" }.join(\", \") }"
},
{
"path": "lib/tasks/artemis.rake",
"chars": 1741,
"preview": "# frozen_string_literal: true\n\nrequire 'json'\n\nrequire 'active_support/core_ext/string/inflections'\nrequire 'graphql/cli"
},
{
"path": "test/adapters_test.rb",
"chars": 8449,
"preview": "require_relative 'helpers/test_helper'\n\nrequire 'artemis/adapters/abstract_adapter'\n\nclass AdaptersTest < ActiveSupport:"
},
{
"path": "test/autoloading_test.rb",
"chars": 4806,
"preview": "require_relative 'helpers/test_helper'\n\nclass AutoLoadingTest < ActiveSupport::TestCase\n test \".load_constant loads the"
},
{
"path": "test/backport/method_call_assertions.rb",
"chars": 1050,
"preview": "require \"minitest/mock\"\n\nmodule MethodCallAssertions # :nodoc:\n private\n def assert_called(object, method_name, messag"
},
{
"path": "test/callbacks_test.rb",
"chars": 1587,
"preview": "require_relative 'helpers/test_helper'\n\nrequire 'active_support/core_ext/module/attribute_accessors'\n\nclass CallbacksTes"
},
{
"path": "test/client_test.rb",
"chars": 6926,
"preview": "require 'helpers/test_helper'\n\nclass ClientTest < ActiveSupport::TestCase\n setup do\n requests.clear\n end\n\n test \"."
},
{
"path": "test/endpoint_test.rb",
"chars": 1722,
"preview": "require_relative 'helpers/test_helper'\n\nclass GraphQLEndpointTest < ActiveSupport::TestCase\n teardown do\n Artemis::G"
},
{
"path": "test/fixtures/github/_repository_fields.graphql",
"chars": 137,
"preview": "fragment on Repository {\n name\n nameWithOwner\n url\n updatedAt\n languages(first: 1) {\n nodes {\n name\n c"
},
{
"path": "test/fixtures/github/repository.graphql",
"chars": 117,
"preview": "query($owner: String!, $name: String!) {\n repository(owner: $owner, name: $name) {\n name\n nameWithOwner\n }\n}\n"
},
{
"path": "test/fixtures/github/schema.json",
"chars": 5074982,
"preview": "{\n \"data\": {\n \"__schema\": {\n \"queryType\": {\n \"name\": \"Query\"\n },\n \"mutationType\": {\n \"n"
},
{
"path": "test/fixtures/github/user.graphql",
"chars": 56,
"preview": "query {\n user(login: \"yuki24\") {\n id\n name\n }\n}\n"
},
{
"path": "test/fixtures/github/user_repositories.graphql",
"chars": 213,
"preview": "query($login: String!, $size: Int!) {\n user(login: $login) {\n id\n name\n repositories(first: $size) {\n nod"
},
{
"path": "test/fixtures/github.rb",
"chars": 34,
"preview": "class Github < Artemis::Client\nend"
},
{
"path": "test/fixtures/responses/github/repository.yml",
"chars": 309,
"preview": "yuki24_artemis:\n data:\n repository:\n name: artemis\n nameWithOwner: yuki24/artemis\n\nyuki24_rambulance:\n da"
},
{
"path": "test/fixtures/responses/github/user.json",
"chars": 123,
"preview": "{\n \"yuki24\": {\n \"data\": {\n \"user\": {\n \"id\": \"foobar\",\n \"name\": \"Yuki Nishijima\"\n }\n }\n }"
},
{
"path": "test/fixtures/responses/spotify_client/artist.yml",
"chars": 83,
"preview": "yoshiki:\n data:\n artist:\n id: pianist-yoshiki\n name: Pianist Yoshiki\n"
},
{
"path": "test/generators/install_generator_test.rb",
"chars": 2995,
"preview": "require_relative '../helpers/test_helper'\nrequire 'rails/generators/test_case'\n\nrequire 'generators/artemis/install/inst"
},
{
"path": "test/generators/mutation_generator_test.rb",
"chars": 2124,
"preview": "require_relative '../helpers/test_helper'\nrequire 'rails/generators/test_case'\n\nrequire 'generators/artemis/mutation/mut"
},
{
"path": "test/generators/query_generator_test.rb",
"chars": 2733,
"preview": "require_relative '../helpers/test_helper'\nrequire 'rails/generators/test_case'\n\nrequire 'generators/artemis/query/query_"
},
{
"path": "test/helpers/fake_server.rb",
"chars": 2467,
"preview": "require 'json'\nrequire 'rack'\nrequire 'webrick'\nrequire 'net/http'\n\nRACK_SERVER = begin\n require 'rackup/"
},
{
"path": "test/helpers/isolated_test_helper.rb",
"chars": 3667,
"preview": "$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)\n\nrequire \"active_support\"\nrequire 'active_support/core_ext/ke"
},
{
"path": "test/helpers/test_helper.rb",
"chars": 1773,
"preview": "$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)\n\nrequire \"active_support\"\nrequire 'active_support/core_ext/ke"
},
{
"path": "test/railtie_test.rb",
"chars": 5980,
"preview": "require \"rack/test\"\nrequire \"rails/version\"\n\nrequire_relative \"helpers/isolated_test_helper\"\n\nclass RailtieTest < Active"
},
{
"path": "test/rake_tasks_test.rb",
"chars": 4674,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"helpers/isolated_test_helper\"\n\nmodule ApplicationTests\n module RakeTes"
},
{
"path": "test/test_helper_test.rb",
"chars": 3335,
"preview": "require_relative 'helpers/test_helper'\nrequire 'artemis/test_helper'\n\nclass TestHelperTest < ActiveSupport::TestCase\n i"
}
]
About this extraction
This page contains the full source code of the yuki24/artemis GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 72 files (5.0 MB), approximately 1.3M tokens, and a symbol index with 183 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.