Skip to content

Commit 6891b31

Browse files
author
betaflag
committed
Create the initial rack app and middleware with examples and tests
0 parents  commit 6891b31

17 files changed

+382
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.ruby-version
2+
doc/
3+
.yardoc/
4+
guides/yardoc/
5+
pkg/
6+
Gemfile.lock
7+
gemfiles/*.lock

Gemfile

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
source 'https://rubygems.org'
2+
gemspec
3+
4+
gem 'rack-test'
5+
gem 'rake'
6+
gem 'minitest'

README.md

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
This is a simple spec-compliant GraphQL rack application and middleware based on the [graphql](https://github.com/rmosolgo/graphql-ruby) gem. Since it's built with Rack, it can be mounted with most ruby web servers.
2+
3+
Install the gem:
4+
5+
```
6+
gem install graphql-server
7+
```
8+
9+
# Using the server
10+
11+
## As a standalone application
12+
13+
```ruby
14+
# app.ru
15+
require 'graphql_server'
16+
17+
type_def = <<-GRAPHQL
18+
type Query {
19+
hello: String
20+
}
21+
GRAPHQL
22+
23+
resolver = {
24+
"Query" => {
25+
"hello" => Proc.new { "world" }
26+
}
27+
}
28+
29+
run GraphqlServer.new(type_def: type_def, resolver: resolver)
30+
```
31+
32+
Start using `rackup`
33+
34+
```
35+
rackup app.ru
36+
```
37+
38+
## As a middleware in your application
39+
40+
```ruby
41+
# app.ru
42+
require 'graphql_server'
43+
44+
type_def = ...
45+
resolver = ...
46+
47+
use GraphqlServer, type_def: type_def, resolver: resolver, path: '/graphql'
48+
```
49+
50+
Start using `rackup`
51+
52+
```
53+
rackup app.ru
54+
```
55+
56+
# Options
57+
58+
## Schema
59+
60+
You can get started fast by writing a type defintions and a resolver hash
61+
62+
```ruby
63+
GraphqlServer.new(type_def: type_def, resolver: resolver)
64+
```
65+
66+
You can also provide your own schema
67+
68+
```ruby
69+
GraphqlServer.new(schema: schema)
70+
```
71+
72+
See the examples folder for more details
73+
74+
## Middleware
75+
76+
When using as a middleware, you can specify the path to mount the graphql endpoint (defaults to `/`)
77+
78+
```ruby
79+
use GraphqlServer, type_def: type_def, resolver: resolver, path: '/graphql'
80+
```

Rakefile

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require 'rake/testtask'
2+
3+
Rake::TestTask.new do |t|
4+
t.libs << 'test'
5+
t.test_files = FileList['test/*_test.rb']
6+
end
7+
8+
desc "Run tests"
9+
task :default => :test

examples/blog/app.ru

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
require 'graphql'
2+
require 'graphql_server'
3+
4+
require_relative 'models/comment'
5+
require_relative 'models/post'
6+
require_relative 'types/comment_type'
7+
require_relative 'types/post_type'
8+
require_relative 'types/query_type'
9+
require_relative 'schema'
10+
11+
run GraphqlServer.new(schema: Schema)

examples/blog/models/comment.rb

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class Comment < OpenStruct
2+
end

examples/blog/models/post.rb

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class Post < OpenStruct
2+
def self.find(id)
3+
Post.new(
4+
id: 1,
5+
title: 'first post',
6+
truncated_preview: 'Hello world!',
7+
comments: [
8+
Comment.new(id: 1, message: 'first comment')
9+
]
10+
)
11+
end
12+
end

examples/blog/schema.rb

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class Schema < GraphQL::Schema
2+
query Types::QueryType
3+
end

examples/blog/types/comment_type.rb

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module Types
2+
class CommentType < GraphQL::Schema::Object
3+
field :id, ID, null: false
4+
field :message, String, null: false
5+
end
6+
end

examples/blog/types/post_type.rb

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module Types
2+
class PostType < GraphQL::Schema::Object
3+
description "A blog post"
4+
5+
field :id, ID, null: false
6+
7+
field :title, String, null: false
8+
9+
# fields should be queried in camel-case (this will be `truncatedPreview`)
10+
field :truncated_preview, String, null: false
11+
12+
# Fields can return lists of other objects:
13+
field :comments, [Types::CommentType], null: true,
14+
# And fields can have their own descriptions:
15+
description: "This post's comments, or null if this post has comments disabled."
16+
end
17+
end

examples/blog/types/query_type.rb

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module Types
2+
class QueryType < GraphQL::Schema::Object
3+
description "The query root of this schema"
4+
5+
# First describe the field signature:
6+
field :post, Types::PostType, null: true do
7+
description "Find a post by ID"
8+
argument :id, ID, required: true
9+
end
10+
11+
# Then provide an implementation:
12+
def post(id:)
13+
Post.find(id)
14+
end
15+
end
16+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
require 'graphql_server'
2+
3+
type_def = <<-GRAPHQL
4+
type Query {
5+
hello: String
6+
}
7+
GRAPHQL
8+
9+
resolver = {
10+
"Query" => {
11+
"hello" => Proc.new { "world" }
12+
}
13+
}
14+
15+
run GraphqlServer.new(type_def: type_def, resolver: resolver)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require 'graphql_server'
2+
3+
type_def = <<-GRAPHQL
4+
type Query {
5+
hello: String
6+
}
7+
GRAPHQL
8+
9+
resolver = {
10+
"Query" => {
11+
"hello" => Proc.new { "world" }
12+
}
13+
}
14+
15+
use GraphqlServer, type_def: type_def, resolver: resolver, path: '/graphql'
16+
run ->(env) { [200, {"Content-Type" => "text/html"}, ["Hello World!"]] }

graphql_server.gemspec

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Gem::Specification.new do |s|
2+
s.name = 'graphql_server'
3+
s.version = '0.0.1'
4+
s.date = '2019-01-18'
5+
s.summary = "A simple spec-compliant GraphQL rack application and middleware"
6+
s.description = "A simple spec-compliant GraphQL rack application and middleware"
7+
s.authors = ["betaflag"]
8+
s.email = '[email protected]'
9+
s.files = ["README.md", "lib/graphql_server.rb"]
10+
s.test_files = s.files.select { |p| p =~ /^test\/.*_test.rb/ }
11+
s.homepage = 'https://github.com/betaflag/graphql-server-ruby'
12+
s.license = 'MIT'
13+
14+
s.add_dependency 'rack', '~> 2.0'
15+
s.add_dependency 'graphql', '~> 1.8.13'
16+
s.add_dependency 'json', '~> 1.8.6'
17+
end

lib/graphql_server.rb

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
require 'graphql'
2+
require 'json'
3+
require 'rack'
4+
5+
class GraphqlServer
6+
class InvalidRequestTypeError < Exception; end;
7+
class PostBodyMissing < Exception; end;
8+
9+
# Initilizes GraphqlServer as a Rack app or middleware
10+
#
11+
# This Rack middleware (or app) implements a spec-compliant GraphQL server which can be queried from any GraphQL client.
12+
# It can be used with a provided GraphQL schema or can build one from a type definition and a resolver hash.
13+
#
14+
# @param [Array<Object>] *args The first argument should be `app` when used as a middleware
15+
# @param String path Also for middleware, we will use path to determine when to process GraphQL, defaults to '/'
16+
# @param String type_def A schema definition string, or a path to a file containing the definition
17+
# @param Hash resolver A hash with callables for handling field resolution
18+
# @param GraphQL::Schema schema Use this schema if `type_def` and `resolver` is nil
19+
# @param Hash context
20+
def initialize(*args , path: nil, type_def: nil, resolver: nil, schema: nil, context: nil)
21+
@app = args && args[0]
22+
@context = context
23+
@path = (@app && !path) ? '/' : path
24+
@schema = type_def && resolver ? GraphQL::Schema.from_definition(type_def, default_resolve: resolver) : schema
25+
end
26+
27+
def middleware?
28+
!@app.nil?
29+
end
30+
31+
def call(env)
32+
request = Rack::Request.new(env)
33+
34+
# only resolve when url matches `path` if we're in a middleware
35+
return @app.call(env) if middleware? && @path != request.path_info
36+
37+
# graphql accepts GET and POST requests
38+
raise InvalidRequestTypeError unless request.get? || request.post?
39+
40+
payload = if request.get?
41+
request.params
42+
elsif request.post?
43+
body = request.body.read
44+
raise PostBodyMissing if body.empty?
45+
payload = JSON.parse(body)
46+
end
47+
48+
response = @schema.execute(
49+
payload['query'],
50+
variables: payload['variables'],
51+
operation_name: payload['operationName'],
52+
context: @context,
53+
).to_json
54+
55+
[200, {'Content-Type' => 'application/json', 'Content-Length' => response.bytesize.to_s}, [response]]
56+
rescue InvalidRequestTypeError
57+
# Method Not Allowed
58+
[405, {"Content-Type" => "text/html"}, ["GraphQL Server supports only GET/POST requests"]]
59+
rescue PostBodyMissing
60+
# Bad Request
61+
[400, {"Content-Type" => "text/html"}, ["POST body missing"]]
62+
end
63+
end

test/graphql_server_test.rb

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
require 'minitest/autorun'
2+
require "minitest/spec"
3+
4+
require 'graphql_server'
5+
require 'rack/test'
6+
require 'graphql'
7+
require 'hello_world_server'
8+
9+
describe GraphqlServer do
10+
include Rack::Test::Methods
11+
12+
describe "app" do
13+
describe "#call" do
14+
let(:app) { HelloWorldServer.app }
15+
16+
it 'responds to a graphql query via get' do
17+
get '/', {'query' => '{ hello }'}
18+
assert_equal 200, last_response.status
19+
expected_body = '{"data":{"hello":"world"}}'
20+
assert_equal expected_body, last_response.body
21+
end
22+
23+
it 'responds to a graphql query via post' do
24+
post '/', {'query' => '{ hello }'}.to_json
25+
assert_equal 200, last_response.status
26+
expected_body = '{"data":{"hello":"world"}}'
27+
assert_equal expected_body, last_response.body
28+
end
29+
30+
it 'does not support PUT queries' do
31+
put '/', {'query' => '{ hello }'}
32+
assert_equal 405, last_response.status
33+
assert_equal 'GraphQL Server supports only GET/POST requests', last_response.body
34+
end
35+
36+
it 'does not support DELETE queries' do
37+
delete '/', {'query' => '{ hello }'}
38+
assert_equal 405, last_response.status
39+
assert_equal 'GraphQL Server supports only GET/POST requests', last_response.body
40+
end
41+
42+
it 'returns error 400 when post body is missing' do
43+
post '/'
44+
assert_equal 400, last_response.status
45+
assert_equal 'POST body missing', last_response.body
46+
end
47+
end
48+
end
49+
50+
describe "middleware" do
51+
describe "#call" do
52+
let(:app) { HelloWorldServer.middleware }
53+
54+
it 'responds to a graphql query via get' do
55+
get '/', {'query' => '{ hello }'}
56+
assert_equal 200, last_response.status
57+
expected_body = '{"data":{"hello":"world"}}'
58+
assert_equal expected_body, last_response.body
59+
end
60+
end
61+
end
62+
end

0 commit comments

Comments
 (0)