Skip to content

Commit ff3a9dd

Browse files
github-actions[bot]CopilotCopilotsergey-tihon
authored
[Repo Assist] test: add $ref primitive-type alias tests for v3 DefinitionCompiler (closes #335) (#342)
* test: add $ref primitive-type alias tests for v3 DefinitionCompiler Add 8 new tests to Schema.TypeMappingTests.fs verifying that component schemas which are primitive-type aliases (e.g. `PhoneNumber: type: string`) resolve to the correct .NET type when referenced via direct `$ref` or `allOf: [$ref]`. Two new helper functions: - `compileDirectRefType`: compiles a property that directly $ref's a component alias - `compileAllOfRefType`: compiles a property using allOf:[$ref] (standard OAS 3.0 pattern) Tests cover: string, integer (int32), integer/int64, number (float32), boolean, string/uuid (Guid), allOf-string, allOf-integer. Related to issue #163. Addresses the test part of issue #335. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks * fix: apply Fantomas formatting to fix CheckFormat CI failure Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/0d9e2ea5-87e2-4c38-be5d-f4ff338cfe71 * refactor: address review comments in alias tests - Extract shared compileSchemaAndGetValueType helper to remove duplicated parse/compile/reflect logic from compilePropertyType, compileDirectRefType, and compileAllOfRefType - Make aliasYaml template robust using TrimEnd() + explicit newline to prevent malformed YAML if caller omits trailing newline - Add required: bool parameter to compileDirectRefType and compileAllOfRefType to support both required and optional cases - Add 4 optional alias-ref tests (direct and allOf, int32 and int64) verifying value-type aliases are wrapped in Option<T> when the property is non-required Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: formatting * fix: skip GitHub API tests gracefully when rate limit exceeded * fix: formatting * fix: syntax --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sergey Tihon <sergey.tihon@gmail.com>
1 parent 3898a25 commit ff3a9dd

File tree

2 files changed

+180
-31
lines changed

2 files changed

+180
-31
lines changed

tests/SwaggerProvider.ProviderTests/v2/Swagger.GitHub.Tests.fs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,28 @@ let taskGitHub() =
4141

4242
client
4343

44+
let isRateLimitError(ex: exn) =
45+
ex.Message.Contains("rate limit")
46+
|| ex.Message.Contains("403 (rate limit")
47+
4448
[<Fact>] // Explicit
4549
let ``Get fsprojects from GitHub``() =
4650
task {
47-
let! repos = github().OrgRepos("fsprojects")
48-
repos.Length |> shouldBeGreaterThan 0
51+
try
52+
let! repos = github().OrgRepos("fsprojects")
53+
repos.Length |> shouldBeGreaterThan 0
54+
with
55+
| :? HttpRequestException as ex when isRateLimitError ex -> Assert.Skip("GitHub API rate limit exceeded - transient CI failure")
56+
| :? AggregateException as aex when isRateLimitError aex -> Assert.Skip("GitHub API rate limit exceeded - transient CI failure")
4957
}
5058

5159
[<Fact>]
5260
let ``Get fsproject from GitHub with Task``() =
5361
task {
54-
let! repos = taskGitHub().OrgRepos("fsprojects")
55-
repos.Length |> shouldBeGreaterThan 0
62+
try
63+
let! repos = taskGitHub().OrgRepos("fsprojects")
64+
repos.Length |> shouldBeGreaterThan 0
65+
with
66+
| :? HttpRequestException as ex when isRateLimitError ex -> Assert.Skip("GitHub API rate limit exceeded - transient CI failure")
67+
| :? AggregateException as aex when isRateLimitError aex -> Assert.Skip("GitHub API rate limit exceeded - transient CI failure")
5668
}

tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs

Lines changed: 164 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,15 @@ open SwaggerProvider.Internal.v3.Compilers
66
open Xunit
77
open FsUnitTyped
88

9-
/// Compile a minimal OpenAPI v3 schema containing one "TestType" object with a single
10-
/// "Value" property defined by `propYaml`, and return that property's compiled .NET type.
11-
let private compilePropertyType (propYaml: string) (required: bool) : Type =
12-
let requiredBlock =
13-
if required then
14-
" required:\n - Value\n"
15-
else
16-
""
17-
18-
let schemaStr =
19-
sprintf
20-
"""openapi: "3.0.0"
21-
info:
22-
title: TypeMappingTest
23-
version: "1.0.0"
24-
paths: {}
25-
components:
26-
schemas:
27-
TestType:
28-
type: object
29-
%s properties:
30-
Value:
31-
%s"""
32-
requiredBlock
33-
propYaml
34-
9+
/// Parse and compile a full OpenAPI v3 schema string, then return the .NET type of
10+
/// the `Value` property on the `TestType` component schema.
11+
let private compileSchemaAndGetValueType(schemaStr: string) : Type =
3512
let settings = OpenApiReaderSettings()
3613
settings.AddYamlReader()
3714

3815
let readResult =
3916
Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, settings = settings)
4017

