diff --git a/packages/react-native-codegen/src/generators/components/GenerateComponentHObjCpp.js b/packages/react-native-codegen/src/generators/components/GenerateComponentHObjCpp.js index 878fa6b3ff7..1e920a80ad1 100644 --- a/packages/react-native-codegen/src/generators/components/GenerateComponentHObjCpp.js +++ b/packages/react-native-codegen/src/generators/components/GenerateComponentHObjCpp.js @@ -11,6 +11,7 @@ 'use strict'; import type { + CommandTypeShape, ComponentShape, SchemaType, CommandsFunctionTypeParamAnnotation, @@ -18,12 +19,69 @@ import type { type FilesOutput = Map; +function getOrdinalNumber(num: number): string { + switch (num) { + case 1: + return '1st'; + case 2: + return '2nd'; + case 3: + return '3rd'; + } + + if (num <= 20) { + return `${num}th`; + } + + return 'unknown'; +} + const protocolTemplate = ` @protocol ::_COMPONENT_NAME_::ViewProtocol ::_METHODS_:: @end `.trim(); +const commandHandlerIfCaseConvertArgTemplate = ` +#if RCT_DEBUG + NSObject *arg::_ARG_NUMBER_:: = args[::_ARG_NUMBER_::]; + if (!RCTValidateTypeOfViewCommandArgument(arg::_ARG_NUMBER_::, ::_EXPECTED_KIND_::, @"::_EXPECTED_KIND_STRING_::", @"::_COMPONENT_NAME_::", commandName, @"::_ARG_NUMBER_STR_::")) { + return; + } +#endif + ::_ARG_CONVERSION_:: +`.trim(); + +const commandHandlerIfCaseTemplate = ` +if ([commandName isEqualToString:@"::_COMMAND_NAME_::"]) { +#if RCT_DEBUG + if ([args count] != ::_NUM_ARGS_::) { + RCTLogError(@"%@ command %@ received %d arguments, expected %d.", @"::_COMPONENT_NAME_::", commandName, (int)[args count], ::_NUM_ARGS_::); + return; + } +#endif + +::_CONVERT_ARGS_:: + + ::_COMMAND_CALL_:: + return; +} +`.trim(); + +const commandHandlerTemplate = ` +RCT_EXTERN inline void ::_COMPONENT_NAME_::HandleCommand( + id<::_COMPONENT_NAME_::ViewProtocol> componentView, + NSString const *commandName, + NSArray const *args) +{ + ::_IF_CASES_:: + +#if RCT_DEBUG + RCTLogError(@"%@ received command %@, which is not a supported command.", @"::_COMPONENT_NAME_::", commandName); +#endif +} +`.trim(); + const template = ` /** * Copyright (c) Facebook, Inc. and its affiliates. @@ -55,6 +113,49 @@ function getObjCParamType(param: CommandsFunctionTypeParamAnnotation): string { } } +function getObjCExpectedKindParamType( + param: CommandsFunctionTypeParamAnnotation, +): string { + switch (param.typeAnnotation.type) { + case 'BooleanTypeAnnotation': + return '[NSNumber class]'; + case 'Int32TypeAnnotation': + return '[NSNumber class]'; + default: + (param.typeAnnotation.type: empty); + throw new Error('Received invalid param type annotation'); + } +} + +function getReadableExpectedKindParamType( + param: CommandsFunctionTypeParamAnnotation, +): string { + switch (param.typeAnnotation.type) { + case 'BooleanTypeAnnotation': + return 'boolean'; + case 'Int32TypeAnnotation': + return 'number'; + default: + (param.typeAnnotation.type: empty); + throw new Error('Received invalid param type annotation'); + } +} + +function getObjCRightHandAssignmentParamType( + param: CommandsFunctionTypeParamAnnotation, + index: number, +): string { + switch (param.typeAnnotation.type) { + case 'BooleanTypeAnnotation': + return `[(NSNumber *)arg${index} boolValue]`; + case 'Int32TypeAnnotation': + return `[(NSNumber *)arg${index} intValue]`; + default: + (param.typeAnnotation.type: empty); + throw new Error('Received invalid param type annotation'); + } +} + function generateProtocol( component: ComponentShape, componentName: string, @@ -84,6 +185,75 @@ function generateProtocol( .replace('::_METHODS_::', commands); } +function generateConvertAndValidateParam( + param: CommandsFunctionTypeParamAnnotation, + index: number, + componentName: string, +): string { + const leftSideType = getObjCParamType(param); + const expectedKind = getObjCExpectedKindParamType(param); + const expectedKindString = getReadableExpectedKindParamType(param); + const argConversion = `${leftSideType} ${ + param.name + } = ${getObjCRightHandAssignmentParamType(param, index)};`; + + return commandHandlerIfCaseConvertArgTemplate + .replace(/::_COMPONENT_NAME_::/g, componentName) + .replace('::_ARG_CONVERSION_::', argConversion) + .replace(/::_ARG_NUMBER_::/g, '' + index) + .replace('::_ARG_NUMBER_STR_::', getOrdinalNumber(index + 1)) + .replace('::_EXPECTED_KIND_::', expectedKind) + .replace('::_EXPECTED_KIND_STRING_::', expectedKindString); +} + +function generateCommandIfCase( + command: CommandTypeShape, + componentName: string, +) { + const params = command.typeAnnotation.params; + + const convertArgs = params + .map((param, index) => + generateConvertAndValidateParam(param, index, componentName), + ) + .join('\n\n') + .trim(); + + const commandCallArgs = + params.length === 0 + ? '' + : params + .map((param, index) => { + return `${index === 0 ? '' : param.name}:${param.name}`; + }) + .join(' '); + const commandCall = `[componentView ${command.name}${commandCallArgs}]`; + + return commandHandlerIfCaseTemplate + .replace(/::_COMPONENT_NAME_::/g, componentName) + .replace(/::_COMMAND_NAME_::/g, command.name) + .replace(/::_NUM_ARGS_::/g, '' + params.length) + .replace('::_CONVERT_ARGS_::', convertArgs) + .replace('::_COMMAND_CALL_::', commandCall); +} + +function generateCommandHandler( + component: ComponentShape, + componentName: string, +): ?string { + if (component.commands.length === 0) { + return null; + } + + const ifCases = component.commands + .map(command => generateCommandIfCase(command, componentName)) + .join('\n\n'); + + return commandHandlerTemplate + .replace(/::_COMPONENT_NAME_::/g, componentName) + .replace('::_IF_CASES_::', ifCases); +} + module.exports = { generate(libraryName: string, schema: SchemaType): FilesOutput { const fileName = 'ComponentViewHelpers.h'; @@ -98,7 +268,12 @@ module.exports = { return Object.keys(components) .map(componentName => { - return generateProtocol(components[componentName], componentName); + return [ + generateProtocol(components[componentName], componentName), + generateCommandHandler(components[componentName], componentName), + ] + .join('\n\n') + .trim(); }) .join('\n\n'); }) diff --git a/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateComponentHObjCpp-test.js.snap b/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateComponentHObjCpp-test.js.snap index bbebf6857e8..7732177b6aa 100644 --- a/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateComponentHObjCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateComponentHObjCpp-test.js.snap @@ -90,6 +90,86 @@ NS_ASSUME_NONNULL_BEGIN - (void)scrollTo:(NSInteger)y animated:(BOOL)animated; @end +RCT_EXTERN inline void CommandNativeComponentHandleCommand( + id componentView, + NSString const *commandName, + NSArray const *args) +{ + if ([commandName isEqualToString:@\\"flashScrollIndicators\\"]) { +#if RCT_DEBUG + if ([args count] != 0) { + RCTLogError(@\\"%@ command %@ received %d arguments, expected %d.\\", @\\"CommandNativeComponent\\", commandName, (int)[args count], 0); + return; + } +#endif + + + + [componentView flashScrollIndicators] + return; +} + +if ([commandName isEqualToString:@\\"hotspotUpdate\\"]) { +#if RCT_DEBUG + if ([args count] != 2) { + RCTLogError(@\\"%@ command %@ received %d arguments, expected %d.\\", @\\"CommandNativeComponent\\", commandName, (int)[args count], 2); + return; + } +#endif + +#if RCT_DEBUG + NSObject *arg0 = args[0]; + if (!RCTValidateTypeOfViewCommandArgument(arg0, [NSNumber class], @\\"number\\", @\\"CommandNativeComponent\\", commandName, @\\"1st\\")) { + return; + } +#endif + NSInteger x = [(NSNumber *)arg0 intValue]; + +#if RCT_DEBUG + NSObject *arg1 = args[1]; + if (!RCTValidateTypeOfViewCommandArgument(arg1, [NSNumber class], @\\"number\\", @\\"CommandNativeComponent\\", commandName, @\\"2nd\\")) { + return; + } +#endif + NSInteger y = [(NSNumber *)arg1 intValue]; + + [componentView hotspotUpdate:x y:y] + return; +} + +if ([commandName isEqualToString:@\\"scrollTo\\"]) { +#if RCT_DEBUG + if ([args count] != 2) { + RCTLogError(@\\"%@ command %@ received %d arguments, expected %d.\\", @\\"CommandNativeComponent\\", commandName, (int)[args count], 2); + return; + } +#endif + +#if RCT_DEBUG + NSObject *arg0 = args[0]; + if (!RCTValidateTypeOfViewCommandArgument(arg0, [NSNumber class], @\\"number\\", @\\"CommandNativeComponent\\", commandName, @\\"1st\\")) { + return; + } +#endif + NSInteger y = [(NSNumber *)arg0 intValue]; + +#if RCT_DEBUG + NSObject *arg1 = args[1]; + if (!RCTValidateTypeOfViewCommandArgument(arg1, [NSNumber class], @\\"boolean\\", @\\"CommandNativeComponent\\", commandName, @\\"2nd\\")) { + return; + } +#endif + BOOL animated = [(NSNumber *)arg1 boolValue]; + + [componentView scrollTo:y animated:animated] + return; +} + +#if RCT_DEBUG + RCTLogError(@\\"%@ received command %@, which is not a supported command.\\", @\\"CommandNativeComponent\\", commandName); +#endif +} + NS_ASSUME_NONNULL_END", } `; @@ -113,6 +193,44 @@ NS_ASSUME_NONNULL_BEGIN - (void)hotspotUpdate:(NSInteger)x y:(NSInteger)y; @end +RCT_EXTERN inline void CommandNativeComponentHandleCommand( + id componentView, + NSString const *commandName, + NSArray const *args) +{ + if ([commandName isEqualToString:@\\"hotspotUpdate\\"]) { +#if RCT_DEBUG + if ([args count] != 2) { + RCTLogError(@\\"%@ command %@ received %d arguments, expected %d.\\", @\\"CommandNativeComponent\\", commandName, (int)[args count], 2); + return; + } +#endif + +#if RCT_DEBUG + NSObject *arg0 = args[0]; + if (!RCTValidateTypeOfViewCommandArgument(arg0, [NSNumber class], @\\"number\\", @\\"CommandNativeComponent\\", commandName, @\\"1st\\")) { + return; + } +#endif + NSInteger x = [(NSNumber *)arg0 intValue]; + +#if RCT_DEBUG + NSObject *arg1 = args[1]; + if (!RCTValidateTypeOfViewCommandArgument(arg1, [NSNumber class], @\\"number\\", @\\"CommandNativeComponent\\", commandName, @\\"2nd\\")) { + return; + } +#endif + NSInteger y = [(NSNumber *)arg1 intValue]; + + [componentView hotspotUpdate:x y:y] + return; +} + +#if RCT_DEBUG + RCTLogError(@\\"%@ received command %@, which is not a supported command.\\", @\\"CommandNativeComponent\\", commandName); +#endif +} + NS_ASSUME_NONNULL_END", } `;