├── .gitignore ├── CHANGELOG.md ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Example.xcscheme ├── Example │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── ExampleApp.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── ExampleUITests │ └── ExampleUITests.swift ├── README.md ├── RPCMethod │ └── RPCMethod.swift └── RPCMethodPerformable │ └── RPCMethodPerformable.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── CodableRPC │ ├── Internal │ ├── ClientConnection.swift │ ├── ClientConnectionManager.swift │ ├── CodableHandler.swift │ ├── NullDelimitedFrameHandler.swift │ └── PromisedMethod.swift │ ├── RPCClient.swift │ ├── RPCClientConfig.swift │ ├── RPCMethod.swift │ ├── RPCServer.swift │ └── RPCServerConfig.swift └── Tests └── CodableRPCTests └── CodableRPCTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## main 4 | 5 | - No changes. 6 | 7 | ## 0.0.1 (2024-03-18) 8 | 9 | - Initial release. 10 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BA2346DE2BA4C4F400010904 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2346DD2BA4C4F400010904 /* ExampleApp.swift */; }; 11 | BA2346E02BA4C4F400010904 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2346DF2BA4C4F400010904 /* ContentView.swift */; }; 12 | BA2346E22BA4C4F500010904 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BA2346E12BA4C4F500010904 /* Assets.xcassets */; }; 13 | BA2346E52BA4C4F500010904 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BA2346E42BA4C4F500010904 /* Preview Assets.xcassets */; }; 14 | BA2346F22BA4C50400010904 /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2346F12BA4C50400010904 /* ExampleUITests.swift */; }; 15 | BA2346FC2BA4CBC400010904 /* CodableRPC in Frameworks */ = {isa = PBXBuildFile; productRef = BA2346FB2BA4CBC400010904 /* CodableRPC */; }; 16 | BA2347272BA4CC4300010904 /* RPCMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2347262BA4CC4300010904 /* RPCMethod.swift */; }; 17 | BA23472D2BA4CC8600010904 /* CodableRPC in Frameworks */ = {isa = PBXBuildFile; productRef = BA23472C2BA4CC8600010904 /* CodableRPC */; }; 18 | BA2347352BA4CD1A00010904 /* RPCMethodPerformable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2347342BA4CD1A00010904 /* RPCMethodPerformable.swift */; }; 19 | BA23473A2BA4CD2800010904 /* CodableRPC in Frameworks */ = {isa = PBXBuildFile; productRef = BA2347392BA4CD2800010904 /* CodableRPC */; }; 20 | BA89264D2BA4D06100A970A0 /* libRPCMethod.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BA2347242BA4CC4300010904 /* libRPCMethod.a */; }; 21 | BA89264E2BA4D06100A970A0 /* libRPCMethodPerformable.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BA2347322BA4CD1A00010904 /* libRPCMethodPerformable.a */; }; 22 | BA8926522BA4D12500A970A0 /* CodableRPC in Frameworks */ = {isa = PBXBuildFile; productRef = BA8926512BA4D12500A970A0 /* CodableRPC */; }; 23 | BA8926532BA4D12500A970A0 /* libRPCMethod.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BA2347242BA4CC4300010904 /* libRPCMethod.a */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | BA2346F52BA4C50400010904 /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = BA2346D22BA4C4F400010904 /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = BA2346D92BA4C4F400010904; 32 | remoteInfo = Example; 33 | }; 34 | BA23473B2BA4CD3A00010904 /* PBXContainerItemProxy */ = { 35 | isa = PBXContainerItemProxy; 36 | containerPortal = BA2346D22BA4C4F400010904 /* Project object */; 37 | proxyType = 1; 38 | remoteGlobalIDString = BA2347232BA4CC4300010904; 39 | remoteInfo = RPCMethod; 40 | }; 41 | BA23473D2BA4CD3A00010904 /* PBXContainerItemProxy */ = { 42 | isa = PBXContainerItemProxy; 43 | containerPortal = BA2346D22BA4C4F400010904 /* Project object */; 44 | proxyType = 1; 45 | remoteGlobalIDString = BA2347312BA4CD1A00010904; 46 | remoteInfo = RPCMethodPerformable; 47 | }; 48 | BA2347432BA4CD7200010904 /* PBXContainerItemProxy */ = { 49 | isa = PBXContainerItemProxy; 50 | containerPortal = BA2346D22BA4C4F400010904 /* Project object */; 51 | proxyType = 1; 52 | remoteGlobalIDString = BA2347232BA4CC4300010904; 53 | remoteInfo = RPCMethod; 54 | }; 55 | BA89264F2BA4D11E00A970A0 /* PBXContainerItemProxy */ = { 56 | isa = PBXContainerItemProxy; 57 | containerPortal = BA2346D22BA4C4F400010904 /* Project object */; 58 | proxyType = 1; 59 | remoteGlobalIDString = BA2347232BA4CC4300010904; 60 | remoteInfo = RPCMethod; 61 | }; 62 | /* End PBXContainerItemProxy section */ 63 | 64 | /* Begin PBXCopyFilesBuildPhase section */ 65 | BA23471C2BA4CC1800010904 /* Embed Frameworks */ = { 66 | isa = PBXCopyFilesBuildPhase; 67 | buildActionMask = 2147483647; 68 | dstPath = ""; 69 | dstSubfolderSpec = 10; 70 | files = ( 71 | ); 72 | name = "Embed Frameworks"; 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | BA2347222BA4CC4300010904 /* CopyFiles */ = { 76 | isa = PBXCopyFilesBuildPhase; 77 | buildActionMask = 2147483647; 78 | dstPath = "include/$(PRODUCT_NAME)"; 79 | dstSubfolderSpec = 16; 80 | files = ( 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | BA2347302BA4CD1A00010904 /* CopyFiles */ = { 85 | isa = PBXCopyFilesBuildPhase; 86 | buildActionMask = 2147483647; 87 | dstPath = "include/$(PRODUCT_NAME)"; 88 | dstSubfolderSpec = 16; 89 | files = ( 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | /* End PBXCopyFilesBuildPhase section */ 94 | 95 | /* Begin PBXFileReference section */ 96 | BA2346DA2BA4C4F400010904 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97 | BA2346DD2BA4C4F400010904 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; 98 | BA2346DF2BA4C4F400010904 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 99 | BA2346E12BA4C4F500010904 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 100 | BA2346E42BA4C4F500010904 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 101 | BA2346EF2BA4C50400010904 /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 102 | BA2346F12BA4C50400010904 /* ExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITests.swift; sourceTree = ""; }; 103 | BA2347242BA4CC4300010904 /* libRPCMethod.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRPCMethod.a; sourceTree = BUILT_PRODUCTS_DIR; }; 104 | BA2347262BA4CC4300010904 /* RPCMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RPCMethod.swift; sourceTree = ""; }; 105 | BA2347322BA4CD1A00010904 /* libRPCMethodPerformable.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRPCMethodPerformable.a; sourceTree = BUILT_PRODUCTS_DIR; }; 106 | BA2347342BA4CD1A00010904 /* RPCMethodPerformable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RPCMethodPerformable.swift; sourceTree = ""; }; 107 | BAB86B3B2BA4D732000E4224 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 108 | /* End PBXFileReference section */ 109 | 110 | /* Begin PBXFrameworksBuildPhase section */ 111 | BA2346D72BA4C4F400010904 /* Frameworks */ = { 112 | isa = PBXFrameworksBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | BA89264D2BA4D06100A970A0 /* libRPCMethod.a in Frameworks */, 116 | BA89264E2BA4D06100A970A0 /* libRPCMethodPerformable.a in Frameworks */, 117 | BA2346FC2BA4CBC400010904 /* CodableRPC in Frameworks */, 118 | ); 119 | runOnlyForDeploymentPostprocessing = 0; 120 | }; 121 | BA2346EC2BA4C50400010904 /* Frameworks */ = { 122 | isa = PBXFrameworksBuildPhase; 123 | buildActionMask = 2147483647; 124 | files = ( 125 | BA8926532BA4D12500A970A0 /* libRPCMethod.a in Frameworks */, 126 | BA8926522BA4D12500A970A0 /* CodableRPC in Frameworks */, 127 | ); 128 | runOnlyForDeploymentPostprocessing = 0; 129 | }; 130 | BA2347212BA4CC4300010904 /* Frameworks */ = { 131 | isa = PBXFrameworksBuildPhase; 132 | buildActionMask = 2147483647; 133 | files = ( 134 | BA23472D2BA4CC8600010904 /* CodableRPC in Frameworks */, 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | BA23472F2BA4CD1A00010904 /* Frameworks */ = { 139 | isa = PBXFrameworksBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | BA23473A2BA4CD2800010904 /* CodableRPC in Frameworks */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXFrameworksBuildPhase section */ 147 | 148 | /* Begin PBXGroup section */ 149 | BA2346D12BA4C4F400010904 = { 150 | isa = PBXGroup; 151 | children = ( 152 | BAB86B3B2BA4D732000E4224 /* README.md */, 153 | BA2346DC2BA4C4F400010904 /* Example */, 154 | BA2346F02BA4C50400010904 /* ExampleUITests */, 155 | BA2347252BA4CC4300010904 /* RPCMethod */, 156 | BA2347332BA4CD1A00010904 /* RPCMethodPerformable */, 157 | BA2346DB2BA4C4F400010904 /* Products */, 158 | BA23472B2BA4CC8600010904 /* Frameworks */, 159 | ); 160 | sourceTree = ""; 161 | }; 162 | BA2346DB2BA4C4F400010904 /* Products */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | BA2346DA2BA4C4F400010904 /* Example.app */, 166 | BA2346EF2BA4C50400010904 /* ExampleUITests.xctest */, 167 | BA2347242BA4CC4300010904 /* libRPCMethod.a */, 168 | BA2347322BA4CD1A00010904 /* libRPCMethodPerformable.a */, 169 | ); 170 | name = Products; 171 | sourceTree = ""; 172 | }; 173 | BA2346DC2BA4C4F400010904 /* Example */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | BA2346DD2BA4C4F400010904 /* ExampleApp.swift */, 177 | BA2346DF2BA4C4F400010904 /* ContentView.swift */, 178 | BA2346E12BA4C4F500010904 /* Assets.xcassets */, 179 | BA2346E32BA4C4F500010904 /* Preview Content */, 180 | ); 181 | path = Example; 182 | sourceTree = ""; 183 | }; 184 | BA2346E32BA4C4F500010904 /* Preview Content */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | BA2346E42BA4C4F500010904 /* Preview Assets.xcassets */, 188 | ); 189 | path = "Preview Content"; 190 | sourceTree = ""; 191 | }; 192 | BA2346F02BA4C50400010904 /* ExampleUITests */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | BA2346F12BA4C50400010904 /* ExampleUITests.swift */, 196 | ); 197 | path = ExampleUITests; 198 | sourceTree = ""; 199 | }; 200 | BA2347252BA4CC4300010904 /* RPCMethod */ = { 201 | isa = PBXGroup; 202 | children = ( 203 | BA2347262BA4CC4300010904 /* RPCMethod.swift */, 204 | ); 205 | path = RPCMethod; 206 | sourceTree = ""; 207 | }; 208 | BA23472B2BA4CC8600010904 /* Frameworks */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | ); 212 | name = Frameworks; 213 | sourceTree = ""; 214 | }; 215 | BA2347332BA4CD1A00010904 /* RPCMethodPerformable */ = { 216 | isa = PBXGroup; 217 | children = ( 218 | BA2347342BA4CD1A00010904 /* RPCMethodPerformable.swift */, 219 | ); 220 | path = RPCMethodPerformable; 221 | sourceTree = ""; 222 | }; 223 | /* End PBXGroup section */ 224 | 225 | /* Begin PBXNativeTarget section */ 226 | BA2346D92BA4C4F400010904 /* Example */ = { 227 | isa = PBXNativeTarget; 228 | buildConfigurationList = BA2346E82BA4C4F500010904 /* Build configuration list for PBXNativeTarget "Example" */; 229 | buildPhases = ( 230 | BA2346D62BA4C4F400010904 /* Sources */, 231 | BA2346D72BA4C4F400010904 /* Frameworks */, 232 | BA2346D82BA4C4F400010904 /* Resources */, 233 | BA23471C2BA4CC1800010904 /* Embed Frameworks */, 234 | ); 235 | buildRules = ( 236 | ); 237 | dependencies = ( 238 | BA23473C2BA4CD3A00010904 /* PBXTargetDependency */, 239 | BA23473E2BA4CD3A00010904 /* PBXTargetDependency */, 240 | ); 241 | name = Example; 242 | packageProductDependencies = ( 243 | BA2346FB2BA4CBC400010904 /* CodableRPC */, 244 | ); 245 | productName = Example; 246 | productReference = BA2346DA2BA4C4F400010904 /* Example.app */; 247 | productType = "com.apple.product-type.application"; 248 | }; 249 | BA2346EE2BA4C50400010904 /* ExampleUITests */ = { 250 | isa = PBXNativeTarget; 251 | buildConfigurationList = BA2346F72BA4C50400010904 /* Build configuration list for PBXNativeTarget "ExampleUITests" */; 252 | buildPhases = ( 253 | BA2346EB2BA4C50400010904 /* Sources */, 254 | BA2346EC2BA4C50400010904 /* Frameworks */, 255 | BA2346ED2BA4C50400010904 /* Resources */, 256 | ); 257 | buildRules = ( 258 | ); 259 | dependencies = ( 260 | BA8926502BA4D11E00A970A0 /* PBXTargetDependency */, 261 | BA2346F62BA4C50400010904 /* PBXTargetDependency */, 262 | ); 263 | name = ExampleUITests; 264 | packageProductDependencies = ( 265 | BA8926512BA4D12500A970A0 /* CodableRPC */, 266 | ); 267 | productName = ExampleUITests; 268 | productReference = BA2346EF2BA4C50400010904 /* ExampleUITests.xctest */; 269 | productType = "com.apple.product-type.bundle.ui-testing"; 270 | }; 271 | BA2347232BA4CC4300010904 /* RPCMethod */ = { 272 | isa = PBXNativeTarget; 273 | buildConfigurationList = BA2347282BA4CC4300010904 /* Build configuration list for PBXNativeTarget "RPCMethod" */; 274 | buildPhases = ( 275 | BA2347202BA4CC4300010904 /* Sources */, 276 | BA2347212BA4CC4300010904 /* Frameworks */, 277 | BA2347222BA4CC4300010904 /* CopyFiles */, 278 | ); 279 | buildRules = ( 280 | ); 281 | dependencies = ( 282 | BA2347402BA4CD4100010904 /* PBXTargetDependency */, 283 | ); 284 | name = RPCMethod; 285 | packageProductDependencies = ( 286 | BA23472C2BA4CC8600010904 /* CodableRPC */, 287 | ); 288 | productName = RPCMethod; 289 | productReference = BA2347242BA4CC4300010904 /* libRPCMethod.a */; 290 | productType = "com.apple.product-type.library.static"; 291 | }; 292 | BA2347312BA4CD1A00010904 /* RPCMethodPerformable */ = { 293 | isa = PBXNativeTarget; 294 | buildConfigurationList = BA2347362BA4CD1A00010904 /* Build configuration list for PBXNativeTarget "RPCMethodPerformable" */; 295 | buildPhases = ( 296 | BA23472E2BA4CD1A00010904 /* Sources */, 297 | BA23472F2BA4CD1A00010904 /* Frameworks */, 298 | BA2347302BA4CD1A00010904 /* CopyFiles */, 299 | ); 300 | buildRules = ( 301 | ); 302 | dependencies = ( 303 | BA2347442BA4CD7200010904 /* PBXTargetDependency */, 304 | BA2347422BA4CD4500010904 /* PBXTargetDependency */, 305 | ); 306 | name = RPCMethodPerformable; 307 | packageProductDependencies = ( 308 | BA2347392BA4CD2800010904 /* CodableRPC */, 309 | ); 310 | productName = RPCMethodPerformable; 311 | productReference = BA2347322BA4CD1A00010904 /* libRPCMethodPerformable.a */; 312 | productType = "com.apple.product-type.library.static"; 313 | }; 314 | /* End PBXNativeTarget section */ 315 | 316 | /* Begin PBXProject section */ 317 | BA2346D22BA4C4F400010904 /* Project object */ = { 318 | isa = PBXProject; 319 | attributes = { 320 | BuildIndependentTargetsInParallel = 1; 321 | LastSwiftUpdateCheck = 1520; 322 | LastUpgradeCheck = 1520; 323 | TargetAttributes = { 324 | BA2346D92BA4C4F400010904 = { 325 | CreatedOnToolsVersion = 15.2; 326 | }; 327 | BA2346EE2BA4C50400010904 = { 328 | CreatedOnToolsVersion = 15.2; 329 | TestTargetID = BA2346D92BA4C4F400010904; 330 | }; 331 | BA2347232BA4CC4300010904 = { 332 | CreatedOnToolsVersion = 15.2; 333 | }; 334 | BA2347312BA4CD1A00010904 = { 335 | CreatedOnToolsVersion = 15.2; 336 | }; 337 | }; 338 | }; 339 | buildConfigurationList = BA2346D52BA4C4F400010904 /* Build configuration list for PBXProject "Example" */; 340 | compatibilityVersion = "Xcode 14.0"; 341 | developmentRegion = en; 342 | hasScannedForEncodings = 0; 343 | knownRegions = ( 344 | en, 345 | Base, 346 | ); 347 | mainGroup = BA2346D12BA4C4F400010904; 348 | packageReferences = ( 349 | BA2346FA2BA4CBC400010904 /* XCRemoteSwiftPackageReference "CodableRPC" */, 350 | ); 351 | productRefGroup = BA2346DB2BA4C4F400010904 /* Products */; 352 | projectDirPath = ""; 353 | projectRoot = ""; 354 | targets = ( 355 | BA2346D92BA4C4F400010904 /* Example */, 356 | BA2346EE2BA4C50400010904 /* ExampleUITests */, 357 | BA2347232BA4CC4300010904 /* RPCMethod */, 358 | BA2347312BA4CD1A00010904 /* RPCMethodPerformable */, 359 | ); 360 | }; 361 | /* End PBXProject section */ 362 | 363 | /* Begin PBXResourcesBuildPhase section */ 364 | BA2346D82BA4C4F400010904 /* Resources */ = { 365 | isa = PBXResourcesBuildPhase; 366 | buildActionMask = 2147483647; 367 | files = ( 368 | BA2346E52BA4C4F500010904 /* Preview Assets.xcassets in Resources */, 369 | BA2346E22BA4C4F500010904 /* Assets.xcassets in Resources */, 370 | ); 371 | runOnlyForDeploymentPostprocessing = 0; 372 | }; 373 | BA2346ED2BA4C50400010904 /* Resources */ = { 374 | isa = PBXResourcesBuildPhase; 375 | buildActionMask = 2147483647; 376 | files = ( 377 | ); 378 | runOnlyForDeploymentPostprocessing = 0; 379 | }; 380 | /* End PBXResourcesBuildPhase section */ 381 | 382 | /* Begin PBXSourcesBuildPhase section */ 383 | BA2346D62BA4C4F400010904 /* Sources */ = { 384 | isa = PBXSourcesBuildPhase; 385 | buildActionMask = 2147483647; 386 | files = ( 387 | BA2346E02BA4C4F400010904 /* ContentView.swift in Sources */, 388 | BA2346DE2BA4C4F400010904 /* ExampleApp.swift in Sources */, 389 | ); 390 | runOnlyForDeploymentPostprocessing = 0; 391 | }; 392 | BA2346EB2BA4C50400010904 /* Sources */ = { 393 | isa = PBXSourcesBuildPhase; 394 | buildActionMask = 2147483647; 395 | files = ( 396 | BA2346F22BA4C50400010904 /* ExampleUITests.swift in Sources */, 397 | ); 398 | runOnlyForDeploymentPostprocessing = 0; 399 | }; 400 | BA2347202BA4CC4300010904 /* Sources */ = { 401 | isa = PBXSourcesBuildPhase; 402 | buildActionMask = 2147483647; 403 | files = ( 404 | BA2347272BA4CC4300010904 /* RPCMethod.swift in Sources */, 405 | ); 406 | runOnlyForDeploymentPostprocessing = 0; 407 | }; 408 | BA23472E2BA4CD1A00010904 /* Sources */ = { 409 | isa = PBXSourcesBuildPhase; 410 | buildActionMask = 2147483647; 411 | files = ( 412 | BA2347352BA4CD1A00010904 /* RPCMethodPerformable.swift in Sources */, 413 | ); 414 | runOnlyForDeploymentPostprocessing = 0; 415 | }; 416 | /* End PBXSourcesBuildPhase section */ 417 | 418 | /* Begin PBXTargetDependency section */ 419 | BA2346F62BA4C50400010904 /* PBXTargetDependency */ = { 420 | isa = PBXTargetDependency; 421 | target = BA2346D92BA4C4F400010904 /* Example */; 422 | targetProxy = BA2346F52BA4C50400010904 /* PBXContainerItemProxy */; 423 | }; 424 | BA23473C2BA4CD3A00010904 /* PBXTargetDependency */ = { 425 | isa = PBXTargetDependency; 426 | target = BA2347232BA4CC4300010904 /* RPCMethod */; 427 | targetProxy = BA23473B2BA4CD3A00010904 /* PBXContainerItemProxy */; 428 | }; 429 | BA23473E2BA4CD3A00010904 /* PBXTargetDependency */ = { 430 | isa = PBXTargetDependency; 431 | target = BA2347312BA4CD1A00010904 /* RPCMethodPerformable */; 432 | targetProxy = BA23473D2BA4CD3A00010904 /* PBXContainerItemProxy */; 433 | }; 434 | BA2347402BA4CD4100010904 /* PBXTargetDependency */ = { 435 | isa = PBXTargetDependency; 436 | productRef = BA23473F2BA4CD4100010904 /* CodableRPC */; 437 | }; 438 | BA2347422BA4CD4500010904 /* PBXTargetDependency */ = { 439 | isa = PBXTargetDependency; 440 | productRef = BA2347412BA4CD4500010904 /* CodableRPC */; 441 | }; 442 | BA2347442BA4CD7200010904 /* PBXTargetDependency */ = { 443 | isa = PBXTargetDependency; 444 | target = BA2347232BA4CC4300010904 /* RPCMethod */; 445 | targetProxy = BA2347432BA4CD7200010904 /* PBXContainerItemProxy */; 446 | }; 447 | BA8926502BA4D11E00A970A0 /* PBXTargetDependency */ = { 448 | isa = PBXTargetDependency; 449 | target = BA2347232BA4CC4300010904 /* RPCMethod */; 450 | targetProxy = BA89264F2BA4D11E00A970A0 /* PBXContainerItemProxy */; 451 | }; 452 | /* End PBXTargetDependency section */ 453 | 454 | /* Begin XCBuildConfiguration section */ 455 | BA2346E62BA4C4F500010904 /* Debug */ = { 456 | isa = XCBuildConfiguration; 457 | buildSettings = { 458 | ALWAYS_SEARCH_USER_PATHS = NO; 459 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 460 | CLANG_ANALYZER_NONNULL = YES; 461 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 462 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 463 | CLANG_ENABLE_MODULES = YES; 464 | CLANG_ENABLE_OBJC_ARC = YES; 465 | CLANG_ENABLE_OBJC_WEAK = YES; 466 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 467 | CLANG_WARN_BOOL_CONVERSION = YES; 468 | CLANG_WARN_COMMA = YES; 469 | CLANG_WARN_CONSTANT_CONVERSION = YES; 470 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 471 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 472 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 473 | CLANG_WARN_EMPTY_BODY = YES; 474 | CLANG_WARN_ENUM_CONVERSION = YES; 475 | CLANG_WARN_INFINITE_RECURSION = YES; 476 | CLANG_WARN_INT_CONVERSION = YES; 477 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 478 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 479 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 480 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 481 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 482 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 483 | CLANG_WARN_STRICT_PROTOTYPES = YES; 484 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 485 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 486 | CLANG_WARN_UNREACHABLE_CODE = YES; 487 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 488 | COPY_PHASE_STRIP = NO; 489 | DEBUG_INFORMATION_FORMAT = dwarf; 490 | ENABLE_STRICT_OBJC_MSGSEND = YES; 491 | ENABLE_TESTABILITY = YES; 492 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 493 | GCC_C_LANGUAGE_STANDARD = gnu17; 494 | GCC_DYNAMIC_NO_PIC = NO; 495 | GCC_NO_COMMON_BLOCKS = YES; 496 | GCC_OPTIMIZATION_LEVEL = 0; 497 | GCC_PREPROCESSOR_DEFINITIONS = ( 498 | "DEBUG=1", 499 | "$(inherited)", 500 | ); 501 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 502 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 503 | GCC_WARN_UNDECLARED_SELECTOR = YES; 504 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 505 | GCC_WARN_UNUSED_FUNCTION = YES; 506 | GCC_WARN_UNUSED_VARIABLE = YES; 507 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 508 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 509 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 510 | MTL_FAST_MATH = YES; 511 | ONLY_ACTIVE_ARCH = YES; 512 | SDKROOT = iphoneos; 513 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 514 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 515 | }; 516 | name = Debug; 517 | }; 518 | BA2346E72BA4C4F500010904 /* Release */ = { 519 | isa = XCBuildConfiguration; 520 | buildSettings = { 521 | ALWAYS_SEARCH_USER_PATHS = NO; 522 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 523 | CLANG_ANALYZER_NONNULL = YES; 524 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 525 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 526 | CLANG_ENABLE_MODULES = YES; 527 | CLANG_ENABLE_OBJC_ARC = YES; 528 | CLANG_ENABLE_OBJC_WEAK = YES; 529 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 530 | CLANG_WARN_BOOL_CONVERSION = YES; 531 | CLANG_WARN_COMMA = YES; 532 | CLANG_WARN_CONSTANT_CONVERSION = YES; 533 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 534 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 535 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 536 | CLANG_WARN_EMPTY_BODY = YES; 537 | CLANG_WARN_ENUM_CONVERSION = YES; 538 | CLANG_WARN_INFINITE_RECURSION = YES; 539 | CLANG_WARN_INT_CONVERSION = YES; 540 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 541 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 542 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 543 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 544 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 545 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 546 | CLANG_WARN_STRICT_PROTOTYPES = YES; 547 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 548 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 549 | CLANG_WARN_UNREACHABLE_CODE = YES; 550 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 551 | COPY_PHASE_STRIP = NO; 552 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 553 | ENABLE_NS_ASSERTIONS = NO; 554 | ENABLE_STRICT_OBJC_MSGSEND = YES; 555 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 556 | GCC_C_LANGUAGE_STANDARD = gnu17; 557 | GCC_NO_COMMON_BLOCKS = YES; 558 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 559 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 560 | GCC_WARN_UNDECLARED_SELECTOR = YES; 561 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 562 | GCC_WARN_UNUSED_FUNCTION = YES; 563 | GCC_WARN_UNUSED_VARIABLE = YES; 564 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 565 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 566 | MTL_ENABLE_DEBUG_INFO = NO; 567 | MTL_FAST_MATH = YES; 568 | SDKROOT = iphoneos; 569 | SWIFT_COMPILATION_MODE = wholemodule; 570 | VALIDATE_PRODUCT = YES; 571 | }; 572 | name = Release; 573 | }; 574 | BA2346E92BA4C4F500010904 /* Debug */ = { 575 | isa = XCBuildConfiguration; 576 | buildSettings = { 577 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 578 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 579 | CODE_SIGN_STYLE = Automatic; 580 | CURRENT_PROJECT_VERSION = 1; 581 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 582 | ENABLE_PREVIEWS = YES; 583 | GENERATE_INFOPLIST_FILE = YES; 584 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 585 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 586 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 587 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 588 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 589 | LD_RUNPATH_SEARCH_PATHS = ( 590 | "$(inherited)", 591 | "@executable_path/Frameworks", 592 | ); 593 | MARKETING_VERSION = 1.0; 594 | PRODUCT_BUNDLE_IDENTIFIER = com.reddit.Example; 595 | PRODUCT_NAME = "$(TARGET_NAME)"; 596 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 597 | SUPPORTS_MACCATALYST = NO; 598 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 599 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 600 | SWIFT_EMIT_LOC_STRINGS = YES; 601 | SWIFT_VERSION = 5.0; 602 | TARGETED_DEVICE_FAMILY = 1; 603 | }; 604 | name = Debug; 605 | }; 606 | BA2346EA2BA4C4F500010904 /* Release */ = { 607 | isa = XCBuildConfiguration; 608 | buildSettings = { 609 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 610 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 611 | CODE_SIGN_STYLE = Automatic; 612 | CURRENT_PROJECT_VERSION = 1; 613 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 614 | ENABLE_PREVIEWS = YES; 615 | GENERATE_INFOPLIST_FILE = YES; 616 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 617 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 618 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 619 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 620 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 621 | LD_RUNPATH_SEARCH_PATHS = ( 622 | "$(inherited)", 623 | "@executable_path/Frameworks", 624 | ); 625 | MARKETING_VERSION = 1.0; 626 | PRODUCT_BUNDLE_IDENTIFIER = com.reddit.Example; 627 | PRODUCT_NAME = "$(TARGET_NAME)"; 628 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 629 | SUPPORTS_MACCATALYST = NO; 630 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 631 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 632 | SWIFT_EMIT_LOC_STRINGS = YES; 633 | SWIFT_VERSION = 5.0; 634 | TARGETED_DEVICE_FAMILY = 1; 635 | }; 636 | name = Release; 637 | }; 638 | BA2346F82BA4C50400010904 /* Debug */ = { 639 | isa = XCBuildConfiguration; 640 | buildSettings = { 641 | CODE_SIGN_STYLE = Automatic; 642 | CURRENT_PROJECT_VERSION = 1; 643 | GENERATE_INFOPLIST_FILE = YES; 644 | MARKETING_VERSION = 1.0; 645 | PRODUCT_BUNDLE_IDENTIFIER = com.reddit.ExampleUITests; 646 | PRODUCT_NAME = "$(TARGET_NAME)"; 647 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 648 | SUPPORTS_MACCATALYST = NO; 649 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 650 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 651 | SWIFT_EMIT_LOC_STRINGS = NO; 652 | SWIFT_VERSION = 5.0; 653 | TARGETED_DEVICE_FAMILY = 1; 654 | TEST_TARGET_NAME = Example; 655 | }; 656 | name = Debug; 657 | }; 658 | BA2346F92BA4C50400010904 /* Release */ = { 659 | isa = XCBuildConfiguration; 660 | buildSettings = { 661 | CODE_SIGN_STYLE = Automatic; 662 | CURRENT_PROJECT_VERSION = 1; 663 | GENERATE_INFOPLIST_FILE = YES; 664 | MARKETING_VERSION = 1.0; 665 | PRODUCT_BUNDLE_IDENTIFIER = com.reddit.ExampleUITests; 666 | PRODUCT_NAME = "$(TARGET_NAME)"; 667 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 668 | SUPPORTS_MACCATALYST = NO; 669 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 670 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 671 | SWIFT_EMIT_LOC_STRINGS = NO; 672 | SWIFT_VERSION = 5.0; 673 | TARGETED_DEVICE_FAMILY = 1; 674 | TEST_TARGET_NAME = Example; 675 | }; 676 | name = Release; 677 | }; 678 | BA2347292BA4CC4300010904 /* Debug */ = { 679 | isa = XCBuildConfiguration; 680 | buildSettings = { 681 | CODE_SIGN_STYLE = Automatic; 682 | OTHER_LDFLAGS = "-ObjC"; 683 | PRODUCT_NAME = "$(TARGET_NAME)"; 684 | SKIP_INSTALL = YES; 685 | SWIFT_VERSION = 5.0; 686 | TARGETED_DEVICE_FAMILY = "1,2"; 687 | }; 688 | name = Debug; 689 | }; 690 | BA23472A2BA4CC4300010904 /* Release */ = { 691 | isa = XCBuildConfiguration; 692 | buildSettings = { 693 | CODE_SIGN_STYLE = Automatic; 694 | OTHER_LDFLAGS = "-ObjC"; 695 | PRODUCT_NAME = "$(TARGET_NAME)"; 696 | SKIP_INSTALL = YES; 697 | SWIFT_VERSION = 5.0; 698 | TARGETED_DEVICE_FAMILY = "1,2"; 699 | }; 700 | name = Release; 701 | }; 702 | BA2347372BA4CD1A00010904 /* Debug */ = { 703 | isa = XCBuildConfiguration; 704 | buildSettings = { 705 | CODE_SIGN_STYLE = Automatic; 706 | OTHER_LDFLAGS = "-ObjC"; 707 | PRODUCT_NAME = "$(TARGET_NAME)"; 708 | SKIP_INSTALL = YES; 709 | SWIFT_VERSION = 5.0; 710 | TARGETED_DEVICE_FAMILY = "1,2"; 711 | }; 712 | name = Debug; 713 | }; 714 | BA2347382BA4CD1A00010904 /* Release */ = { 715 | isa = XCBuildConfiguration; 716 | buildSettings = { 717 | CODE_SIGN_STYLE = Automatic; 718 | OTHER_LDFLAGS = "-ObjC"; 719 | PRODUCT_NAME = "$(TARGET_NAME)"; 720 | SKIP_INSTALL = YES; 721 | SWIFT_VERSION = 5.0; 722 | TARGETED_DEVICE_FAMILY = "1,2"; 723 | }; 724 | name = Release; 725 | }; 726 | /* End XCBuildConfiguration section */ 727 | 728 | /* Begin XCConfigurationList section */ 729 | BA2346D52BA4C4F400010904 /* Build configuration list for PBXProject "Example" */ = { 730 | isa = XCConfigurationList; 731 | buildConfigurations = ( 732 | BA2346E62BA4C4F500010904 /* Debug */, 733 | BA2346E72BA4C4F500010904 /* Release */, 734 | ); 735 | defaultConfigurationIsVisible = 0; 736 | defaultConfigurationName = Release; 737 | }; 738 | BA2346E82BA4C4F500010904 /* Build configuration list for PBXNativeTarget "Example" */ = { 739 | isa = XCConfigurationList; 740 | buildConfigurations = ( 741 | BA2346E92BA4C4F500010904 /* Debug */, 742 | BA2346EA2BA4C4F500010904 /* Release */, 743 | ); 744 | defaultConfigurationIsVisible = 0; 745 | defaultConfigurationName = Release; 746 | }; 747 | BA2346F72BA4C50400010904 /* Build configuration list for PBXNativeTarget "ExampleUITests" */ = { 748 | isa = XCConfigurationList; 749 | buildConfigurations = ( 750 | BA2346F82BA4C50400010904 /* Debug */, 751 | BA2346F92BA4C50400010904 /* Release */, 752 | ); 753 | defaultConfigurationIsVisible = 0; 754 | defaultConfigurationName = Release; 755 | }; 756 | BA2347282BA4CC4300010904 /* Build configuration list for PBXNativeTarget "RPCMethod" */ = { 757 | isa = XCConfigurationList; 758 | buildConfigurations = ( 759 | BA2347292BA4CC4300010904 /* Debug */, 760 | BA23472A2BA4CC4300010904 /* Release */, 761 | ); 762 | defaultConfigurationIsVisible = 0; 763 | defaultConfigurationName = Release; 764 | }; 765 | BA2347362BA4CD1A00010904 /* Build configuration list for PBXNativeTarget "RPCMethodPerformable" */ = { 766 | isa = XCConfigurationList; 767 | buildConfigurations = ( 768 | BA2347372BA4CD1A00010904 /* Debug */, 769 | BA2347382BA4CD1A00010904 /* Release */, 770 | ); 771 | defaultConfigurationIsVisible = 0; 772 | defaultConfigurationName = Release; 773 | }; 774 | /* End XCConfigurationList section */ 775 | 776 | /* Begin XCRemoteSwiftPackageReference section */ 777 | BA2346FA2BA4CBC400010904 /* XCRemoteSwiftPackageReference "CodableRPC" */ = { 778 | isa = XCRemoteSwiftPackageReference; 779 | repositoryURL = "https://github.com/reddit/CodableRPC"; 780 | requirement = { 781 | branch = main; 782 | kind = branch; 783 | }; 784 | }; 785 | /* End XCRemoteSwiftPackageReference section */ 786 | 787 | /* Begin XCSwiftPackageProductDependency section */ 788 | BA2346FB2BA4CBC400010904 /* CodableRPC */ = { 789 | isa = XCSwiftPackageProductDependency; 790 | package = BA2346FA2BA4CBC400010904 /* XCRemoteSwiftPackageReference "CodableRPC" */; 791 | productName = CodableRPC; 792 | }; 793 | BA23472C2BA4CC8600010904 /* CodableRPC */ = { 794 | isa = XCSwiftPackageProductDependency; 795 | package = BA2346FA2BA4CBC400010904 /* XCRemoteSwiftPackageReference "CodableRPC" */; 796 | productName = CodableRPC; 797 | }; 798 | BA2347392BA4CD2800010904 /* CodableRPC */ = { 799 | isa = XCSwiftPackageProductDependency; 800 | package = BA2346FA2BA4CBC400010904 /* XCRemoteSwiftPackageReference "CodableRPC" */; 801 | productName = CodableRPC; 802 | }; 803 | BA23473F2BA4CD4100010904 /* CodableRPC */ = { 804 | isa = XCSwiftPackageProductDependency; 805 | package = BA2346FA2BA4CBC400010904 /* XCRemoteSwiftPackageReference "CodableRPC" */; 806 | productName = CodableRPC; 807 | }; 808 | BA2347412BA4CD4500010904 /* CodableRPC */ = { 809 | isa = XCSwiftPackageProductDependency; 810 | package = BA2346FA2BA4CBC400010904 /* XCRemoteSwiftPackageReference "CodableRPC" */; 811 | productName = CodableRPC; 812 | }; 813 | BA8926512BA4D12500A970A0 /* CodableRPC */ = { 814 | isa = XCSwiftPackageProductDependency; 815 | package = BA2346FA2BA4CBC400010904 /* XCRemoteSwiftPackageReference "CodableRPC" */; 816 | productName = CodableRPC; 817 | }; 818 | /* End XCSwiftPackageProductDependency section */ 819 | }; 820 | rootObject = BA2346D22BA4C4F400010904 /* Project object */; 821 | } 822 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "codablerpc", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/reddit/CodableRPC", 7 | "state" : { 8 | "branch" : "main", 9 | "revision" : "0d71398caba13b33ec786915d540e80a806c8936" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-atomics", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-atomics.git", 16 | "state" : { 17 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 18 | "version" : "1.2.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-collections", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-collections.git", 25 | "state" : { 26 | "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", 27 | "version" : "1.1.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-nio", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-nio.git", 34 | "state" : { 35 | "revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982", 36 | "version" : "2.64.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-system", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-system.git", 43 | "state" : { 44 | "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", 45 | "version" : "1.2.1" 46 | } 47 | } 48 | ], 49 | "version" : 2 50 | } 51 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 35 | 41 | 42 | 43 | 46 | 52 | 53 | 54 | 55 | 56 | 66 | 68 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | var body: some View { 5 | VStack { 6 | Image(systemName: "globe") 7 | .imageScale(.large) 8 | .foregroundStyle(.tint) 9 | Text("Hello, world!") 10 | } 11 | .padding() 12 | } 13 | } 14 | 15 | #Preview { 16 | ContentView() 17 | } 18 | -------------------------------------------------------------------------------- /Example/Example/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CodableRPC 3 | import RPCMethod 4 | import RPCMethodPerformable 5 | 6 | @main 7 | struct ExampleApp: App { 8 | var server: RPCServer? 9 | 10 | init() { 11 | let server = RPCServer(dependencyProvider: MethodDependencies()) 12 | self.server = server 13 | try! server.start(host: "127.0.0.1", port: 1234) 14 | } 15 | 16 | var body: some Scene { 17 | WindowGroup { 18 | ContentView() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ExampleUITests/ExampleUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CodableRPC 3 | import RPCMethod 4 | 5 | final class ExampleUITests: XCTestCase { 6 | func testExample() throws { 7 | // Launch the app containing the RPC server. 8 | let app = XCUIApplication() 9 | app.launch() 10 | 11 | // Connect the client. 12 | let client = RPCClient() 13 | try client.connect(host: "127.0.0.1", port: 1234) 14 | 15 | for _ in 0..<100 { 16 | // Perform the 'ping' method. 17 | let result = try client.call(.ping) 18 | 19 | switch result { 20 | case .pong(let time): 21 | print("Server time:", Int(time)) 22 | } 23 | 24 | wait(forDuration: 1) 25 | } 26 | 27 | try client.disconnect() 28 | } 29 | 30 | private func wait(forDuration duration: TimeInterval) { 31 | CFRunLoopRunInMode(.defaultMode, duration, false) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Example/README.md: -------------------------------------------------------------------------------- 1 | # CodableRPC Example 2 | 3 | This project demonstrates using CodableRPC in a UI test to execute methods on an iOS app. Open the project and run the tests, you should see a timestamp returned from the app printed in the console. 4 | -------------------------------------------------------------------------------- /Example/RPCMethod/RPCMethod.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CodableRPC 3 | 4 | // An RPCMethod enum defines the methods that can be performed by the server. 5 | public enum ExampleRPCMethod: RPCMethod { 6 | public typealias Result = ExampleMethodResult 7 | 8 | case ping 9 | } 10 | 11 | // The method result enum defines the responses returned by the server. 12 | public enum ExampleMethodResult: Codable { 13 | case pong(TimeInterval) 14 | } 15 | -------------------------------------------------------------------------------- /Example/RPCMethodPerformable/RPCMethodPerformable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CodableRPC 3 | import RPCMethod 4 | 5 | // Construct external dependencies needed by the RPC methods. These objects would likely be 6 | // provided by your dependency injection system. 7 | public struct MethodDependencies { 8 | public let timeProvider: TimeProvider 9 | 10 | public init(timeProvider: TimeProvider = TimeProvider()) { 11 | self.timeProvider = timeProvider 12 | } 13 | 14 | public struct TimeProvider { 15 | public init () {} 16 | 17 | public var currentTime: TimeInterval { 18 | Date().timeIntervalSinceReferenceDate 19 | } 20 | } 21 | } 22 | 23 | // Extend the 'RPCMethod' method type in your server target with 'RPCMethodPerformable' to 24 | // implement the method that performs the methods. 25 | extension ExampleRPCMethod: RPCMethodPerformable { 26 | public func perform(dependencyProvider: MethodDependencies) async throws -> ExampleMethodResult { 27 | switch self { 28 | case .ping: 29 | return .pong(dependencyProvider.timeProvider.currentTime) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, reddit 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-atomics", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-atomics.git", 7 | "state" : { 8 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 9 | "version" : "1.2.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-collections", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-collections.git", 16 | "state" : { 17 | "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", 18 | "version" : "1.1.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-nio", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-nio.git", 25 | "state" : { 26 | "revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982", 27 | "version" : "2.64.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-system", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-system.git", 34 | "state" : { 35 | "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", 36 | "version" : "1.2.1" 37 | } 38 | } 39 | ], 40 | "version" : 2 41 | } 42 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "CodableRPC", 6 | platforms: [ 7 | .macOS(.v14), 8 | .iOS(.v13) 9 | ], 10 | products: [ 11 | .library( 12 | name: "CodableRPC", 13 | targets: ["CodableRPC"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0") 17 | ], 18 | targets: [ 19 | .target( 20 | name: "CodableRPC", 21 | dependencies: [ 22 | .product(name: "NIOCore", package: "swift-nio"), 23 | .product(name: "NIOPosix", package: "swift-nio"), 24 | .product(name: "NIOFoundationCompat", package: "swift-nio") 25 | ] 26 | ), 27 | .testTarget( 28 | name: "CodableRPCTests", 29 | dependencies: ["CodableRPC"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodableRPC 2 | 3 | CodableRPC is a general purpose RPC client & server implemented in Swift that uses [Codable](https://developer.apple.com/documentation/swift/encoding-decoding-and-serialization) for serialization, enabling you to write idiomatic and type-safe procedure calls. 4 | 5 | While a general purpose RPC implementation, Reddit uses CodableRPC to support ad hoc communication between XCTest UI tests and the iOS app. 6 | 7 | ## Usage 8 | 9 | ```swift 10 | // An RPCMethod enum defines the methods that can be performed by the server. 11 | enum ExampleRPCMethod: RPCMethod { 12 | typealias Result = ExampleMethodResult 13 | 14 | case ping 15 | } 16 | 17 | // The method result enum defines the responses returned by the server. 18 | enum ExampleMethodResult: Codable { 19 | case pong(TimeInterval) 20 | } 21 | 22 | // Construct external dependencies needed by the RPC methods. These objects would likely be 23 | // provided by your dependency injection system. 24 | struct MethodDependencies { 25 | let timeProvider = TimeProvider() 26 | 27 | struct TimeProvider { 28 | var currentTime: TimeInterval { 29 | Date().timeIntervalSinceReferenceDate 30 | } 31 | } 32 | } 33 | 34 | // Extend the 'RPCMethod' method type in your server target with 'RPCMethodPerformable' to 35 | // implement the method that performs the methods. 36 | extension ExampleRPCMethod: RPCMethodPerformable { 37 | func perform(dependencyProvider: MethodDependencies) async throws -> ExampleMethodResult { 38 | switch self { 39 | case .ping: 40 | return .pong(dependencyProvider.timeProvider.currentTime) 41 | } 42 | } 43 | } 44 | 45 | // Start the server. 46 | let server = RPCServer(dependencyProvider: MethodDependencies()) 47 | try server.start(host: "127.0.0.1", port: 1234) 48 | 49 | // Connect the client. 50 | let client = RPCClient() 51 | try client.connect(host: "127.0.0.1", port: 1234) 52 | 53 | // Perform the 'ping' method. 54 | let result = try client.call(.ping) 55 | 56 | switch result { 57 | case .pong(let currentTime): 58 | print(currentTime) 59 | } 60 | ``` 61 | 62 | The included [example project](https://github.com/reddit/CodableRPC/tree/main/Example) demonstrates using CodableRPC to communicate between a UI test and the iOS app under test. Notice that the example project declares the `ExampleRPCMethod` enum in a target that is imported by both the app target and the test target, whereas the `RPCMethodPerformable` conformance is declared in a target that is only imported by the app target. This setup is necessary to decouple your test target from the dependencies that implement the RPC methods. 63 | 64 | ## Configuration 65 | 66 | Both `RPCServer` and `RPCClient` can be initialized with custom configurations, though the defaults should be reasonable for simple workloads. See the documentation on `RPCServerConfig` and `RPCClientConfig` for details. 67 | -------------------------------------------------------------------------------- /Sources/CodableRPC/Internal/ClientConnection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | import NIOPosix 4 | import os.log 5 | 6 | /// An RPC client connection. 7 | final class ClientConnection { 8 | private typealias MethodResult = Result 9 | 10 | private let log: OSLog 11 | private var channel: Channel? 12 | private var group: MultiThreadedEventLoopGroup? 13 | 14 | init(log: OSLog, channel: Channel, group: MultiThreadedEventLoopGroup) { 15 | self.log = log 16 | self.channel = channel 17 | self.group = group 18 | } 19 | 20 | /// Performs the given method on the server. 21 | @discardableResult 22 | func call(_ method: Method, timeout: TimeAmount) throws -> Method.Result { 23 | guard let channel else { 24 | throw RPCClientError.connectionClosed 25 | } 26 | 27 | let promise: EventLoopPromise = channel.eventLoop.makePromise() 28 | let promisedMethod = PromisedMethod(method: method, promise: promise) 29 | let future = channel.writeAndFlush(promisedMethod) 30 | future.cascadeFailure(to: promise) 31 | 32 | let task = channel.eventLoop.scheduleTask(in: timeout) { 33 | promise.fail(RPCClientError.callTimeout) 34 | } 35 | 36 | let result = try promise.futureResult.wait() 37 | task.cancel() 38 | switch result { 39 | case .success(let success): 40 | return success 41 | 42 | case .failure(let error): 43 | throw error 44 | } 45 | } 46 | 47 | /// Closes the client connection. 48 | func close() { 49 | do { 50 | // Wait for the channel to close, but ignore errors as it may already be closed. 51 | try channel?.close().wait() 52 | channel = nil 53 | } catch { 54 | os_log("RPCClient: channel close error: %{public}s", log: log, "\(error)") 55 | } 56 | 57 | do { 58 | // Wait for the group to shutdown, but ignore any errors as we've no way to recover. 59 | try group?.syncShutdownGracefully() 60 | group = nil 61 | } catch { 62 | os_log("RPCClient: group shutdown error: %{public}s", log: log, "\(error)") 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/CodableRPC/Internal/ClientConnectionManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | import NIOPosix 4 | import os.log 5 | 6 | /// A connection manager with exponential backoff. 7 | final class ClientConnectionManager { 8 | private typealias MethodResult = Result 9 | 10 | private let config: RPCClientConfig 11 | private let log: OSLog 12 | 13 | init(config: RPCClientConfig, log: OSLog) { 14 | self.config = config 15 | self.log = log 16 | } 17 | 18 | /// Opens a connection to the given host and port. 19 | func connect(host: String, port: Int) throws -> ClientConnection { 20 | let group = MultiThreadedEventLoopGroup(numberOfThreads: Int(config.numberOfThreads)) 21 | let eventLoop = group.any() 22 | let promise = eventLoop.makePromise(of: ClientConnection.self) 23 | connect( 24 | eventLoop: eventLoop, 25 | promise: promise, 26 | group: group, 27 | attempt: 1, 28 | host: host, 29 | port: port 30 | ) 31 | return try promise.futureResult.wait() 32 | } 33 | 34 | // MARK: - Private 35 | 36 | private func connect( 37 | eventLoop: EventLoop, 38 | promise: EventLoopPromise>, 39 | group: MultiThreadedEventLoopGroup, 40 | attempt: Int, 41 | host: String, 42 | port: Int 43 | ) { 44 | let bootstrap = ClientBootstrap(group: group) 45 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 46 | .channelInitializer { [log] channel in 47 | let frameHandler = NullDelimitedFrameHandler() 48 | return channel.pipeline.addHandlers( 49 | [ 50 | ByteToMessageHandler(frameHandler), 51 | MessageToByteHandler(frameHandler), 52 | CodableHandler(), 53 | MethodHandler(log: log), 54 | ] 55 | ) 56 | } 57 | bootstrap.connect(host: host, port: port).whenComplete { [config, log] result in 58 | switch result { 59 | case .success(let channel): 60 | let connection = ClientConnection( 61 | log: log, 62 | channel: channel, 63 | group: group 64 | ) 65 | promise.succeed(connection) 66 | case .failure(let error): 67 | guard attempt < config.maxConnectionAttempts else { 68 | promise.fail(error) 69 | return 70 | } 71 | 72 | let interval = min(config.maxRetryInterval, config.connectionRetryBase * pow(Double(2), Double(attempt - 1))) 73 | 74 | eventLoop.scheduleTask(in: .milliseconds(Int64(interval * 1_000))) { 75 | self.connect( 76 | eventLoop: eventLoop, 77 | promise: promise, 78 | group: group, 79 | attempt: attempt + 1, 80 | host: host, 81 | port: port 82 | ) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | private class MethodHandler: ChannelInboundHandler, ChannelOutboundHandler { 90 | typealias InboundIn = Result 91 | typealias OutboundIn = PromisedMethod 92 | typealias OutboundOut = Method 93 | 94 | private let log: OSLog 95 | private var queue = CircularBuffer>() 96 | 97 | init(log: OSLog) { 98 | self.log = log 99 | } 100 | 101 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 102 | let promisedMethod = unwrapOutboundIn(data) 103 | queue.append(promisedMethod.promise) 104 | context.write(wrapOutboundOut(promisedMethod.method), promise: promise) 105 | } 106 | 107 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 108 | if queue.isEmpty { 109 | // Method has already been handled, forward read to next handler. 110 | return context.fireChannelRead(data) 111 | } 112 | 113 | let promise = queue.removeFirst() 114 | let result = unwrapInboundIn(data) 115 | promise.succeed(result) 116 | } 117 | 118 | func errorCaught(context: ChannelHandlerContext, error: Error) { 119 | os_log("RPCClient: server error %{public}s", log: log, "\(error)") 120 | 121 | if queue.isEmpty { 122 | // Method has already been handled, forward error to next handler. 123 | return context.fireErrorCaught(error) 124 | } 125 | 126 | let promise = queue.removeFirst() 127 | switch error { 128 | case is CodableHandlerError: 129 | promise.succeed(.failure(.init(description: error.localizedDescription))) 130 | 131 | default: 132 | // Unhandled error, close the connection. 133 | promise.fail(error) 134 | context.close(promise: nil) 135 | } 136 | } 137 | 138 | func channelInactive(context: ChannelHandlerContext) { 139 | if !queue.isEmpty { 140 | errorCaught(context: context, error: RPCClientError.connectionResetByPeer) 141 | } 142 | } 143 | 144 | func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { 145 | context.fireUserInboundEventTriggered(event) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Sources/CodableRPC/Internal/CodableHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | import NIOFoundationCompat 4 | 5 | /// A Codable channel handler. 6 | final class CodableHandler: ChannelInboundHandler, ChannelOutboundHandler { 7 | typealias InboundIn = ByteBuffer 8 | typealias InboundOut = In 9 | typealias OutboundIn = Out 10 | typealias OutboundOut = ByteBuffer 11 | 12 | private let decoder = JSONDecoder() 13 | private let encoder = JSONEncoder() 14 | 15 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 16 | var buffer = unwrapInboundIn(data) 17 | 18 | do { 19 | guard let decodable = try buffer.readJSONDecodable(In.self, decoder: decoder, length: buffer.readableBytes) else { 20 | context.fireErrorCaught(CodableHandlerError.insufficientBytes) 21 | return 22 | } 23 | context.fireChannelRead(wrapInboundOut(decodable)) 24 | } catch let error as DecodingError { 25 | context.fireErrorCaught(CodableHandlerError.invalidJSON(error)) 26 | } catch { 27 | context.fireErrorCaught(error) 28 | } 29 | } 30 | 31 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 32 | do { 33 | let encodable = unwrapOutboundIn(data) 34 | let data = try encoder.encode(encodable) 35 | var buffer = context.channel.allocator.buffer(capacity: data.count) 36 | buffer.writeBytes(data) 37 | context.write(wrapOutboundOut(buffer), promise: promise) 38 | } catch let error as EncodingError { 39 | promise?.fail(CodableHandlerError.invalidJSON(error)) 40 | } catch { 41 | promise?.fail(error) 42 | } 43 | } 44 | } 45 | 46 | enum CodableHandlerError: Swift.Error { 47 | case invalidJSON(Error) 48 | case insufficientBytes 49 | } 50 | -------------------------------------------------------------------------------- /Sources/CodableRPC/Internal/NullDelimitedFrameHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | 4 | /// A NULL byte delimited frame encoder & decoder. 5 | final class NullDelimitedFrameHandler: ByteToMessageDecoder, MessageToByteEncoder { 6 | typealias InboundOut = ByteBuffer 7 | typealias OutboundIn = ByteBuffer 8 | 9 | private let delimiter = UInt8(ascii: "\0") 10 | private var lastIndex = 0 11 | 12 | func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState { 13 | guard buffer.readableBytes >= 2 else { return .needMoreData } 14 | 15 | let readableBytesView = buffer.readableBytesView.dropFirst(lastIndex) 16 | 17 | guard let index = readableBytesView.firstIndex(of: delimiter) else { 18 | lastIndex = buffer.readableBytes 19 | return .needMoreData 20 | } 21 | 22 | let length = index - buffer.readerIndex 23 | 24 | guard let slice = buffer.readSlice(length: length) else { throw FrameHandlerError.badFrame } 25 | 26 | buffer.moveReaderIndex(forwardBy: 1) 27 | lastIndex = 0 28 | 29 | context.fireChannelRead(wrapInboundOut(slice)) 30 | return .continue 31 | } 32 | 33 | func decodeLast(context: ChannelHandlerContext, buffer: inout ByteBuffer, seenEOF: Bool) throws -> DecodingState { 34 | while try decode(context: context, buffer: &buffer) == .continue {} 35 | return .needMoreData 36 | } 37 | 38 | func encode(data: OutboundIn, out: inout ByteBuffer) throws { 39 | var payload = data 40 | out.writeBuffer(&payload) 41 | out.writeBytes([delimiter]) 42 | } 43 | } 44 | 45 | private enum FrameHandlerError: Error { 46 | case badFrame 47 | } 48 | -------------------------------------------------------------------------------- /Sources/CodableRPC/Internal/PromisedMethod.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | 4 | struct PromisedMethod { 5 | let method: Method 6 | let promise: EventLoopPromise 7 | } 8 | -------------------------------------------------------------------------------- /Sources/CodableRPC/RPCClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | import os.log 4 | 5 | /// An RPC client that communicates with a server using the generic `Method` type. 6 | /// The remote server must be specialized with the same type as the client. 7 | /// 8 | /// All connections perform exponential backoff. See `RPCClientConfig` for relevant configuration 9 | /// options. 10 | public final class RPCClient { 11 | private let config: RPCClientConfig 12 | 13 | private var connection: ClientConnection? 14 | 15 | public init(config: RPCClientConfig = .init()) { 16 | self.config = config 17 | } 18 | 19 | deinit { 20 | assert(state == .initialized || state == .disconnected) 21 | } 22 | 23 | /// Initiates a connection to a server at the given address. 24 | public func connect(host: String, port: Int) throws { 25 | guard state == .initialized || state == .disconnected else { 26 | throw RPCClientError.invalidState(expected: [.initialized, .disconnected], actual: state) 27 | } 28 | 29 | state = .connecting(host: host, port: port) 30 | let connectionManager = ClientConnectionManager(config: config, log: config.log) 31 | connection = try connectionManager.connect(host: host, port: port) 32 | state = .connected 33 | } 34 | 35 | /// Disconnects from the server. 36 | public func disconnect() throws { 37 | guard state == .connected else { return } 38 | 39 | state = .disconnecting 40 | connection?.close() 41 | connection = nil 42 | state = .disconnected 43 | } 44 | 45 | /// Performs the given method on the server. 46 | @discardableResult 47 | public func call(_ method: Method, timeout: TimeAmount = .seconds(10)) throws -> Method.Result { 48 | guard state == .connected, let connection else { 49 | throw RPCClientError.invalidState(expected: [.connected], actual: state) 50 | } 51 | 52 | return try connection.call(method, timeout: timeout) 53 | } 54 | 55 | // MARK: - Private 56 | 57 | private var _state = RPCClientState.initialized 58 | private let lock = NSLock() 59 | private var state: RPCClientState { 60 | get { 61 | let localState: RPCClientState 62 | lock.lock() 63 | localState = _state 64 | lock.unlock() 65 | return localState 66 | } 67 | set { 68 | lock.lock() 69 | _state = newValue 70 | os_log("RPCClient: %{public}s", log: config.log, "\(newValue)") 71 | lock.unlock() 72 | } 73 | } 74 | } 75 | 76 | public enum RPCClientState: Equatable, CustomStringConvertible { 77 | case initialized 78 | case connecting(host: String, port: Int) 79 | case connected 80 | case disconnecting 81 | case disconnected 82 | 83 | public var description: String { 84 | switch self { 85 | case .initialized: 86 | return "initialized" 87 | case .connecting(let host, let port): 88 | return "connecting to \(host):\(port)" 89 | case .connected: 90 | return "connected" 91 | case .disconnecting: 92 | return "disconnecting" 93 | case .disconnected: 94 | return "disconnected" 95 | } 96 | } 97 | } 98 | 99 | public enum RPCClientError: Error, LocalizedError { 100 | case callTimeout 101 | case connectionResetByPeer 102 | case invalidState(expected: [RPCClientState], actual: RPCClientState) 103 | case connectionClosed 104 | 105 | public var errorDescription: String? { 106 | "\(String(describing: Self.self)).\(caseDescription)" 107 | } 108 | 109 | private var caseDescription: String { 110 | switch self { 111 | case .callTimeout: 112 | return "callTimeout" 113 | case .connectionResetByPeer: 114 | return "connectionResetByPeer" 115 | case .invalidState(let expected, let actual): 116 | return "invalidState(expected: \(expected), actual: \(actual))" 117 | case .connectionClosed: 118 | return "connectionClosed" 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/CodableRPC/RPCClientConfig.swift: -------------------------------------------------------------------------------- 1 | import os.log 2 | 3 | /// Configuration options for the client. 4 | public struct RPCClientConfig { 5 | /// An OSLog instance used for logging client activity. 6 | public let log: OSLog 7 | 8 | /// The number of threads used for dispatching RPC calls. 9 | public let numberOfThreads: UInt 10 | 11 | /// The maximum number of connection attempts to perform before failing. 12 | public let maxConnectionAttempts: UInt 13 | 14 | /// The base retry interval in seconds. This value grows exponentially with each retry up to `maxRetryInterval`. 15 | public let connectionRetryBase: Double 16 | 17 | /// The maximum interval between retries in seconds. 18 | public let maxRetryInterval: Double 19 | 20 | /// - Parameters: 21 | /// - log: An OSLog instance used for logging client activity. A reasonable default is provided. 22 | /// - numberOfThreads: The number of threads used for dispatching RPC calls. 1 is a reasonable default for most workloads. Default: 1. 23 | /// - maxConnectionAttempts: The maximum number of connection attempts to perform before failing. Default: 10. 24 | /// - connectionRetryBase: The base retry interval in seconds. This value grows exponentially with each retry up to `maxRetryInterval`. Default: 0.1 seconds. 25 | /// - maxRetryInterval: The maximum interval between retries in seconds. Default: 10 seconds. 26 | public init( 27 | log: OSLog = OSLog(subsystem: "codable_rpc", category: "client"), 28 | numberOfThreads: UInt = 1, 29 | maxConnectionAttempts: UInt = 10, 30 | connectionRetryBase: Double = 0.1, 31 | maxRetryInterval: Double = 10 32 | ) { 33 | self.log = log 34 | self.numberOfThreads = numberOfThreads 35 | self.maxConnectionAttempts = maxConnectionAttempts 36 | self.connectionRetryBase = connectionRetryBase 37 | self.maxRetryInterval = maxRetryInterval 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/CodableRPC/RPCMethod.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An RPC method used to communicate between the client and server. 4 | public protocol RPCMethod: Codable { 5 | associatedtype Result: Codable 6 | } 7 | 8 | /// A protocol that declares the method used to perform the RPC method on the server. 9 | /// 10 | /// You may implement this protocol as an extension in order to avoid linking server dependencies 11 | /// with the client. 12 | public protocol RPCMethodPerformable: RPCMethod { 13 | associatedtype DependencyProvider 14 | 15 | func perform(dependencyProvider: DependencyProvider) async throws -> Result 16 | } 17 | 18 | /// A simple Error type that conforms to Codable. 19 | public struct CodableError: Error, Codable, LocalizedError { 20 | public let description: String 21 | 22 | public init(description: String) { 23 | self.description = description 24 | } 25 | 26 | public var errorDescription: String? { 27 | description 28 | } 29 | } 30 | 31 | extension Result: Codable where Success: Codable, Failure == CodableError { 32 | enum CodingKeys: String, CodingKey { 33 | case success 34 | case failure 35 | } 36 | 37 | public init(from decoder: Decoder) throws { 38 | let container = try decoder.container(keyedBy: CodingKeys.self) 39 | if let value = try container.decodeIfPresent(Success.self, forKey: .success) { 40 | self = .success(value) 41 | } else if let value = try container.decodeIfPresent(Failure.self, forKey: .failure) { 42 | self = .failure(value) 43 | } else { 44 | self = .failure(.init(description: "RPCMethod: Unexpected branch decoding CodableError")) 45 | } 46 | } 47 | 48 | public func encode(to encoder: Encoder) throws { 49 | var container = encoder.container(keyedBy: CodingKeys.self) 50 | 51 | switch self { 52 | case .success(let value): 53 | try container.encode(value, forKey: .success) 54 | 55 | case .failure(let error): 56 | try container.encode(error, forKey: .failure) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/CodableRPC/RPCServer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | import NIOPosix 4 | import os.log 5 | 6 | /// An RPC server that receives communications from a client using the generic `Method` type. 7 | /// The connecting client must be specialized with the same type as the server. 8 | public final class RPCServer { 9 | private let config: RPCServerConfig 10 | private let dependencyProvider: Method.DependencyProvider 11 | 12 | private var group: MultiThreadedEventLoopGroup? 13 | private var channel: Channel? 14 | 15 | /// - Parameters: 16 | /// - config: The configuration of the server instance. 17 | /// - dependencyProvider: An object providing external dependencies for use by `RPCMethodPerformable`. 18 | public init(config: RPCServerConfig = .init(), dependencyProvider: Method.DependencyProvider) { 19 | self.config = config 20 | self.dependencyProvider = dependencyProvider 21 | } 22 | 23 | deinit { 24 | assert(state == .initialized || state == .stopped) 25 | } 26 | 27 | /// Starts the server and binds to the given address. 28 | public func start(host: String, port: Int) throws { 29 | guard state == .initialized || state == .stopped else { 30 | throw RPCServerError.invalidState(expected: [.initialized, .stopped], actual: state) 31 | } 32 | 33 | state = .starting(host: host, port: port) 34 | let group = MultiThreadedEventLoopGroup(numberOfThreads: config.numberOfThreads) 35 | self.group = group 36 | 37 | // The channel options used below may not have detailed documentation when viewing Quick Help, 38 | // but they're actually pretty well documented if you navigate to `ChannelOptions` and search 39 | // for the option struct. 40 | 41 | let bootstrap = ServerBootstrap(group: group) 42 | .serverChannelOption(ChannelOptions.backlog, value: 256) 43 | .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 44 | .childChannelInitializer { [log = config.log, dependencyProvider] channel in 45 | let frameHandler = NullDelimitedFrameHandler() 46 | return channel.pipeline.addHandlers( 47 | [ 48 | ByteToMessageHandler(frameHandler), 49 | MessageToByteHandler(frameHandler), 50 | CodableHandler>(), 51 | MethodHandler(log: log, dependencyProvider: dependencyProvider), 52 | ] 53 | ) 54 | } 55 | .childChannelOption(ChannelOptions.tcpOption(.tcp_nodelay), value: 1) 56 | .childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 57 | 58 | channel = try bootstrap.bind(host: host, port: port).wait() 59 | state = .started 60 | } 61 | 62 | /// Stops the server and disconnects any clients. 63 | public func stop() throws { 64 | guard state == .started else { return } 65 | 66 | state = .stopping 67 | 68 | // Wait for the channel to close, but ignore errors as it may already be closed. 69 | try? channel?.close().wait() 70 | channel = nil 71 | 72 | // Wait for the group to shutdown, but ignore errors as we've no way to recover. 73 | try? group?.syncShutdownGracefully() 74 | group = nil 75 | 76 | state = .stopped 77 | } 78 | 79 | private var _state = RPCServerState.initialized 80 | private let lock = NSLock() 81 | private var state: RPCServerState { 82 | get { 83 | let localState: RPCServerState 84 | lock.lock() 85 | localState = _state 86 | lock.unlock() 87 | return localState 88 | } 89 | set { 90 | lock.lock() 91 | _state = newValue 92 | os_log("RPCServer: %{public}s", log: config.log, "\(newValue)") 93 | lock.unlock() 94 | } 95 | } 96 | } 97 | 98 | private class MethodHandler: ChannelInboundHandler { 99 | typealias InboundIn = Method 100 | typealias OutboundOut = Result 101 | 102 | private let log: OSLog 103 | private let dependencyProvider: Method.DependencyProvider 104 | 105 | init(log: OSLog, dependencyProvider: Method.DependencyProvider) { 106 | self.log = log 107 | self.dependencyProvider = dependencyProvider 108 | } 109 | 110 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 111 | let promise: EventLoopPromise = context.eventLoop.makePromise() 112 | promise.futureResult.whenComplete { result in 113 | switch result { 114 | case .success(let callResult): 115 | context.channel.writeAndFlush(self.wrapOutboundOut(callResult), promise: nil) 116 | case .failure(let error): 117 | self.errorCaught(context: context, error: error) 118 | } 119 | } 120 | let method = unwrapInboundIn(data) 121 | 122 | Task { 123 | do { 124 | let result = try await method.perform(dependencyProvider: dependencyProvider) 125 | promise.succeed(.success(result)) 126 | } catch { 127 | let msg = "RPCServer: Failure performing method \(String(describing: method)): \(error.localizedDescription)" 128 | promise.succeed(.failure(CodableError(description: msg))) 129 | } 130 | } 131 | } 132 | 133 | func errorCaught(context: ChannelHandlerContext, error: Error) { 134 | os_log("RPCServer: client error %{public}s", log: log, "\(error)") 135 | 136 | let codableError = CodableError(description: error.localizedDescription) 137 | let result = wrapOutboundOut(.failure(codableError)) 138 | context.channel.writeAndFlush(result, promise: nil) 139 | 140 | // Close the client connection. 141 | context.close(promise: nil) 142 | } 143 | 144 | func channelActive(context: ChannelHandlerContext) { 145 | if let ip = context.remoteAddress?.ipAddress, 146 | let port = context.remoteAddress?.port { 147 | os_log("RPCServer: client %{public}s connected", log: log, "\(ip):\(port)") 148 | } 149 | } 150 | 151 | func channelInactive(context: ChannelHandlerContext) { 152 | if let ip = context.remoteAddress?.ipAddress, 153 | let port = context.remoteAddress?.port { 154 | os_log("RPCServer: client %{public}s disconnected", log: log, "\(ip):\(port)") 155 | } 156 | } 157 | 158 | func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { 159 | context.fireUserInboundEventTriggered(event) 160 | } 161 | } 162 | 163 | private enum RPCServerState: Equatable, CustomStringConvertible { 164 | case initialized 165 | case starting(host: String, port: Int) 166 | case started 167 | case stopping 168 | case stopped 169 | 170 | var description: String { 171 | switch self { 172 | case .initialized: 173 | return "initialized" 174 | case .starting(let host, let port): 175 | return "starting on \(host):\(port)" 176 | case .started: 177 | return "started" 178 | case .stopping: 179 | return "stopping" 180 | case .stopped: 181 | return "stopped" 182 | } 183 | } 184 | } 185 | 186 | private enum RPCServerError: Error, LocalizedError { 187 | case invalidState(expected: [RPCServerState], actual: RPCServerState) 188 | 189 | var errorDescription: String? { 190 | "\(String(describing: Self.self)).\(caseDescription)" 191 | } 192 | 193 | private var caseDescription: String { 194 | switch self { 195 | case .invalidState(let expected, let actual): 196 | return "invalidState(expected: \(expected), actual: \(actual))" 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Sources/CodableRPC/RPCServerConfig.swift: -------------------------------------------------------------------------------- 1 | import os.log 2 | 3 | /// Configuration options for the server. 4 | public struct RPCServerConfig { 5 | /// An OSLog instance used for logging sever activity. 6 | public let log: OSLog 7 | 8 | /// The number of threads used for processing RPC calls from clients. 9 | public let numberOfThreads: Int 10 | 11 | /// - Parameters: 12 | /// - log: An OSLog instance used for logging sever activity. A reasonable default is provided. 13 | /// - numberOfThreads: The number of threads used for processing RPC calls from clients. 14 | public init( 15 | log: OSLog = OSLog(subsystem: "codable_rpc", category: "server"), 16 | numberOfThreads: Int = 1 17 | ) { 18 | self.log = log 19 | self.numberOfThreads = numberOfThreads 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/CodableRPCTests/CodableRPCTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CodableRPC 3 | import XCTest 4 | 5 | final class CodableRPCTests: XCTestCase { 6 | private var server: RPCServer! 7 | private var client: RPCClient! 8 | private let host = "127.0.0.1" 9 | private var port = 0 10 | 11 | override func setUpWithError() throws { 12 | try super.setUpWithError() 13 | // Random port to avoid collisions when tests are run in parallel. 14 | port = Int.random(in: 1_024...9_999) 15 | server = RPCServer(dependencyProvider: DependencyProvider()) 16 | client = RPCClient() 17 | try connect() 18 | } 19 | 20 | override func tearDownWithError() throws { 21 | try disconnect() 22 | server = nil 23 | client = nil 24 | try super.tearDownWithError() 25 | } 26 | 27 | func testSuccessMethod() throws { 28 | let parameters = ThingParameters(message: "abc123") 29 | let result = try client.call(.getThing(parameters)) 30 | 31 | switch result { 32 | case .thing(let result): 33 | XCTAssertEqual(parameters.message, result.message) 34 | case .none: 35 | XCTFail("Unexpected result.") 36 | } 37 | } 38 | 39 | func testFailureMethod() throws { 40 | XCTAssertThrowsError(try client.call(.doThingWithFailure)) { error in 41 | XCTAssertEqual( 42 | error.localizedDescription, 43 | "RPCServer: Failure performing method doThingWithFailure: this is fine" 44 | ) 45 | } 46 | } 47 | 48 | func testMultipleCalls() throws { 49 | for i in 0..<10 { 50 | let parameters = ThingParameters(message: "\(i)") 51 | let result = try client.call(.getThing(parameters)) 52 | 53 | switch result { 54 | case .thing(let result): 55 | XCTAssertEqual(parameters.message, result.message) 56 | case .none: 57 | XCTFail("Unexpected result.") 58 | } 59 | } 60 | } 61 | 62 | func testAsyncMethod() throws { 63 | for i in 0..<10 { 64 | let parameters = ThingParameters(message: "\(i)") 65 | let result = try client.call(.getThingAsync(parameters)) 66 | 67 | switch result { 68 | case .thing(let result): 69 | XCTAssertEqual(parameters.message, result.message) 70 | case .none: 71 | XCTFail("Unexpected result.") 72 | } 73 | } 74 | } 75 | 76 | func testCallTimeout() throws { 77 | XCTAssertThrowsError(try client.call(.sleep(0.5), timeout: .milliseconds(100))) { error in 78 | if let clientError = error as? RPCClientError, case .callTimeout = clientError { 79 | // Success. 80 | } else { 81 | XCTFail("Unexpected error: \(error)") 82 | } 83 | } 84 | } 85 | 86 | // MARK: - Private 87 | 88 | private func connect() throws { 89 | try server.start(host: host, port: port) 90 | try client.connect(host: host, port: port) 91 | } 92 | 93 | private func disconnect() throws { 94 | try client.disconnect() 95 | try server.stop() 96 | } 97 | } 98 | 99 | enum TestMethod: RPCMethod { 100 | typealias Result = TestMethodResult 101 | 102 | case getThing(ThingParameters) 103 | case getThingAsync(ThingParameters) 104 | case doThingWithFailure 105 | case sleep(TimeInterval) 106 | } 107 | 108 | extension TestMethod: RPCMethodPerformable { 109 | func perform(dependencyProvider: DependencyProvider) async throws -> TestMethodResult { 110 | switch self { 111 | case .getThing(let parameters): 112 | return .thing(ThingResult(message: parameters.message)) 113 | 114 | case .doThingWithFailure: 115 | throw CodableError(description: "this is fine") 116 | 117 | case .getThingAsync(let parameters): 118 | return await withCheckedContinuation { continuation in 119 | let dither = Double.random(in: 0..<0.1) 120 | DispatchQueue.global().asyncAfter(deadline: .now() + dither) { 121 | let result = ThingResult(message: parameters.message) 122 | continuation.resume(with: .success(.thing(result))) 123 | } 124 | } 125 | 126 | case .sleep(let interval): 127 | return await withCheckedContinuation { continuation in 128 | DispatchQueue.global().asyncAfter(deadline: .now() + interval) { 129 | continuation.resume(with: .success(.none)) 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | struct DependencyProvider {} 137 | 138 | enum TestMethodResult: Codable { 139 | case thing(ThingResult) 140 | case none 141 | } 142 | 143 | struct ThingParameters: Codable { 144 | let message: String 145 | } 146 | 147 | struct ThingResult: Codable { 148 | let message: String 149 | } 150 | --------------------------------------------------------------------------------