@@ -2075,7 +2075,7 @@ fn Caller(comptime E: type) type {
20752075 fn jsValueToZig (self : * const Self , comptime named_function : anytype , comptime T : type , js_value : v8.Value ) ! T {
20762076 switch (@typeInfo (T )) {
20772077 .optional = > | o | {
2078- if (js_value .isNull () or js_value . isUndefined ()) {
2078+ if (js_value .isNullOrUndefined ()) {
20792079 return null ;
20802080 }
20812081 return try self .jsValueToZig (named_function , o .child , js_value );
@@ -2093,11 +2093,8 @@ fn Caller(comptime E: type) type {
20932093 return error .InvalidArgument ;
20942094 }
20952095 if (@hasField (TypeLookup , @typeName (ptr .child ))) {
2096- const obj = js_value .castTo (v8 .Object );
2097- if (obj .internalFieldCount () == 0 ) {
2098- return error .InvalidArgument ;
2099- }
2100- return E .typeTaggedAnyOpaque (named_function , * Receiver (ptr .child ), obj );
2096+ const js_obj = js_value .castTo (v8 .Object );
2097+ return E .typeTaggedAnyOpaque (named_function , * Receiver (ptr .child ), js_obj );
21012098 }
21022099 },
21032100 .slice = > {
@@ -2193,58 +2190,50 @@ fn Caller(comptime E: type) type {
21932190 },
21942191 else = > {},
21952192 },
2196- .@"struct" = > | s | {
2197- if (@hasDecl (T , "_CALLBACK_ID_KLUDGE" )) {
2198- if (! js_value .isFunction ()) {
2199- return error .InvalidArgument ;
2200- }
2201-
2202- const func = v8 .Persistent (v8 .Function ).init (self .isolate , js_value .castTo (v8 .Function ));
2203- const scope = self .scope ;
2204- try scope .trackCallback (func );
2205-
2206- return .{
2207- .func = func ,
2208- .scope = scope ,
2209- .id = js_value .castTo (v8 .Object ).getIdentityHash (),
2210- };
2211- }
2212-
2213- const js_obj = js_value .castTo (v8 .Object );
2214-
2215- if (comptime isJsObject (T )) {
2216- // Caller wants an opaque JsObject. Probably a parameter
2217- // that it needs to pass back into a callback
2218- return E.JsObject {
2219- .js_obj = js_obj ,
2220- .scope = self .scope ,
2221- };
2222- }
2223-
2224- if (! js_value .isObject ()) {
2193+ .@"struct" = > {
2194+ return try (self .jsValueToStruct (named_function , T , js_value )) orelse {
22252195 return error .InvalidArgument ;
2196+ };
2197+ },
2198+ .@"union" = > | u | {
2199+ // see probeJsValueToZig for some explanation of what we're
2200+ // trying to do
2201+
2202+ // the first field that we find which the js_value could be
2203+ // coerced to.
2204+ var coerce_index : ? usize = null ;
2205+
2206+ // the first field that we find which the js_Value is
2207+ // compatible with. A compatible field has higher precedence
2208+ // than a coercible, but still isn't a perfect match.
2209+ var compatible_index : ? usize = null ;
2210+
2211+ inline for (u .fields , 0.. ) | field , i | {
2212+ switch (try self .probeJsValueToZig (named_function , field .type , js_value )) {
2213+ .value = > | v | return @unionInit (T , field .name , v ),
2214+ .ok = > {
2215+ // a perfect match like above case, except the probing
2216+ // didn't get the value for us.
2217+ return @unionInit (T , field .name , try self .jsValueToZig (named_function , field .type , js_value ));
2218+ },
2219+ .coerce = > if (coerce_index == null ) {
2220+ coerce_index = i ;
2221+ },
2222+ .compatible = > if (compatible_index == null ) {
2223+ compatible_index = i ;
2224+ },
2225+ .invalid = > {},
2226+ }
22262227 }
22272228
2228- const context = self .context ;
2229- const isolate = self .isolate ;
2230-
2231- var value : T = undefined ;
2232- inline for (s .fields ) | field | {
2233- const name = field .name ;
2234- const key = v8 .String .initUtf8 (isolate , name );
2235- if (js_obj .has (context , key .toValue ())) {
2236- @field (value , name ) = try self .jsValueToZig (named_function , field .type , try js_obj .getValue (context , key ));
2237- } else if (@typeInfo (field .type ) == .optional ) {
2238- @field (value , name ) = null ;
2239- } else {
2240- if (field .defaultValue ()) | dflt | {
2241- @field (value , name ) = dflt ;
2242- } else {
2243- return error .JSWrongObject ;
2244- }
2229+ // We didn't find a perfect match.
2230+ const closest = compatible_index orelse coerce_index orelse return error .InvalidArgument ;
2231+ inline for (u .fields , 0.. ) | field , i | {
2232+ if (i == closest ) {
2233+ return @unionInit (T , field .name , try self .jsValueToZig (named_function , field .type , js_value ));
22452234 }
22462235 }
2247- return value ;
2236+ unreachable ;
22482237 },
22492238 else = > {},
22502239 }
@@ -2299,6 +2288,230 @@ fn Caller(comptime E: type) type {
22992288 return error .InvalidArgument ;
23002289 }
23012290
2291+ // Extracted so that it can be used in both jsValueToZig and in
2292+ // probeJsValueToZig. Avoids having to duplicate this logic when probing.
2293+ fn jsValueToStruct (self : * const Self , comptime named_function : anytype , comptime T : type , js_value : v8.Value ) ! ? T {
2294+ if (@hasDecl (T , "_CALLBACK_ID_KLUDGE" )) {
2295+ if (! js_value .isFunction ()) {
2296+ return error .InvalidArgument ;
2297+ }
2298+
2299+ const func = v8 .Persistent (v8 .Function ).init (self .isolate , js_value .castTo (v8 .Function ));
2300+ const scope = self .scope ;
2301+ try scope .trackCallback (func );
2302+
2303+ return .{
2304+ .func = func ,
2305+ .scope = scope ,
2306+ .id = js_value .castTo (v8 .Object ).getIdentityHash (),
2307+ };
2308+ }
2309+
2310+ const js_obj = js_value .castTo (v8 .Object );
2311+
2312+ if (comptime isJsObject (T )) {
2313+ // Caller wants an opaque JsObject. Probably a parameter
2314+ // that it needs to pass back into a callback
2315+ return E.JsObject {
2316+ .js_obj = js_obj ,
2317+ .scope = self .scope ,
2318+ };
2319+ }
2320+
2321+ if (! js_value .isObject ()) {
2322+ return null ;
2323+ }
2324+
2325+ const context = self .context ;
2326+ const isolate = self .isolate ;
2327+
2328+ var value : T = undefined ;
2329+ inline for (@typeInfo (T ).@"struct" .fields ) | field | {
2330+ const name = field .name ;
2331+ const key = v8 .String .initUtf8 (isolate , name );
2332+ if (js_obj .has (context , key .toValue ())) {
2333+ @field (value , name ) = try self .jsValueToZig (named_function , field .type , try js_obj .getValue (context , key ));
2334+ } else if (@typeInfo (field .type ) == .optional ) {
2335+ @field (value , name ) = null ;
2336+ } else {
2337+ const dflt = field .defaultValue () orelse return null ;
2338+ @field (value , name ) = dflt ;
2339+ }
2340+ }
2341+ return value ;
2342+ }
2343+
2344+ // Probing is part of trying to map a JS value to a Zig union. There's
2345+ // a lot of ambiguity in this process, in part because some JS values
2346+ // can almost always be coerced. For example, anything can be coerced
2347+ // into an integer (it just becomes 0), or a float (becomes NaN) or a
2348+ // string.
2349+ //
2350+ // The way we'll do this is that, if there's a direct match, we'll use it
2351+ // If there's a potential match, we'll keep looking for a direct match
2352+ // and only use the (first) potential match as a fallback.
2353+ //
2354+ // Finally, I considered adding this probing directly into jsValueToZig
2355+ // but I decided doing this separately was better. However, the goal is
2356+ // obviously that probing is consistent with jsValueToZig.
2357+ fn ProbeResult (comptime T : type ) type {
2358+ return union (enum ) {
2359+ // The js_value maps directly to T
2360+ value : T ,
2361+
2362+ // The value is a T. This is almost the same as returning value: T,
2363+ // but the caller still has to get T by calling jsValueToZig.
2364+ // We prefer returning .{.ok => {}}, to avoid reducing duplication
2365+ // with jsValueToZig, but in some cases where probing has a cost
2366+ // AND yields the value anyways, we'll use .{.value = T}.
2367+ ok : void ,
2368+
2369+ // the js_value is compatible with T (i.e. a int -> float),
2370+ compatible : void ,
2371+
2372+ // the js_value can be coerced to T (this is a lower precedence
2373+ // than compatible)
2374+ coerce : void ,
2375+
2376+ // the js_value cannot be turned into T
2377+ invalid : void ,
2378+ };
2379+ }
2380+ fn probeJsValueToZig (self : * const Self , comptime named_function : anytype , comptime T : type , js_value : v8.Value ) ! ProbeResult (T ) {
2381+ switch (@typeInfo (T )) {
2382+ .optional = > | o | {
2383+ if (js_value .isNullOrUndefined ()) {
2384+ return .{ .value = null };
2385+ }
2386+ return self .probeJsValueToZig (named_function , o .child , js_value );
2387+ },
2388+ .float = > {
2389+ if (js_value .isNumber () or js_value .isNumberObject ()) {
2390+ if (js_value .isInt32 () or js_value .isUint32 () or js_value .isBigInt () or js_value .isBigIntObject ()) {
2391+ // int => float is a reasonable match
2392+ return .{ .compatible = {} };
2393+ }
2394+ return .{ .ok = {} };
2395+ }
2396+ // anything can be coerced into a float, it becomes NaN
2397+ return .{ .coerce = {} };
2398+ },
2399+ .int = > {
2400+ if (js_value .isNumber () or js_value .isNumberObject ()) {
2401+ if (js_value .isInt32 () or js_value .isUint32 () or js_value .isBigInt () or js_value .isBigIntObject ()) {
2402+ return .{ .ok = {} };
2403+ }
2404+ // float => int is kind of reasonable, I guess
2405+ return .{ .compatible = {} };
2406+ }
2407+ // anything can be coerced into a int, it becomes 0
2408+ return .{ .coerce = {} };
2409+ },
2410+ .bool = > {
2411+ if (js_value .isBoolean () or js_value .isBooleanObject ()) {
2412+ return .{ .ok = {} };
2413+ }
2414+ // anything can be coerced into a boolean, it will become
2415+ // true or false based on..some complex rules I don't know.
2416+ return .{ .coerce = {} };
2417+ },
2418+ .pointer = > | ptr | switch (ptr .size ) {
2419+ .one = > {
2420+ if (! js_value .isObject ()) {
2421+ return .{ .invalid = {} };
2422+ }
2423+ if (@hasField (TypeLookup , @typeName (ptr .child ))) {
2424+ const js_obj = js_value .castTo (v8 .Object );
2425+ // There's a bit of overhead in doing this, so instead
2426+ // of having a version of typeTaggedAnyOpaque which
2427+ // returns a boolean or an optional, we rely on the
2428+ // main implementation and just handle the error.
2429+ const attempt = E .typeTaggedAnyOpaque (named_function , * Receiver (ptr .child ), js_obj );
2430+ if (attempt ) | value | {
2431+ return .{ .value = value };
2432+ } else | _ | {
2433+ return .{ .invalid = {} };
2434+ }
2435+ }
2436+ // probably an error, but not for us to deal with
2437+ return .{ .invalid = {} };
2438+ },
2439+ .slice = > {
2440+ if (js_value .isTypedArray ()) {
2441+ switch (ptr .child ) {
2442+ u8 = > if (ptr .sentinel () == null ) {
2443+ if (js_value .isUint8Array () or js_value .isUint8ClampedArray ()) {
2444+ return .{ .ok = {} };
2445+ }
2446+ },
2447+ i8 = > if (js_value .isInt8Array ()) {
2448+ return .{ .ok = {} };
2449+ },
2450+ u16 = > if (js_value .isUint16Array ()) {
2451+ return .{ .ok = {} };
2452+ },
2453+ i16 = > if (js_value .isInt16Array ()) {
2454+ return .{ .ok = {} };
2455+ },
2456+ u32 = > if (js_value .isUint32Array ()) {
2457+ return .{ .ok = {} };
2458+ },
2459+ i32 = > if (js_value .isInt32Array ()) {
2460+ return .{ .ok = {} };
2461+ },
2462+ u64 = > if (js_value .isBigUint64Array ()) {
2463+ return .{ .ok = {} };
2464+ },
2465+ i64 = > if (js_value .isBigInt64Array ()) {
2466+ return .{ .ok = {} };
2467+ },
2468+ else = > {},
2469+ }
2470+ return .{ .invalid = {} };
2471+ }
2472+
2473+ if (ptr .child == u8 ) {
2474+ if (js_value .isString ()) {
2475+ return .{ .ok = {} };
2476+ }
2477+ // anything can be coerced into a string
2478+ return .{ .coerce = {} };
2479+ }
2480+
2481+ if (! js_value .isArray ()) {
2482+ return error .InvalidArgument ;
2483+ }
2484+
2485+ // This can get tricky.
2486+ const js_arr = js_value .castTo (v8 .Array );
2487+
2488+ if (js_arr .length () == 0 ) {
2489+ // not so tricky in this case.
2490+ return .{ .value = &.{} };
2491+ }
2492+
2493+ // We settle for just probing the first value. Ok, actually
2494+ // not tricky in this case either.
2495+ const context = self .contxt ;
2496+ const js_obj = js_arr .castTo (v8 .Object );
2497+ return self .probeJsValueToZig (named_function , ptr .child , try js_obj .getAtIndex (context , 0 ));
2498+ },
2499+ else = > {},
2500+ },
2501+ .@"struct" = > {
2502+ // We don't want to duplicate the code for this, so we call
2503+ // the actual coversion function.
2504+ const value = (try self .jsValueToStruct (named_function , T , js_value )) orelse {
2505+ return .{ .invalid = {} };
2506+ };
2507+ return .{ .value = value };
2508+ },
2509+ else = > {},
2510+ }
2511+
2512+ return .{ .invalid = {} };
2513+ }
2514+
23022515 fn zigValueToJs (self : * const Self , value : anytype ) ! v8.Value {
23032516 return self .scope .zigValueToJs (value );
23042517 }
0 commit comments