41-
// Ensure the schema was parsed successfully before using it
4218
match readResult.Diagnostic with
4319
| null -> ()
4420
| diagnostic when diagnostic.Errors |> Seq.isEmpty |> not ->
@@ -66,6 +42,34 @@ components:
6642
| null -> failwith "Property 'Value' not found on TestType"
6743
| prop -> prop.PropertyType
6844

45+
/// Compile a minimal OpenAPI v3 schema containing one "TestType" object with a single
46+
/// "Value" property defined by `propYaml`, and return that property's compiled .NET type.
47+
let private compilePropertyType (propYaml: string) (required: bool) : Type =
48+
let requiredBlock =
49+
if required then
50+
" required:\n - Value\n"
51+
else
52+
""
53+
54+
let schemaStr =
55+
sprintf
56+
"""openapi: "3.0.0"
57+
info:
58+
title: TypeMappingTest
59+
version: "1.0.0"
60+
paths: {}
61+
components:
62+
schemas:
63+
TestType:
64+
type: object
65+
%s properties:
66+
Value:
67+
%s"""
68+
requiredBlock
69+
propYaml
70+
71+
compileSchemaAndGetValueType schemaStr
72+
6973
// ── Required primitive types ─────────────────────────────────────────────────
7074

7175
[<Fact>]
@@ -208,3 +212,136 @@ let ``optional byte array is not wrapped in Option``() =
208212

