also looking at this
feat: finish native customization and platform seams
#401
21 files
+707
-17
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -185,7 +185,7 @@ const number_keys = [_][]const u8{ "splashMaxSeconds", "requestTimeoutSeconds" } | ||
| 185 | 185 | /// value inside it throws; only a null at the top level is dropped. |
| 186 | 186 | const optional_string_map_keys = [_][]const u8{ "nativeScreens", "shareStorage" }; |
| 187 | 187 | /// Swift `Codable` structs, whose fields are checked by `checkObject`. |
| 188 | const optional_object_keys = [_]Nested{ .{ .key = "backgroundRefresh", .fields = &background_refresh_fields }, .{ .key = "crashReporting", .fields = &crash_reporting_fields } }; | |
| 188 | const optional_object_keys = [_]Nested{ .{ .key = "backgroundRefresh", .fields = &background_refresh_fields }, .{ .key = "crashReporting", .fields = &crash_reporting_fields }, .{ .key = "tabBar", .fields = &tab_bar_fields } }; | |
| 189 | 189 | /// Swift arrays of `Codable` structs. Every element is checked. |
| 190 | 190 | const optional_array_of_object_keys = [_]Nested{ .{ .key = "tabs", .fields = &tab_fields }, .{ .key = "appShortcuts", .fields = &app_shortcut_fields } }; |
| 191 | 191 | |
| @@ -199,8 +199,8 @@ const Nested = struct { key: []const u8, fields: []const Field }; | ||
| 199 | 199 | /// ignores a property's initial value, so `BackgroundRefresh.enabled`, which |
| 200 | 200 | /// defaults to false, still throws when it is missing or null. An optional |
| 201 | 201 | /// field may be missing or null. Both checked against `swiftc`. |
| 202 | const Field = struct { name: []const u8, kind: Kind, required: bool }; | |
| 203 | const Kind = enum { string, bool, number, string_array }; | |
| 202 | const Field = struct { name: []const u8, kind: Kind, required: bool, object_fields: []const Field = &.{} }; | |
| 203 | const Kind = enum { string, bool, number, string_array, object }; | |
| 204 | 204 | |
| 205 | 205 | /// `CraftConfig.BackgroundRefresh`. |
| 206 | 206 | const background_refresh_fields = [_]Field{ |
| @@ -219,6 +219,15 @@ const crash_reporting_fields = [_]Field{ | ||
| 219 | 219 | .{ .name = "sessions", .kind = .bool, .required = false }, |
| 220 | 220 | }; |
| 221 | 221 | |
| 222 | /// `CraftConfig.TabBar` and its optional selected-indicator styling. | |
| 223 | const tab_selection_fields = [_]Field{ | |
| 224 | .{ .name = "cornerRadius", .kind = .number, .required = false }, | |
| 225 | .{ .name = "color", .kind = .string, .required = false }, | |
| 226 | }; | |
| 227 | const tab_bar_fields = [_]Field{ | |
| 228 | .{ .name = "selection", .kind = .object, .required = false, .object_fields = &tab_selection_fields }, | |
| 229 | }; | |
| 230 | ||
| 222 | 231 | /// `CraftConfig.Tab`. |
| 223 | 232 | const tab_fields = [_]Field{ |
| 224 | 233 | .{ .name = "id", .kind = .string, .required = true }, |
| @@ -260,6 +269,7 @@ fn isKind(v: std.json.Value, kind: Kind) bool { | ||
| 260 | 269 | .bool => v == .bool, |
| 261 | 270 | .number => isNumber(v), |
| 262 | 271 | .string_array => isStringArray(v), |
| 272 | .object => v == .object, | |
| 263 | 273 | }; |
| 264 | 274 | } |
| 265 | 275 | |
| @@ -277,6 +287,7 @@ fn checkObject(v: std.json.Value, fields: []const Field) bool { | ||
| 277 | 287 | continue; |
| 278 | 288 | } |
| 279 | 289 | if (!isKind(present, field.kind)) return false; |
| 290 | if (field.kind == .object and !checkObject(present, field.object_fields)) return false; | |
| 280 | 291 | } |
| 281 | 292 | return true; |
| 282 | 293 | } |
| @@ -840,6 +851,9 @@ test "nested types decode field by field, defaults or not" { | ||
| 840 | 851 | "\"tabs\":[{\"id\":\"health\",\"title\":\"Health\",\"selectedSymbol\":null,\"selectedIconTint\":null,\"selectedIconPulse\":null}]", |
| 841 | 852 | "\"crashReporting\":{\"key\":\"bughq_k\"}", |
| 842 | 853 | "\"crashReporting\":{\"key\":\"bughq_k\",\"host\":\"https://bughq.org\",\"environment\":null,\"sessions\":false}", |
| 854 | "\"tabBar\":{}", | |
| 855 | "\"tabBar\":{\"selection\":{\"cornerRadius\":8,\"color\":\"#123456\"}}", | |
| 856 | "\"tabBar\":{\"selection\":null}", | |
| 843 | 857 | }) |good| { |
| 844 | 858 | const json = try completeConfig(testing.allocator, good); |
| 845 | 859 | defer testing.allocator.free(json); |
| @@ -862,6 +876,9 @@ test "nested types decode field by field, defaults or not" { | ||
| 862 | 876 | "\"tabs\":[{\"id\":\"a\",\"title\":\"A\",\"selectedIconPulse\":\"false\"}]", |
| 863 | 877 | "\"tabs\":[null]", |
| 864 | 878 | "\"tabs\":{}", |
| 879 | "\"tabBar\":[]", | |
| 880 | "\"tabBar\":{\"selection\":{\"cornerRadius\":\"8\"}}", | |
| 881 | "\"tabBar\":{\"selection\":{\"color\":42}}", | |
| 865 | 882 | }) |bad| { |
| 866 | 883 | const json = try completeConfig(testing.allocator, bad); |
| 867 | 884 | defer testing.allocator.free(json); |
| @@ -1123,13 +1123,15 @@ test "Zig validates every field of the nested types Swift decodes, the way Swift | ||
| 1123 | 1123 | "bool" |
| 1124 | 1124 | else if (std.mem.eql(u8, base, "Double")) |
| 1125 | 1125 | "number" |
| 1126 | else if (std.mem.eql(u8, base, "TabSelection")) | |
| 1127 | "object" | |
| 1126 | 1128 | else { |
| 1127 | 1129 | std.debug.print("CraftConfig.{s}.{s} is a {s}, which ios_config.zig has no Kind for.\n", .{ struct_name, member.name, base }); |
| 1128 | 1130 | return error.NestedFieldKindUnknown; |
| 1129 | 1131 | }; |
| 1130 | 1132 | |
| 1131 | 1133 | var expected_buf: [256]u8 = undefined; |
| 1132 | const expected = try std.fmt.bufPrint(&expected_buf, ".,", .{ | |
| 1134 | const expected = try std.fmt.bufPrint(&expected_buf, ".{{ .name = \"{s}\", .kind = .{s}, .required = {s}", .{ | |
| 1133 | 1135 | member.name, kind, if (optional) "false" else "true", |
| 1134 | 1136 | }); |
| 1135 | 1137 | if (std.mem.indexOf(u8, zig_fields, expected) == null) { |