Skip to content

Commit 1bdbbc2

Browse files
Merge pull request #99 from d-exclaimation/partial-schema-instance
Instance-based PartialSchema
2 parents 955e549 + 85a5fbe commit 1bdbbc2

File tree

3 files changed

+227
-7
lines changed

3 files changed

+227
-7
lines changed

README.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,12 @@ let api = MessageAPI(
103103

104104
Schemas may also be created in a modular way using `SchemaBuilder`:
105105

106-
```
106+
<blockquote>
107+
108+
<details open="true">
109+
<summary>SchemaBuilder API</summary>
110+
111+
```swift
107112
let builder = SchemaBuilder(Resolver.self, Context.self)
108113
builder.add(
109114
Type(Message.self) {
@@ -121,6 +126,66 @@ let api = MessageAPI(
121126
)
122127
```
123128

129+
</details>
130+
<details>
131+
<summary>PartialSchema implementation</summary>
132+
133+
```swift
134+
final class ChatSchema: PartialSchema<Resolver, Context> {
135+
@TypeDefinitions
136+
public override var types: Types {
137+
Type(Message.self) {
138+
Field("content", at: \.content)
139+
}
140+
}
141+
142+
@FieldDefinitions
143+
public override var query: Fields {
144+
Field("message", at: Resolver.message)
145+
}
146+
}
147+
let schema = try SchemaBuilder(Resolver.self, Context.self)
148+
.use(partials: [ChatSchema(), ...])
149+
.build()
150+
151+
let api = MessageAPI(
152+
resolver: Resolver()
153+
schema: schema
154+
)
155+
```
156+
157+
</details>
158+
159+
<details>
160+
<summary>PartialSchema instance</summary>
161+
162+
```swift
163+
let chatSchema = PartialSchema<Resolver, Context>(
164+
types: {
165+
Type(Message.self) {
166+
Field("content", at: \.content)
167+
}
168+
},
169+
query: {
170+
Field("message", at: Resolver.message)
171+
}
172+
)
173+
let schema = try SchemaBuilder(Resolver.self, Context.self)
174+
.use(partials: [chatSchema, ...])
175+
.build()
176+
177+
let api = MessageAPI(
178+
resolver: Resolver()
179+
schema: schema
180+
)
181+
```
182+
183+
</details>
184+
185+
---
186+
187+
</blockquote>
188+
124189
⭐️ Notice that `API` allows dependency injection. You could pass mocks of `resolver` and `context` when testing, for example.
125190

126191
#### Querying

Sources/Graphiti/SchemaBuilders/PartialSchema.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,35 @@ open class PartialSchema<Resolver, Context> {
1313
/// A type that represents a set of operation field definitions
1414
public typealias Fields = [FieldComponent<Resolver, Context>]
1515

16+
/// Stored predefined "default" properties for a instance-based declaration
17+
private let typedef: Types
18+
private let querydef: Fields
19+
private let mutationdef: Fields
20+
private let subscriptiondef: Fields
21+
22+
public init(
23+
@TypeDefinitions types: () -> Types = { [] },
24+
@FieldDefinitions query: () -> Fields = { [] },
25+
@FieldDefinitions mutation: () -> Fields = { [] },
26+
@FieldDefinitions subscription: () -> Fields = { [] }
27+
) {
28+
typedef = types()
29+
querydef = query()
30+
mutationdef = mutation()
31+
subscriptiondef = subscription()
32+
}
33+
1634
/// Definitions of types
17-
open var types: Types { [] }
35+
open var types: Types { typedef }
1836

1937
/// Definitions of query operation fields
20-
open var query: Fields { [] }
38+
open var query: Fields { querydef }
2139

2240
/// Definitions of mutation operation fields
23-
open var mutation: Fields { [] }
41+
open var mutation: Fields { mutationdef }
2442

2543
/// Definitions of subscription operation fields
26-
open var subscription: Fields { [] }
27-
28-
public init() {}
44+
open var subscription: Fields { subscriptiondef }
2945
}
3046

3147
public extension Schema {

Tests/GraphitiTests/PartialSchemaTests.swift

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ class PartialSchemaTests: XCTestCase {
9999
func testPartialSchemaWithBuilder() throws {
100100
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
101101

102+
defer {
103+
try? group.syncShutdownGracefully()
104+
}
105+
102106
let builder = SchemaBuilder(StarWarsResolver.self, StarWarsContext.self)
103107

104108
builder.use(partials: [BaseSchema(), SearchSchema()])
@@ -135,6 +139,10 @@ class PartialSchemaTests: XCTestCase {
135139
func testPartialSchema() throws {
136140
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
137141

142+
defer {
143+
try? group.syncShutdownGracefully()
144+
}
145+
138146
/// Double check if static func works and the types are inferred properly
139147
let schema = try Schema.create(from: [BaseSchema(), SearchSchema()])
140148

@@ -168,6 +176,10 @@ class PartialSchemaTests: XCTestCase {
168176
func testPartialSchemaOutOfOrder() throws {
169177
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
170178

179+
defer {
180+
try? group.syncShutdownGracefully()
181+
}
182+
171183
/// Double check if ordering of partial schema doesn't matter
172184
let schema = try Schema.create(from: [SearchSchema(), BaseSchema()])
173185

@@ -197,4 +209,131 @@ class PartialSchemaTests: XCTestCase {
197209
])
198210
)
199211
}
212+
213+
func testInstancePartialSchema() throws {
214+
let baseSchema = PartialSchema<StarWarsResolver, StarWarsContext>(
215+
types: {
216+
Interface(Character.self) {
217+
Field("id", at: \.id)
218+
.description("The id of the character.")
219+
Field("name", at: \.name)
220+
.description("The name of the character.")
221+
Field("friends", at: \.friends, as: [TypeReference<Character>].self)
222+
.description(
223+
"The friends of the character, or an empty list if they have none."
224+
)
225+
Field("appearsIn", at: \.appearsIn)
226+
.description("Which movies they appear in.")
227+
Field("secretBackstory", at: \.secretBackstory)
228+
.description("All secrets about their past.")
229+
}
230+
231+
Enum(Episode.self) {
232+
Value(.newHope)
233+
.description("Released in 1977.")
234+
Value(.empire)
235+
.description("Released in 1980.")
236+
Value(.jedi)
237+
.description("Released in 1983.")
238+
}.description("One of the films in the Star Wars Trilogy.")
239+
},
240+
query: {
241+
Field("hero", at: StarWarsResolver.hero, as: Character.self) {
242+
Argument("episode", at: \.episode)
243+
.description(
244+
"If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode."
245+
)
246+
}.description("Returns a hero based on the given episode.")
247+
}
248+
)
249+
250+
let searchSchema = PartialSchema<StarWarsResolver, StarWarsContext>(
251+
types: {
252+
Type(Planet.self) {
253+
Field("id", at: \.id)
254+
Field("name", at: \.name)
255+
Field("diameter", at: \.diameter)
256+
Field("rotationPeriod", at: \.rotationPeriod)
257+
Field("orbitalPeriod", at: \.orbitalPeriod)
258+
Field("residents", at: \.residents)
259+
}.description(
260+
"A large mass, planet or planetoid in the Star Wars Universe, at the time of 0 ABY."
261+
)
262+
Type(Human.self, interfaces: [Character.self]) {
263+
Field("id", at: \.id)
264+
Field("name", at: \.name)
265+
Field("appearsIn", at: \.appearsIn)
266+
Field("homePlanet", at: \.homePlanet)
267+
Field("friends", at: Human.getFriends, as: [Character].self)
268+
.description(
269+
"The friends of the human, or an empty list if they have none."
270+
)
271+
Field("secretBackstory", at: Human.getSecretBackstory)
272+
.description("Where are they from and how they came to be who they are.")
273+
}.description("A humanoid creature in the Star Wars universe.")
274+
Type(Droid.self, interfaces: [Character.self]) {
275+
Field("id", at: \.id)
276+
Field("name", at: \.name)
277+
Field("appearsIn", at: \.appearsIn)
278+
Field("primaryFunction", at: \.primaryFunction)
279+
Field("friends", at: Droid.getFriends, as: [Character].self)
280+
.description(
281+
"The friends of the droid, or an empty list if they have none."
282+
)
283+
Field("secretBackstory", at: Droid.getSecretBackstory)
284+
.description("Where are they from and how they came to be who they are.")
285+
}.description("A mechanical creature in the Star Wars universe.")
286+
Union(SearchResult.self, members: Planet.self, Human.self, Droid.self)
287+
},
288+
query: {
289+
Field("human", at: StarWarsResolver.human) {
290+
Argument("id", at: \.id)
291+
.description("Id of the human.")
292+
}
293+
Field("droid", at: StarWarsResolver.droid) {
294+
Argument("id", at: \.id)
295+
.description("Id of the droid.")
296+
}
297+
Field("search", at: StarWarsResolver.search, as: [SearchResult].self) {
298+
Argument("query", at: \.query)
299+
.defaultValue("R2-D2")
300+
}
301+
}
302+
)
303+
304+
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
305+
306+
defer {
307+
try? group.syncShutdownGracefully()
308+
}
309+
310+
/// Double check if ordering of partial schema doesn't matter
311+
let schema = try Schema.create(from: [searchSchema, baseSchema])
312+
313+
struct PartialSchemaTestAPI: API {
314+
let resolver: StarWarsResolver
315+
let schema: Schema<StarWarsResolver, StarWarsContext>
316+
}
317+
318+
let api = PartialSchemaTestAPI(resolver: StarWarsResolver(), schema: schema)
319+
320+
XCTAssertEqual(
321+
try api.execute(
322+
request: """
323+
query {
324+
human(id: "1000") {
325+
name
326+
}
327+
}
328+
""",
329+
context: StarWarsContext(),
330+
on: group
331+
).wait(),
332+
GraphQLResult(data: [
333+
"human": [
334+
"name": "Luke Skywalker",
335+
],
336+
])
337+
)
338+
}
200339
}

0 commit comments

Comments
 (0)