@seamapi/nextlove-sdk-generator 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/lib/generate-php-sdk/utils/deep-extract-resource-object-schemas.js +10 -0
- package/lib/generate-php-sdk/utils/deep-extract-resource-object-schemas.js.map +1 -1
- package/lib/generate-php-sdk/utils/generate-resource-object-class.js +9 -7
- package/lib/generate-php-sdk/utils/generate-resource-object-class.js.map +1 -1
- package/lib/generate-swift-sdk/generate-swift-sdk.d.ts +1 -0
- package/lib/generate-swift-sdk/generate-swift-sdk.js +107 -0
- package/lib/generate-swift-sdk/generate-swift-sdk.js.map +1 -0
- package/lib/generate-swift-sdk/map-swift-type.d.ts +2 -0
- package/lib/generate-swift-sdk/map-swift-type.js +21 -0
- package/lib/generate-swift-sdk/map-swift-type.js.map +1 -0
- package/lib/generate-swift-sdk/route-group-class-file.d.ts +20 -0
- package/lib/generate-swift-sdk/route-group-class-file.js +68 -0
- package/lib/generate-swift-sdk/route-group-class-file.js.map +1 -0
- package/lib/generate-swift-sdk/templates/anyjson.swift.template.d.ts +2 -0
- package/lib/generate-swift-sdk/templates/anyjson.swift.template.js +168 -0
- package/lib/generate-swift-sdk/templates/anyjson.swift.template.js.map +1 -0
- package/lib/generate-swift-sdk/templates/package.swift.template.d.ts +2 -0
- package/lib/generate-swift-sdk/templates/package.swift.template.js +28 -0
- package/lib/generate-swift-sdk/templates/package.swift.template.js.map +1 -0
- package/lib/generate-swift-sdk/templates/readme.md.template.d.ts +2 -0
- package/lib/generate-swift-sdk/templates/readme.md.template.js +48 -0
- package/lib/generate-swift-sdk/templates/readme.md.template.js.map +1 -0
- package/lib/generate-swift-sdk/templates/seamclient.swift.template.d.ts +5 -0
- package/lib/generate-swift-sdk/templates/seamclient.swift.template.js +73 -0
- package/lib/generate-swift-sdk/templates/seamclient.swift.template.js.map +1 -0
- package/lib/types.d.ts +5 -0
- package/package.json +3 -1
- package/src/index.ts +1 -0
- package/src/lib/generate-php-sdk/utils/deep-extract-resource-object-schemas.ts +10 -0
- package/src/lib/generate-php-sdk/utils/generate-resource-object-class.ts +14 -7
- package/src/lib/generate-swift-sdk/generate-swift-sdk.ts +135 -0
- package/src/lib/generate-swift-sdk/map-swift-type.ts +27 -0
- package/src/lib/generate-swift-sdk/route-group-class-file.ts +89 -0
- package/src/lib/generate-swift-sdk/templates/anyjson.swift.template.ts +167 -0
- package/src/lib/generate-swift-sdk/templates/package.swift.template.ts +27 -0
- package/src/lib/generate-swift-sdk/templates/readme.md.template.ts +47 -0
- package/src/lib/generate-swift-sdk/templates/seamclient.swift.template.ts +87 -0
- package/src/lib/types.ts +9 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { camelCase } from "change-case";
|
|
2
|
+
export default ({ namespaceToRouteGroupClassName, }) => `import Foundation
|
|
3
|
+
#if canImport(FoundationNetworking)
|
|
4
|
+
import FoundationNetworking
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
enum SeamClientError: Error {
|
|
8
|
+
case couldNotMakeRequest(request: URLRequest, error: Error)
|
|
9
|
+
case non2xxResponseData(response: URLResponse, errorType: String?, errorMessage: String?, requestID: String?)
|
|
10
|
+
case responseJSONDecodingError(error: DecodingError)
|
|
11
|
+
case unknown(error: Error)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class SeamClient {
|
|
15
|
+
var apiKey: String
|
|
16
|
+
var baseURL: URL
|
|
17
|
+
let decoder = JSONDecoder()
|
|
18
|
+
let urlSession = URLSession.init(configuration: .default)
|
|
19
|
+
|
|
20
|
+
${Array.from(namespaceToRouteGroupClassName.entries())
|
|
21
|
+
.map(([namespace, route_group_class_name]) => `var ${camelCase(namespace)}: ${route_group_class_name}! = nil`)
|
|
22
|
+
.join("\n ")}
|
|
23
|
+
|
|
24
|
+
init(apiKey: String, baseURL: URL = URL(string: "https://connect.getseam.com")!) {
|
|
25
|
+
self.apiKey = apiKey
|
|
26
|
+
self.baseURL = baseURL
|
|
27
|
+
|
|
28
|
+
${Array.from(namespaceToRouteGroupClassName.entries())
|
|
29
|
+
.map(([namespace, route_group_class_name]) => `self.${camelCase(namespace)} = ${route_group_class_name}(client: self)`)
|
|
30
|
+
.join("\n ")}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func request<ResponseSchema: Decodable>(method: String, path: String, responseSchema: ResponseSchema.Type, jsonBody: Data? = nil) async -> Result<ResponseSchema, SeamClientError> {
|
|
34
|
+
let url = self.baseURL.appendingPathComponent(path)
|
|
35
|
+
var request = URLRequest(url: url)
|
|
36
|
+
request.httpMethod = method
|
|
37
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
38
|
+
request.setValue("Bearer \\(apiKey)", forHTTPHeaderField: "Authorization")
|
|
39
|
+
|
|
40
|
+
if let jsonBody = jsonBody {
|
|
41
|
+
request.httpBody = jsonBody
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let requestResult: (Data, URLResponse)
|
|
45
|
+
do {
|
|
46
|
+
requestResult = try await self.urlSession.data(for: request)
|
|
47
|
+
} catch let error {
|
|
48
|
+
return .failure(.couldNotMakeRequest(request: request, error: error))
|
|
49
|
+
}
|
|
50
|
+
let (data, response) = requestResult
|
|
51
|
+
|
|
52
|
+
guard let httpResponse = response as? HTTPURLResponse,
|
|
53
|
+
(200...299).contains(httpResponse.statusCode) else {
|
|
54
|
+
// todo: better error
|
|
55
|
+
return .failure(.non2xxResponseData(
|
|
56
|
+
response: response,
|
|
57
|
+
errorType: nil,
|
|
58
|
+
errorMessage: nil,
|
|
59
|
+
requestID: nil
|
|
60
|
+
))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
do {
|
|
64
|
+
let decoded = try decoder.decode(ResponseSchema.self, from: data)
|
|
65
|
+
return .success(decoded)
|
|
66
|
+
} catch let error as DecodingError {
|
|
67
|
+
return .failure(.responseJSONDecodingError(error: error))
|
|
68
|
+
} catch let error {
|
|
69
|
+
return .failure(.unknown(error: error))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}`;
|
|
73
|
+
//# sourceMappingURL=seamclient.swift.template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seamclient.swift.template.js","sourceRoot":"","sources":["../../../src/lib/generate-swift-sdk/templates/seamclient.swift.template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAMvC,eAAe,CAAC,EACd,8BAA8B,GACJ,EAAE,EAAE,CAAC;;;;;;;;;;;;;;;;;;IAkB7B,KAAK,CAAC,IAAI,CAAC,8BAA8B,CAAC,OAAO,EAAE,CAAC;KACnD,GAAG,CACF,CAAC,CAAC,SAAS,EAAE,sBAAsB,CAAC,EAAE,EAAE,CACtC,OAAO,SAAS,CAAC,SAAS,CAAC,KAAK,sBAAsB,SAAS,CAClE;KACA,IAAI,CAAC,MAAM,CAAC;;;;;;MAMX,KAAK,CAAC,IAAI,CAAC,8BAA8B,CAAC,OAAO,EAAE,CAAC;KACnD,GAAG,CACF,CAAC,CAAC,SAAS,EAAE,sBAAsB,CAAC,EAAE,EAAE,CACtC,QAAQ,SAAS,CACf,SAAS,CACV,MAAM,sBAAsB,gBAAgB,CAChD;KACA,IAAI,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0CnB,CAAA"}
|
package/lib/types.d.ts
CHANGED
|
@@ -39,20 +39,25 @@ export type PrimitiveSchema = {
|
|
|
39
39
|
} | {
|
|
40
40
|
type: "boolean";
|
|
41
41
|
enum?: boolean[];
|
|
42
|
+
nullable?: boolean;
|
|
42
43
|
} | {
|
|
43
44
|
type: "integer";
|
|
44
45
|
enum?: number[];
|
|
46
|
+
nullable?: boolean;
|
|
45
47
|
} | {
|
|
46
48
|
type: "number";
|
|
49
|
+
nullable?: boolean;
|
|
47
50
|
};
|
|
48
51
|
export type ArraySchema = {
|
|
49
52
|
type: "array";
|
|
50
53
|
items: PropertySchema;
|
|
54
|
+
nullable?: boolean;
|
|
51
55
|
};
|
|
52
56
|
export type ObjSchema = {
|
|
53
57
|
type: "object";
|
|
54
58
|
properties: Record<string, PropertySchema>;
|
|
55
59
|
required: string[];
|
|
60
|
+
nullable?: boolean;
|
|
56
61
|
};
|
|
57
62
|
export type RefSchema = {
|
|
58
63
|
$ref: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seamapi/nextlove-sdk-generator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Utilities for building NextLove SDK Generators",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"generate:ruby-sdk": "tsx ./scripts/generate-ruby-sdk.ts && prettier -w ./output/ruby",
|
|
59
59
|
"generate:csharp-sdk": "tsx ./scripts/generate-csharp-sdk.ts && dotnet csharpier ./output/csharp",
|
|
60
60
|
"generate:php-sdk": "tsx ./scripts/generate-php-sdk.ts && prettier -w ./output/php",
|
|
61
|
+
"generate:swift-sdk": "tsx ./scripts/generate-swift-sdk.ts && swiftformat .",
|
|
61
62
|
"test:csharp-sdk": "dotnet test ./output/csharp"
|
|
62
63
|
},
|
|
63
64
|
"engines": {
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"axios": "^1.5.0",
|
|
70
71
|
"change-case": "^4.1.2",
|
|
71
72
|
"lodash": "^4.17.21",
|
|
73
|
+
"tsx": "^4.7.0",
|
|
72
74
|
"yargs": "^17.7.2"
|
|
73
75
|
},
|
|
74
76
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -4,3 +4,4 @@ export * from "./lib/generate-csharp-sdk/generate-csharp-sdk.js"
|
|
|
4
4
|
export * from "./lib/generate-python-sdk/index.js"
|
|
5
5
|
export * from "./lib/generate-ruby-sdk/index.js"
|
|
6
6
|
export * from "./lib/generate-php-sdk/index.js"
|
|
7
|
+
export * from "./lib/generate-swift-sdk/generate-swift-sdk.js"
|
|
@@ -82,11 +82,15 @@ export function deepExtractResourceObjectSchemas({
|
|
|
82
82
|
property_schema.type === "array" &&
|
|
83
83
|
"items" in property_schema &&
|
|
84
84
|
"properties" in property_schema.items
|
|
85
|
+
const is_nullable_set = "nullable" in property_schema
|
|
85
86
|
|
|
86
87
|
if (is_object_schema) {
|
|
87
88
|
property_schemas_with_type_references[property_name] = {
|
|
88
89
|
type: "object",
|
|
89
90
|
reference_object_type_name,
|
|
91
|
+
...(is_nullable_set && {
|
|
92
|
+
nullable: property_schema.nullable,
|
|
93
|
+
}),
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
createResourceSchema({
|
|
@@ -98,6 +102,9 @@ export function deepExtractResourceObjectSchemas({
|
|
|
98
102
|
property_schemas_with_type_references[property_name] = {
|
|
99
103
|
type: "array",
|
|
100
104
|
reference_object_type_name,
|
|
105
|
+
...(is_nullable_set && {
|
|
106
|
+
nullable: property_schema.nullable,
|
|
107
|
+
}),
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
createResourceSchema({
|
|
@@ -127,6 +134,9 @@ export function deepExtractResourceObjectSchemas({
|
|
|
127
134
|
} else {
|
|
128
135
|
property_schemas_with_type_references[property_name] = {
|
|
129
136
|
type: property_schema.type as any,
|
|
137
|
+
...(is_nullable_set && {
|
|
138
|
+
nullable: property_schema.nullable,
|
|
139
|
+
}),
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
142
|
}
|
|
@@ -26,11 +26,18 @@ export const generateResourceObjectClass = ({
|
|
|
26
26
|
prop_schema.type === "array" &&
|
|
27
27
|
"reference_object_type_name" in prop_schema
|
|
28
28
|
|
|
29
|
+
const is_property_required =
|
|
30
|
+
"nullable" in prop_schema
|
|
31
|
+
? !prop_schema.nullable
|
|
32
|
+
: required_property_names.includes(prop_name)
|
|
33
|
+
|
|
29
34
|
if (is_object_referencing_resource) {
|
|
30
35
|
return `${prop_name}: ${
|
|
36
|
+
is_property_required ? "" : `isset($json->${prop_name}) ? `
|
|
37
|
+
}${
|
|
31
38
|
prop_schema.reference_object_type_name
|
|
32
39
|
}::from_json($json->${prop_name})${
|
|
33
|
-
|
|
40
|
+
is_property_required ? "," : " : null,"
|
|
34
41
|
}`
|
|
35
42
|
} else if (is_array_referencing_resource) {
|
|
36
43
|
return `${prop_name}: array_map(
|
|
@@ -39,7 +46,7 @@ export const generateResourceObjectClass = ({
|
|
|
39
46
|
),`
|
|
40
47
|
} else {
|
|
41
48
|
return `${prop_name}: $json->${prop_name}${
|
|
42
|
-
|
|
49
|
+
is_property_required ? "," : " ?? null,"
|
|
43
50
|
}`
|
|
44
51
|
}
|
|
45
52
|
}
|
|
@@ -74,13 +81,13 @@ export const generateResourceObjectClass = ({
|
|
|
74
81
|
const php_type = is_object_referencing_resource
|
|
75
82
|
? prop_schema.reference_object_type_name
|
|
76
83
|
: getPhpType("type" in prop_schema ? prop_schema.type : "mixed")
|
|
84
|
+
const is_property_required =
|
|
85
|
+
"nullable" in prop_schema
|
|
86
|
+
? !prop_schema.nullable
|
|
87
|
+
: required_property_names.includes(prop_name)
|
|
77
88
|
|
|
78
89
|
return `public ${php_type}${
|
|
79
|
-
php_type === "mixed"
|
|
80
|
-
? ""
|
|
81
|
-
: required_property_names.includes(prop_name)
|
|
82
|
-
? ""
|
|
83
|
-
: " | null"
|
|
90
|
+
php_type === "mixed" ? "" : is_property_required ? "" : " | null"
|
|
84
91
|
} $${prop_name},`
|
|
85
92
|
})
|
|
86
93
|
.join("\n ")}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import axios from "axios"
|
|
2
|
+
import type { ObjSchema, OpenAPISchema, Route } from "lib/types.js"
|
|
3
|
+
import readmeMdTemplate from "./templates/readme.md.template.js"
|
|
4
|
+
import packageSwiftTemplate from "./templates/package.swift.template.js"
|
|
5
|
+
import seamClientSwiftTemplate from "./templates/seamclient.swift.template.js"
|
|
6
|
+
import anyJsonSwiftTemplate from "./templates/anyjson.swift.template.js"
|
|
7
|
+
import { pascalCase } from "change-case"
|
|
8
|
+
import { flattenObjSchema, getParameterAndResponseSchema } from "lib/index.js"
|
|
9
|
+
import { mapSwiftType } from "./map-swift-type.js"
|
|
10
|
+
import { RouteGroupClassfile } from "./route-group-class-file.js"
|
|
11
|
+
|
|
12
|
+
export const generateSwiftSDK = async () => {
|
|
13
|
+
const openapi: OpenAPISchema = await axios
|
|
14
|
+
.get("https://connect.getseam.com/openapi.json")
|
|
15
|
+
.then((res) => res.data)
|
|
16
|
+
|
|
17
|
+
const fs: Record<string, string> = {}
|
|
18
|
+
|
|
19
|
+
fs["README.md"] = readmeMdTemplate()
|
|
20
|
+
fs["Package.swift"] = packageSwiftTemplate()
|
|
21
|
+
|
|
22
|
+
const resources = Object.entries(openapi.components.schemas).map(
|
|
23
|
+
([schema_name, schema]) =>
|
|
24
|
+
[schema_name, flattenObjSchema(schema as any)] as [string, ObjSchema]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
fs["Sources/SeamAPI/AnyJSON.swift"] = anyJsonSwiftTemplate()
|
|
28
|
+
|
|
29
|
+
// Create a struct for each declared resource
|
|
30
|
+
fs["Sources/SeamAPI/Schemas.swift"] = [
|
|
31
|
+
"import Foundation",
|
|
32
|
+
...resources
|
|
33
|
+
.map(([snake_name, schema]) => {
|
|
34
|
+
const props = Object.entries(schema.properties)
|
|
35
|
+
.map(([name, property_schema]) => {
|
|
36
|
+
const is_optional =
|
|
37
|
+
!schema.required?.includes(name) ||
|
|
38
|
+
(property_schema as any).nullable
|
|
39
|
+
|
|
40
|
+
return `let ${name}: ${mapSwiftType(property_schema, "AnyJSON")}${
|
|
41
|
+
is_optional ? "?" : ""
|
|
42
|
+
}`
|
|
43
|
+
})
|
|
44
|
+
.join("\n")
|
|
45
|
+
|
|
46
|
+
return `struct ${pascalCase(snake_name)}: Decodable {\n${props}\n}`
|
|
47
|
+
})
|
|
48
|
+
.filter((x) => x),
|
|
49
|
+
].join("\n")
|
|
50
|
+
|
|
51
|
+
// Construct route groups by namespace
|
|
52
|
+
const namespaceToRouteGroupClassName = new Map<string, string>()
|
|
53
|
+
|
|
54
|
+
const routes: Route[] = Object.entries(openapi.paths).map(([path, v]) => ({
|
|
55
|
+
path,
|
|
56
|
+
...v,
|
|
57
|
+
}))
|
|
58
|
+
const class_map: Record<string, RouteGroupClassfile> = {}
|
|
59
|
+
const namespaces: string[][] = []
|
|
60
|
+
for (const route of routes) {
|
|
61
|
+
if (!route.post) continue
|
|
62
|
+
if (!route.post["x-fern-sdk-group-name"]) continue
|
|
63
|
+
const group_names = [...route.post["x-fern-sdk-group-name"]]
|
|
64
|
+
const namespace = group_names.join("_")
|
|
65
|
+
group_names.reverse()
|
|
66
|
+
const class_name = pascalCase(group_names.join("_")) + "Routes"
|
|
67
|
+
if (!class_map[class_name]) {
|
|
68
|
+
namespaces.push(route.post["x-fern-sdk-group-name"])
|
|
69
|
+
class_map[class_name] = new RouteGroupClassfile(class_name, namespace)
|
|
70
|
+
namespaceToRouteGroupClassName.set(namespace, class_name)
|
|
71
|
+
}
|
|
72
|
+
const cls = class_map[class_name]
|
|
73
|
+
|
|
74
|
+
if (!cls) {
|
|
75
|
+
console.warn(`No class for "${route.path}", skipping`)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { parameter_schema, response_obj_type, response_arr_type } =
|
|
80
|
+
getParameterAndResponseSchema(route)
|
|
81
|
+
|
|
82
|
+
if (!response_obj_type && !response_arr_type) {
|
|
83
|
+
console.warn(`No response object/array ref for "${route.path}", skipping`)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!parameter_schema) {
|
|
88
|
+
console.warn(`No parameter schema for "${route.path}", skipping`)
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
cls.addMethod({
|
|
93
|
+
method_name: route.post["x-fern-sdk-method-name"],
|
|
94
|
+
path: route.path,
|
|
95
|
+
bodyParameters: Object.entries(parameter_schema.properties)
|
|
96
|
+
.filter(
|
|
97
|
+
([_, param_val]) =>
|
|
98
|
+
param_val && typeof param_val === "object" && "type" in param_val
|
|
99
|
+
)
|
|
100
|
+
.map(([param_name, param_schema]) => {
|
|
101
|
+
const name = param_name
|
|
102
|
+
return {
|
|
103
|
+
name,
|
|
104
|
+
swiftType: mapSwiftType(param_schema),
|
|
105
|
+
isRequired: parameter_schema.required?.includes(name) ?? false,
|
|
106
|
+
}
|
|
107
|
+
}),
|
|
108
|
+
return_path: [route.post["x-fern-sdk-return-value"]],
|
|
109
|
+
return_resource: response_obj_type
|
|
110
|
+
? pascalCase(response_obj_type)
|
|
111
|
+
: `[${pascalCase(response_arr_type)}]`,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const route_groups = Object.values(class_map).filter((r) => !r.isEmpty)
|
|
116
|
+
|
|
117
|
+
// Write all route groups
|
|
118
|
+
for (const route_group of route_groups) {
|
|
119
|
+
fs[`Sources/SeamAPI/Routes/${pascalCase(route_group.namespace!)}.swift`] =
|
|
120
|
+
route_group.serializeToClass()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Remove route groups that were filtered out because they were empty
|
|
124
|
+
for (const namespace of namespaceToRouteGroupClassName.keys()) {
|
|
125
|
+
if (!route_groups.find((r) => r.namespace === namespace)) {
|
|
126
|
+
namespaceToRouteGroupClassName.delete(namespace)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fs["Sources/SeamAPI/SeamClient.swift"] = seamClientSwiftTemplate({
|
|
131
|
+
namespaceToRouteGroupClassName,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return fs
|
|
135
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PropertySchema } from "lib/types.ts"
|
|
2
|
+
|
|
3
|
+
export const mapSwiftType = (
|
|
4
|
+
property_schema: PropertySchema,
|
|
5
|
+
fallback = "String"
|
|
6
|
+
) => {
|
|
7
|
+
if (typeof property_schema !== "object") {
|
|
8
|
+
return "Any"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if ("type" in property_schema && property_schema.type === "string") {
|
|
12
|
+
// todo: handle date-time
|
|
13
|
+
return "String"
|
|
14
|
+
}
|
|
15
|
+
if ("type" in property_schema && property_schema.type === "integer")
|
|
16
|
+
return "Int"
|
|
17
|
+
if ("type" in property_schema && property_schema.type === "boolean")
|
|
18
|
+
return "Bool"
|
|
19
|
+
if ("type" in property_schema && property_schema.type === "number")
|
|
20
|
+
return "Float"
|
|
21
|
+
if ("type" in property_schema && property_schema.type === "array")
|
|
22
|
+
return "[String]" // TODO, make more specific
|
|
23
|
+
if ("type" in property_schema && property_schema.type === "object")
|
|
24
|
+
return `[String: ${fallback}]` // TODO, make more specific
|
|
25
|
+
|
|
26
|
+
return fallback
|
|
27
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export type RouteGroupClassfileMethod = {
|
|
2
|
+
method_name: string
|
|
3
|
+
path: string
|
|
4
|
+
bodyParameters: {
|
|
5
|
+
name: string
|
|
6
|
+
swiftType: string
|
|
7
|
+
isRequired: boolean
|
|
8
|
+
}[]
|
|
9
|
+
return_path: string[]
|
|
10
|
+
return_resource: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class RouteGroupClassfile {
|
|
14
|
+
private methods: RouteGroupClassfileMethod[] = []
|
|
15
|
+
constructor(
|
|
16
|
+
private name: string,
|
|
17
|
+
public namespace?: string
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
addMethod(method: RouteGroupClassfileMethod) {
|
|
21
|
+
this.methods.push(method)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get isEmpty() {
|
|
25
|
+
return this.methods.length === 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
serializeToClass() {
|
|
29
|
+
return `import Foundation
|
|
30
|
+
#if canImport(FoundationNetworking)
|
|
31
|
+
import FoundationNetworking
|
|
32
|
+
#endif
|
|
33
|
+
|
|
34
|
+
class ${this.name} {
|
|
35
|
+
private let client: SeamClient
|
|
36
|
+
init(client: SeamClient) {
|
|
37
|
+
self.client = client
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
${this.methods
|
|
41
|
+
.map(
|
|
42
|
+
({ method_name, bodyParameters, path, return_path, return_resource }) =>
|
|
43
|
+
` func ${method_name}(${bodyParameters
|
|
44
|
+
.map(({ name, swiftType, isRequired }) =>
|
|
45
|
+
isRequired ? `${name}: ${swiftType}` : `${name}: ${swiftType}? = nil`
|
|
46
|
+
)
|
|
47
|
+
.join(", ")}) async -> Result<${return_resource}, SeamClientError> {
|
|
48
|
+
${bodyParameters.length > 0 ? "var" : "let"} jsonBody: [String: Any] = [:]
|
|
49
|
+
|
|
50
|
+
${bodyParameters
|
|
51
|
+
.map(({ name, isRequired }) =>
|
|
52
|
+
isRequired
|
|
53
|
+
? `jsonBody["${name}"] = ${name}`
|
|
54
|
+
: ` if let ${name} = ${name} {
|
|
55
|
+
jsonBody["${name}"] = ${name}
|
|
56
|
+
}`
|
|
57
|
+
)
|
|
58
|
+
.join("\n\n")}
|
|
59
|
+
|
|
60
|
+
let encodedJsonBody: Data?
|
|
61
|
+
do {
|
|
62
|
+
encodedJsonBody = try JSONSerialization.data(withJSONObject: jsonBody, options: [])
|
|
63
|
+
} catch let error {
|
|
64
|
+
return .failure(.unknown(error: error))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
struct Response: Decodable {
|
|
68
|
+
let ${return_path[0]}: ${return_resource}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let response = await client.request(
|
|
72
|
+
method: "POST",
|
|
73
|
+
path: "${path}",
|
|
74
|
+
responseSchema: Response.self,
|
|
75
|
+
jsonBody: encodedJsonBody
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
switch response {
|
|
79
|
+
case .success(let response):
|
|
80
|
+
return .success(response.${return_path[0]})
|
|
81
|
+
case .failure(let error):
|
|
82
|
+
return .failure(error)
|
|
83
|
+
}
|
|
84
|
+
}`
|
|
85
|
+
)
|
|
86
|
+
.join("\n\n")}
|
|
87
|
+
}`.trim()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export default () => `// Thanks to https://adamrackis.dev/blog/swift-codable-any
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
func decode(fromObject container: KeyedDecodingContainer<AnyJSONCodingKeys>) -> [String: Any] {
|
|
5
|
+
var result: [String: Any] = [:]
|
|
6
|
+
|
|
7
|
+
for key in container.allKeys {
|
|
8
|
+
if let val = try? container.decode(Int.self, forKey: key) {
|
|
9
|
+
result[key.stringValue] = val
|
|
10
|
+
} else if let val = try? container.decode(Double.self, forKey: key) {
|
|
11
|
+
result[key.stringValue] = val
|
|
12
|
+
} else if let val = try? container.decode(String.self, forKey: key) {
|
|
13
|
+
result[key.stringValue] = val
|
|
14
|
+
} else if let val = try? container.decode(Bool.self, forKey: key) {
|
|
15
|
+
result[key.stringValue] = val
|
|
16
|
+
} else if let nestedContainer = try? container.nestedContainer(
|
|
17
|
+
keyedBy: AnyJSONCodingKeys.self, forKey: key)
|
|
18
|
+
{
|
|
19
|
+
result[key.stringValue] = decode(fromObject: nestedContainer)
|
|
20
|
+
} else if var nestedArray = try? container.nestedUnkeyedContainer(forKey: key) {
|
|
21
|
+
result[key.stringValue] = decode(fromArray: &nestedArray)
|
|
22
|
+
} else if (try? container.decodeNil(forKey: key)) == true {
|
|
23
|
+
result.updateValue(Any?(nil) as Any, forKey: key.stringValue)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func decode(fromArray container: inout UnkeyedDecodingContainer) -> [Any] {
|
|
31
|
+
var result: [Any] = []
|
|
32
|
+
|
|
33
|
+
while !container.isAtEnd {
|
|
34
|
+
if let value = try? container.decode(String.self) {
|
|
35
|
+
result.append(value)
|
|
36
|
+
} else if let value = try? container.decode(Int.self) {
|
|
37
|
+
result.append(value)
|
|
38
|
+
} else if let value = try? container.decode(Double.self) {
|
|
39
|
+
result.append(value)
|
|
40
|
+
} else if let value = try? container.decode(Bool.self) {
|
|
41
|
+
result.append(value)
|
|
42
|
+
} else if let nestedContainer = try? container.nestedContainer(keyedBy: AnyJSONCodingKeys.self) {
|
|
43
|
+
result.append(decode(fromObject: nestedContainer))
|
|
44
|
+
} else if var nestedArray = try? container.nestedUnkeyedContainer() {
|
|
45
|
+
result.append(decode(fromArray: &nestedArray))
|
|
46
|
+
} else if (try? container.decodeNil()) == true {
|
|
47
|
+
result.append(Any?(nil) as Any)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func encodeValue(
|
|
55
|
+
fromObjectContainer container: inout KeyedEncodingContainer<AnyJSONCodingKeys>, map: [String: Any]
|
|
56
|
+
) throws {
|
|
57
|
+
for k in map.keys {
|
|
58
|
+
let value = map[k]
|
|
59
|
+
let encodingKey = AnyJSONCodingKeys(stringValue: k)
|
|
60
|
+
|
|
61
|
+
if let value = value as? String {
|
|
62
|
+
try container.encode(value, forKey: encodingKey)
|
|
63
|
+
} else if let value = value as? Int {
|
|
64
|
+
try container.encode(value, forKey: encodingKey)
|
|
65
|
+
} else if let value = value as? Double {
|
|
66
|
+
try container.encode(value, forKey: encodingKey)
|
|
67
|
+
} else if let value = value as? Bool {
|
|
68
|
+
try container.encode(value, forKey: encodingKey)
|
|
69
|
+
} else if let value = value as? [String: Any] {
|
|
70
|
+
var keyedContainer = container.nestedContainer(
|
|
71
|
+
keyedBy: AnyJSONCodingKeys.self, forKey: encodingKey)
|
|
72
|
+
try encodeValue(fromObjectContainer: &keyedContainer, map: value)
|
|
73
|
+
} else if let value = value as? [Any] {
|
|
74
|
+
var unkeyedContainer = container.nestedUnkeyedContainer(forKey: encodingKey)
|
|
75
|
+
try encodeValue(fromArrayContainer: &unkeyedContainer, arr: value)
|
|
76
|
+
} else {
|
|
77
|
+
try container.encodeNil(forKey: encodingKey)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func encodeValue(fromArrayContainer container: inout UnkeyedEncodingContainer, arr: [Any]) throws {
|
|
83
|
+
for value in arr {
|
|
84
|
+
if let value = value as? String {
|
|
85
|
+
try container.encode(value)
|
|
86
|
+
} else if let value = value as? Int {
|
|
87
|
+
try container.encode(value)
|
|
88
|
+
} else if let value = value as? Double {
|
|
89
|
+
try container.encode(value)
|
|
90
|
+
} else if let value = value as? Bool {
|
|
91
|
+
try container.encode(value)
|
|
92
|
+
} else if let value = value as? [String: Any] {
|
|
93
|
+
var keyedContainer = container.nestedContainer(keyedBy: AnyJSONCodingKeys.self)
|
|
94
|
+
try encodeValue(fromObjectContainer: &keyedContainer, map: value)
|
|
95
|
+
} else if let value = value as? [Any] {
|
|
96
|
+
var unkeyedContainer = container.nestedUnkeyedContainer()
|
|
97
|
+
try encodeValue(fromArrayContainer: &unkeyedContainer, arr: value)
|
|
98
|
+
} else {
|
|
99
|
+
try container.encodeNil()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
struct AnyJSONCodingKeys: CodingKey {
|
|
107
|
+
var stringValue: String
|
|
108
|
+
|
|
109
|
+
init(stringValue: String) {
|
|
110
|
+
self.stringValue = stringValue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
var intValue: Int?
|
|
114
|
+
|
|
115
|
+
init?(intValue: Int) {
|
|
116
|
+
self.init(stringValue: "\(intValue)")
|
|
117
|
+
self.intValue = intValue
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
struct AnyJSON: Codable {
|
|
122
|
+
var value: Any?
|
|
123
|
+
|
|
124
|
+
init(value: Any?) {
|
|
125
|
+
self.value = value
|
|
126
|
+
}
|
|
127
|
+
init(from decoder: Decoder) throws {
|
|
128
|
+
if let container = try? decoder.container(keyedBy: AnyJSONCodingKeys.self) {
|
|
129
|
+
self.value = decode(fromObject: container)
|
|
130
|
+
} else if var array = try? decoder.unkeyedContainer() {
|
|
131
|
+
self.value = decode(fromArray: &array)
|
|
132
|
+
} else if let value = try? decoder.singleValueContainer() {
|
|
133
|
+
if value.decodeNil() {
|
|
134
|
+
self.value = nil
|
|
135
|
+
} else {
|
|
136
|
+
if let result = try? value.decode(Int.self) { self.value = result }
|
|
137
|
+
if let result = try? value.decode(Double.self) { self.value = result }
|
|
138
|
+
if let result = try? value.decode(String.self) { self.value = result }
|
|
139
|
+
if let result = try? value.decode(Bool.self) { self.value = result }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func encode(to encoder: Encoder) throws {
|
|
145
|
+
if let map = self.value as? [String: Any] {
|
|
146
|
+
var container = encoder.container(keyedBy: AnyJSONCodingKeys.self)
|
|
147
|
+
try encodeValue(fromObjectContainer: &container, map: map)
|
|
148
|
+
} else if let arr = self.value as? [Any] {
|
|
149
|
+
var container = encoder.unkeyedContainer()
|
|
150
|
+
try encodeValue(fromArrayContainer: &container, arr: arr)
|
|
151
|
+
} else {
|
|
152
|
+
var container = encoder.singleValueContainer()
|
|
153
|
+
|
|
154
|
+
if let value = self.value as? String {
|
|
155
|
+
try! container.encode(value)
|
|
156
|
+
} else if let value = self.value as? Int {
|
|
157
|
+
try! container.encode(value)
|
|
158
|
+
} else if let value = self.value as? Double {
|
|
159
|
+
try! container.encode(value)
|
|
160
|
+
} else if let value = self.value as? Bool {
|
|
161
|
+
try! container.encode(value)
|
|
162
|
+
} else {
|
|
163
|
+
try! container.encodeNil()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}`
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default () => `// swift-tools-version: 5.7
|
|
2
|
+
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
3
|
+
|
|
4
|
+
import PackageDescription
|
|
5
|
+
|
|
6
|
+
let package = Package(
|
|
7
|
+
name: "SeamAPI",
|
|
8
|
+
platforms: [
|
|
9
|
+
.macOS(.v12)
|
|
10
|
+
],
|
|
11
|
+
products: [
|
|
12
|
+
// Products define the executables and libraries a package produces, making them visible to other packages.
|
|
13
|
+
.library(
|
|
14
|
+
name: "SeamAPI",
|
|
15
|
+
targets: ["SeamAPI"]),
|
|
16
|
+
],
|
|
17
|
+
targets: [
|
|
18
|
+
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
|
19
|
+
// Targets can depend on other targets in this package and products from dependencies.
|
|
20
|
+
.target(
|
|
21
|
+
name: "SeamAPI"),
|
|
22
|
+
.testTarget(
|
|
23
|
+
name: "SeamAPITests",
|
|
24
|
+
dependencies: ["SeamAPI"]),
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
`
|