graphql 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graph_ql/directive.rb +36 -0
- data/lib/graph_ql/directives/directive_chain.rb +33 -0
- data/lib/graph_ql/directives/include_directive.rb +15 -0
- data/lib/graph_ql/directives/skip_directive.rb +15 -0
- data/lib/graph_ql/enum.rb +34 -0
- data/lib/graph_ql/fields/abstract_field.rb +37 -0
- data/lib/graph_ql/fields/access_field.rb +24 -0
- data/lib/graph_ql/fields/field.rb +34 -0
- data/lib/graph_ql/interface.rb +14 -0
- data/lib/graph_ql/introspection/arguments_field.rb +5 -0
- data/lib/graph_ql/introspection/directive_type.rb +12 -0
- data/lib/graph_ql/introspection/enum_value_type.rb +10 -0
- data/lib/graph_ql/introspection/enum_values_field.rb +15 -0
- data/lib/graph_ql/introspection/field_type.rb +11 -0
- data/lib/graph_ql/introspection/fields_field.rb +14 -0
- data/lib/graph_ql/introspection/input_fields_field.rb +12 -0
- data/lib/graph_ql/introspection/input_value_type.rb +10 -0
- data/lib/graph_ql/introspection/of_type_field.rb +12 -0
- data/lib/graph_ql/introspection/possible_types_field.rb +12 -0
- data/lib/graph_ql/introspection/schema_type.rb +32 -0
- data/lib/graph_ql/introspection/type_kind_enum.rb +7 -0
- data/lib/graph_ql/introspection/type_type.rb +22 -0
- data/lib/graph_ql/parser/nodes.rb +72 -0
- data/lib/graph_ql/parser/parser.rb +108 -0
- data/lib/graph_ql/parser/transform.rb +86 -0
- data/lib/graph_ql/parser/visitor.rb +47 -0
- data/lib/graph_ql/query.rb +50 -0
- data/lib/graph_ql/query/arguments.rb +25 -0
- data/lib/graph_ql/query/field_resolution_strategy.rb +83 -0
- data/lib/graph_ql/query/fragment_spread_resolution_strategy.rb +16 -0
- data/lib/graph_ql/query/inline_fragment_resolution_strategy.rb +14 -0
- data/lib/graph_ql/query/operation_resolver.rb +28 -0
- data/lib/graph_ql/query/selection_resolver.rb +20 -0
- data/lib/graph_ql/query/type_resolver.rb +19 -0
- data/lib/graph_ql/repl.rb +27 -0
- data/lib/graph_ql/schema.rb +30 -0
- data/lib/graph_ql/schema/type_reducer.rb +44 -0
- data/lib/graph_ql/type_kinds.rb +15 -0
- data/lib/graph_ql/types/abstract_type.rb +14 -0
- data/lib/graph_ql/types/boolean_type.rb +6 -0
- data/lib/graph_ql/types/float_type.rb +6 -0
- data/lib/graph_ql/types/input_object_type.rb +17 -0
- data/lib/graph_ql/types/input_value.rb +10 -0
- data/lib/graph_ql/types/int_type.rb +6 -0
- data/lib/graph_ql/types/list_type.rb +10 -0
- data/lib/graph_ql/types/non_null_type.rb +18 -0
- data/lib/graph_ql/types/non_null_with_bang.rb +5 -0
- data/lib/graph_ql/types/object_type.rb +62 -0
- data/lib/graph_ql/types/scalar_type.rb +5 -0
- data/lib/graph_ql/types/string_type.rb +6 -0
- data/lib/graph_ql/types/type_definer.rb +16 -0
- data/lib/graph_ql/union.rb +35 -0
- data/lib/graph_ql/validations/fields_are_defined_on_type.rb +44 -0
- data/lib/graph_ql/validations/fields_will_merge.rb +80 -0
- data/lib/graph_ql/validations/fragments_are_used.rb +24 -0
- data/lib/graph_ql/validator.rb +29 -0
- data/lib/graph_ql/version.rb +3 -0
- data/lib/graphql.rb +92 -99
- data/readme.md +17 -177
- data/spec/graph_ql/directive_spec.rb +81 -0
- data/spec/graph_ql/enum_spec.rb +5 -0
- data/spec/graph_ql/fields/field_spec.rb +10 -0
- data/spec/graph_ql/interface_spec.rb +13 -0
- data/spec/graph_ql/introspection/directive_type_spec.rb +40 -0
- data/spec/graph_ql/introspection/schema_type_spec.rb +39 -0
- data/spec/graph_ql/introspection/type_type_spec.rb +104 -0
- data/spec/graph_ql/parser/parser_spec.rb +120 -0
- data/spec/graph_ql/parser/transform_spec.rb +109 -0
- data/spec/graph_ql/parser/visitor_spec.rb +31 -0
- data/spec/graph_ql/query/operation_resolver_spec.rb +14 -0
- data/spec/graph_ql/query_spec.rb +82 -0
- data/spec/graph_ql/schema/type_reducer_spec.rb +24 -0
- data/spec/graph_ql/types/input_object_type_spec.rb +12 -0
- data/spec/graph_ql/types/object_type_spec.rb +35 -0
- data/spec/graph_ql/union_spec.rb +27 -0
- data/spec/graph_ql/validations/fields_are_defined_on_type_spec.rb +28 -0
- data/spec/graph_ql/validations/fields_will_merge_spec.rb +40 -0
- data/spec/graph_ql/validations/fragments_are_used_spec.rb +28 -0
- data/spec/graph_ql/validator_spec.rb +24 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/support/dummy_app.rb +123 -63
- data/spec/support/dummy_data.rb +11 -0
- metadata +107 -59
- data/lib/graphql/call.rb +0 -8
- data/lib/graphql/connection.rb +0 -65
- data/lib/graphql/field.rb +0 -12
- data/lib/graphql/field_definer.rb +0 -25
- data/lib/graphql/introspection/call_type.rb +0 -13
- data/lib/graphql/introspection/connection.rb +0 -9
- data/lib/graphql/introspection/field_type.rb +0 -10
- data/lib/graphql/introspection/root_call_argument_node.rb +0 -5
- data/lib/graphql/introspection/root_call_type.rb +0 -20
- data/lib/graphql/introspection/schema_call.rb +0 -8
- data/lib/graphql/introspection/schema_type.rb +0 -17
- data/lib/graphql/introspection/type_call.rb +0 -8
- data/lib/graphql/introspection/type_type.rb +0 -18
- data/lib/graphql/node.rb +0 -244
- data/lib/graphql/parser/parser.rb +0 -39
- data/lib/graphql/parser/transform.rb +0 -22
- data/lib/graphql/query.rb +0 -109
- data/lib/graphql/root_call.rb +0 -202
- data/lib/graphql/root_call_argument.rb +0 -11
- data/lib/graphql/root_call_argument_definer.rb +0 -17
- data/lib/graphql/schema/all.rb +0 -46
- data/lib/graphql/schema/schema.rb +0 -87
- data/lib/graphql/schema/schema_validation.rb +0 -32
- data/lib/graphql/syntax/call.rb +0 -8
- data/lib/graphql/syntax/field.rb +0 -9
- data/lib/graphql/syntax/fragment.rb +0 -7
- data/lib/graphql/syntax/node.rb +0 -8
- data/lib/graphql/syntax/query.rb +0 -8
- data/lib/graphql/syntax/variable.rb +0 -7
- data/lib/graphql/types/boolean_type.rb +0 -3
- data/lib/graphql/types/number_type.rb +0 -3
- data/lib/graphql/types/object_type.rb +0 -6
- data/lib/graphql/types/string_type.rb +0 -3
- data/lib/graphql/version.rb +0 -3
- data/spec/graphql/node_spec.rb +0 -69
- data/spec/graphql/parser/parser_spec.rb +0 -168
- data/spec/graphql/parser/transform_spec.rb +0 -157
- data/spec/graphql/query_spec.rb +0 -274
- data/spec/graphql/root_call_spec.rb +0 -69
- data/spec/graphql/schema/schema_spec.rb +0 -93
- data/spec/graphql/schema/schema_validation_spec.rb +0 -48
- data/spec/support/nodes.rb +0 -175
@@ -0,0 +1,44 @@
|
|
1
|
+
class GraphQL::Validations::FieldsAreDefinedOnType
|
2
|
+
TYPE_INFERRENCE_ROOTS = [GraphQL::Nodes::OperationDefinition, GraphQL::Nodes::FragmentDefinition]
|
3
|
+
FIELD_MODIFIERS = [GraphQL::TypeKinds::LIST]
|
4
|
+
|
5
|
+
def validate(context)
|
6
|
+
visitor = context.visitor
|
7
|
+
TYPE_INFERRENCE_ROOTS.each do |node_class|
|
8
|
+
visitor[node_class] << -> (node){ validate_document_part(node, context) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def validate_document_part(part, context)
|
14
|
+
if part.is_a?(GraphQL::Nodes::FragmentDefinition)
|
15
|
+
type = context.schema.types[part.type]
|
16
|
+
validate_selections(type, part.selections, context)
|
17
|
+
elsif part.is_a?(GraphQL::Nodes::OperationDefinition)
|
18
|
+
type = context.schema.public_send(part.operation_type) # mutation root or query root
|
19
|
+
validate_selections(type, part.selections, context)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_selections(type, selections, context)
|
24
|
+
selections
|
25
|
+
.select {|f| f.is_a?(GraphQL::Nodes::Field) } # don't worry about fragments
|
26
|
+
.each do |ast_field|
|
27
|
+
field = type.fields[ast_field.name]
|
28
|
+
if field.nil?
|
29
|
+
context.errors << "Field '#{ast_field.name}' doesn't exist on type '#{type.name}'"
|
30
|
+
else
|
31
|
+
field_type = get_field_type(field)
|
32
|
+
validate_selections(field_type, ast_field.selections, context)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_field_type(field)
|
38
|
+
if FIELD_MODIFIERS.include?(field.type.kind)
|
39
|
+
field.type.of_type
|
40
|
+
else
|
41
|
+
field.type
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class GraphQL::Validations::FieldsWillMerge
|
2
|
+
HAS_SELECTIONS = [GraphQL::Nodes::OperationDefinition, GraphQL::Nodes::InlineFragment]
|
3
|
+
|
4
|
+
def validate(context)
|
5
|
+
fragments = {}
|
6
|
+
has_selections = []
|
7
|
+
visitor = context.visitor
|
8
|
+
HAS_SELECTIONS.each do |node_class|
|
9
|
+
visitor[node_class] << -> (node) { has_selections << node }
|
10
|
+
end
|
11
|
+
visitor[GraphQL::Nodes::FragmentDefinition] << -> (node) { fragments[node.name] = node }
|
12
|
+
visitor[GraphQL::Nodes::Document].leave << -> (node) {
|
13
|
+
has_selections.each { |node| validate_selections(node.selections, {}, fragments, context.errors)}
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def validate_selections(selections, name_to_field, fragments, errors)
|
20
|
+
selections.each do |field|
|
21
|
+
if field.is_a?(GraphQL::Nodes::InlineFragment)
|
22
|
+
validate_selections(field.selections, name_to_field, fragments, errors)
|
23
|
+
elsif field.is_a?(GraphQL::Nodes::FragmentSpread)
|
24
|
+
fragment = fragments[field.name]
|
25
|
+
validate_selections(fragment.selections, name_to_field, fragments, errors)
|
26
|
+
else
|
27
|
+
field_errors = validate_field(field, name_to_field)
|
28
|
+
errors.push(*field_errors)
|
29
|
+
validate_selections(field.selections, name_to_field, fragments, errors)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate_field(field, name_to_field)
|
35
|
+
name_in_selection = field.alias || field.name
|
36
|
+
name_to_field[name_in_selection] ||= field
|
37
|
+
first_field_def = name_to_field[name_in_selection]
|
38
|
+
comparison = FieldDefinitionComparison.new(name_in_selection, first_field_def, field)
|
39
|
+
comparison.errors
|
40
|
+
end
|
41
|
+
|
42
|
+
# Compare two field definitions, add errors to the list if there are any
|
43
|
+
class FieldDefinitionComparison
|
44
|
+
NAMED_VALUES = [GraphQL::Nodes::Enum, GraphQL::Nodes::VariableIdentifier]
|
45
|
+
attr_reader :errors
|
46
|
+
def initialize(name, prev_def, next_def)
|
47
|
+
errors = []
|
48
|
+
if prev_def.name != next_def.name
|
49
|
+
errors << "Field '#{name}' has a field conflict: #{prev_def.name} or #{next_def.name}?"
|
50
|
+
end
|
51
|
+
prev_arguments = reduce_list(prev_def.arguments)
|
52
|
+
next_arguments = reduce_list(next_def.arguments)
|
53
|
+
if prev_arguments != next_arguments
|
54
|
+
errors << "Field '#{name}' has an argument conflict: #{JSON.dump(prev_arguments)} or #{JSON.dump(next_arguments)}?"
|
55
|
+
end
|
56
|
+
prev_directive_names = prev_def.directives.map(&:name)
|
57
|
+
next_directive_names = next_def.directives.map(&:name)
|
58
|
+
if prev_directive_names != next_directive_names
|
59
|
+
errors << "Field '#{name}' has a directive conflict: [#{prev_directive_names.join(", ")}] or [#{next_directive_names.join(", ")}]?"
|
60
|
+
end
|
61
|
+
prev_directive_args = prev_def.directives.map {|d| reduce_list(d.arguments) }
|
62
|
+
next_directive_args = next_def.directives.map {|d| reduce_list(d.arguments) }
|
63
|
+
if prev_directive_args != next_directive_args
|
64
|
+
errors << "Field '#{name}' has a directive argument conflict: #{JSON.dump(prev_directive_args)} or #{JSON.dump(next_directive_args)}?"
|
65
|
+
end
|
66
|
+
@errors = errors
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Turn AST tree into a hash
|
72
|
+
# can't look up args, the names just have to match
|
73
|
+
def reduce_list(args)
|
74
|
+
args.reduce({}) do |memo, a|
|
75
|
+
memo[a.name] = NAMED_VALUES.include?(a.value.class) ? a.value.name : a.value
|
76
|
+
memo
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class GraphQL::Validations::FragmentsAreUsed
|
2
|
+
def validate(context)
|
3
|
+
v = context.visitor
|
4
|
+
used_fragment_names = []
|
5
|
+
defined_fragment_names = []
|
6
|
+
v[GraphQL::Nodes::FragmentSpread] << -> (node) { used_fragment_names << node.name }
|
7
|
+
v[GraphQL::Nodes::FragmentDefinition] << -> (node) { defined_fragment_names << node.name}
|
8
|
+
v[GraphQL::Nodes::Document].leave << -> (node) { add_errors(context.errors, used_fragment_names, defined_fragment_names) }
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def add_errors(errors, used_fragment_names, defined_fragment_names)
|
14
|
+
undefined_fragment_names = used_fragment_names - defined_fragment_names
|
15
|
+
if undefined_fragment_names.any?
|
16
|
+
errors << "Some fragments were used but not defined: #{undefined_fragment_names.join(", ")}"
|
17
|
+
end
|
18
|
+
|
19
|
+
unused_fragment_names = defined_fragment_names - used_fragment_names
|
20
|
+
if unused_fragment_names.any?
|
21
|
+
errors << "Some fragments were defined but not used: #{unused_fragment_names.join(", ")}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class GraphQL::Validator
|
2
|
+
VALIDATORS = [
|
3
|
+
GraphQL::Validations::FragmentsAreUsed,
|
4
|
+
]
|
5
|
+
|
6
|
+
def initialize(schema:, validators: VALIDATORS)
|
7
|
+
@schema = schema
|
8
|
+
@validators = validators
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate(document)
|
12
|
+
context = Context.new(@schema, document)
|
13
|
+
@validators.each do |validator|
|
14
|
+
validator.new.validate(context)
|
15
|
+
end
|
16
|
+
context.visitor.visit(document)
|
17
|
+
context.errors
|
18
|
+
end
|
19
|
+
|
20
|
+
class Context
|
21
|
+
attr_reader :schema, :document, :errors, :visitor
|
22
|
+
def initialize(schema, document)
|
23
|
+
@schema = schema
|
24
|
+
@document = document
|
25
|
+
@visitor = GraphQL::Visitor.new
|
26
|
+
@errors = []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/graphql.rb
CHANGED
@@ -1,121 +1,114 @@
|
|
1
1
|
require "active_support/core_ext/object/blank"
|
2
2
|
require "active_support/core_ext/string/inflections"
|
3
|
+
require "active_support/dependencies/autoload"
|
3
4
|
require "json"
|
4
5
|
require "parslet"
|
5
6
|
|
6
7
|
module GraphQL
|
7
|
-
|
8
|
-
autoload(:
|
9
|
-
autoload(:
|
10
|
-
autoload(:
|
11
|
-
autoload(:
|
12
|
-
autoload(:Query
|
13
|
-
autoload(:
|
14
|
-
autoload(:
|
15
|
-
autoload(:
|
16
|
-
autoload(:
|
8
|
+
extend ActiveSupport::Autoload
|
9
|
+
autoload(:Directive)
|
10
|
+
autoload(:Enum)
|
11
|
+
autoload(:Interface)
|
12
|
+
autoload(:Parser)
|
13
|
+
autoload(:Query)
|
14
|
+
autoload(:Repl)
|
15
|
+
autoload(:Schema)
|
16
|
+
autoload(:TypeKinds)
|
17
|
+
autoload(:Union)
|
18
|
+
autoload(:Validator)
|
19
|
+
autoload(:VERSION)
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
autoload(:
|
21
|
-
autoload(:
|
22
|
-
autoload(:FieldType, "graphql/introspection/field_type")
|
23
|
-
autoload(:RootCallArgumentNode, "graphql/introspection/root_call_argument_node")
|
24
|
-
autoload(:RootCallType, "graphql/introspection/root_call_type")
|
25
|
-
autoload(:SchemaCall, "graphql/introspection/schema_call")
|
26
|
-
autoload(:SchemaType, "graphql/introspection/schema_type")
|
27
|
-
autoload(:TypeCall, "graphql/introspection/type_call")
|
28
|
-
autoload(:TypeType, "graphql/introspection/type_type")
|
21
|
+
autoload_under "directives" do
|
22
|
+
autoload(:DirectiveChain)
|
23
|
+
autoload(:IncludeDirective)
|
24
|
+
autoload(:SkipDirective)
|
29
25
|
end
|
30
26
|
|
31
|
-
|
32
|
-
|
33
|
-
autoload(:
|
34
|
-
autoload(:
|
27
|
+
autoload_under "fields" do
|
28
|
+
autoload(:AbstractField)
|
29
|
+
autoload(:AccessField)
|
30
|
+
autoload(:Field)
|
35
31
|
end
|
36
32
|
|
37
|
-
|
38
|
-
|
39
|
-
autoload(:
|
40
|
-
autoload(:
|
41
|
-
autoload(:
|
33
|
+
autoload_under "introspection" do
|
34
|
+
autoload(:ArgumentsField)
|
35
|
+
autoload(:DirectiveType)
|
36
|
+
autoload(:EnumValuesField)
|
37
|
+
autoload(:EnumValueType)
|
38
|
+
autoload(:FieldType)
|
39
|
+
autoload(:FieldsField)
|
40
|
+
autoload(:InputValueType)
|
41
|
+
autoload(:InputFieldsField)
|
42
|
+
autoload(:OfTypeField)
|
43
|
+
autoload(:PossibleTypesField)
|
44
|
+
autoload(:SchemaType)
|
45
|
+
autoload(:TypeKindEnum)
|
46
|
+
autoload(:TypeType)
|
42
47
|
end
|
43
48
|
|
44
|
-
|
45
|
-
|
46
|
-
autoload(:
|
47
|
-
autoload(:
|
48
|
-
autoload(:
|
49
|
-
autoload(:Fragment, "graphql/syntax/fragment")
|
50
|
-
autoload(:Node, "graphql/syntax/node")
|
51
|
-
autoload(:Variable, "graphql/syntax/variable")
|
49
|
+
autoload_under "parser" do
|
50
|
+
autoload(:Nodes)
|
51
|
+
autoload(:Parser)
|
52
|
+
autoload(:Transform)
|
53
|
+
autoload(:Visitor)
|
52
54
|
end
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
-
autoload(:
|
57
|
-
autoload(:
|
58
|
-
autoload(:
|
59
|
-
autoload(:
|
56
|
+
autoload_under "types" do
|
57
|
+
autoload(:AbstractType)
|
58
|
+
autoload(:BOOLEAN_TYPE)
|
59
|
+
autoload(:ScalarType)
|
60
|
+
autoload(:FLOAT_TYPE)
|
61
|
+
autoload(:InputObjectType)
|
62
|
+
autoload(:InputValue)
|
63
|
+
autoload(:INT_TYPE)
|
64
|
+
autoload(:ListType)
|
65
|
+
autoload(:NonNullType)
|
66
|
+
autoload(:NonNullWithBang)
|
67
|
+
autoload(:ObjectType)
|
68
|
+
autoload(:STRING_TYPE)
|
69
|
+
autoload(:TypeDefiner)
|
60
70
|
end
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
class_name = node_class.name
|
68
|
-
defined_field_names = node_class.all_fields.keys
|
69
|
-
super("#{class_name}##{field_name} was requested, but it isn't defined. Defined fields are: #{defined_field_names}")
|
70
|
-
end
|
71
|
-
end
|
72
|
-
# The class that this node is supposed to expose isn't defined
|
73
|
-
class ExposesClassMissingError < Error
|
74
|
-
def initialize(node_class)
|
75
|
-
super("#{node_class.name} exposes #{node_class.exposes_class_names.join(", ")}, but that class wasn't found.")
|
76
|
-
end
|
77
|
-
end
|
78
|
-
# There's no Node defined for that kind of object.
|
79
|
-
class NodeNotDefinedError < Error
|
80
|
-
def initialize(node_name)
|
81
|
-
super("#{node_name} was requested but was not found. Defined nodes are: #{SCHEMA.type_names}")
|
82
|
-
end
|
83
|
-
end
|
84
|
-
# This node doesn't have a connection with that name.
|
85
|
-
class ConnectionNotDefinedError < Error
|
86
|
-
def initialize(node_name)
|
87
|
-
super("#{node_name} was requested but was not found. Defined connections are: #{SCHEMA.connection_names}")
|
88
|
-
end
|
89
|
-
end
|
90
|
-
# The root call of this query isn't in the schema.
|
91
|
-
class RootCallNotDefinedError < Error
|
92
|
-
def initialize(name)
|
93
|
-
super("Call '#{name}' was requested but was not found. Defined calls are: #{SCHEMA.call_names}")
|
94
|
-
end
|
71
|
+
|
72
|
+
module Validations
|
73
|
+
extend ActiveSupport::Autoload
|
74
|
+
autoload(:FieldsWillMerge)
|
75
|
+
autoload(:FragmentsAreUsed)
|
76
|
+
autoload(:FieldsAreDefinedOnType)
|
95
77
|
end
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
78
|
+
|
79
|
+
PARSER = Parser.new
|
80
|
+
TRANSFORM = Transform.new
|
81
|
+
|
82
|
+
def self.parse(string, as: nil)
|
83
|
+
parser = as ? GraphQL::PARSER.send(as) : GraphQL::PARSER
|
84
|
+
tree = parser.parse(string)
|
85
|
+
GraphQL::TRANSFORM.apply(tree)
|
86
|
+
rescue Parslet::ParseFailed => error
|
87
|
+
line, col = error.cause.source.line_and_column
|
88
|
+
raise [line, col, string].join(", ")
|
102
89
|
end
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
90
|
+
|
91
|
+
|
92
|
+
module Definable
|
93
|
+
def attr_definable(*names)
|
94
|
+
attr_accessor(*names)
|
95
|
+
names.each do |name|
|
96
|
+
ivar_name = "@#{name}".to_sym
|
97
|
+
define_method(name) do |new_value=nil|
|
98
|
+
new_value && self.instance_variable_set(ivar_name, new_value)
|
99
|
+
instance_variable_get(ivar_name)
|
100
|
+
end
|
101
|
+
end
|
107
102
|
end
|
108
103
|
end
|
109
104
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
Dir["#{File.dirname(__FILE__)}/graphql/#{preload_dir}/*.rb"].each { |f| require f }
|
105
|
+
module Forwardable
|
106
|
+
def delegate(*methods, to:)
|
107
|
+
methods.each do |method_name|
|
108
|
+
define_method(method_name) do |*args|
|
109
|
+
self.public_send(to).public_send(method_name, *args)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
119
113
|
end
|
120
|
-
|
121
|
-
end
|
114
|
+
end
|
data/readme.md
CHANGED
@@ -2,190 +2,30 @@
|
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/rmosolgo/graphql-ruby.svg?branch=master)](https://travis-ci.org/rmosolgo/graphql-ruby)
|
4
4
|
[![Gem Version](https://badge.fury.io/rb/graphql.svg)](https://rubygems.org/gems/graphql)
|
5
|
-
[![Dependency Status](https://gemnasium.com/rmosolgo/graphql-ruby.svg)](https://gemnasium.com/rmosolgo/graphql-ruby)
|
6
5
|
[![Code Climate](https://codeclimate.com/github/rmosolgo/graphql-ruby/badges/gpa.svg)](https://codeclimate.com/github/rmosolgo/graphql-ruby)
|
7
6
|
[![Test Coverage](https://codeclimate.com/github/rmosolgo/graphql-ruby/badges/coverage.svg)](https://codeclimate.com/github/rmosolgo/graphql-ruby)
|
8
7
|
[![built with love](https://cloud.githubusercontent.com/assets/2231765/6766607/d07992c6-cfc9-11e4-813f-d9240714dd50.png)](http://rmosolgo.github.io/react-badges/)
|
9
8
|
|
9
|
+
__Current status__: rewriting according to spec, see also the previous [prototype implementation](https://github.com/rmosolgo/graphql-ruby/tree/74ad3c30a6d8db010ec3856f5871f8a02fcfba42)!
|
10
10
|
|
11
|
-
|
11
|
+
## Overview
|
12
12
|
|
13
|
-
|
13
|
+
- See the __[test schema](https://github.com/rmosolgo/graphql-ruby/blob/master/spec/support/dummy_app.rb)__ for an example GraphQL schema in Ruby.
|
14
|
+
- See __[query_spec.rb](https://github.com/rmosolgo/graphql-ruby/blob/master/spec/graph_ql/query_spec.rb)__ for an example of query execution.
|
14
15
|
|
15
|
-
## To
|
16
|
+
## To Do:
|
16
17
|
|
17
|
-
-
|
18
|
-
-
|
19
|
-
-
|
20
|
-
-
|
21
|
-
-
|
18
|
+
- Validations:
|
19
|
+
- implement lots of validators
|
20
|
+
- build error object with position
|
21
|
+
- Express failure with `errors` key (http://facebook.github.io/graphql/#sec-Errors)
|
22
|
+
- directives:
|
23
|
+
- `@skip` has precedence over `@include`
|
24
|
+
- directives on fragments: http://facebook.github.io/graphql/#sec-Fragment-Directives
|
25
|
+
- field merging (https://github.com/graphql/graphql-js/issues/19#issuecomment-118515077)
|
22
26
|
|
23
|
-
##
|
27
|
+
## Goals:
|
24
28
|
|
25
|
-
-
|
26
|
-
-
|
27
|
-
|
28
|
-
<a href="http://graphql-ruby-demo.herokuapp.com/" target="_blank"><img src="https://cloud.githubusercontent.com/assets/2231765/6217972/5d24edda-b5ce-11e4-9e07-3548304af862.png" style="max-width: 800px;"/></a>
|
29
|
-
|
30
|
-
|
31
|
-
## Usage
|
32
|
-
|
33
|
-
- Implement _nodes_ that wrap objects in your application
|
34
|
-
- Implement _calls_ that return those objects (and may mutate the application state)
|
35
|
-
- Execute _queries_ and return the result.
|
36
|
-
|
37
|
-
### Nodes
|
38
|
-
|
39
|
-
Nodes are delegators that wrap objects in your app. You must whitelist fields by declaring them in the class definition.
|
40
|
-
|
41
|
-
|
42
|
-
```ruby
|
43
|
-
class FishNode < GraphQL::Node
|
44
|
-
exposes "Fish"
|
45
|
-
cursor(:id)
|
46
|
-
field.number(:id)
|
47
|
-
field.string(:name)
|
48
|
-
field.string(:species)
|
49
|
-
# specify an `AquariumNode`:
|
50
|
-
field.aquarium(:aquarium)
|
51
|
-
end
|
52
|
-
```
|
53
|
-
|
54
|
-
You can also declare connections between objects:
|
55
|
-
|
56
|
-
```ruby
|
57
|
-
class AquariumNode < GraphQL::Node
|
58
|
-
exposes "Aquarium"
|
59
|
-
cursor(:id)
|
60
|
-
field.number(:id)
|
61
|
-
field.number(:occupancy)
|
62
|
-
field.connection(:fishes)
|
63
|
-
end
|
64
|
-
```
|
65
|
-
|
66
|
-
You can make custom connections:
|
67
|
-
|
68
|
-
```ruby
|
69
|
-
class FishSchoolConnection < GraphQL::Connection
|
70
|
-
type :fish_school # now it is a field type
|
71
|
-
call :largest, -> (prev_value, number) { fishes.sort_by(&:weight).first(number.to_i) }
|
72
|
-
|
73
|
-
field.number(:count) # delegated to `target`
|
74
|
-
field.boolean(:has_more)
|
75
|
-
|
76
|
-
def has_more
|
77
|
-
# the `largest()` call may have removed some items:
|
78
|
-
target.count < original_target.count
|
79
|
-
end
|
80
|
-
end
|
81
|
-
```
|
82
|
-
|
83
|
-
Then use them:
|
84
|
-
|
85
|
-
```ruby
|
86
|
-
class AquariumNode < GraphQL::Node
|
87
|
-
field.fish_school(:fishes)
|
88
|
-
end
|
89
|
-
```
|
90
|
-
|
91
|
-
And in queries:
|
92
|
-
|
93
|
-
```
|
94
|
-
aquarium(1) {
|
95
|
-
name,
|
96
|
-
occupancy,
|
97
|
-
fishes.largest(3) {
|
98
|
-
edges {
|
99
|
-
node { name, species }
|
100
|
-
},
|
101
|
-
count,
|
102
|
-
has_more
|
103
|
-
}
|
104
|
-
}
|
105
|
-
}
|
106
|
-
```
|
107
|
-
|
108
|
-
### Calls
|
109
|
-
|
110
|
-
Calls selectively expose your application to the world. They always return values and they may perform mutations.
|
111
|
-
|
112
|
-
Calls declare returns, declare arguments, and implement `#execute!`.
|
113
|
-
|
114
|
-
This call just finds values:
|
115
|
-
|
116
|
-
```ruby
|
117
|
-
class FindFishCall < GraphQL::RootCall
|
118
|
-
returns :fish
|
119
|
-
argument.number(:id)
|
120
|
-
def execute!(id)
|
121
|
-
Fish.find(id)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
```
|
125
|
-
|
126
|
-
This call performs a mutation:
|
127
|
-
|
128
|
-
```ruby
|
129
|
-
class RelocateFishCall < GraphQL::RootCall
|
130
|
-
returns :fish, :previous_aquarium, :new_aquarium
|
131
|
-
argument.number(:fish_id)
|
132
|
-
argument.number(:new_aquarium_id)
|
133
|
-
|
134
|
-
def execute!(fish_id, new_aquarium_id)
|
135
|
-
fish = Fish.find(fish_id)
|
136
|
-
|
137
|
-
# context is defined by the query, see below
|
138
|
-
if !context[:user].can_move?(fish)
|
139
|
-
raise RelocateNotAllowedError
|
140
|
-
end
|
141
|
-
|
142
|
-
previous_aquarium = fish.aquarium
|
143
|
-
new_aquarium = Aquarium.find(new_aquarium_id)
|
144
|
-
fish.update_attributes(aquarium: new_aquarium)
|
145
|
-
{
|
146
|
-
fish: fish,
|
147
|
-
previous_aquarium: previous_aquarium,
|
148
|
-
new_aquarium: new_aquarium,
|
149
|
-
}
|
150
|
-
end
|
151
|
-
end
|
152
|
-
```
|
153
|
-
|
154
|
-
### Queries
|
155
|
-
|
156
|
-
When your system is setup, you can perform queries from a string.
|
157
|
-
|
158
|
-
```ruby
|
159
|
-
query_str = "find_fish(1) { name, species } "
|
160
|
-
query = GraphQL::Query.new(query_str)
|
161
|
-
result = query.as_result
|
162
|
-
|
163
|
-
result
|
164
|
-
# {
|
165
|
-
# "1" => {
|
166
|
-
# "name" => "Sharky",
|
167
|
-
# "species" => "Goldfish",
|
168
|
-
# }
|
169
|
-
# }
|
170
|
-
```
|
171
|
-
|
172
|
-
Each query may also define a `context` object which will be accessible at every point in execution.
|
173
|
-
|
174
|
-
```ruby
|
175
|
-
query_str = "move_fish(1, 3) { fish { name }, new_aquarium { occupancy } }"
|
176
|
-
query_ctx = {user: current_user, request: request}
|
177
|
-
query = GraphQL::Query.new(query_str, context: query_ctx)
|
178
|
-
result = query.as_result
|
179
|
-
|
180
|
-
result
|
181
|
-
# {
|
182
|
-
# "fish" => {
|
183
|
-
# "name" => "Sharky"
|
184
|
-
# },
|
185
|
-
# "new_aquarium" => {
|
186
|
-
# "occupancy" => 12
|
187
|
-
# }
|
188
|
-
# }
|
189
|
-
```
|
190
|
-
|
191
|
-
You could do something like this [inside a Rails controller](https://github.com/rmosolgo/graphql-ruby-demo/blob/master/app/controllers/queries_controller.rb#L5).
|
29
|
+
- Implement the GraphQL spec & support a Relay front end
|
30
|
+
- Provide idiomatic, plain-Ruby API with similarities to reference implementation where possible
|
31
|
+
- Support `graphql-rails`
|