@@ -2013,7 +2013,7 @@ fn Caller(comptime E: type) type {
20132013 fn jsValueToZig (self : * const Self , comptime named_function : anytype , comptime T : type , js_value : v8.Value ) ! T {
20142014 switch (@typeInfo (T )) {
20152015 .optional = > | o | {
2016- if (js_value .isNull () or js_value . isUndefined ()) {
2016+ if (js_value .isNullOrUndefined ()) {
20172017 return null ;
20182018 }
20192019 return try self .jsValueToZig (named_function , o .child , js_value );
@@ -2031,11 +2031,8 @@ fn Caller(comptime E: type) type {
20312031 return error .InvalidArgument ;
20322032 }
20332033 if (@hasField (TypeLookup , @typeName (ptr .child ))) {
2034- const obj = js_value .castTo (v8 .Object );
2035- if (obj .internalFieldCount () == 0 ) {
2036- return error .InvalidArgument ;
2037- }
2038- return E .typeTaggedAnyOpaque (named_function , * Receiver (ptr .child ), obj );
2034+ const js_obj = js_value .castTo (v8 .Object );
2035+ return E .typeTaggedAnyOpaque (named_function , * Receiver (ptr .child ), js_obj );
20392036 }
20402037 },
20412038 .slice = > {
@@ -2131,58 +2128,50 @@ fn Caller(comptime E: type) type {
21312128 },
21322129 else = > {},
21332130 },
2134- .@"struct" = > | s | {
2135- if (@hasDecl (T , "_CALLBACK_ID_KLUDGE" )) {
2136- if (! js_value .isFunction ()) {
2137- return error .InvalidArgument ;
2138- }
2139-
2140- const func = v8 .Persistent (v8 .Function ).init (self .isolate , js_value .castTo (v8 .Function ));
2141- const scope = self .scope ;
2142- try scope .trackCallback (func );
2143-
2144- return .{
2145- .func = func ,
2146- .scope = scope ,
2147- .id = js_value .castTo (v8 .Object ).getIdentityHash (),
2148- };
2149- }
2150-
2151- if (! js_value .isObject ()) {
2131+ .@"struct" = > {
2132+ return try (self .jsValueToStruct (named_function , T , js_value )) orelse {
21522133 return error .InvalidArgument ;
2134+ };
2135+ },
2136+ .@"union" = > | u | {
2137+ // see probeJsValueToZig for some explanation of what we're
2138+ // trying to do
2139+
2140+ // the first field that we find which the js_value could be
2141+ // coerced to.
2142+ var coerce_index : ? usize = null ;
2143+
2144+ // the first field that we find which the js_Value is
2145+ // compatible with. A compatible field has higher precedence
2146+ // than a coercible, but still isn't a perfect match.
2147+ var compatible_index : ? usize = null ;
2148+
2149+ inline for (u .fields , 0.. ) | field , i | {
2150+ switch (try self .probeJsValueToZig (named_function , field .type , js_value )) {
2151+ .value = > | v | return @unionInit (T , field .name , v ),
2152+ .ok = > {
2153+ // a perfect match like above case, except the probing
2154+ // didn't get the value for us.
2155+ return @unionInit (T , field .name , try self .jsValueToZig (named_function , field .type , js_value ));
2156+ },
2157+ .coerce = > if (coerce_index == null ) {
2158+ coerce_index = i ;
2159+ },
2160+ .compatible = > if (compatible_index == null ) {
2161+ compatible_index = i ;
2162+ },
2163+ .invalid = > {},
2164+ }
21532165 }
21542166
2155- const js_obj = js_value .castTo (v8 .Object );
2156-
2157- if (comptime isJsObject (T )) {
2158- // Caller wants an opaque JsObject. Probably a parameter
2159- // that it needs to pass back into a callback
2160- return E.JsObject {
2161- .js_obj = js_obj ,
2162- .scope = self .scope ,
2163- };
2164- }
2165-
2166- const context = self .context ;
2167- const isolate = self .isolate ;
2168-
2169- var value : T = undefined ;
2170- inline for (s .fields ) | field | {
2171- const name = field .name ;
2172- const key = v8 .String .initUtf8 (isolate , name );
2173- if (js_obj .has (context , key .toValue ())) {
2174- @field (value , name ) = try self .jsValueToZig (named_function , field .type , try js_obj .getValue (context , key ));
2175- } else if (@typeInfo (field .type ) == .optional ) {
2176- @field (value , name ) = null ;
2177- } else {
2178- if (field .defaultValue ()) | dflt | {
2179- @field (value , name ) = dflt ;
2180- } else {
2181- return error .JSWrongObject ;
2182- }
2167+ // We didn't find a perfect match.
2168+ const closest = compatible_index orelse coerce_index orelse return error .InvalidArgument ;
2169+ inline for (u .fields , 0.. ) | field , i | {
2170+ if (i == closest ) {
2171+ return @unionInit (T , field .name , try self .jsValueToZig (named_function , field .type , js_value ));
21832172 }
21842173 }
2185- return value ;
2174+ unreachable ;
21862175 },
21872176 else = > {},
21882177 }
@@ -2237,6 +2226,230 @@ fn Caller(comptime E: type) type {
22372226 return error .InvalidArgument ;
22382227 }
22392228
2229+ // Extracted so that it can be used in both jsValueToZig and in
2230+ // probeJsValueToZig. Avoids having to duplicate this logic when probing.
2231+ fn jsValueToStruct (self : * const Self , comptime named_function : anytype , comptime T : type , js_value : v8.Value ) ! ? T {
2232+ if (@hasDecl (T , "_CALLBACK_ID_KLUDGE" )) {
2233+ if (! js_value .isFunction ()) {
2234+ return error .InvalidArgument ;
2235+ }
2236+
2237+ const func = v8 .Persistent (v8 .Function ).init (self .isolate , js_value .castTo (v8 .Function ));
2238+ const scope = self .scope ;
2239+ try scope .trackCallback (func );
2240+
2241+ return .{
2242+ .func = func ,
2243+ .scope = scope ,
2244+ .id = js_value .castTo (v8 .Object ).getIdentityHash (),
2245+ };
2246+ }
2247+
2248+ if (! js_value .isObject ()) {
2249+ return null ;
2250+ }
2251+
2252+ const js_obj = js_value .castTo (v8 .Object );
2253+
2254+ if (comptime isJsObject (T )) {
2255+ // Caller wants an opaque JsObject. Probably a parameter
2256+ // that it needs to pass back into a callback
2257+ return E.JsObject {
2258+ .js_obj = js_obj ,
2259+ .scope = self .scope ,
2260+ };
2261+ }
2262+
2263+ const context = self .context ;
2264+ const isolate = self .isolate ;
2265+
2266+ var value : T = undefined ;
2267+ inline for (@typeInfo (T ).@"struct" .fields ) | field | {
2268+ const name = field .name ;
2269+ const key = v8 .String .initUtf8 (isolate , name );
2270+ if (js_obj .has (context , key .toValue ())) {
2271+ @field (value , name ) = try self .jsValueToZig (named_function , field .type , try js_obj .getValue (context , key ));
2272+ } else if (@typeInfo (field .type ) == .optional ) {
2273+ @field (value , name ) = null ;
2274+ } else {
2275+ const dflt = field .defaultValue () orelse return null ;
2276+ @field (value , name ) = dflt ;
2277+ }
2278+ }
2279+ return value ;
2280+ }
2281+
2282+ // Probing is part of trying to map a JS value to a Zig union. There's
2283+ // a lot of ambiguity in this process, in part because some JS values
2284+ // can almost always be coerced. For example, anything can be coerced
2285+ // into an integer (it just becomes 0), or a float (becomes NaN) or a
2286+ // string.
2287+ //
2288+ // The way we'll do this is that, if there's a direct match, we'll use it
2289+ // If there's a potential match, we'll keep looking for a direct match
2290+ // and only use the (first) potential match as a fallback.
2291+ //
2292+ // Finally, I considered adding this probing directly into jsValueToZig
2293+ // but I decided doing this separately was better. However, the goal is
2294+ // obviously that probing is consistent with jsValueToZig.
2295+ fn ProbeResult (comptime T : type ) type {
2296+ return union (enum ) {
2297+ // The js_value maps directly to T
2298+ value : T ,
2299+
2300+ // The value is a T. This is almost the same as returning value: T,
2301+ // but the caller still has to get T by calling jsValueToZig.
2302+ // We prefer returning .{.ok => {}}, to avoid reducing duplication
2303+ // with jsValueToZig, but in some cases where probing has a cost
2304+ // AND yields the value anyways, we'll use .{.value = T}.
2305+ ok : void ,
2306+
2307+ // the js_value is compatible with T (i.e. a int -> float),
2308+ compatible : void ,
2309+
2310+ // the js_value can be coerced to T (this is a lower precedence
2311+ // than compatible)
2312+ coerce : void ,
2313+
2314+ // the js_value cannot be turned into T
2315+ invalid : void ,
2316+ };
2317+ }
2318+ fn probeJsValueToZig (self : * const Self , comptime named_function : anytype , comptime T : type , js_value : v8.Value ) ! ProbeResult (T ) {
2319+ switch (@typeInfo (T )) {
2320+ .optional = > | o | {
2321+ if (js_value .isNullOrUndefined ()) {
2322+ return .{ .value = null };
2323+ }
2324+ return self .probeJsValueToZig (named_function , o .child , js_value );
2325+ },
2326+ .float = > {
2327+ if (js_value .isNumber () or js_value .isNumberObject ()) {
2328+ if (js_value .isInt32 () or js_value .isUint32 () or js_value .isBigInt () or js_value .isBigIntObject ()) {
2329+ // int => float is a reasonable match
2330+ return .{ .compatible = {} };
2331+ }
2332+ return .{ .ok = {} };
2333+ }
2334+ // anything can be coerced into a float, it becomes NaN
2335+ return .{ .coerce = {} };
2336+ },
2337+ .int = > {
2338+ if (js_value .isNumber () or js_value .isNumberObject ()) {
2339+ if (js_value .isInt32 () or js_value .isUint32 () or js_value .isBigInt () or js_value .isBigIntObject ()) {
2340+ return .{ .ok = {} };
2341+ }
2342+ // float => int is kind of reasonable, I guess
2343+ return .{ .compatible = {} };
2344+ }
2345+ // anything can be coerced into a int, it becomes 0
2346+ return .{ .coerce = {} };
2347+ },
2348+ .bool = > {
2349+ if (js_value .isBoolean () or js_value .isBooleanObject ()) {
2350+ return .{ .ok = {} };
2351+ }
2352+ // anything can be coerced into a boolean, it will become
2353+ // true or false based on..some complex rules I don't know.
2354+ return .{ .coerce = {} };
2355+ },
2356+ .pointer = > | ptr | switch (ptr .size ) {
2357+ .one = > {
2358+ if (! js_value .isObject ()) {
2359+ return .{ .invalid = {} };
2360+ }
2361+ if (@hasField (TypeLookup , @typeName (ptr .child ))) {
2362+ const js_obj = js_value .castTo (v8 .Object );
2363+ // There's a bit of overhead in doing this, so instead
2364+ // of having a version of typeTaggedAnyOpaque which
2365+ // returns a boolean or an optional, we rely on the
2366+ // main implementation and just handle the error.
2367+ const attempt = E .typeTaggedAnyOpaque (named_function , * Receiver (ptr .child ), js_obj );
2368+ if (attempt ) | value | {
2369+ return .{ .value = value };
2370+ } else | _ | {
2371+ return .{ .invalid = {} };
2372+ }
2373+ }
2374+ // probably an error, but not for us to deal with
2375+ return .{ .invalid = {} };
2376+ },
2377+ .slice = > {
2378+ if (js_value .isTypedArray ()) {
2379+ switch (ptr .child ) {
2380+ u8 = > if (ptr .sentinel () == null ) {
2381+ if (js_value .isUint8Array () or js_value .isUint8ClampedArray ()) {
2382+ return .{ .ok = {} };
2383+ }
2384+ },
2385+ i8 = > if (js_value .isInt8Array ()) {
2386+ return .{ .ok = {} };
2387+ },
2388+ u16 = > if (js_value .isUint16Array ()) {
2389+ return .{ .ok = {} };
2390+ },
2391+ i16 = > if (js_value .isInt16Array ()) {
2392+ return .{ .ok = {} };
2393+ },
2394+ u32 = > if (js_value .isUint32Array ()) {
2395+ return .{ .ok = {} };
2396+ },
2397+ i32 = > if (js_value .isInt32Array ()) {
2398+ return .{ .ok = {} };
2399+ },
2400+ u64 = > if (js_value .isBigUint64Array ()) {
2401+ return .{ .ok = {} };
2402+ },
2403+ i64 = > if (js_value .isBigInt64Array ()) {
2404+ return .{ .ok = {} };
2405+ },
2406+ else = > {},
2407+ }
2408+ return .{ .invalid = {} };
2409+ }
2410+
2411+ if (ptr .child == u8 ) {
2412+ if (js_value .isString ()) {
2413+ return .{ .ok = {} };
2414+ }
2415+ // anything can be coerced into a string
2416+ return .{ .coerce = {} };
2417+ }
2418+
2419+ if (! js_value .isArray ()) {
2420+ return error .InvalidArgument ;
2421+ }
2422+
2423+ // This can get tricky.
2424+ const js_arr = js_value .castTo (v8 .Array );
2425+
2426+ if (js_arr .length () == 0 ) {
2427+ // not so tricky in this case.
2428+ return .{ .value = &.{} };
2429+ }
2430+
2431+ // We settle for just probing the first value. Ok, actually
2432+ // not tricky in this case either.
2433+ const context = self .contxt ;
2434+ const js_obj = js_arr .castTo (v8 .Object );
2435+ return self .probeJsValueToZig (named_function , ptr .child , try js_obj .getAtIndex (context , 0 ));
2436+ },
2437+ else = > {},
2438+ },
2439+ .@"struct" = > {
2440+ // We don't want to duplicate the code for this, so we call
2441+ // the actual coversion function.
2442+ const value = (try self .jsValueToStruct (named_function , T , js_value )) orelse {
2443+ return .{ .invalid = {} };
2444+ };
2445+ return .{ .value = value };
2446+ },
2447+ else = > {},
2448+ }
2449+
2450+ return .{ .invalid = {} };
2451+ }
2452+
22402453 fn zigValueToJs (self : * const Self , value : anytype ) ! v8.Value {
22412454 return self .scope .zigValueToJs (value );
22422455 }
0 commit comments