Skip to content

Commit

Permalink
react-native-code-gen Add Union Type support for C++ TurboModules
Browse files Browse the repository at this point in the history
Summary:
There are cases where we want to pass a union type into a TurboModule method which is handy to restrict certain arguments to a restricted set of values, e.g. restricting quality to ```'low'```, ```high``` or resolution to ```720```, ```1080```.

- We are not generating an ```union``` type in C++ but rather just cast type safe to the corresponding member type.

- We don't support mixed primitive union types at this time, e.g. ```export type ChooseSomething = 'One' | 'Two' | 3 | false;``` does not work.

- We can support mixed object union types such as ... ```export type ChooseObject = {} | {low: string};``` - which need special logic in the C++ TM to correctly parse the resulting jsi::Object

This is for C++ only, Java and ObjC are not supported atm.

Changelog:
[General][Added] - react-native-code-gen Add Union Type support for C++ TurboModules

Reviewed By: javache

Differential Revision: D38919688

fbshipit-source-id: 0fd37545b32b4f2059a8babda62dab4a85de37a9
  • Loading branch information
christophpurrer authored and facebook-github-bot committed Aug 24, 2022
1 parent f3def13 commit 355feaf
Show file tree
Hide file tree
Showing 18 changed files with 323 additions and 20 deletions.
11 changes: 11 additions & 0 deletions packages/react-native-codegen/src/CodegenSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,16 @@ export type NativeModulePromiseTypeAnnotation = $ReadOnly<{
type: 'PromiseTypeAnnotation',
}>;

export type UnionTypeAnnotationMemberType =
| 'NumberTypeAnnotation'
| 'ObjectTypeAnnotation'
| 'StringTypeAnnotation';

export type NativeModuleUnionTypeAnnotation = $ReadOnly<{
type: 'UnionTypeAnnotation',
memberType: UnionTypeAnnotationMemberType,
}>;

export type NativeModuleMixedTypeAnnotation = $ReadOnly<{
type: 'MixedTypeAnnotation',
}>;
Expand All @@ -311,6 +321,7 @@ export type NativeModuleBaseTypeAnnotation =
| NativeModuleTypeAliasTypeAnnotation
| NativeModuleArrayTypeAnnotation<Nullable<NativeModuleBaseTypeAnnotation>>
| NativeModuleObjectTypeAnnotation
| NativeModuleUnionTypeAnnotation
| NativeModuleMixedTypeAnnotation;

