Skip to content

Commit

Permalink
Merge pull request #144 from NeedleInAJayStack/feature/new-directives
Browse files Browse the repository at this point in the history
Adds `oneOf` and `specifiedBy` directives
  • Loading branch information
NeedleInAJayStack authored Jun 9, 2024
2 parents 8171c0e + cb45688 commit 87649db
Show file tree
Hide file tree
Showing 9 changed files with 454 additions and 20 deletions.
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

0 comments on commit 87649db

Please sign in to comment.