From a7d11c8e79464bfa1bdc8b11c9cf358a09ade3f0 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Wed, 21 Jul 2021 12:07:28 -0700 Subject: [PATCH] Bring back all the pluralization (#1879) * Revert "Remove inflection option, pluralizer and dependency" * Move from InflectorKit fork to origin with 1.0.0 minimum * Update to comply with InflectorKit 1.0.0 deprecations * Enable code generation options to accept additional inflection rules * Update PluralizerTest function names to match #1849 * Shuffle parameter documentation order to match parameter input order --- Apollo.xcodeproj/project.pbxproj | 43 ++++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++ Package.resolved | 9 +++ Package.swift | 4 + Package@swift-5.2.swift | 4 + .../ApolloCodegenOptions.swift | 6 +- Sources/ApolloCodegenLib/Pluralizer.swift | 56 +++++++++++++ SwiftScripts/Package.resolved | 9 +++ .../ApolloCodegenTests/PluralizerTests.swift | 78 +++++++++++++++++++ 9 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 Sources/ApolloCodegenLib/Pluralizer.swift create mode 100644 Tests/ApolloCodegenTests/PluralizerTests.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 42b3dc69ae..c3dab60fb9 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ 9B455CE52492D0A3002255A9 /* ApolloExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE22492D0A3002255A9 /* ApolloExtension.swift */; }; 9B455CE72492D0A3002255A9 /* Collection+Apollo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */; }; 9B455CEB2492FB03002255A9 /* String+SHA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CEA2492FB03002255A9 /* String+SHA.swift */; }; + 9B47518D2575AA850001FB87 /* Pluralizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B47516D2575AA690001FB87 /* Pluralizer.swift */; }; + 9B4751AD2575B5070001FB87 /* PluralizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4751AC2575B5070001FB87 /* PluralizerTests.swift */; }; 9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */; }; 9B518C87235F819E004C426D /* CLIDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C85235F8125004C426D /* CLIDownloader.swift */; }; 9B518C8C235F8B5F004C426D /* ApolloFilePathHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C8A235F8B05004C426D /* ApolloFilePathHelper.swift */; }; @@ -240,6 +242,7 @@ DED46035261CEA660086EF63 /* ApolloTestSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; }; DED46042261CEA8A0086EF63 /* TestServerURLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED45C172615308E0086EF63 /* TestServerURLs.swift */; }; DED46051261CEAD20086EF63 /* StarWarsAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FCE2CFA1E6C213D00E34457 /* StarWarsAPI.framework */; }; + E6E4209226A7DF4200B82624 /* InflectorKit in Frameworks */ = {isa = PBXBuildFile; productRef = E6E4209126A7DF4200B82624 /* InflectorKit */; }; E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86D8E03214B32DA0028EFE1 /* JSONTests.swift */; }; F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */; }; F82E62E122BCD223000C311B /* AutomaticPersistedQueriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82E62E022BCD223000C311B /* AutomaticPersistedQueriesTests.swift */; }; @@ -548,6 +551,8 @@ 9B455CE22492D0A3002255A9 /* ApolloExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloExtension.swift; sourceTree = ""; }; 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+Apollo.swift"; sourceTree = ""; }; 9B455CEA2492FB03002255A9 /* String+SHA.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SHA.swift"; sourceTree = ""; }; + 9B47516D2575AA690001FB87 /* Pluralizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pluralizer.swift; sourceTree = ""; }; + 9B4751AC2575B5070001FB87 /* PluralizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluralizerTests.swift; sourceTree = ""; }; 9B4AA8AD239EFDC9003E1300 /* Apollo-Target-CodegenTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-CodegenTests.xcconfig"; sourceTree = ""; }; 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionClient.swift; sourceTree = ""; }; 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPBinAPI.swift; sourceTree = ""; }; @@ -820,6 +825,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E6E4209226A7DF4200B82624 /* InflectorKit in Frameworks */, DECD47C3262F779800924527 /* ApolloUtils.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1135,6 +1141,14 @@ name = Extensions; sourceTree = ""; }; + 9B4751BD2575BAFB0001FB87 /* Naming */ = { + isa = PBXGroup; + children = ( + 9B47516D2575AA690001FB87 /* Pluralizer.swift */, + ); + name = Naming; + sourceTree = ""; + }; 9B6835472463486200337AE6 /* ApolloUtils */ = { isa = PBXGroup; children = ( @@ -1149,6 +1163,7 @@ isa = PBXGroup; children = ( 9FE1E54C2588C5E000AA967E /* Frontend */, + 9B4751BD2575BAFB0001FB87 /* Naming */, 9BCB585D240758B2002F766E /* Extensions */, 9BD681332405F6BB000874CB /* Codegen */, 9BD681322405F69C000874CB /* CLI */, @@ -1217,6 +1232,7 @@ 9BAEEC14234C132600808306 /* CLIExtractorTests.swift */, 9BAEEC0D234BB95B00808306 /* FileManagerExtensionsTests.swift */, 9B68F0542416B33300E97318 /* LineByLineComparison.swift */, + 9B4751AC2575B5070001FB87 /* PluralizerTests.swift */, 9B8C3FB4248DA3E000707B13 /* URLExtensionsTests.swift */, 9BAEEC0C234BB95B00808306 /* Info.plist */, ); @@ -1859,10 +1875,12 @@ buildRules = ( ); dependencies = ( + E6E4209426A7DF5800B82624 /* PBXTargetDependency */, 9B683549246348CB00337AE6 /* PBXTargetDependency */, ); name = ApolloCodegenLib; packageProductDependencies = ( + E6E4209126A7DF4200B82624 /* InflectorKit */, ); productName = ApolloCodegenLib; productReference = 9B7B6F47233C26D100F32205 /* ApolloCodegenLib.framework */; @@ -2204,6 +2222,7 @@ packageReferences = ( 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */, DE8C84F2268BBF8000C54D02 /* XCRemoteSwiftPackageReference "Starscream" */, + E6E4209026A7DF4200B82624 /* XCRemoteSwiftPackageReference "InflectorKit" */, ); productRefGroup = 9FC750451D2A532C00458D91 /* Products */; projectDirPath = ""; @@ -2419,6 +2438,7 @@ 9F62DFD02590710E00E6E808 /* GraphQLSource.swift in Sources */, 9BAEEBF32346DDAD00808306 /* CodegenLogger.swift in Sources */, 9F628EB52593651B00F94F9D /* GraphQLValue.swift in Sources */, + 9B47518D2575AA850001FB87 /* Pluralizer.swift in Sources */, 9B518C8C235F8B5F004C426D /* ApolloFilePathHelper.swift in Sources */, 9F628E9525935BE600F94F9D /* GraphQLType.swift in Sources */, 9B518C87235F819E004C426D /* CLIDownloader.swift in Sources */, @@ -2475,6 +2495,7 @@ 9F62DF8E2590539A00E6E808 /* SchemaIntrospectionTests.swift in Sources */, 9B68F0552416B33300E97318 /* LineByLineComparison.swift in Sources */, 9BAEEC15234C132600808306 /* CLIExtractorTests.swift in Sources */, + 9B4751AD2575B5070001FB87 /* PluralizerTests.swift in Sources */, 9BAEEC19234C297800808306 /* ApolloCodegenTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2853,6 +2874,10 @@ target = 9FCE2CF91E6C213D00E34457 /* StarWarsAPI */; targetProxy = DED4606A261CEDD10086EF63 /* PBXContainerItemProxy */; }; + E6E4209426A7DF5800B82624 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = E6E4209326A7DF5800B82624 /* InflectorKit */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -3382,6 +3407,14 @@ minimumVersion = 3.1.2; }; }; + E6E4209026A7DF4200B82624 /* XCRemoteSwiftPackageReference "InflectorKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mattt/InflectorKit.git"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 1.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3415,6 +3448,16 @@ package = 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */; productName = SQLite; }; + E6E4209126A7DF4200B82624 /* InflectorKit */ = { + isa = XCSwiftPackageProductDependency; + package = E6E4209026A7DF4200B82624 /* XCRemoteSwiftPackageReference "InflectorKit" */; + productName = InflectorKit; + }; + E6E4209326A7DF5800B82624 /* InflectorKit */ = { + isa = XCSwiftPackageProductDependency; + package = E6E4209026A7DF4200B82624 /* XCRemoteSwiftPackageReference "InflectorKit" */; + productName = InflectorKit; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 9FC7503B1D2A532C00458D91 /* Project object */; diff --git a/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 72f493cc0c..5ce0c43a29 100644 --- a/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "InflectorKit", + "repositoryURL": "https://github.com/mattt/InflectorKit.git", + "state": { + "branch": null, + "revision": "d8cbcc04972690aaa5fc760a2b9ddb3e9f0decd7", + "version": "1.0.0" + } + }, { "package": "SQLite.swift", "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", diff --git a/Package.resolved b/Package.resolved index 3f3dd5a02c..316c975370 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "InflectorKit", + "repositoryURL": "https://github.com/mattt/InflectorKit", + "state": { + "branch": null, + "revision": "d8cbcc04972690aaa5fc760a2b9ddb3e9f0decd7", + "version": "1.0.0" + } + }, { "package": "SQLite.swift", "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", diff --git a/Package.swift b/Package.swift index 6862f29fd4..76921dbf7c 100644 --- a/Package.swift +++ b/Package.swift @@ -42,6 +42,9 @@ let package = Package( .package( url: "https://github.com/apollographql/Starscream", .upToNextMinor(from: "3.1.2")), + .package( + url: "https://github.com/mattt/InflectorKit", + .upToNextMinor(from: "1.0.0")), ], targets: [ .target( @@ -70,6 +73,7 @@ let package = Package( name: "ApolloCodegenLib", dependencies: [ "ApolloUtils", + .product(name: "InflectorKit", package: "InflectorKit"), ], exclude: [ "Info.plist", diff --git a/Package@swift-5.2.swift b/Package@swift-5.2.swift index 4f3e580b67..545addd952 100644 --- a/Package@swift-5.2.swift +++ b/Package@swift-5.2.swift @@ -33,6 +33,9 @@ let package = Package( .package( url: "https://github.com/daltoniam/Starscream", .upToNextMinor(from: "3.1.1")), + .package( + url: "https://github.com/mattt/InflectorKit", + .upToNextMinor(from: "1.0.0")), ], targets: [ .target( @@ -47,6 +50,7 @@ let package = Package( name: "ApolloCodegenLib", dependencies: [ "ApolloCore", + "InflectorKit", ]), .target( name: "ApolloSQLite", diff --git a/Sources/ApolloCodegenLib/ApolloCodegenOptions.swift b/Sources/ApolloCodegenLib/ApolloCodegenOptions.swift index 520892d5f4..29a2d3683e 100644 --- a/Sources/ApolloCodegenLib/ApolloCodegenOptions.swift +++ b/Sources/ApolloCodegenLib/ApolloCodegenOptions.swift @@ -63,6 +63,7 @@ public struct ApolloCodegenOptions { } let codegenEngine: CodeGenerationEngine + let additionalInflectionRules: [InflectionRule] let includes: String let mergeInFieldsFromFragmentSpreads: Bool let namespace: String? @@ -81,9 +82,10 @@ public struct ApolloCodegenOptions { /// /// - Parameters: /// - codegenEngine: The code generation engine to use. Defaults to `CodeGenerationEngine.default` + /// - additionalInflectionRules: Any non-default rules for pluralization or singularization you wish to include. Defaults to an empty array. Only used by the Swift code generation engine. /// - includes: Glob of files to search for GraphQL operations. This should be used to find queries *and* any client schema extensions. Defaults to `./**/*.graphql`, which will search for `.graphql` files throughout all subfolders of the folder where the script is run. /// - mergeInFieldsFromFragmentSpreads: Set true to merge fragment fields onto its enclosing type. Defaults to true. - /// - modifier: [EXPERIMENTAL SWIFT CODEGEN ONLY] - The access modifier to use on everything created by this tool. Defaults to `.public`. + /// - modifier: The access modifier to use on everything created by this tool. Defaults to `.public`. Only used by the Swift code generation engine. /// - namespace: [optional] The namespace to emit generated code into. Defaults to nil. /// - omitDeprecatedEnumCases: Whether deprecated enum cases should be omitted from generated code. Defaults to false. /// - only: [optional] Parse all input files, but only output generated code for the file at this URL if non-nil. Defaults to nil. @@ -94,6 +96,7 @@ public struct ApolloCodegenOptions { /// - urlToSchemaFile: The URL to your schema file. /// - downloadTimeout: The maximum time to wait before indicating that the download timed out, in seconds. Defaults to 30 seconds. public init(codegenEngine: CodeGenerationEngine = .default, + additionalInflectionRules: [InflectionRule] = [], includes: String = "./**/*.graphql", mergeInFieldsFromFragmentSpreads: Bool = true, modifier: AccessModifier = .public, @@ -107,6 +110,7 @@ public struct ApolloCodegenOptions { urlToSchemaFile: URL, downloadTimeout: Double = 30.0) { self.codegenEngine = codegenEngine + self.additionalInflectionRules = additionalInflectionRules self.includes = includes self.mergeInFieldsFromFragmentSpreads = mergeInFieldsFromFragmentSpreads self.modifier = modifier diff --git a/Sources/ApolloCodegenLib/Pluralizer.swift b/Sources/ApolloCodegenLib/Pluralizer.swift new file mode 100644 index 0000000000..8fae50bd69 --- /dev/null +++ b/Sources/ApolloCodegenLib/Pluralizer.swift @@ -0,0 +1,56 @@ +import Foundation +import InflectorKit + +/// The types of inflection rules that can be used to customize pluralization. +public enum InflectionRule { + + /// A pluralization rule that allows taking a singular word and pluralizing it. + /// - singularRegex: A regular expression representing the single version of the word + /// - replacementRegex: A regular expression representing how to replace the singular version. + case pluralization(singularRegex: String, replacementRegex: String) + + /// A singularization rule that allows taking a plural word and singularizing it. + /// - pluralRegex: A regular expression represeinting the plural version of the word + /// - replacementRegex: A regular expression representing how to replace the singular version + case singularization(pluralRegex: String, replacementRegex: String) + + /// A definition of an irregular pluralization rule not easily captured by regex - for example "person" and "people". + /// - singular: The singular version of the word + /// - plural: The plural version of the word. + case irregular(singular: String, plural: String) + + /// A definition of a word that should never be pluralized or de-pluralized because it's the same no matter what the count - for example, "fish". + /// - word: The word that should never be adjusted. + case uncountable(word: String) +} + +struct Pluralizer { + + private let inflector: StringInflector + + init(rules: [InflectionRule] = []) { + let inflector = StringInflector.default + for rule in rules { + switch rule { + case .pluralization(let pluralRegex, let replacementRegex): + inflector.addPluralRule(pluralRegex, replacement: replacementRegex) + case .singularization(let singularRegex, let replacementRegex): + inflector.addSingularRule(singularRegex, replacement: replacementRegex) + case .irregular(let singular, let plural): + inflector.addIrregular(singular: singular, plural: plural) + case .uncountable(let word): + inflector.addUncountable(word) + } + } + + self.inflector = inflector + } + + func singularize(_ string: String) -> String { + self.inflector.singularize(string) + } + + func pluralize(_ string: String) -> String { + self.inflector.pluralize(string) + } +} diff --git a/SwiftScripts/Package.resolved b/SwiftScripts/Package.resolved index bba40c14e3..08fa68facf 100644 --- a/SwiftScripts/Package.resolved +++ b/SwiftScripts/Package.resolved @@ -10,6 +10,15 @@ "version": "0.17.0" } }, + { + "package": "InflectorKit", + "repositoryURL": "https://github.com/mattt/InflectorKit", + "state": { + "branch": null, + "revision": "d8cbcc04972690aaa5fc760a2b9ddb3e9f0decd7", + "version": "1.0.0" + } + }, { "package": "MarkdownGenerator", "repositoryURL": "https://github.com/eneko/MarkdownGenerator.git", diff --git a/Tests/ApolloCodegenTests/PluralizerTests.swift b/Tests/ApolloCodegenTests/PluralizerTests.swift new file mode 100644 index 0000000000..d2aedb6537 --- /dev/null +++ b/Tests/ApolloCodegenTests/PluralizerTests.swift @@ -0,0 +1,78 @@ +import Foundation +import XCTest +@testable import ApolloCodegenLib + +class PluralizerTests: XCTestCase { + + func testSingularization_givenSimpleWord_shouldSingularize() { + let pluralizer = Pluralizer() + let pluralized = "Cats" + let singular = pluralizer.singularize(pluralized) + XCTAssertEqual(singular, "Cat") + } + + func testPluralization_givenSimpleWord_shouldPluralize() { + let pluralizer = Pluralizer() + let singular = "Cat" + let pluralized = pluralizer.pluralize(singular) + XCTAssertEqual(pluralized, "Cats") + } + + func testSingularization_addingSingularizationRule_shouldSingularize() { + let defaultPluralizer = Pluralizer() + let pluralized = "Atlases" + let beforeRule = defaultPluralizer.singularize(pluralized) + + // This should be wrong because we haven't applied the rule yet. + XCTAssertEqual(beforeRule, "Atlase") + + let pluralizerWithRule = Pluralizer(rules: [ + .singularization(pluralRegex: "(atlas)(es)?$", replacementRegex: "$1") + ]) + + let afterRule = pluralizerWithRule.singularize(pluralized) + + // Now that we've applied the rule, this should be correct + XCTAssertEqual(afterRule, "Atlas") + } + + func testPluralization_addingPluralizationRule_shouldPluralize() { + let defaultPluralizer = Pluralizer() + let singular = "Atlas" + let beforeRule = defaultPluralizer.pluralize(singular) + + // This should be wrong because we haven't applied the rule yet. + XCTAssertEqual(beforeRule, "Atlas") + + let pluralizerWithRule = Pluralizer(rules: [ + .pluralization(singularRegex: "(atla)s", replacementRegex: "$1ses") + ]) + let singularized = pluralizerWithRule.pluralize(singular) + + // Now that we've applied the rule, this should be correct + XCTAssertEqual(singularized, "Atlases") + } + + func testPluralization_givenSpecificCasing_shouldNotChangeCasing() { + let pluralizer = Pluralizer() + let singular = "CAT" + let pluralized = pluralizer.pluralize(singular) + XCTAssertEqual(pluralized, "CATs") + + let singularWithLowercase = "CaT" + let pluralizedWithLowercase = pluralizer.pluralize(singularWithLowercase) + XCTAssertEqual(pluralizedWithLowercase, "CaTs") + } + + func testSingularization_givenCasedSuffix_shouldSingularize() { + let pluralizer = Pluralizer() + let pluralizedAllCaps = "CTAS" + let singularizedAllCaps = pluralizer.singularize(pluralizedAllCaps) + XCTAssertEqual(singularizedAllCaps, "CTA") + + let pluralizedWithOneLowercase = "CTAs" + let singularizedWithOneLowercase = pluralizer.singularize(pluralizedWithOneLowercase) + XCTAssertEqual(singularizedWithOneLowercase, "CTA") + } + +}