export type NativeModuleParamTypeAnnotation =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ function serializeArg(
return wrap(val => `${val}.asObject(rt).asFunction(rt)`);
case 'GenericObjectTypeAnnotation':
return wrap(val => `${val}.asObject(rt)`);
case 'UnionTypeAnnotation':
switch (typeAnnotation.memberType) {
case 'NumberTypeAnnotation':
return wrap(val => `${val}.asNumber()`);
case 'ObjectTypeAnnotation':
return wrap(val => `${val}.asObject(rt)`);
case 'StringTypeAnnotation':
return wrap(val => `${val}.asString(rt)`);
default:
throw new Error(
`Unsupported union member type for param "${arg.name}, found: ${realTypeAnnotation.memberType}"`,
);
}
case 'ObjectTypeAnnotation':
return wrap(val => `${val}.asObject(rt)`);
case 'MixedTypeAnnotation':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ function translatePrimitiveJSTypeToCpp(
return wrap('bool');
case 'GenericObjectTypeAnnotation':
return wrap('jsi::Object');
case 'UnionTypeAnnotation':
switch (typeAnnotation.memberType) {
case 'NumberTypeAnnotation':
return wrap('double');
case 'ObjectTypeAnnotation':
return wrap('jsi::Object');
case 'StringTypeAnnotation':
return wrap('jsi::String');
default:
throw new Error(createErrorMessage(realTypeAnnotation.type));
}
case 'ObjectTypeAnnotation':
return wrap('jsi::Object');
case 'ArrayTypeAnnotation':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function translateFunctionParamToJavaType(
imports.add('com.facebook.react.bridge.Callback');
return 'Callback';
default:
(realTypeAnnotation.type: 'MixedTypeAnnotation');
(realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation');
throw new Error(createErrorMessage(realTypeAnnotation.type));
}
}
Expand Down Expand Up @@ -220,7 +220,7 @@ function translateFunctionReturnTypeToJavaType(
imports.add('com.facebook.react.bridge.WritableArray');
return wrapIntoNullableIfNeeded('WritableArray');
default:
(realTypeAnnotation.type: 'MixedTypeAnnotation');
(realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation');
throw new Error(createErrorMessage(realTypeAnnotation.type));
}
}
Expand Down Expand Up @@ -272,7 +272,7 @@ function getFalsyReturnStatementFromReturnType(
case 'ArrayTypeAnnotation':
return 'return null;';
default:
(realTypeAnnotation.type: 'MixedTypeAnnotation');
(realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation');
throw new Error(createErrorMessage(realTypeAnnotation.type));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ function translateReturnTypeToKind(
case 'ArrayTypeAnnotation':
return 'ArrayKind';
default:
(realTypeAnnotation.type: 'MixedTypeAnnotation');
(realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation');
throw new Error(
`Unknown prop type for returning value, found: ${realTypeAnnotation.type}"`,
);
Expand Down Expand Up @@ -226,7 +226,7 @@ function translateParamTypeToJniType(
case 'FunctionTypeAnnotation':
return 'Lcom/facebook/react/bridge/Callback;';
default:
(realTypeAnnotation.type: 'MixedTypeAnnotation');
(realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation');
throw new Error(
`Unknown prop type for method arg, found: ${realTypeAnnotation.type}"`,
);
Expand Down Expand Up @@ -278,7 +278,7 @@ function translateReturnTypeToJniType(
case 'ArrayTypeAnnotation':
return 'Lcom/facebook/react/bridge/WritableArray;';
default:
(realTypeAnnotation.type: 'MixedTypeAnnotation');
(realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation');
throw new Error(
`Unknown prop type for method return type, found: ${realTypeAnnotation.type}"`,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ class StructCollector {
}
case 'MixedTypeAnnotation':
throw new Error('Mixed types are unsupported in structs');
case 'UnionTypeAnnotation':
throw new Error('Union types are unsupported in structs');
default: {
return wrapNullable(nullable, typeAnnotation);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ function getReturnObjCType(
case 'GenericObjectTypeAnnotation':
return wrapIntoNullableIfNeeded('NSDictionary *');
default:
(typeAnnotation.type: 'MixedTypeAnnotation');
(typeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation');
throw new Error(
`Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`,
);
Expand Down Expand Up @@ -378,7 +378,7 @@ function getReturnJSType(
case 'GenericObjectTypeAnnotation':
return 'ObjectKind';
default:
(typeAnnotation.type: 'MixedTypeAnnotation');
(typeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation');
throw new Error(
`Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,51 @@ const CXX_ONLY_NATIVE_MODULES: SchemaType = {
],
},
},
{
name: 'getUnion',
optional: false,
typeAnnotation: {
type: 'FunctionTypeAnnotation',
returnTypeAnnotation: {
type: 'UnionTypeAnnotation',
memberType: 'ObjectTypeAnnotation',
},
params: [
{
name: 'chooseInt',
optional: false,
typeAnnotation: {
type: 'UnionTypeAnnotation',
memberType: 'NumberTypeAnnotation',
},
},
{
name: 'chooseFloat',
optional: false,
typeAnnotation: {
type: 'UnionTypeAnnotation',
memberType: 'NumberTypeAnnotation',
},
},
{
name: 'chooseObject',
optional: false,
typeAnnotation: {
type: 'UnionTypeAnnotation',
memberType: 'ObjectTypeAnnotation',
},
},
{
name: 'chooseString',
optional: false,
typeAnnotation: {
type: 'UnionTypeAnnotation',
memberType: 'StringTypeAnnotation',
},
},
],
},
},
],
},
moduleNames: ['SampleTurboModuleCxx'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,15 @@ static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getNullableNu
auto result = static_cast<NativeSampleTurboModuleCxxSpecJSI *>(&turboModule)->getNullableNumberFromNullableAlias(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt)));
return result ? jsi::Value(std::move(*result)) : jsi::Value::null();
}
static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getUnion(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
return static_cast<NativeSampleTurboModuleCxxSpecJSI *>(&turboModule)->getUnion(rt, args[0].asNumber(), args[1].asNumber(), args[2].asObject(rt), args[3].asString(rt));
}
NativeSampleTurboModuleCxxSpecJSI::NativeSampleTurboModuleCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
: TurboModule(\\"SampleTurboModuleCxx\\", jsInvoker) {
methodMap_[\\"getMixed\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getMixed};
methodMap_[\\"getNullableNumberFromNullableAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getNullableNumberFromNullableAlias};
methodMap_[\\"getUnion\\"] = MethodMetadata {4, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getUnion};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ protected:
public:
virtual jsi::Value getMixed(jsi::Runtime &rt, jsi::Value arg) = 0;
virtual std::optional<double> getNullableNumberFromNullableAlias(jsi::Runtime &rt, std::optional<jsi::Object> a) = 0;
virtual jsi::Object getUnion(jsi::Runtime &rt, double chooseInt, double chooseFloat, jsi::Object chooseObject, jsi::String chooseString) = 0;
};
Expand Down Expand Up @@ -246,6 +247,14 @@ private:
return bridging::callFromJs<std::optional<double>>(
rt, &T::getNullableNumberFromNullableAlias, jsInvoker_, instance_, std::move(a));
}
jsi::Object getUnion(jsi::Runtime &rt, double chooseInt, double chooseFloat, jsi::Object chooseObject, jsi::String chooseString) override {
static_assert(
bridging::getParameterCount(&T::getUnion) == 5,
\\"Expected getUnion(...) to have 5 parameters\\");
return bridging::callFromJs<jsi::Object>(
rt, &T::getUnion, jsInvoker_, instance_, std::move(chooseInt), std::move(chooseFloat), std::move(chooseObject), std::move(chooseString));
}
private:
T *instance_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -593,9 +593,15 @@ const CXX_ONLY_NATIVE_MODULE = `
import type {TurboModule} from '../RCTExport';
import * as TurboModuleRegistry from '../TurboModuleRegistry';
export type ChooseInt = 1 | 2 | 3;
export type ChooseFloat = 1.44 | 2.88 | 5.76;
export type ChooseObject = {} | {low: string};
export type ChooseString = 'One' | 'Two' | 'Three';
export interface Spec extends TurboModule {
+getCallback: () => () => void;
+getMixed: (arg: mixed) => mixed;
+getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject;
}
export default TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModuleCxx');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,51 @@ exports[`RN Codegen Flow Parser can generate fixture CXX_ONLY_NATIVE_MODULE 1`]
}
]
}
},
{
'name': 'getUnion',
'optional': false,
'typeAnnotation': {
'type': 'FunctionTypeAnnotation',
'returnTypeAnnotation': {
'type': 'UnionTypeAnnotation',
'memberType': 'ObjectTypeAnnotation'
},
'params': [
{
'name': 'chooseInt',
'optional': false,
'typeAnnotation': {
'type': 'UnionTypeAnnotation',
'memberType': 'NumberTypeAnnotation'
}
},
{
'name': 'chooseFloat',
'optional': false,
'typeAnnotation': {
'type': 'UnionTypeAnnotation',
'memberType': 'NumberTypeAnnotation'
}
},
{
'name': 'chooseObject',
'optional': false,
'typeAnnotation': {
'type': 'UnionTypeAnnotation',
'memberType': 'ObjectTypeAnnotation'
}
},
{
'name': 'chooseString',
'optional': false,
'typeAnnotation': {
'type': 'UnionTypeAnnotation',
'memberType': 'StringTypeAnnotation'
}
}
]
}
}
]
},
Expand Down
25 changes: 25 additions & 0 deletions packages/react-native-codegen/src/parsers/flow/modules/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,30 @@ class UnsupportedFunctionReturnTypeAnnotationParserError extends ParserError {
}
}

/**
* Union parsing errors
*/

class UnsupportedUnionTypeAnnotationParserError extends ParserError {
constructor(
hasteModuleName: string,
arrayElementTypeAST: $FlowFixMe,
types: string[],
) {
super(
hasteModuleName,
arrayElementTypeAST,
`Union members must be of the same type, but multiple types were found ${types.join(
', ',
)}'.`,
);
}
}

/**
* Module parsing errors
*/

class UnusedModuleFlowInterfaceParserError extends ParserError {
constructor(hasteModuleName: string, flowInterface: $FlowFixMe) {
super(
Expand Down Expand Up @@ -323,6 +347,7 @@ module.exports = {
UnsupportedFlowTypeAnnotationParserError,
UnsupportedFunctionParamTypeAnnotationParserError,
UnsupportedFunctionReturnTypeAnnotationParserError,
UnsupportedUnionTypeAnnotationParserError,
UnsupportedModulePropertyParserError,
UnsupportedObjectPropertyTypeAnnotationParserError,
UnsupportedObjectPropertyValueTypeAnnotationParserError,
Expand Down
28 changes: 28 additions & 0 deletions packages/react-native-codegen/src/parsers/flow/modules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const {
UnsupportedFlowTypeAnnotationParserError,
UnsupportedFunctionParamTypeAnnotationParserError,
UnsupportedFunctionReturnTypeAnnotationParserError,
UnsupportedUnionTypeAnnotationParserError,
UnsupportedModulePropertyParserError,
UnsupportedObjectPropertyTypeAnnotationParserError,
UnsupportedObjectPropertyValueTypeAnnotationParserError,
Expand Down Expand Up @@ -367,6 +368,33 @@ function translateTypeAnnotation(
),
);
}
case 'UnionTypeAnnotation': {
if (cxxOnly) {
// Remap literal names
const unionTypes = typeAnnotation.types
.map(
item =>
item.type
.replace('NumberLiteralTypeAnnotation', 'NumberTypeAnnotation')
.replace('StringLiteralTypeAnnotation', 'StringTypeAnnotation'),
// ObjectAnnotation is already 'correct'
)
.filter((value, index, self) => self.indexOf(value) === index);
// Only support unionTypes of the same kind
if (unionTypes.length > 1) {
throw new UnsupportedUnionTypeAnnotationParserError(
hasteModuleName,
typeAnnotation,
unionTypes,
);
}
return wrapNullable(nullable, {
type: 'UnionTypeAnnotation',
memberType: unionTypes[0],
});
}
// Fallthrough
}
case 'MixedTypeAnnotation': {
if (cxxOnly) {
return wrapNullable(nullable, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,9 +608,15 @@ const CXX_ONLY_NATIVE_MODULE = `
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
export type ChooseInt = 1 | 2 | 3;
export type ChooseFloat = 1.44 | 2.88 | 5.76;
export type ChooseObject = {} | {low: string};
export type ChooseString = 'One' | 'Two' | 'Three';
export interface Spec extends TurboModule {
readonly getCallback: () => () => void;
readonly getMixed: (arg: unknown) => unknown;
readonly getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject;
}
export default TurboModuleRegistry.getEnforcing<Spec>(
Expand Down
Loading

0 comments on commit 355feaf

Please sign in to comment.