209213
// byte[*] is a reference type — not wrapped in Option<T>
210214
ty |> shouldEqual(typeof<byte>.MakeArrayType(1))
215+
216+
// ── $ref primitive-type alias helpers ────────────────────────────────────────
217+
218+
/// Compile a schema where `TestType.Value` directly references a component alias schema
219+
/// (e.g., `$ref: '#/components/schemas/AliasType'`) and return the resolved .NET type.
220+
let private compileDirectRefType (aliasYaml: string) (required: bool) : Type =
221+
let requiredBlock =
222+
if required then
223+
" required:\n - Value\n"
224+
else
225+
""
226+
227+
let schemaStr =
228+
sprintf
229+
"""openapi: "3.0.0"
230+
info:
231+
title: RefAliasTest
232+
version: "1.0.0"
233+
paths: {}
234+
components:
235+
schemas:
236+
AliasType:
237+
%s TestType:
238+
type: object
239+
%s properties:
240+
Value:
241+
$ref: '#/components/schemas/AliasType'
242+
"""
243+
(aliasYaml.TrimEnd() + "\n")
244+
requiredBlock
245+
246+
compileSchemaAndGetValueType schemaStr
247+
248+
/// Compile a schema where `TestType.Value` uses `allOf: [$ref]` to reference a component alias
249+
/// (the standard OpenAPI 3.0 pattern for annotating a reference) and return the resolved .NET type.
250+
let private compileAllOfRefType (aliasYaml: string) (required: bool) : Type =
251+
let requiredBlock =
252+
if required then
253+
" required:\n - Value\n"
254+
else
255+
""
256+
257+
let schemaStr =
258+
sprintf
259+
"""openapi: "3.0.0"
260+
info:
261+
title: AllOfRefAliasTest
262+
version: "1.0.0"
263+
paths: {}
264+
components:
265+
schemas:
266+
AliasType:
267+
%s TestType:
268+
type: object
269+
%s properties:
270+
Value:
271+
allOf:
272+
- $ref: '#/components/schemas/AliasType'
273+
"""
274+
(aliasYaml.TrimEnd() + "\n")
275+
requiredBlock
276+
277+
compileSchemaAndGetValueType schemaStr
278+
279+
// ── $ref to primitive-type alias (direct $ref) ───────────────────────────────
280+
281+
[<Fact>]
282+
let ``direct $ref to string alias resolves to string``() =
283+
let ty = compileDirectRefType " type: string\n" true
284+
ty |> shouldEqual typeof<string>
285+
286+
[<Fact>]
287+
let ``direct $ref to integer alias resolves to int32``() =
288+
let ty = compileDirectRefType " type: integer\n" true
289+
ty |> shouldEqual typeof<int32>
290+
291+
[<Fact>]
292+
let ``direct $ref to int64 alias resolves to int64``() =
293+
let ty = compileDirectRefType " type: integer\n format: int64\n" true
294+
ty |> shouldEqual typeof<int64>
295+
296+
[<Fact>]
297+
let ``direct $ref to number alias resolves to float32``() =
298+
let ty = compileDirectRefType " type: number\n" true
299+
ty |> shouldEqual typeof<float32>
300+
301+
[<Fact>]
302+
let ``direct $ref to boolean alias resolves to bool``() =
303+
let ty = compileDirectRefType " type: boolean\n" true
304+
ty |> shouldEqual typeof<bool>
305+
306+
[<Fact>]
307+
let ``direct $ref to uuid string alias resolves to Guid``() =
308+
let ty = compileDirectRefType " type: string\n format: uuid\n" true
309+
ty |> shouldEqual typeof<Guid>
310+
311+
// ── $ref to primitive-type alias (via allOf wrapper) ─────────────────────────
312+
// allOf: [$ref] is the standard OpenAPI 3.0 pattern for annotating a $ref with
313+
// additional constraints (e.g. description, nullable) without repeating the schema.
314+
315+
[<Fact>]
316+
let ``allOf $ref to string alias resolves to string``() =
317+
let ty = compileAllOfRefType " type: string\n" true
318+
ty |> shouldEqual typeof<string>
319+
320+
[<Fact>]
321+
let ``allOf $ref to integer alias resolves to int32``() =
322+
let ty = compileAllOfRefType " type: integer\n" true
323+
ty |> shouldEqual typeof<int32>
324+
325+
// ── Optional $ref to primitive-type alias ─────────────────────────────────────
326+
// When a $ref/allOf alias property is non-required, value types must be wrapped
327+
// in Option<T> consistent with the behaviour of ordinary optional primitive properties.
328+
329+
[<Fact>]
330+
let ``optional direct $ref to integer alias resolves to Option<int32>``() =
331+
let ty = compileDirectRefType " type: integer\n" false
332+
ty |> shouldEqual typeof<int32 option>
333+
334+
[<Fact>]
335+
let ``optional direct $ref to int64 alias resolves to Option<int64>``() =
336+
let ty = compileDirectRefType " type: integer\n format: int64\n" false
337+
ty |> shouldEqual typeof<int64 option>
338+
339+
[<Fact>]
340+
let ``optional allOf $ref to integer alias resolves to Option<int32>``() =
341+
let ty = compileAllOfRefType " type: integer\n" false
342+
ty |> shouldEqual typeof<int32 option>
343+
344+
[<Fact>]
345+
let ``optional allOf $ref to int64 alias resolves to Option<int64>``() =
346+
let ty = compileAllOfRefType " type: integer\n format: int64\n" false
347+
ty |> shouldEqual typeof<int64 option>

0 commit comments

Comments
 (0)