Skip to content

Commit 9845fb4

Browse files
Merge pull request #92 from d-exclaimation/feat/partial-schema
Declarative Partial Schema
2 parents 67fdc7a + 60df13e commit 9845fb4

File tree

6 files changed

+252
-21
lines changed

6 files changed

+252
-21
lines changed

Sources/Graphiti/Definition/TypeProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ extension TypeProvider {
255255

256256
return objectType
257257
}
258-
258+
259259
private func getGraphQLName(of type: Any.Type) -> String {
260260
return graphQLNameMap[AnyType(type)] ?? Reflection.name(for: type)
261261
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/// A partial schema that declare a set of type, query, mutation, and/or subscription definition
2+
/// which can be compiled together into 1 schema.
3+
open class PartialSchema<Resolver, Context> {
4+
/// A custom parameter attribute that constructs type definitions from closures.
5+
public typealias TypeDefinitions = TypeComponentBuilder<Resolver, Context>
6+
7+
/// A custom parameter attribute that constructs operation field definitions from closures.
8+
public typealias FieldDefinitions = FieldComponentBuilder<Resolver, Context>
9+
10+
/// A type that represents a set of type definitions
11+
public typealias Types = [TypeComponent<Resolver, Context>]
12+
13+
/// A type that represents a set of operation field definitions
14+
public typealias Fields = [FieldComponent<Resolver, Context>]
15+
16+
/// Definitions of types
17+
open var types: Types { [] }
18+
19+
/// Definitions of query operation fields
20+
open var query: Fields { [] }
21+
22+
/// Definitions of mutation operation fields
23+
open var mutation: Fields { [] }
24+
25+
/// Definitions of subscription operation fields
26+
open var subscription: Fields { [] }
27+
28+
public init() {}
29+
}
30+
31+
public extension Schema {
32+
/// Create a schema from partial schemas
33+
/// - Parameter partials: Partial schemas that declare types, query, mutation, and/or subscription definiton
34+
/// - Returns: A compiled schema will all definitions given from the partial schemas
35+
static func create(
36+
from partials: [PartialSchema<Resolver, Context>]
37+
) throws -> Schema<Resolver, Context> {
38+
try SchemaBuilder(Resolver.self, Context.self)
39+
.use(partials: partials)
40+
.build()
41+
}
42+
}

Sources/Graphiti/SchemaBuilders/SchemaBuilder.swift

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ public final class SchemaBuilder<Resolver, Context> {
1111
private var subscriptionFields: [FieldComponent<Resolver, Context>]
1212

1313
public init(
14-
_ resolverType: Resolver.Type,
15-
_ contextType: Context.Type
14+
_: Resolver.Type,
15+
_: Context.Type
1616
) {
1717
coders = Coders()
1818
typeComponents = []
19-
19+
2020
queryName = "Query"
2121
queryFields = []
2222
mutationName = "Mutation"
@@ -33,19 +33,19 @@ public final class SchemaBuilder<Resolver, Context> {
3333
coders = newCoders
3434
return self
3535
}
36-
36+
3737
@discardableResult
3838
public func setQueryName(to name: String) -> Self {
3939
queryName = name
4040
return self
4141
}
42-
42+
4343
@discardableResult
4444
public func setMutationName(to name: String) -> Self {
4545
mutationName = name
4646
return self
4747
}
48-
48+
4949
@discardableResult
5050
public func setSubscriptionName(to name: String) -> Self {
5151
subscriptionName = name
@@ -57,49 +57,73 @@ public final class SchemaBuilder<Resolver, Context> {
5757
/// - Parameter component: The query operations to add
5858
/// - Returns: This object for method chaining
5959
public func add(
60-
@TypeComponentBuilder<Resolver, Context> _ components: () -> [TypeComponent<Resolver, Context>]
60+
@TypeComponentBuilder<Resolver, Context> _ components: ()
61+
-> [TypeComponent<Resolver, Context>]
6162
) -> Self {
6263
for component in components() {
6364
typeComponents.append(component)
6465
}
6566
return self
6667
}
67-
68+
6869
@discardableResult
6970
/// Adds multiple query operation definitions to the schema.
7071
/// - Parameter component: The query operations to add
7172
/// - Returns: This object for method chaining
7273
public func addQuery(
73-
@FieldComponentBuilder<Resolver, Context> _ fields: () -> [FieldComponent<Resolver, Context>]
74+
@FieldComponentBuilder<Resolver, Context> _ fields: ()
75+
-> [FieldComponent<Resolver, Context>]
7476
) -> Self {
7577
for field in fields() {
76-
self.queryFields.append(field)
78+
queryFields.append(field)
7779
}
7880
return self
7981
}
80-
82+
8183
@discardableResult
8284
/// Adds multiple mutation operation definitions to the schema.
8385
/// - Parameter component: The query operations to add
8486
/// - Returns: This object for method chaining
8587
public func addMutation(
86-
@FieldComponentBuilder<Resolver, Context> _ fields: () -> [FieldComponent<Resolver, Context>]
88+
@FieldComponentBuilder<Resolver, Context> _ fields: ()
89+
-> [FieldComponent<Resolver, Context>]
8790
) -> Self {
8891
for field in fields() {
89-
self.mutationFields.append(field)
92+
mutationFields.append(field)
9093
}
9194
return self
9295
}
93-
96+
9497
@discardableResult
9598
/// Adds multiple subscription operation definitions to the schema.
9699
/// - Parameter component: The query operations to add
97100
/// - Returns: This object for method chaining
98101
public func addSubscription(
99-
@FieldComponentBuilder<Resolver, Context> _ fields: () -> [FieldComponent<Resolver, Context>]
102+
@FieldComponentBuilder<Resolver, Context> _ fields: ()
103+
-> [FieldComponent<Resolver, Context>]
100104
) -> Self {
101105
for field in fields() {
102-
self.subscriptionFields.append(field)
106+
subscriptionFields.append(field)
107+
}
108+
return self
109+
}
110+
111+
@discardableResult
112+
/// Adds multiple type, query, mutation, and subscription definitions using partial schemas to the schema.
113+
/// - Parameter partials: Partial schemas that declare types, query, mutation, and/or subscription definiton
114+
/// - Returns: Thie object for method chaining
115+
public func use(partials: [PartialSchema<Resolver, Context>]) -> Self {
116+
for type in partials.flatMap({ $0.types }) {
117+
typeComponents.append(type)
118+
}
119+
for query in partials.flatMap({ $0.query }) {
120+
queryFields.append(query)
121+
}
122+
for mutation in partials.flatMap({ $0.mutation }) {
123+
mutationFields.append(mutation)
124+
}
125+
for subscription in partials.flatMap({ $0.subscription }) {
126+
subscriptionFields.append(subscription)
103127
}
104128
return self
105129
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import Graphiti
2+
import GraphQL
3+
import NIO
4+
import XCTest
5+
6+
class PartialSchemaTests: XCTestCase {
7+
class BaseSchema: PartialSchema<StarWarsResolver, StarWarsContext> {
8+
@TypeDefinitions
9+
override var types: Types {
10+
Interface(Character.self) {
11+
Field("id", at: \.id)
12+
.description("The id of the character.")
13+
Field("name", at: \.name)
14+
.description("The name of the character.")
15+
Field("friends", at: \.friends, as: [TypeReference<Character>].self)
16+
.description(
17+
"The friends of the character, or an empty list if they have none."
18+
)
19+
Field("appearsIn", at: \.appearsIn)
20+
.description("Which movies they appear in.")
21+
Field("secretBackstory", at: \.secretBackstory)
22+
.description("All secrets about their past.")
23+
}
24+
25+
Enum(Episode.self) {
26+
Value(.newHope)
27+
.description("Released in 1977.")
28+
Value(.empire)
29+
.description("Released in 1980.")
30+
Value(.jedi)
31+
.description("Released in 1983.")
32+
}.description("One of the films in the Star Wars Trilogy.")
33+
}
34+
35+
@FieldDefinitions
36+
override var query: Fields {
37+
Field("hero", at: StarWarsResolver.hero, as: Character.self) {
38+
Argument("episode", at: \.episode)
39+
.description(
40+
"If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode."
41+
)
42+
}.description("Returns a hero based on the given episode.")
43+
}
44+
}
45+
46+
class SearchSchema: PartialSchema<StarWarsResolver, StarWarsContext> {
47+
@TypeDefinitions
48+
override var types: Types {
49+
Type(Planet.self) {
50+
Field("id", at: \.id)
51+
Field("name", at: \.name)
52+
Field("diameter", at: \.diameter)
53+
Field("rotationPeriod", at: \.rotationPeriod)
54+
Field("orbitalPeriod", at: \.orbitalPeriod)
55+
Field("residents", at: \.residents)
56+
}.description(
57+
"A large mass, planet or planetoid in the Star Wars Universe, at the time of 0 ABY."
58+
)
59+
Type(Human.self, interfaces: [Character.self]) {
60+
Field("id", at: \.id)
61+
Field("name", at: \.name)
62+
Field("appearsIn", at: \.appearsIn)
63+
Field("homePlanet", at: \.homePlanet)
64+
Field("friends", at: Human.getFriends, as: [Character].self)
65+
.description("The friends of the human, or an empty list if they have none.")
66+
Field("secretBackstory", at: Human.getSecretBackstory)
67+
.description("Where are they from and how they came to be who they are.")
68+
}.description("A humanoid creature in the Star Wars universe.")
69+
Type(Droid.self, interfaces: [Character.self]) {
70+
Field("id", at: \.id)
71+
Field("name", at: \.name)
72+
Field("appearsIn", at: \.appearsIn)
73+
Field("primaryFunction", at: \.primaryFunction)
74+
Field("friends", at: Droid.getFriends, as: [Character].self)
75+
.description("The friends of the droid, or an empty list if they have none.")
76+
Field("secretBackstory", at: Droid.getSecretBackstory)
77+
.description("Where are they from and how they came to be who they are.")
78+
}.description("A mechanical creature in the Star Wars universe.")
79+
Union(SearchResult.self, members: Planet.self, Human.self, Droid.self)
80+
}
81+
82+
@FieldDefinitions
83+
override var query: Fields {
84+
Field("human", at: StarWarsResolver.human) {
85+
Argument("id", at: \.id)
86+
.description("Id of the human.")
87+
}
88+
Field("droid", at: StarWarsResolver.droid) {
89+
Argument("id", at: \.id)
90+
.description("Id of the droid.")
91+
}
92+
Field("search", at: StarWarsResolver.search, as: [SearchResult].self) {
93+
Argument("query", at: \.query)
94+
.defaultValue("R2-D2")
95+
}
96+
}
97+
}
98+
99+
func testPartialSchemaWithBuilder() throws {
100+
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
101+
102+
let builder = SchemaBuilder(StarWarsResolver.self, StarWarsContext.self)
103+
104+
builder.use(partials: [BaseSchema(), SearchSchema()])
105+
106+
let schema = try builder.build()
107+
108+
struct PartialSchemaTestAPI: API {
109+
let resolver: StarWarsResolver
110+
let schema: Schema<StarWarsResolver, StarWarsContext>
111+
}
112+
113+
let api = PartialSchemaTestAPI(resolver: StarWarsResolver(), schema: schema)
114+
115+
XCTAssertEqual(
116+
try api.execute(
117+
request: """
118+
query {
119+
human(id: "1000") {
120+
name
121+
}
122+
}
123+
""",
124+
context: StarWarsContext(),
125+
on: group
126+
).wait(),
127+
GraphQLResult(data: [
128+
"human": [
129+
"name": "Luke Skywalker",
130+
],
131+
])
132+
)
133+
}
134+
135+
func testPartialSchema() throws {
136+
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
137+
138+
/// Double check if static func works and the types are inferred properly
139+
let schema = try Schema.create(from: [BaseSchema(), SearchSchema()])
140+
141+
struct PartialSchemaTestAPI: API {
142+
let resolver: StarWarsResolver
143+
let schema: Schema<StarWarsResolver, StarWarsContext>
144+
}
145+
146+
let api = PartialSchemaTestAPI(resolver: StarWarsResolver(), schema: schema)
147+
148+
XCTAssertEqual(
149+
try api.execute(
150+
request: """
151+
query {
152+
human(id: "1000") {
153+
name
154+
}
155+
}
156+
""",
157+
context: StarWarsContext(),
158+
on: group
159+
).wait(),
160+
GraphQLResult(data: [
161+
"human": [
162+
"name": "Luke Skywalker",
163+
],
164+
])
165+
)
166+
}
167+
}

Tests/GraphitiTests/SchemaBuilderTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,12 @@ class SchemaBuilderTests: XCTestCase {
9494
}
9595

9696
let schema = try builder.build()
97-
97+
9898
struct SchemaBuilderTestAPI: API {
9999
let resolver: StarWarsResolver
100100
let schema: Schema<StarWarsResolver, StarWarsContext>
101101
}
102-
102+
103103
let api = SchemaBuilderTestAPI(resolver: StarWarsResolver(), schema: schema)
104104

105105
XCTAssertEqual(

Tests/GraphitiTests/UnionTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ class UnionTests: XCTestCase {
2828
}
2929
}
3030

31-
3231
_ = try Schema<StarWarsResolver, StarWarsContext> {
3332
Type(Planet.self) {
3433
Field("id", at: \.id)
@@ -54,6 +53,5 @@ class UnionTests: XCTestCase {
5453
}
5554
}
5655
}
57-
5856
}
5957
}

0 commit comments

Comments
 (0)