|
1 | | -# Contributing Guide |
2 | | - |
3 | | -This guide covers development practices, testing patterns, and contribution workflow for JsonApiToolkit. |
4 | | - |
5 | | -## Development Setup |
6 | | - |
7 | | -### Prerequisites |
8 | | - |
9 | | -- .NET 9.0 SDK |
10 | | -- Git |
11 | | -- An IDE (VS Code, Rider, or Visual Studio) |
12 | | - |
13 | | -### Building |
14 | | - |
15 | | -```bash |
16 | | -# Restore dependencies |
17 | | -dotnet restore |
18 | | - |
19 | | -# Build |
20 | | -dotnet build --configuration Release |
21 | | - |
22 | | -# Run tests |
23 | | -dotnet test --configuration Release |
24 | | - |
25 | | -# Format code |
26 | | -dotnet csharpier format . |
27 | | -``` |
28 | | - |
29 | | -## Testing Patterns |
30 | | - |
31 | | -### Test Organization |
32 | | - |
33 | | -Tests are organized by component in the `JsonApiToolkit.Tests` project: |
34 | | - |
35 | | -``` |
36 | | -JsonApiToolkit.Tests/ |
37 | | -├── Configuration/ # JsonApiOptions, QueryComplexityAnalyzer |
38 | | -├── Controllers/ # JsonApiController behavior |
39 | | -├── Extensions/ |
40 | | -│ ├── Filtering/ # FilterHandler, FilterExpressionBuilder |
41 | | -│ ├── Pagination/ # PaginationHandler |
42 | | -│ ├── Sorting/ # SortingHandler |
43 | | -│ └── QueryHelpersTests.cs |
44 | | -├── Filters/ # JsonApiExceptionFilter |
45 | | -├── Integration/ # Full HTTP pipeline tests |
46 | | -├── Mapping/ # JsonApiMapper, InclusionMapper, EntityMapper |
47 | | -├── Models/ # Test entities and model tests |
48 | | -├── Parsing/ # JsonApiQueryParser |
49 | | -├── Security/ # DoS protection, bypass attempts |
50 | | -└── Validation/ # IncludeValidator, AllowedIncludes |
51 | | -``` |
52 | | - |
53 | | -### Test Naming Convention |
54 | | - |
55 | | -Follow the pattern: `MethodName_Scenario_ExpectedBehavior` |
56 | | - |
57 | | -```csharp |
58 | | -// Good examples |
59 | | -[Fact] |
60 | | -public void ApplyPagination_WithPageSizeZero_ClampsToOne() { } |
61 | | - |
62 | | -[Fact] |
63 | | -public void ConvertToPropertyType_WithInvalidGuid_ThrowsFormatException() { } |
64 | | - |
65 | | -[Fact] |
66 | | -public async Task GetArticles_FilterSortPaginate_AppliesAllOperationsAsync() { } |
67 | | -``` |
68 | | - |
69 | | -### Test Categories |
70 | | - |
71 | | -#### 1. Unit Tests |
72 | | - |
73 | | -Test individual methods in isolation: |
74 | | - |
75 | | -```csharp |
76 | | -[Fact] |
77 | | -public void CountFilters_WithNestedGroups_CountsAllFilters() |
78 | | -{ |
79 | | - var group = new FilterGroup |
80 | | - { |
81 | | - Filters = [new() { Field = "a", Value = "1" }], |
82 | | - Groups = [new FilterGroup { Filters = [new() { Field = "b", Value = "2" }] }], |
83 | | - }; |
84 | | - |
85 | | - int count = QueryComplexityAnalyzer.CountFilters(group); |
86 | | - |
87 | | - Assert.Equal(2, count); |
88 | | -} |
89 | | -``` |
90 | | - |
91 | | -#### 2. Integration Tests |
92 | | - |
93 | | -Test the full HTTP pipeline with TestServer: |
94 | | - |
95 | | -```csharp |
96 | | -[Fact] |
97 | | -public async Task GetArticles_WithPagination_ReturnsCorrectPageAsync() |
98 | | -{ |
99 | | - var response = await _client.GetAsync("/api/articles?page[number]=1&page[size]=2"); |
100 | | - |
101 | | - Assert.Equal(HttpStatusCode.OK, response.StatusCode); |
102 | | - |
103 | | - var document = JsonSerializer.Deserialize<JsonApiCollectionDocument<ResourceObject>>( |
104 | | - await response.Content.ReadAsStringAsync(), |
105 | | - _jsonOptions |
106 | | - ); |
107 | | - |
108 | | - Assert.Equal(2, document?.Data?.Count()); |
109 | | -} |
110 | | -``` |
111 | | - |
112 | | -#### 3. Boundary Tests |
113 | | - |
114 | | -Test edge cases and limits: |
115 | | - |
116 | | -```csharp |
117 | | -[Theory] |
118 | | -[InlineData(0, 1)] // Zero clamps to 1 |
119 | | -[InlineData(-1, 1)] // Negative clamps to 1 |
120 | | -[InlineData(int.MaxValue, 100)] // Exceeds max, clamps to max |
121 | | -public void ApplyPagination_WithBoundaryPageSize_ClampsCorrectly( |
122 | | - int inputSize, |
123 | | - int expectedSize) |
124 | | -{ |
125 | | - // Test implementation |
126 | | -} |
127 | | -``` |
128 | | - |
129 | | -#### 4. Error Condition Tests |
130 | | - |
131 | | -Test that errors are handled correctly: |
132 | | - |
133 | | -```csharp |
134 | | -[Fact] |
135 | | -public void ConvertToPropertyType_WithInvalidInt_ThrowsFormatException() |
136 | | -{ |
137 | | - var exception = Assert.Throws<FormatException>(() => |
138 | | - QueryHelpers.ConvertToPropertyType("not-a-number", typeof(int)) |
139 | | - ); |
140 | | - |
141 | | - Assert.Contains("Failed to convert filter value", exception.Message); |
142 | | -} |
143 | | -``` |
144 | | - |
145 | | -### Test Infrastructure |
146 | | - |
147 | | -#### In-Memory Database |
148 | | - |
149 | | -Use EF Core in-memory database for integration tests: |
150 | | - |
151 | | -```csharp |
152 | | -public class TestDbContext : DbContext |
153 | | -{ |
154 | | - public DbSet<TestEntity> Entities { get; set; } = null!; |
155 | | - |
156 | | - public TestDbContext(DbContextOptions<TestDbContext> options) |
157 | | - : base(options) { } |
158 | | -} |
159 | | - |
160 | | -// In test setup |
161 | | -services.AddDbContext<TestDbContext>(options => |
162 | | - options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}") |
163 | | -); |
164 | | -``` |
165 | | - |
166 | | -#### Test Server Setup |
167 | | - |
168 | | -```csharp |
169 | | -_host = new HostBuilder() |
170 | | - .ConfigureWebHost(webBuilder => |
171 | | - { |
172 | | - webBuilder |
173 | | - .UseTestServer() |
174 | | - .ConfigureServices(services => |
175 | | - { |
176 | | - services.AddDbContext<TestDbContext>(options => |
177 | | - options.UseInMemoryDatabase(databaseName) |
178 | | - ); |
179 | | - services.AddControllers(); |
180 | | - services.AddJsonApiToolkit(); |
181 | | - }) |
182 | | - .Configure(app => |
183 | | - { |
184 | | - app.UseRouting(); |
185 | | - app.UseEndpoints(endpoints => endpoints.MapControllers()); |
186 | | - |
187 | | - // Seed data |
188 | | - using var scope = app.ApplicationServices.CreateScope(); |
189 | | - var context = scope.ServiceProvider.GetRequiredService<TestDbContext>(); |
190 | | - SeedTestData(context); |
191 | | - }); |
192 | | - }) |
193 | | - .Build(); |
194 | | -``` |
195 | | - |
196 | | -### Coverage Requirements |
197 | | - |
198 | | -Before merging to main: |
199 | | - |
200 | | -- All public methods must have tests |
201 | | -- All filter operators must have positive and negative tests |
202 | | -- All query handlers must have boundary tests |
203 | | -- Integration tests must cover the full query pipeline |
204 | | - |
205 | | -## Code Style |
206 | | - |
207 | | -### Formatting |
208 | | - |
209 | | -All code is formatted with CSharpier. Run before committing: |
210 | | - |
211 | | -```bash |
212 | | -dotnet csharpier format . |
213 | | -``` |
214 | | - |
215 | | -CI will fail if code is not formatted. |
216 | | - |
217 | | -### Naming Conventions |
218 | | - |
219 | | -| Element | Convention | Example | |
220 | | -|---------|-----------|---------| |
221 | | -| Classes | PascalCase | `FilterHandler` | |
222 | | -| Interfaces | IPascalCase | `IFilterHandler` | |
223 | | -| Methods | PascalCase | `ApplyPagination` | |
224 | | -| Parameters | camelCase | `queryParameters` | |
225 | | -| Private fields | _camelCase | `_logger` | |
226 | | -| Constants | PascalCase | `DefaultPageSize` | |
227 | | - |
228 | | -### Documentation |
229 | | - |
230 | | -- Public APIs must have XML documentation |
231 | | -- Include `<summary>`, `<param>`, and `<returns>` where applicable |
232 | | -- Keep comments concise and meaningful |
233 | | - |
234 | | -## Git Workflow |
235 | | - |
236 | | -### Branching |
237 | | - |
238 | | -Use conventional commit prefixes for branch names: |
239 | | - |
240 | | -| Type | Branch | Example | |
241 | | -|------|--------|---------| |
242 | | -| Bug fix | `fix/` | `fix/pagination-zero-divide` | |
243 | | -| Feature | `feat/` | `feat/sparse-fieldsets` | |
244 | | -| Tests | `test/` | `test/security-tests` | |
245 | | -| Docs | `docs/` | `docs/contributing-guide` | |
246 | | -| Refactor | `refactor/` | `refactor/di-interfaces` | |
247 | | - |
248 | | -### Commit Messages |
249 | | - |
250 | | -Follow conventional commits: |
251 | | - |
252 | | -```bash |
253 | | -# Bug fixes |
254 | | -git commit -m "fix: prevent division by zero in pagination" |
255 | | - |
256 | | -# Features |
257 | | -git commit -m "feat: add sparse fieldsets support" |
258 | | - |
259 | | -# Tests |
260 | | -git commit -m "test: add security tests for DoS protection" |
261 | | - |
262 | | -# Breaking changes |
263 | | -git commit -m "feat!: require DI in JsonApiController" |
264 | | -``` |
265 | | - |
266 | | -### Pull Requests |
267 | | - |
268 | | -1. Create a branch from `main` |
269 | | -2. Make changes with conventional commits |
270 | | -3. Ensure all tests pass: `dotnet test` |
271 | | -4. Ensure code is formatted: `dotnet csharpier check .` |
272 | | -5. Create PR with descriptive title and bullet-point description |
273 | | -6. Wait for CI checks to pass |
274 | | -7. Squash merge to main |
275 | | - |
276 | | -## Release Process |
277 | | - |
278 | | -Releases are managed by release-please. When PRs are merged to main: |
279 | | - |
280 | | -1. release-please creates/updates a Release PR |
281 | | -2. The Release PR accumulates changes based on conventional commits |
282 | | -3. When ready, merge the Release PR |
283 | | -4. This triggers a GitHub Release and NuGet publish |
284 | | - |
285 | | -### Version Bumps |
286 | | - |
287 | | -| Commit Prefix | Version Bump | |
288 | | -|---------------|--------------| |
289 | | -| `fix:` | Patch (1.0.x) | |
290 | | -| `feat:` | Minor (1.x.0) | |
291 | | -| `feat!:` or `fix!:` | Major (x.0.0) | |
292 | | -| `docs:`, `test:`, `refactor:` | No bump | |
293 | | - |
294 | | -## Questions? |
295 | | - |
296 | | -Open an issue on GitHub: https://github.com/Intility/JsonApiToolkit/issues |
| 1 | +# Contributing Guide |
| 2 | + |
| 3 | +## Prerequisites |
| 4 | + |
| 5 | +- .NET 10 SDK (pinned in `global.json` to `10.0.201`) |
| 6 | +- `uv` for the docs site (`brew install uv` or `mise use uv`) |
| 7 | + |
| 8 | +## Setup |
| 9 | + |
| 10 | +```bash |
| 11 | +dotnet tool restore # csharpier + dotnet-api-docs |
| 12 | +dotnet restore |
| 13 | +``` |
| 14 | + |
| 15 | +> **Intility contributors:** `NuGet.config` currently resolves dependencies from `nuget.pkg.github.com/Intility`. Until the package moves to public NuGet, restore needs `NUGET_AUTH_TOKEN` set to a GitHub PAT with `read:packages`. |
| 16 | +
|
| 17 | +## Daily Commands |
| 18 | + |
| 19 | +```bash |
| 20 | +dotnet build --configuration Release |
| 21 | +dotnet test --configuration Release |
| 22 | +dotnet csharpier format . # CI fails on unformatted code |
| 23 | +``` |
| 24 | + |
| 25 | +## Docs |
| 26 | + |
| 27 | +The site is built with mkdocs (Material) and served from GitHub Pages. |
| 28 | + |
| 29 | +```bash |
| 30 | +uv venv |
| 31 | +uv pip install -r docs/requirements.txt |
| 32 | +uv run mkdocs serve # http://127.0.0.1:8000, live reload |
| 33 | +uv run mkdocs build --strict # what CI runs; do this before pushing |
| 34 | +``` |
| 35 | + |
| 36 | +If you change C# XML doc comments, regenerate the API reference first: |
| 37 | + |
| 38 | +```bash |
| 39 | +dotnet build JsonApiToolkit/JsonApiToolkit.csproj -c Release -o JsonApiToolkit/bin/docs |
| 40 | +dotnet tool run dotnet-api-docs -- --input JsonApiToolkit/bin/docs --output docs/api --strict |
| 41 | +``` |
| 42 | + |
| 43 | +## Tests |
| 44 | + |
| 45 | +Tests live in `JsonApiToolkit.Tests/` and use xUnit + EF Core in-memory databases. Integration tests spin up a `TestServer` via `HostBuilder`; see existing tests in `Integration/` for the pattern. |
| 46 | + |
| 47 | +Naming: `MethodName_Scenario_ExpectedBehavior` (e.g. `ApplyPagination_WithPageSizeZero_ClampsToOne`). |
| 48 | + |
| 49 | +When adding behavior, add tests covering the happy path, boundaries, and error conditions. |
| 50 | + |
| 51 | +## Commits |
| 52 | + |
| 53 | +Conventional Commits, enforced indirectly by Release Please: |
| 54 | + |
| 55 | +| Prefix | Bump | Notes | |
| 56 | +|---|---|---| |
| 57 | +| `fix:` | patch | | |
| 58 | +| `feat:` | minor | | |
| 59 | +| `feat!:` / `fix!:` | major | breaking change | |
| 60 | +| `perf:`, `refactor:`, `docs:`, `build:`, `ci:`, `test:`, `style:` | none | shows in changelog | |
| 61 | +| `chore:` | none | hidden from changelog | |
| 62 | + |
| 63 | +Branch names: `feat/`, `fix/`, `refactor/`, `docs/`, `test/`, `chore/`, etc. |
| 64 | + |
| 65 | +## Pull Requests |
| 66 | + |
| 67 | +1. Branch from `main`. |
| 68 | +2. Format (`dotnet csharpier format .`) and run tests locally. |
| 69 | +3. Open a PR with a descriptive title and bullet summary. |
| 70 | +4. CI must pass `build-and-test` and `Docs: Build` (required status checks). |
| 71 | +5. Squash merge (the only merge method allowed on `main`). |
| 72 | + |
| 73 | +## Releases |
| 74 | + |
| 75 | +Handled by [Release Please](https://github.com/googleapis/release-please). Merging to `main` updates a release PR that accumulates changes. Merging the release PR cuts a GitHub Release, publishes to NuGet, and bumps the version in `JsonApiToolkit.csproj` and `mkdocs.yaml`. |
| 76 | + |
| 77 | +## AI-Assisted Contributions |
| 78 | + |
| 79 | +Project-specific guidance for AI tools lives in `AGENTS.md` at the repo root (with `CLAUDE.md` as a symlink). Update it there if you add conventions other contributors' tools should follow. |
| 80 | + |
| 81 | +## Questions |
| 82 | + |
| 83 | +Open an issue: <https://github.com/intility/Intility.JsonApiToolkit/issues> |
0 commit comments