Skip to content

Commit a7b757d

Browse files
committed
create user accounts database and populate it with info from GitHub's API
1 parent 645212b commit a7b757d

File tree

58 files changed

+1328
-770
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1328
-770
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
extension GitHubClient
2+
{
3+
@frozen public
4+
enum AuthenticationError:Error, Sendable
5+
{
6+
case status(StatusError)
7+
case response(any Error)
8+
}
9+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
extension GitHubClient
2+
{
3+
@frozen public
4+
struct StatusError:Equatable, Sendable, Error
5+
{
6+
/// The response status code, if it could be parsed, nil otherwise.
7+
public
8+
let code:UInt?
9+
10+
@inlinable public
11+
init(code:UInt?)
12+
{
13+
self.code = code
14+
}
15+
}
16+
}
17+
extension GitHubClient.StatusError:CustomStringConvertible
18+
{
19+
public
20+
var description:String
21+
{
22+
self.code?.description ?? "unknown"
23+
}
24+
}

Sources/GitHubClient/GitHubClient.swift

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import NIOCore
55
import NIOHPACK
66

77
@frozen public
8-
struct GitHubClient<Application> where Application:GitHubApplication
8+
struct GitHubClient<Application>
99
{
10-
private
10+
@usableFromInline internal
1111
let http2:HTTP2Client
1212
public
1313
let app:Application
@@ -19,18 +19,19 @@ struct GitHubClient<Application> where Application:GitHubApplication
1919
self.app = app
2020
}
2121
}
22-
extension GitHubClient:Identifiable
22+
extension GitHubClient:Identifiable where Application:GitHubApplication
2323
{
2424
@inlinable public
2525
var id:String { self.app.client }
2626

2727
@inlinable public
2828
var secret:String { self.app.secret }
2929
}
30-
extension GitHubClient
30+
extension GitHubClient where Application:GitHubApplication<GitHubApp.Credentials>
3131
{
3232
public
33-
func refresh(token:String) async -> Result<GitHubTokens, GitHubAuthenticationError>
33+
func refresh(
34+
token:String) async throws -> GitHubApp.Credentials
3435
{
3536
let request:HPACKHeaders =
3637
[
@@ -46,11 +47,15 @@ extension GitHubClient
4647
"accept": "application/vnd.github+json",
4748
]
4849

49-
return await self.authenticate(sending: request)
50+
return try await self.authenticate(sending: request)
5051
}
51-
52+
}
53+
extension GitHubClient
54+
where Application:GitHubApplication, Application.Credentials:JSONObjectDecodable
55+
{
5256
public
53-
func exchange(code:String) async -> Result<GitHubTokens, GitHubAuthenticationError>
57+
func exchange(
58+
code:String) async throws -> Application.Credentials
5459
{
5560
let request:HPACKHeaders =
5661
[
@@ -65,28 +70,21 @@ extension GitHubClient
6570
"accept": "application/vnd.github+json",
6671
]
6772

68-
return await self.authenticate(sending: request)
73+
return try await self.authenticate(sending: request)
6974
}
7075

7176
private
72-
func authenticate(
73-
sending request:HPACKHeaders) async -> Result<GitHubTokens, GitHubAuthenticationError>
77+
func authenticate(sending request:HPACKHeaders)
78+
async throws -> Application.Credentials
7479
{
75-
let response:HTTP2Client.Facet
76-
do
77-
{
78-
response = try await self.http2.fetch(request)
79-
}
80-
catch let error
81-
{
82-
return .failure(.fetch(error))
83-
}
80+
let response:HTTP2Client.Facet = try await self.http2.fetch(request)
8481

85-
guard let headers:HPACKHeaders = response.headers,
86-
headers[canonicalForm: ":status"] == ["200"]
87-
else
82+
switch response.status
8883
{
89-
return .failure(.status)
84+
case 200?:
85+
break
86+
case let status:
87+
throw AuthenticationError.status(.init(code: status))
9088
}
9189

9290
var json:JSON = .init(utf8: [])
@@ -97,11 +95,60 @@ extension GitHubClient
9795

9896
do
9997
{
100-
return .success(try json.decode())
98+
return try json.decode()
10199
}
102100
catch let error
103101
{
104-
return .failure(.response(error))
102+
throw AuthenticationError.response(error)
103+
}
104+
}
105+
}
106+
extension GitHubClient<GitHubAPI>
107+
{
108+
public
109+
func user(with token:String) async throws -> GitHubAPI.User
110+
{
111+
try await self.get(from: "/user", with: token)
112+
}
113+
114+
@inlinable public
115+
func get<Response>(_:Response.Type = Response.self,
116+
from endpoint:String,
117+
with token:String? = nil) async throws -> Response where Response:JSONObjectDecodable
118+
{
119+
var request:HPACKHeaders =
120+
[
121+
":method": "GET",
122+
":scheme": "https",
123+
":authority": "api.github.com",
124+
":path": endpoint,
125+
126+
// GitHub will reject the API request if the user-agent is not set.
127+
"user-agent": self.app.agent,
128+
"accept": "application/vnd.github+json"
129+
]
130+
if let token:String
131+
{
132+
request.add(name: "authorization", value: "Bearer \(token)")
105133
}
134+
135+
let response:HTTP2Client.Facet = try await self.http2.fetch(request)
136+
137+
// TODO: support If-None-Match
138+
switch response.status
139+
{
140+
case 200?:
141+
break
142+
case let status:
143+
throw StatusError.init(code: status)
144+
}
145+
146+
var json:JSON = .init(utf8: [])
147+
for buffer:ByteBuffer in response.buffers
148+
{
149+
json.utf8 += buffer.readableBytesView
150+
}
151+
152+
return try json.decode()
106153
}
107154
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import JSON
2+
3+
extension GitHubAPI
4+
{
5+
@frozen public
6+
struct User:Identifiable, Equatable, Sendable
7+
{
8+
public
9+
let id:Int32
10+
11+
/// The user’s @-name.
12+
public
13+
var login:String
14+
/// The user’s icon URL.
15+
public
16+
var icon:String
17+
/// The user’s node id. This is GitHub’s analogue of a Unidoc scalar.
18+
public
19+
var node:String
20+
21+
/// The user’s location, if set.
22+
public
23+
var location:String?
24+
/// The user’s hiring status, if set.
25+
public
26+
var hireable:Bool?
27+
/// The user’s company name, if set.
28+
public
29+
var company:String?
30+
/// The user’s public email address, if set.
31+
public
32+
var email:String?
33+
/// The user’s display name, if set.
34+
public
35+
var name:String?
36+
/// The user’s blog URL, if set.
37+
public
38+
var blog:String?
39+
/// The user’s bio, if set.
40+
public
41+
var bio:String?
42+
/// The user’s X account, if set.
43+
public
44+
var x:String?
45+
46+
public
47+
var publicRepos:Int
48+
public
49+
var publicGists:Int
50+
public
51+
var followers:Int
52+
public
53+
var following:Int
54+
public
55+
var created:String
56+
public
57+
var updated:String
58+
59+
@inlinable public
60+
init(id:Int32,
61+
login:String,
62+
icon:String,
63+
node:String,
64+
location:String? = nil,
65+
hireable:Bool? = nil,
66+
company:String? = nil,
67+
email:String? = nil,
68+
name:String? = nil,
69+
blog:String? = nil,
70+
bio:String? = nil,
71+
x:String? = nil,
72+
publicRepos:Int = 0,
73+
publicGists:Int = 0,
74+
followers:Int = 0,
75+
following:Int = 0,
76+
created:String,
77+
updated:String)
78+
{
79+
self.id = id
80+
self.login = login
81+
self.icon = icon
82+
self.node = node
83+
self.location = location
84+
self.hireable = hireable
85+
self.company = company
86+
self.email = email
87+
self.name = name
88+
self.blog = blog
89+
self.bio = bio
90+
self.x = x
91+
self.publicRepos = publicRepos
92+
self.publicGists = publicGists
93+
self.followers = followers
94+
self.following = following
95+
self.created = created
96+
self.updated = updated
97+
}
98+
}
99+
}
100+
extension GitHubAPI.User:JSONObjectDecodable
101+
{
102+
public
103+
enum CodingKey:String
104+
{
105+
case id
106+
case login
107+
case icon = "avatar_url"
108+
case node = "node_id"
109+
case location
110+
case hireable
111+
case company
112+
case email
113+
case name
114+
case blog
115+
case bio
116+
case x = "twitter_username"
117+
case publicRepos = "public_repos"
118+
case publicGists = "public_gists"
119+
case followers
120+
case following
121+
case created = "created_at"
122+
case updated = "updated_at"
123+
}
124+
125+
public
126+
init(json:JSON.ObjectDecoder<CodingKey>) throws
127+
{
128+
self.init(id: try json[.id].decode(),
129+
login: try json[.login].decode(),
130+
icon: try json[.icon].decode(),
131+
node: try json[.node].decode(),
132+
location: try json[.location]?.decode(),
133+
hireable: try json[.hireable]?.decode(),
134+
company: try json[.company]?.decode(),
135+
email: try json[.email]?.decode(),
136+
name: try json[.name]?.decode(),
137+
blog: try json[.blog]?.decode(),
138+
bio: try json[.bio]?.decode(),
139+
x: try json[.x]?.decode(),
140+
publicRepos: try json[.publicRepos].decode(),
141+
publicGists: try json[.publicGists].decode(),
142+
followers: try json[.followers].decode(),
143+
following: try json[.following].decode(),
144+
created: try json[.created].decode(),
145+
updated: try json[.updated].decode())
146+
}
147+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@frozen public
2+
struct GitHubAPI
3+
{
4+
public
5+
let agent:String
6+
7+
@inlinable public
8+
init(agent:String)
9+
{
10+
self.agent = agent
11+
}
12+
}

Sources/GitHubIntegration/GitHubTokens.swift renamed to Sources/GitHubIntegration/GitHubApp.Credentials.swift

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
import JSON
22

3-
@frozen public
4-
struct GitHubTokens:Equatable, Hashable, Sendable
3+
extension GitHubApp
54
{
6-
public
7-
let refresh:GitHubToken
8-
public
9-
let access:GitHubToken
10-
11-
@inlinable public
12-
init(refresh:GitHubToken, access:GitHubToken)
5+
@frozen public
6+
struct Credentials:Equatable, Hashable, Sendable
137
{
14-
self.refresh = refresh
15-
self.access = access
8+
public
9+
let refresh:Token
10+
public
11+
let access:Token
12+
13+
@inlinable public
14+
init(refresh:Token, access:Token)
15+
{
16+
self.refresh = refresh
17+
self.access = access
18+
}
1619
}
1720
}
18-
extension GitHubTokens:JSONObjectDecodable
21+
extension GitHubApp.Credentials:JSONObjectDecodable
1922
{
2023
public
2124
enum CodingKey:String

0 commit comments

Comments
 (0)