Skip to content

Commit bd50166

Browse files
authored
Enhance macro options (#75)
## Description This pull request enhances the customization options for macro expansion on `Schemable`. ```swift @Schemable + @ObjectOptions(.additionalProperties { false }) struct Person { let firstName: String let lastName: String? - @NumberOptions(minimum: 0, maximum: 120) + @NumberOptions(.minimum(0), .maximum(120)) let age: Int } ``` Notice that the `@ObjectOptions` macro now supports additional properties object options and other closure based options that were removed when parsing was introduced. Instead of the option settings have a new trait-based syntax that allows for extensibility and the closures/builders mentioned above. Closes #19 , #27 ## Type of Change - [x] Bug fix - [x] New feature - [x] Breaking change - [x] Documentation update ## Additional Notes Add any other context or screenshots about the pull request here.
1 parent 2ba78e4 commit bd50166

File tree

19 files changed

+565
-149
lines changed

19 files changed

+565
-149
lines changed

.github/actions/xcode-setup/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ runs:
66
- name: Xcode setup
77
shell: bash
88
run: |
9-
sudo xcode-select -s /Applications/Xcode_16.1.app
9+
sudo xcode-select -s /Applications/Xcode_16.3.app

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111

1212
jobs:
1313
build_and_test:
14-
runs-on: macos-latest
14+
runs-on: macos-15
1515

1616
name: Build and Test on macOS with Swift 6.0
1717
steps:
@@ -29,7 +29,7 @@ jobs:
2929
- name: Build
3030
run: swift build -v
3131
- name: Run tests
32-
run: swift test --enable-experimental-swift-testing
32+
run: swift test
3333

3434
build_linux:
3535
name: Build on Linux

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,20 @@ Use the power of Swift result builders to generate JSON schema documents.
4646

4747
<details>
4848
<summary>Generated JSON Schema</summary>
49+
50+
`Schema` returned from `personSchema.definition()` conforms to `Codable`.
51+
52+
```swift
53+
let encoder = JSONEncoder()
54+
encoder.outputFormatting = .prettyPrinted
55+
56+
let schemaData = try! encoder.encode(personSchema.definition())
57+
let string = String(data: schemaData, encoding: .utf8)!
58+
print(string)
59+
```
4960

5061
```json
5162
{
52-
"$id": "https://example.com/person.schema.json",
53-
"$schema": "https://json-schema.org/draft/2020-12/schema",
5463
"title": "Person",
5564
"type": "object",
5665
"properties": {
@@ -78,10 +87,11 @@ Use the `@Schemable` macro from `JSONSchemaBuilder` to automatically generate th
7887

7988
```swift
8089
@Schemable
90+
@ObjectOptions(.additionalProperties { false })
8191
struct Person {
8292
let firstName: String
8393
let lastName: String?
84-
@NumberOptions(minimum: 0, maximum: 120)
94+
@NumberOptions(.minimum(0), .maximum(120))
8595
let age: Int
8696
}
8797
```
@@ -113,6 +123,7 @@ struct Person {
113123
}
114124
.required()
115125
}
126+
.additionalProperties { false }
116127
}
117128
}
118129
}
@@ -236,8 +247,7 @@ dump(result2, name: "Instance 2 Validation Result")
236247
<details>
237248
<summary>Instance 2 Validation Result</summary>
238249

239-
```
240-
▿ Instance 2 Validation Result: JSONSchema.ValidationResult
250+
``` Instance 2 Validation Result: JSONSchema.ValidationResult
241251
- isValid: false
242252
▿ keywordLocation: #
243253
- path: 0 elements

Sources/JSONSchemaBuilder/Documentation.docc/Articles/Macros.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ There are also type specific attributes that can be used to customize the genera
5555
```swift
5656
@Schemable
5757
struct Person {
58-
@SchemaOptions(description: "The person's first name.")
58+
@SchemaOptions(.description("The person's first name."))
5959
let firstName: String
6060

61-
@SchemaOptions(description: "The person's last name.")
61+
@SchemaOptions(.description("The person's last name."))
6262
let lastName: String
6363

64-
@SchemaOptions(description: "Age in years")
65-
@JSONInteger(minimum: 0, maximum: 120)
64+
@SchemaOptions(.description("Age in years"))
65+
@NumberOptions(.minimum(0), .maximum(120))
6666
let age: Int
6767
}
6868
```

Sources/JSONSchemaBuilder/Macros/SchemaOptions/SchemaOptions.swift

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,48 @@ import JSONSchema
22

33
@attached(peer)
44
public macro SchemaOptions(
5-
title: String? = nil,
6-
description: String? = nil,
7-
default: JSONValue? = nil,
8-
examples: JSONValue? = nil,
9-
readOnly: Bool? = nil,
10-
writeOnly: Bool? = nil,
11-
deprecated: Bool? = nil,
12-
comment: String? = nil
5+
_ traits: SchemaTrait...
136
) = #externalMacro(module: "JSONSchemaMacro", type: "SchemaOptionsMacro")
7+
8+
public protocol SchemaTrait {}
9+
10+
public struct SchemaOptionsTrait: SchemaTrait {
11+
fileprivate init() {}
12+
13+
fileprivate static let errorMessage =
14+
"This method should only be used within @SchemaOptions macro"
15+
}
16+
17+
extension SchemaTrait where Self == SchemaOptionsTrait {
18+
public static func title(_ value: String) -> SchemaOptionsTrait {
19+
fatalError(SchemaOptionsTrait.errorMessage)
20+
}
21+
22+
public static func description(_ value: String) -> SchemaOptionsTrait {
23+
fatalError(SchemaOptionsTrait.errorMessage)
24+
}
25+
26+
public static func `default`(_ value: JSONValue) -> SchemaOptionsTrait {
27+
fatalError(SchemaOptionsTrait.errorMessage)
28+
}
29+
30+
public static func examples(_ value: JSONValue) -> SchemaOptionsTrait {
31+
fatalError(SchemaOptionsTrait.errorMessage)
32+
}
33+
34+
public static func readOnly(_ value: Bool) -> SchemaOptionsTrait {
35+
fatalError(SchemaOptionsTrait.errorMessage)
36+
}
37+
38+
public static func writeOnly(_ value: Bool) -> SchemaOptionsTrait {
39+
fatalError(SchemaOptionsTrait.errorMessage)
40+
}
41+
42+
public static func deprecated(_ value: Bool) -> SchemaOptionsTrait {
43+
fatalError(SchemaOptionsTrait.errorMessage)
44+
}
45+
46+
public static func comment(_ value: String) -> SchemaOptionsTrait {
47+
fatalError(SchemaOptionsTrait.errorMessage)
48+
}
49+
}

Sources/JSONSchemaBuilder/Macros/SchemaOptions/TypeSpecific/ArrayOptions.swift

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,54 @@ import JSONSchema
22

33
@attached(peer)
44
public macro ArrayOptions(
5-
minContains: Int? = nil,
6-
maxContains: Int? = nil,
7-
minItems: Int? = nil,
8-
maxItems: Int? = nil,
9-
uniqueItems: Bool? = nil
5+
_ traits: ArrayTrait...
106
) = #externalMacro(module: "JSONSchemaMacro", type: "ArrayOptionsMacro")
7+
8+
public protocol ArrayTrait {}
9+
10+
public struct ArraySchemaTrait: ArrayTrait {
11+
fileprivate init() {}
12+
13+
fileprivate static let errorMessage = "This method should only be used within @ArrayOptions macro"
14+
}
15+
16+
extension ArrayTrait where Self == ArraySchemaTrait {
17+
public static func minContains(_ value: Int) -> ArraySchemaTrait {
18+
fatalError(ArraySchemaTrait.errorMessage)
19+
}
20+
21+
public static func maxContains(_ value: Int) -> ArraySchemaTrait {
22+
fatalError(ArraySchemaTrait.errorMessage)
23+
}
24+
25+
public static func minItems(_ value: Int) -> ArraySchemaTrait {
26+
fatalError(ArraySchemaTrait.errorMessage)
27+
}
28+
29+
public static func maxItems(_ value: Int) -> ArraySchemaTrait {
30+
fatalError(ArraySchemaTrait.errorMessage)
31+
}
32+
33+
public static func uniqueItems(_ value: Bool = true) -> ArraySchemaTrait {
34+
fatalError(ArraySchemaTrait.errorMessage)
35+
}
36+
37+
public static func prefixItems(
38+
@JSONSchemaCollectionBuilder<JSONValue> _ prefixItems: @escaping () -> [JSONComponents
39+
.AnyComponent<JSONValue>]
40+
) -> ArraySchemaTrait {
41+
fatalError(ArraySchemaTrait.errorMessage)
42+
}
43+
44+
public static func unevaluatedItems<Component: JSONSchemaComponent>(
45+
@JSONSchemaBuilder _ unevaluatedItems: @escaping () -> Component
46+
) -> ArraySchemaTrait {
47+
fatalError(ArraySchemaTrait.errorMessage)
48+
}
49+
50+
public static func contains(
51+
@JSONSchemaBuilder _ contains: @escaping () -> any JSONSchemaComponent
52+
) -> ArraySchemaTrait {
53+
fatalError(ArraySchemaTrait.errorMessage)
54+
}
55+
}

Sources/JSONSchemaBuilder/Macros/SchemaOptions/TypeSpecific/NumberOptions.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,36 @@ import JSONSchema
22

33
@attached(peer)
44
public macro NumberOptions(
5-
multipleOf: Double? = nil,
6-
minimum: Double? = nil,
7-
exclusiveMinimum: Bool? = nil,
8-
maximum: Double? = nil,
9-
exclusiveMaximum: Bool? = nil
5+
_ traits: NumberTrait...
106
) = #externalMacro(module: "JSONSchemaMacro", type: "NumberOptionsMacro")
7+
8+
public protocol NumberTrait {}
9+
10+
public struct NumberSchemaTrait: NumberTrait {
11+
fileprivate init() {}
12+
13+
fileprivate static let errorMessage =
14+
"This method should only be used within @NumberOptions macro"
15+
}
16+
17+
extension NumberTrait where Self == NumberSchemaTrait {
18+
public static func multipleOf(_ value: Double) -> NumberSchemaTrait {
19+
fatalError(NumberSchemaTrait.errorMessage)
20+
}
21+
22+
public static func minimum(_ value: Double) -> NumberSchemaTrait {
23+
fatalError(NumberSchemaTrait.errorMessage)
24+
}
25+
26+
public static func exclusiveMinimum(_ value: Double) -> NumberSchemaTrait {
27+
fatalError(NumberSchemaTrait.errorMessage)
28+
}
29+
30+
public static func maximum(_ value: Double) -> NumberSchemaTrait {
31+
fatalError(NumberSchemaTrait.errorMessage)
32+
}
33+
34+
public static func exclusiveMaximum(_ value: Double) -> NumberSchemaTrait {
35+
fatalError(NumberSchemaTrait.errorMessage)
36+
}
37+
}

Sources/JSONSchemaBuilder/Macros/SchemaOptions/TypeSpecific/ObjectOptions.swift

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,48 @@ import JSONSchema
22

33
@attached(peer)
44
public macro ObjectOptions(
5-
minProperties: Int? = nil,
6-
maxProperties: Int? = nil
5+
_ traits: ObjectTrait...
76
) = #externalMacro(module: "JSONSchemaMacro", type: "ObjectOptionsMacro")
7+
8+
public protocol ObjectTrait {}
9+
10+
public struct ObjectSchemaTrait: ObjectTrait {
11+
fileprivate init() {}
12+
13+
fileprivate static let errorMessage =
14+
"This method should only be used within @ObjectOptions macro"
15+
}
16+
17+
extension ObjectTrait where Self == ObjectSchemaTrait {
18+
public static func additionalProperties(
19+
@JSONSchemaBuilder _ content: @escaping () -> some JSONSchemaComponent
20+
) -> ObjectSchemaTrait {
21+
fatalError(ObjectSchemaTrait.errorMessage)
22+
}
23+
24+
public static func patternProperties(
25+
@JSONPropertySchemaBuilder _ patternProperties: @escaping () -> some PropertyCollection
26+
) -> ObjectSchemaTrait {
27+
fatalError(ObjectSchemaTrait.errorMessage)
28+
}
29+
30+
public static func unevaluatedProperties(
31+
@JSONSchemaBuilder _ content: @escaping () -> some JSONSchemaComponent
32+
) -> ObjectSchemaTrait {
33+
fatalError(ObjectSchemaTrait.errorMessage)
34+
}
35+
36+
public static func minProperties(_ value: Int) -> ObjectSchemaTrait {
37+
fatalError(ObjectSchemaTrait.errorMessage)
38+
}
39+
40+
public static func maxProperties(_ value: Int) -> ObjectSchemaTrait {
41+
fatalError(ObjectSchemaTrait.errorMessage)
42+
}
43+
44+
public static func propertyNames(
45+
@JSONSchemaBuilder _ content: @escaping () -> some JSONSchemaComponent
46+
) -> ObjectSchemaTrait {
47+
fatalError(ObjectSchemaTrait.errorMessage)
48+
}
49+
}

Sources/JSONSchemaBuilder/Macros/SchemaOptions/TypeSpecific/StringOptions.swift

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,32 @@ import JSONSchema
22

33
@attached(peer)
44
public macro StringOptions(
5-
minLength: Int? = nil,
6-
maxLength: Int? = nil,
7-
pattern: String? = nil,
8-
format: String? = nil
5+
_ traits: StringTrait...
96
) = #externalMacro(module: "JSONSchemaMacro", type: "StringOptionsMacro")
7+
8+
public protocol StringTrait {}
9+
10+
public struct StringSchemaTrait: StringTrait {
11+
fileprivate init() {}
12+
13+
fileprivate static let errorMessage =
14+
"This method should only be used within @StringOptions macro"
15+
}
16+
17+
extension StringTrait where Self == StringSchemaTrait {
18+
public static func minLength(_ value: Int) -> StringSchemaTrait {
19+
fatalError(StringSchemaTrait.errorMessage)
20+
}
21+
22+
public static func maxLength(_ value: Int) -> StringSchemaTrait {
23+
fatalError(StringSchemaTrait.errorMessage)
24+
}
25+
26+
public static func pattern(_ value: String) -> StringSchemaTrait {
27+
fatalError(StringSchemaTrait.errorMessage)
28+
}
29+
30+
public static func format(_ value: String) -> StringSchemaTrait {
31+
fatalError(StringSchemaTrait.errorMessage)
32+
}
33+
}

Sources/JSONSchemaClient/main.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ struct Flight: Sendable {
4343
let origin: String
4444
let destination: String?
4545
let airline: Airline
46-
@NumberOptions(multipleOf: 0.5)
46+
@NumberOptions(.multipleOf(0.5))
4747
let duration: Double
4848
}
4949

@@ -67,6 +67,10 @@ let nameBuilder = JSONObject {
6767
}
6868
let schema = nameBuilder.definition()
6969

70+
let schemaData = try! encoder.encode(nameBuilder.definition())
71+
let string = String(data: schemaData, encoding: .utf8)!
72+
print(string)
73+
7074
let instance1: JSONValue = ["name": "Alice"]
7175
let instance2: JSONValue = ["name": ""]
7276

@@ -76,6 +80,18 @@ let result2 = schema.validate(instance2)
7680
dump(result2, name: "Instance 2 Validation Result")
7781

7882
@Schemable
83+
@ObjectOptions(.additionalProperties { false })
7984
public struct Weather {
8085
let temperature: Double
8186
}
87+
88+
@Schemable
89+
@ObjectOptions(
90+
.additionalProperties {
91+
JSONString()
92+
.pattern("^[a-zA-Z]+$")
93+
}
94+
)
95+
public struct Weather20 {
96+
let temperature: Double
97+
}

0 commit comments

Comments
 (0)