giraph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 200536c7e29c300962bf1e9306f3656852b19a88
4
+ data.tar.gz: b6c30010a1e6406d47ca41d96ca66dd09dc3a00e
5
+ SHA512:
6
+ metadata.gz: 922c37a7505fac9f68bf53611b4a1a0d139a5fa88a9f40e59a7b2886513d6a29d259b4c3e32f2654063c5f1c3d4ba014c56d80cd5a8ece5a310ee39f233c8e18
7
+ data.tar.gz: eb2a773e12531711f3abd82e72b2fc80c7cda9ce954f472eb86261c64fe947e21348ba6a4476e9ca87f4b6f0e48a0ac0c3ad2eb0fa09e7928e36b870b4f0ce85
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ *.swp
3
+ *.gem
4
+ Gemfile.lock
5
+ coverage/
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.1
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.1.0
5
+ - 2.3.0
6
+ script: bundle exec rake --trace
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in giraph.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Upserve
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Giraph
2
+
3
+ _(Pronounced with a G as in GIF)_
4
+
5
+ Ever wanted to have multiple GraphQL endpoints presented under one?
6
+
7
+ Ever felt like interactions between micro-services are not DRY enough?
8
+
9
+ If so, you'll feel right at home with Giraph.
10
+
11
+ Giraph allows you to plug-in a remote GraphQL endpoint under another one as a regular field.
12
+ You can now plug a remote GraphQL endpoint in as a field anywhere within your type hierarchy,
13
+ allow clients to send a single unified query, have the remote servers resolve their respective
14
+ sub-queries and return a valid response that all seamlessly comes together.
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'giraph'
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+ Or install it yourself as:
29
+
30
+ $ gem install giraph
31
+
32
+ ## Usage
33
+
34
+ TODO: Write usage instructions here
35
+
36
+ ## Development
37
+
38
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
39
+
40
+ 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).
41
+
42
+ ## Contributing
43
+
44
+ Bug reports and pull requests are welcome on GitHub at https://github.com/upserve/giraph.
45
+
46
+
47
+ ## License
48
+
49
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
50
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: [:spec]
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "giraph"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/giraph.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'giraph/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'giraph'
8
+ spec.version = Giraph::VERSION
9
+ spec.authors = ['Erman Celen']
10
+ spec.email = ['erman@upserve.com']
11
+
12
+ spec.summary = 'Composable GraphQL for micro-services'
13
+ spec.description = 'Expose a remote GraphQL endpoint within another as a regular field'
14
+ spec.homepage = 'https://github.com/upserve/giraph'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'bin'
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.required_ruby_version = '~> 2.1'
24
+
25
+ spec.add_runtime_dependency 'graphql', '0.15.3'
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.11'
28
+ spec.add_development_dependency 'pry'
29
+ spec.add_development_dependency 'pry-byebug'
30
+ spec.add_development_dependency 'rake'
31
+ spec.add_development_dependency 'rspec', '~> 3.0'
32
+ end
@@ -0,0 +1,33 @@
1
+ module Giraph
2
+ # Wrapped methods on GraphQL::Field class
3
+ module Extensions
4
+ module Field
5
+ # Wrap the 'resolve' method on Field so that we can
6
+ # intercept the registered resolution Proc and resolve
7
+ # on already-resolved values from the JSON returned
8
+ # to the sub-query through the remote endpoint
9
+ def resolve(object, arguments, context)
10
+ # Giraph always parses a remote response with a special Hash
11
+ # class called 'Giraph::Remote::Response', which is a no-op sub-class
12
+ # of Hash. This way we can easily recognize this special
13
+ # "resolve on already resolved response" case while still
14
+ # allowing regular Hash to be resolved normally.
15
+ if object.instance_of? Giraph::Remote::Response
16
+ # If the field was aliased, response will be keyed by the alias
17
+ field = context.ast_node.alias || context.ast_node.name
18
+ object[field.to_sym]
19
+ else
20
+ # Not Giraph, let it through...
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # Monkey-patch GraphQL Field class to wrap the default 'resolve'
29
+ module GraphQL
30
+ class Field
31
+ prepend Giraph::Extensions::Field
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ module Giraph
2
+ module Remote
3
+ class InvalidResponse < StandardError; end
4
+
5
+ # sends the reconstructed subquery request and parses the response.
6
+ class Connector
7
+ def initialize(endpoint)
8
+ @endpoint = endpoint
9
+ end
10
+
11
+ # The resolver method for the connection field.
12
+ def resolve(context, query_string, query_variables)
13
+ # Remote can return data, error or totally freak out,
14
+ # we handle all here, and note anything of relevance
15
+ result = run_query(query_string, query_variables)
16
+ return_data_or_raise(result) do |exception|
17
+ # Tack on details for host's version of the query
18
+ exception.ast_node = context.ast_node
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def run_query(query, variable)
25
+ Net::HTTP.post_form(URI(@endpoint), query: query, variables: variable)
26
+ end
27
+
28
+ def return_data_or_raise(response, &block)
29
+ result = Remote::Response.from_json(response.body)
30
+
31
+ # Remote returned a valid result set, pass it through
32
+ return result[:data] if result[:data]
33
+
34
+ # Remote returned a GraphQL error, raise it as such
35
+ raise remote_execution_error(result[:errors], block)
36
+ rescue JSON::ParserError
37
+ # Remote server returned an invalid result (non-JSON response)
38
+ # meaning something went wrong. Raise an error that reflects this.
39
+ raise(
40
+ InvalidResponse,
41
+ "Remote endpoint returned: '#{response.code} #{response.msg}'"
42
+ )
43
+ end
44
+
45
+ def remote_execution_error(errors, block)
46
+ GraphQL::ExecutionError.new(errors).tap do |ex|
47
+ # Allow exception to be modified if needed
48
+ block.call(ex) if block
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,20 @@
1
+ module Giraph
2
+ module Remote
3
+ # Field resolver to plug a remote GraphQL mutation root into a local type
4
+ class Mutation < Query
5
+ def self.bind(endpoint, &block)
6
+ new(
7
+ endpoint,
8
+ Remote::Connector.new(endpoint, mutation: true),
9
+ &block
10
+ )
11
+ end
12
+
13
+ private
14
+
15
+ def query_type
16
+ 'mutation'
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,61 @@
1
+ module Giraph
2
+ module Remote
3
+ # Field resolver to plug a remote GraphQL query root into a local type
4
+ class Query
5
+ def self.bind(endpoint, &block)
6
+ new(
7
+ endpoint,
8
+ Remote::Connector.new(endpoint),
9
+ &block
10
+ )
11
+ end
12
+
13
+ def initialize(endpoint, connector, &block)
14
+ @endpoint = endpoint
15
+ @evaluator = block || method(:default_evaluator)
16
+ @connector = connector
17
+ end
18
+
19
+ # Reconstructs a valid GraphQL root-query from the current
20
+ # field in question, including all variables and params,
21
+ # hands over to connector to execute remotely.
22
+ def call(obj, args, ctx)
23
+ # Given an evaluator block, continue if only it evaluates to non-nil!
24
+ return unless (remote_root = @evaluator.call(obj, args, ctx))
25
+
26
+ subquery = Subquery.new(ctx)
27
+
28
+ # Continue with remote query execution
29
+ connector.resolve(
30
+ ctx,
31
+ query_string(subquery),
32
+ query_variables(subquery, remote_root)
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def query_type
39
+ 'query'
40
+ end
41
+
42
+ def query_string(subquery)
43
+ # Full GraphQL query for remote
44
+ "#{query_type} #{subquery.subquery_string}"
45
+ end
46
+
47
+ def query_variables(subquery, remote_root)
48
+ # Variable hash to send along
49
+ subquery.variable_string do |dict|
50
+ dict.merge(__giraph_root__: remote_root)
51
+ end
52
+ end
53
+
54
+ def default_evaluator(*args)
55
+ {}
56
+ end
57
+
58
+ attr_reader :endpoint, :connector
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,14 @@
1
+ module Giraph
2
+ module Remote
3
+ # Dummy Hash-wrapper to enable precise `instance_of?` checks
4
+ # Allows us to differentiate JSON responses that are
5
+ # coming from remote GraphQL interface vs regular Hash objects
6
+ # including all nested hashes within a response.
7
+ class Response < Hash
8
+ # Factory method to encapsulate special parsing logic
9
+ def self.from_json(raw_json)
10
+ JSON.parse(raw_json, symbolize_names: true, object_class: self)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ module Giraph
2
+ # Proc-like class to allow declerative links to external
3
+ # resolution handlers (so the "definition" gem can stay pure resolution-wise)
4
+ class Resolver
5
+ class UnknownOperation < StandardError; end
6
+
7
+ def self.for(method_name)
8
+ new(method_name)
9
+ end
10
+
11
+ def initialize(method_name)
12
+ @method_name = method_name
13
+ end
14
+
15
+ # Resolves the field by calling the previously given method
16
+ # on the registered Resolver object for the current operation
17
+ # type (currently query or mutation)
18
+ def call(obj, args, ctx)
19
+ # Find out operation type (query, mutation, etc.)
20
+ op_type = ctx.query.selected_operation.operation_type.to_sym
21
+
22
+ # Ensure there is a registered resolver for it
23
+ unless (resolver = ctx[:__giraph_resolver__][op_type])
24
+ raise UnknownOperation, "No resolver found for '#{op_type}' op type"
25
+ end
26
+
27
+ # Call the requested method on resolver
28
+ resolver.public_send(@method_name, obj, args, ctx)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,48 @@
1
+ module Giraph
2
+ # GraphQL::Schema wrapper allowing decleration of
3
+ # resolver objects per op type (query or mutation)
4
+ class Schema < DelegateClass(GraphQL::Schema)
5
+ # Extract special arguments for resolver objects,
6
+ # let the rest pass-through
7
+ def initialize(**args)
8
+ @query_resolver = args.delete(:query_resolver)
9
+ @mutation_resolver = args.delete(:mutation_resolver)
10
+ super(GraphQL::Schema.new(**args))
11
+ end
12
+
13
+ # Defer the execution only after setting up
14
+ # context and root_value with resolvers and remote arguments
15
+ def execute(query, **args)
16
+ args = args
17
+ .merge(with_giraph_root(args))
18
+ .merge(with_giraph_resolvers(args))
19
+
20
+ super(query, **args)
21
+ end
22
+
23
+ private
24
+
25
+ def with_giraph_root(args)
26
+ # Extract & remove the special __giraph_root__ key
27
+ # from variables passed in, if any
28
+ vars = args[:variables] || {}
29
+ root = vars.delete('__giraph_root__') || {}
30
+
31
+ # Set given pseudo-root as root_value for the execution
32
+ { root_value: root }
33
+ end
34
+
35
+ def with_giraph_resolvers(args)
36
+ # Pass on resolver objects in context
37
+ # which will then be used by Giraph resolvers
38
+ # to direct per-field resolution
39
+ context = args[:context] || {}
40
+ context[:__giraph_resolver__] = {
41
+ query: @query_resolver,
42
+ mutation: @mutation_resolver
43
+ }
44
+
45
+ { context: context }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,72 @@
1
+ module Giraph
2
+ # Defines a thin & sane API for the sub-query extraction
3
+ # from the context provided to resolvers via AST, graphql-ruby API
4
+ # and a touch of regex.
5
+ class Subquery
6
+ attr_reader :query, :query_string
7
+
8
+ def initialize(context)
9
+ @context = context
10
+
11
+ @query = context.query
12
+ @query_string = context.ast_node.to_query_string
13
+ end
14
+
15
+ def subquery_string
16
+ "GiraphQuery #{query_variables} #{query_selections}"
17
+ end
18
+
19
+ def variable_string
20
+ dict = variable_assignments
21
+ dict = yield dict if block_given?
22
+
23
+ dict.to_json
24
+ end
25
+
26
+ private
27
+
28
+ def variable_assignments
29
+ # This re-encodes and passes on all the variable values
30
+ # paseed to the original query endpoint
31
+ query.instance_variable_get('@provided_variables')
32
+ end
33
+
34
+ def query_selections
35
+ # We get the following from node's sub-query:
36
+ # nodeName { field1 { field12 } field2(a: $a) field3 }
37
+ # we want:
38
+ # { field1 { field12 } field2(a: $a) field3 }
39
+ # as the name of node is local information to parent host
40
+ # and is no use to the remote host.
41
+ query_string.sub(/^[^{]+/, '')
42
+ end
43
+
44
+ def query_variables
45
+ # Recreates the declared query parameters to be passed on
46
+ # to the remote host.
47
+ declarations = query
48
+ .selected_operation
49
+ .variables
50
+ .select(&method(:variable_used?))
51
+ .map(&method(:variable_decleration))
52
+
53
+ "(#{declarations.join(', ')})" unless declarations.empty?
54
+ end
55
+
56
+ def variable_used?(variable)
57
+ query_string[/\$#{Regexp.quote(variable.name)}\W/]
58
+ end
59
+
60
+ def variable_decleration(variable)
61
+ "$#{variable.name}: #{variable_type(variable)}"
62
+ end
63
+
64
+ def variable_type(variable)
65
+ if variable.type.is_a?(GraphQL::Language::Nodes::NonNullType)
66
+ variable.type.of_type.name + '!'
67
+ else
68
+ variable.type.name
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,3 @@
1
+ module Giraph
2
+ VERSION = "0.1.0"
3
+ end
data/lib/giraph.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'json'
2
+ require 'graphql'
3
+
4
+ require 'giraph/version'
5
+
6
+ require 'giraph/subquery'
7
+ require 'giraph/extensions/field'
8
+ require 'giraph/remote/response'
9
+ require 'giraph/remote/connector'
10
+ require 'giraph/remote/query'
11
+ require 'giraph/remote/mutation'
12
+ require 'giraph/resolver'
13
+ require 'giraph/schema'
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: giraph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Erman Celen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-08-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: graphql
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.15.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.15.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry-byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ description: Expose a remote GraphQL endpoint within another as a regular field
98
+ email:
99
+ - erman@upserve.com
100
+ executables:
101
+ - console
102
+ - setup
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".ruby-version"
108
+ - ".travis.yml"
109
+ - Gemfile
110
+ - LICENSE.txt
111
+ - README.md
112
+ - Rakefile
113
+ - bin/console
114
+ - bin/setup
115
+ - giraph.gemspec
116
+ - lib/giraph.rb
117
+ - lib/giraph/extensions/field.rb
118
+ - lib/giraph/remote/connector.rb
119
+ - lib/giraph/remote/mutation.rb
120
+ - lib/giraph/remote/query.rb
121
+ - lib/giraph/remote/response.rb
122
+ - lib/giraph/resolver.rb
123
+ - lib/giraph/schema.rb
124
+ - lib/giraph/subquery.rb
125
+ - lib/giraph/version.rb
126
+ homepage: https://github.com/upserve/giraph
127
+ licenses:
128
+ - MIT
129
+ metadata: {}
130
+ post_install_message:
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '2.1'
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubyforge_project:
146
+ rubygems_version: 2.5.1
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: Composable GraphQL for micro-services
150
+ test_files: []