Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds oneOf and specifiedBy directives #144

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Sources/GraphQL/Type/Definition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ extension GraphQLNonNull: GraphQLWrapperType {}
public final class GraphQLScalarType {
public let name: String
public let description: String?
public let specifiedByURL: String?
public let kind: TypeKind = .scalar

let serialize: (Any) throws -> Map
Expand All @@ -178,13 +179,15 @@ public final class GraphQLScalarType {
public init(
name: String,
description: String? = nil,
specifiedByURL: String? = nil,
serialize: @escaping (Any) throws -> Map,
parseValue: ((Map) throws -> Map)? = nil,
parseLiteral: ((Value) throws -> Map)? = nil
) throws {
try assertValid(name: name)
self.name = name
self.description = description
self.specifiedByURL = specifiedByURL
self.serialize = serialize
self.parseValue = parseValue ?? defaultParseValue
self.parseLiteral = parseLiteral ?? defaultParseLiteral
Expand Down Expand Up @@ -218,6 +221,7 @@ extension GraphQLScalarType: Encodable {
private enum CodingKeys: String, CodingKey {
case name
case description
case specifiedByURL
case kind
}
}
Expand All @@ -229,6 +233,8 @@ extension GraphQLScalarType: KeySubscriptable {
return name
case CodingKeys.description.rawValue:
return description
case CodingKeys.specifiedByURL.rawValue:
return specifiedByURL
case CodingKeys.kind.rawValue:
return kind
default:
Expand Down Expand Up @@ -1217,12 +1223,14 @@ public final class GraphQLInputObjectType {
public let name: String
public let description: String?
public let fields: InputObjectFieldDefinitionMap
public let isOneOf: Bool
public let kind: TypeKind = .inputObject

public init(
name: String,
description: String? = nil,
fields: InputObjectFieldMap = [:]
fields: InputObjectFieldMap = [:],
isOneOf: Bool = false
) throws {
try assertValid(name: name)
self.name = name
Expand All @@ -1231,6 +1239,7 @@ public final class GraphQLInputObjectType {
name: name,
fields: fields
)
self.isOneOf = isOneOf
}

func replaceTypeReferences(typeMap: TypeMap) throws {
Expand All @@ -1245,6 +1254,7 @@ extension GraphQLInputObjectType: Encodable {
case name
case description
case fields
case isOneOf
case kind
}
}
Expand All @@ -1258,6 +1268,8 @@ extension GraphQLInputObjectType: KeySubscriptable {
return description
case CodingKeys.fields.rawValue:
return fields
case CodingKeys.isOneOf.rawValue:
return isOneOf
case CodingKeys.kind.rawValue:
return kind
default:
Expand Down
27 changes: 27 additions & 0 deletions Sources/GraphQL/Type/Directives.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,38 @@ public let GraphQLDeprecatedDirective = try! GraphQLDirective(
]
)

/**
* Used to provide a URL for specifying the behavior of custom scalar definitions.
*/
public let GraphQLSpecifiedByDirective = try! GraphQLDirective(
name: "specifiedBy",
description: "Exposes a URL that specifies the behavior of this scalar.",
locations: [.scalar],
args: [
"url": GraphQLArgument(
type: GraphQLNonNull(GraphQLString),
description: "The URL that specifies the behavior of this scalar."
),
]
)

/**
* Used to indicate an Input Object is a OneOf Input Object.
*/
public let GraphQLOneOfDirective = try! GraphQLDirective(
name: "oneOf",
description: "Indicates exactly one field must be supplied and this field must not be `null`.",
locations: [.inputObject],
args: [:]
)

/**
* The full list of specified directives.
*/
let specifiedDirectives: [GraphQLDirective] = [
GraphQLIncludeDirective,
GraphQLSkipDirective,
GraphQLDeprecatedDirective,
GraphQLSpecifiedByDirective,
GraphQLOneOfDirective,
]
12 changes: 11 additions & 1 deletion Sources/GraphQL/Type/Introspection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ let __Type: GraphQLObjectType = try! GraphQLObjectType(
"many kinds of types in GraphQL as represented by the `__TypeKind` enum." +
"\n\nDepending on the kind of a type, certain fields describe " +
"information about that type. Scalar types provide no information " +
"beyond a name and description, while Enum types provide their values. " +
"beyond a name and description and optional `specifiedByURL`, while Enum types provide their values. " +
"Object and Interface types provide the fields they describe. Abstract " +
"types, Union and Interface, provide the Object types possible " +
"at runtime. List and NonNull types compose other types.",
Expand Down Expand Up @@ -217,6 +217,7 @@ let __Type: GraphQLObjectType = try! GraphQLObjectType(
),
"name": GraphQLField(type: GraphQLString),
"description": GraphQLField(type: GraphQLString),
"specifiedByURL": GraphQLField(type: GraphQLString),
"fields": GraphQLField(
type: GraphQLList(GraphQLNonNull(__Field)),
args: [
Expand Down Expand Up @@ -310,6 +311,15 @@ let __Type: GraphQLObjectType = try! GraphQLObjectType(
}
),
"ofType": GraphQLField(type: GraphQLTypeReference("__Type")),
"isOneOf": GraphQLField(
type: GraphQLBoolean,
resolve: { type, _, _, _ in
if let type = type as? GraphQLInputObjectType {
return type.isOneOf
}
return false
}
),
]
)

Expand Down
16 changes: 16 additions & 0 deletions Sources/GraphQL/Utilities/IsValidValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ func validate(value: Map, forType type: GraphQLInputType) throws -> [String] {
}
}

// Ensure only one field in oneOf input is defined
if objectType.isOneOf {
let keys = dictionary.filter { $1 != .undefined }.keys
if keys.count != 1 {
errors.append(
"Exactly one key must be specified for OneOf type \"\(objectType.name)\"."
)
}

let key = keys[0]
let value = dictionary[key]
if value == .null {
errors.append("Field \"\(key)\" must be non-null.")
}
}

// Ensure every defined field is valid.
for (fieldName, field) in fields {
let newErrors = try validate(value: value[fieldName], forType: field.type).map {
Expand Down
12 changes: 12 additions & 0 deletions Sources/GraphQL/Utilities/ValueFromAST.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ func valueFromAST(
}
}
}

if objectType.isOneOf {
let keys = object.filter { $1 != .undefined }.keys
if keys.count != 1 {
return .undefined // Invalid: not exactly one key, intentionally return no value.
}

if object[keys[0]] == .null {
return .undefined // Invalid: value not non-null, intentionally return no value.
}
}

return .dictionary(object)
}

Expand Down
67 changes: 64 additions & 3 deletions Sources/GraphQL/Validation/Rules/ValuesOfCorrectTypeRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ func ValuesOfCorrectTypeRule(context: ValidationContext) -> Visitor {
return .break // Don't traverse further.
}
// Ensure every required field exists.
let fieldNodeMap = Dictionary(grouping: object.fields) { field in
field.name.value
var fieldNodeMap = [String: ObjectField]()
for field in object.fields {
fieldNodeMap[field.name.value] = field
}
for (fieldName, fieldDef) in type.fields {
if fieldNodeMap[fieldName] == nil, isRequiredInputField(fieldDef) {
Expand All @@ -52,7 +53,15 @@ func ValuesOfCorrectTypeRule(context: ValidationContext) -> Visitor {
}
}

// TODO: Add oneOf support
if type.isOneOf {
validateOneOfInputObject(
context: context,
node: object,
type: type,
fieldNodeMap: fieldNodeMap,
variableDefinitions: variableDefinitions
)
}
return .continue
}
if let field = node as? ObjectField {
Expand Down Expand Up @@ -172,3 +181,55 @@ func isValidValueNode(_ context: ValidationContext, _ node: Value) {
}
}
}

func validateOneOfInputObject(
context: ValidationContext,
node: ObjectValue,
type: GraphQLInputObjectType,
fieldNodeMap: [String: ObjectField],
variableDefinitions: [String: VariableDefinition]
) {
let keys = Array(fieldNodeMap.keys)
let isNotExactlyOneField = keys.count != 1

if isNotExactlyOneField {
context.report(
error: GraphQLError(
message: "OneOf Input Object \"\(type.name)\" must specify exactly one key.",
nodes: [node]
)
)
return
}

let value = fieldNodeMap[keys[0]]?.value
let isNullLiteral = value == nil || value?.kind == .nullValue

if isNullLiteral {
context.report(
error: GraphQLError(
message: "Field \"\(type.name).\(keys[0])\" must be non-null.",
nodes: [node]
)
)
return
}

if let value = value, value.kind == .variable {
let variable = value as! Variable // Force unwrap is safe because of variable definition
let variableName = variable.name.value

if
let definition = variableDefinitions[variableName],
definition.type.kind != .nonNullType
{
context.report(
error: GraphQLError(
message: "Variable \"\(variableName)\" must be non-nullable to be used for OneOf Input Object \"\(type.name)\".",
nodes: [node]
)
)
return
}
}
}
Loading
Loading