Skip to content

Commit 59dbfd8

Browse files
committed
Store wiki in the main repo
Use a workflow to update the wiki repo on push to main
1 parent d7301f2 commit 59dbfd8

13 files changed

+562
-4
lines changed

.github/wiki

-1
This file was deleted.

.github/workflows/wiki.yml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Publish wiki
2+
on:
3+
push:
4+
branches: [main]
5+
paths:
6+
- wiki/**
7+
- .github/workflows/wiki.yml
8+
concurrency:
9+
group: wiki
10+
cancel-in-progress: true
11+
permissions:
12+
contents: write
13+
jobs:
14+
publish-wiki:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v3
18+
- uses: Andrew-Chen-Wang/github-wiki-action@v4

.gitmodules

-3
This file was deleted.

wiki/Configure-API-keys.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Before you can run the tests, there are a few things you need to do.
2+
3+
- Set up API keys
4+
- Make your keys available to the test suite
5+
6+
Head on over to the [API Key Management](https://account.arena.net/applications) page and create two keys.
7+
8+
1. a key with full permissions
9+
2. a key with 'account' permission only
10+
11+
The account-only key is used to verify some assumptions about what happens when you don't have a permission. For example, you can get an account summary with or without guild information if you have the 'guilds' permission or not.
12+
13+
### User secrets
14+
15+
Next add your keys to the test suite with the user-secrets tool.
16+
17+
Open a terminal inside the Git root directory and type:
18+
19+
```sh
20+
dotnet user-secrets --project GW2SDK.Tests set ApiKeyBasic XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
21+
dotnet user-secrets --project GW2SDK.Tests set ApiKey YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYYYYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY
22+
```
23+
24+
Visual Studio Code users can instead use the command palette (Ctrl+Shift+P or Cmd+Shift+P).
25+
26+
\> Tasks: Run Task
27+
28+
![Command palette](img/run-task-api-keys.png)
29+
30+
Repeat this for both keys.
31+
32+
![Enter API key](img/enter-api-key.png)

wiki/Home.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Welcome to the wiki for contributors!
2+
3+
## Topics
4+
5+
- [[Project goals]]
6+
- [[PC requirements]]
7+
- [[Solution layout]]
8+
- [[Configure API keys]]
9+
10+
## About
11+
12+
You must be a contributor to edit this wiki directly. However, everyone can copy, modify and redistribute it without limitation. If you are reading this in a browser, there is a helpful widget in the sidebar to clone this wiki locally.
13+
14+
It's not currently possible to create a pull request for wiki pages. As an alternative, you may create a patch:
15+
16+
1. git clone https://github.com/sliekens/gw2sdk.wiki.git
17+
2. git checkout -b changes
18+
3. now make the changes
19+
4. git add .
20+
5. git commit -m changes
21+
6. git format-patch master
22+
23+
This will create a set of patch files, one per commit. I recommend uploading the patch files to a [Gist](https://gist.github.com/) and then creating an issue here with the link to your Gist.

wiki/How-to-add-endpoints.md

+292
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
2+
The pattern for implementing an endpoint is always the same.
3+
4+
1. Add records that model the shape of the API data
5+
2. Add a JSON converter for each record
6+
3. Add one or more `IHttpRequest` where you encapsulate the sending and processing of the API request
7+
4. Add a query class which acts as a facade
8+
5. Add your facade class as a public property to the `Gw2Client` class
9+
10+
Finally you should also add some integration tests.
11+
12+
A good code example for studying is the Quaggans endpoint (in /GW2SDK/Features/Quaggans).
13+
14+
## Example record
15+
16+
``` csharp
17+
using GuildWars2.Annotations;
18+
using JetBrains.Annotations;
19+
20+
namespace GuildWars2.Quaggans;
21+
22+
// The record is sealed because it is not intended for inheritance
23+
// The [DataTransferObject] attribute is used to enforce this pattern of sealed, immutable DTOs
24+
// The [PublicAPI] attribute tells ReSharper not to log a warning for unused code
25+
[PublicAPI, DataTransferObject]
26+
public sealed record Quaggan
27+
{
28+
// Every property is required to require an assignment (even if the property is nullable)
29+
public required string Id { get; init; }
30+
31+
// Every property is also immutable (init-only) to discourage modifying data you don't own
32+
public required string PictureHref { get; init; }
33+
}
34+
```
35+
36+
## Example JSON converter
37+
38+
``` csharp
39+
using System;
40+
using System.Text.Json;
41+
using GuildWars2.Json;
42+
using JetBrains.Annotations;
43+
44+
namespace GuildWars2.Quaggans;
45+
46+
[PublicAPI]
47+
public static class QuagganJson
48+
{
49+
// The JSON conversion is a static extension method for JsonElement
50+
// The conversion is crafted by hand for performance and to compensate
51+
// for the lack of MissingMemberHandling in System.Text.Json
52+
public static Quaggan GetQuaggan(
53+
this JsonElement json,
54+
MissingMemberBehavior missingMemberBehavior
55+
)
56+
{
57+
// Define all the members of the JSON data using RequiredMember<T>,
58+
// OptionalMember<T> or NullableMember<T> (only for value types)
59+
RequiredMember<string> id = new("id");
60+
RequiredMember<string> url = new("url");
61+
62+
// Iterate over all the properties of the JSON object
63+
foreach (var member in json.EnumerateObject())
64+
{
65+
// Compare each JSON property name to the names above
66+
// and copy the value when there is a match
67+
if (member.NameEquals(id.Name))
68+
{
69+
id.Value = member.Value;
70+
}
71+
else if (member.NameEquals(url.Name))
72+
{
73+
url.Value = member.Value;
74+
}
75+
else if (missingMemberBehavior == MissingMemberBehavior.Error)
76+
{
77+
// Throw an error for unexpected JSON properties
78+
// (unless MissingMemberBehavior is set to Undefined)
79+
throw new InvalidOperationException(Strings.UnexpectedMember(member.Name));
80+
}
81+
}
82+
83+
// Create an instance of your record with all the values collected from the JsonElement
84+
// The GetValue() method can throw an exception when no value is found for a RequiredMember
85+
return new Quaggan
86+
{
87+
Id = id.GetValue(),
88+
PictureHref = url.GetValue()
89+
};
90+
}
91+
}
92+
```
93+
94+
## Example HTTP request
95+
96+
``` csharp
97+
using System.Net.Http;
98+
using System.Threading;
99+
using System.Threading.Tasks;
100+
using GuildWars2.Http;
101+
using JetBrains.Annotations;
102+
using static System.Net.Http.HttpMethod;
103+
104+
namespace GuildWars2.Quaggans;
105+
106+
[PublicAPI]
107+
public sealed class QuagganByIdRequest : IHttpRequest<Replica<Quaggan>>
108+
{
109+
// A static template from which you can create HttpRequestMessages
110+
private static readonly HttpRequestMessageTemplate Template = new(Get, "v2/quaggans")
111+
{
112+
// Enable Gzip compression
113+
AcceptEncoding = "gzip"
114+
};
115+
116+
public QuagganByIdRequest(string quagganId)
117+
{
118+
QuagganId = quagganId;
119+
}
120+
121+
// The query string argument to use for this request
122+
public string QuagganId { get; }
123+
124+
public MissingMemberBehavior MissingMemberBehavior { get; init; }
125+
126+
public async Task<Replica<Quaggan>> SendAsync(
127+
HttpClient httpClient,
128+
CancellationToken cancellationToken
129+
)
130+
{
131+
// Create a new template from the one above with the final request configuration
132+
var request = Template with
133+
{
134+
// QueryBuilder makes it easy to add query string arguments to the template
135+
// You must use the recommended schema version, it is enforced by automated tests
136+
Arguments = new QueryBuilder
137+
{
138+
{ "id", QuagganId },
139+
{ "v", SchemaVersion.Recommended }
140+
}
141+
};
142+
143+
// You can pass a Template to HttpClient.SendAsync and
144+
// it gets implicitly converted to HttpRequestMessage
145+
// Always use HttpCompletionOption.ResponseHeadersRead for better performance
146+
using var response = await httpClient.SendAsync(
147+
request,
148+
HttpCompletionOption.ResponseHeadersRead,
149+
cancellationToken
150+
)
151+
.ConfigureAwait(false);
152+
153+
// A helper method ensures that there is a usable result (no errors)
154+
await response.EnsureResult(cancellationToken).ConfigureAwait(false);
155+
156+
// Another helper method creates a JsonDocument from the response content
157+
using var json = await response.Content.ReadAsJsonAsync(cancellationToken)
158+
.ConfigureAwait(false);
159+
160+
161+
// Create and return a Replica<T>
162+
// A Replica<T> is a container for your record that contains additional response headers
163+
return new Replica<Quaggan>
164+
{
165+
Value = json.RootElement.GetQuaggan(MissingMemberBehavior),
166+
ResultContext = response.Headers.GetResultContext(),
167+
PageContext = response.Headers.GetPageContext(),
168+
Date = response.Headers.Date.GetValueOrDefault(),
169+
Expires = response.Content.Headers.Expires,
170+
LastModified = response.Content.Headers.LastModified
171+
};
172+
}
173+
}
174+
```
175+
176+
## Example query class
177+
178+
``` csharp
179+
using System;
180+
using System.Collections.Generic;
181+
using System.Net.Http;
182+
using System.Threading;
183+
using System.Threading.Tasks;
184+
using JetBrains.Annotations;
185+
186+
namespace GuildWars2.Quaggans;
187+
188+
[PublicAPI]
189+
public sealed class QuaggansQuery
190+
{
191+
private readonly HttpClient http;
192+
193+
public QuaggansQuery(HttpClient http)
194+
{
195+
// Ensure HttpClient is not null (defensive programming)
196+
this.http = http ?? throw new ArgumentNullException(nameof(http));
197+
198+
// Ensure a base address is set
199+
http.BaseAddress ??= BaseAddress.DefaultUri;
200+
}
201+
202+
// Create a user-friendly method for every endpoint
203+
// Always add cancellation support
204+
// Only support MissingMemberBehavior when it makes sense
205+
public Task<Replica<HashSet<Quaggan>>> GetQuaggans(
206+
MissingMemberBehavior missingMemberBehavior = default,
207+
CancellationToken cancellationToken = default
208+
)
209+
{
210+
QuaggansRequest request = new() { MissingMemberBehavior = missingMemberBehavior };
211+
return request.SendAsync(http, cancellationToken);
212+
}
213+
214+
public Task<Replica<HashSet<string>>> GetQuaggansIndex(
215+
CancellationToken cancellationToken = default
216+
)
217+
{
218+
QuaggansIndexRequest request = new();
219+
return request.SendAsync(http, cancellationToken);
220+
}
221+
222+
// This is the example from before
223+
public Task<Replica<Quaggan>> GetQuagganById(
224+
string quagganId,
225+
MissingMemberBehavior missingMemberBehavior = default,
226+
CancellationToken cancellationToken = default
227+
)
228+
{
229+
QuagganByIdRequest request = new(quagganId)
230+
{
231+
MissingMemberBehavior = missingMemberBehavior
232+
};
233+
return request.SendAsync(http, cancellationToken);
234+
}
235+
236+
public Task<Replica<HashSet<Quaggan>>> GetQuaggansByIds(
237+
IReadOnlyCollection<string> quagganIds,
238+
MissingMemberBehavior missingMemberBehavior = default,
239+
CancellationToken cancellationToken = default
240+
)
241+
{
242+
QuaggansByIdsRequest request =
243+
new(quagganIds) { MissingMemberBehavior = missingMemberBehavior };
244+
return request.SendAsync(http, cancellationToken);
245+
}
246+
247+
public Task<Replica<HashSet<Quaggan>>> GetQuaggansByPage(
248+
int pageIndex,
249+
int? pageSize = default,
250+
MissingMemberBehavior missingMemberBehavior = default,
251+
CancellationToken cancellationToken = default
252+
)
253+
{
254+
QuaggansByPageRequest request = new(pageIndex)
255+
{
256+
PageSize = pageSize,
257+
MissingMemberBehavior = missingMemberBehavior
258+
};
259+
return request.SendAsync(http, cancellationToken);
260+
}
261+
}
262+
```
263+
264+
## Example Gw2Client
265+
266+
Add your new query class to the existing Gw2Client class.
267+
268+
``` diff
269+
using System;
270+
using System.Net.Http;
271+
// ...
272+
+ using GuildWars2.Quaggans;
273+
using JetBrains.Annotations;
274+
275+
namespace GuildWars2;
276+
277+
[PublicAPI]
278+
public sealed class Gw2Client
279+
{
280+
private readonly HttpClient httpClient;
281+
282+
public Gw2Client(HttpClient httpClient)
283+
{
284+
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
285+
this.httpClient.BaseAddress ??= BaseAddress.DefaultUri;
286+
}
287+
288+
// ...
289+
290+
+ public QuaggansQuery Quaggans => new(httpClient);
291+
}
292+
```

0 commit comments

Comments
 (0)