├── .github ├── release-drafter.yml └── workflows │ ├── docs.yml │ ├── homebrew.yml │ ├── release-drafter.yml │ ├── release.yml │ ├── swift.yml │ └── swiftlint.yml ├── .gitignore ├── .jazzy.yml ├── .swiftlint.yml ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Grebe-CLI │ ├── Arguments.swift │ ├── Commands │ │ ├── Command.swift │ │ ├── GenerateCommand.swift │ │ └── SetupCommand.swift │ ├── Files │ │ ├── Gitignore.swift │ │ ├── IWritableFile.swift │ │ ├── PackageSwift.swift │ │ └── Readme.swift │ └── main.swift ├── Grebe-Framework │ ├── Calls │ │ ├── GBidirectionalStreamingCall.swift │ │ ├── GClientStreamingCall.swift │ │ ├── GServerStreamingCall.swift │ │ ├── GUnaryCall.swift │ │ └── IGCall.swift │ └── Client │ │ ├── GClient.swift │ │ ├── GRPCClientInitializable.swift │ │ └── IGClient.swift └── Grebe-Generate │ ├── CommandLineTool.swift │ ├── Generator-Grebe.swift │ ├── Generator.swift │ ├── ProtoFile.swift │ ├── ProtoMethod.swift │ ├── ProtoService.swift │ └── main.swift └── Tests ├── Grebe-FrameworkTests ├── Call Tests │ ├── BaseCallTest.swift │ ├── BidirectionalStreamingCallTests.swift │ ├── ClientStreamingCallTests.swift │ ├── ServerStreamingCallTests.swift │ └── UnaryCallTests.swift ├── Extensions │ └── Publisher+Extensions.swift ├── Generated │ ├── Echo+Init.swift │ ├── test_scenarios.grpc.swift │ ├── test_scenarios.pb.swift │ └── test_scenarios.proto ├── Mock Clients │ ├── BaseMockClient.swift │ ├── BidrectionalStreamingMockClient.swift │ ├── CallMocks.swift │ ├── ClientStreamingMockClient.swift │ ├── ServerStreamingMockClient.swift │ └── UnaryServiceMockClient.swift └── XCTestManifests.swift └── LinuxMain.swift /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | branches: [release] 2 | name-template: '$NEXT_PATCH_VERSION' 3 | tag-template: '$NEXT_PATCH_VERSION' 4 | categories: 5 | - title: '🚀 Features' 6 | labels: 7 | - 'feature' 8 | - 'enhancement' 9 | - title: '🐛 Bug Fixes' 10 | labels: 11 | - 'fix' 12 | - 'bugfix' 13 | - 'bug' 14 | - title: '🧰 Maintenance' 15 | label: 'chore' 16 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 17 | template: | 18 | ## Changes 19 | 20 | $CHANGES 21 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy_docs: 9 | runs-on: macos-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Publish Jazzy Docs 13 | uses: steven0351/publish-jazzy-docs@v1 14 | with: 15 | personal_access_token: ${{ secrets.ACCESS_TOKEN }} 16 | config: .jazzy.yml 17 | -------------------------------------------------------------------------------- /.github/workflows/homebrew.yml: -------------------------------------------------------------------------------- 1 | name: Update Hombrew Tap 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | deployTap: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Update Homebrew formula 11 | uses: dawidd6/action-homebrew-bump-formula@v2 12 | with: 13 | token: ${{ secrets.HOMEBREW }} 14 | formula: apodini/tap/grebe 15 | force: false 16 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | - name: Notify Example Project 16 | run: | 17 | curl -XPOST -u "${{ secrets.PAT_USERNAME}}:${{secrets.ACCESS_TOKEN}}"\ 18 | -H "Accept: application/vnd.github.everest-preview+json"\ 19 | -H "Content-Type: application/json" https://api.github.com/repos/YOURNAME/APPLICATION_NAME/dispatches --data '{"event_type": "build_application"}' 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: GH Release 13 | uses: softprops/action-gh-release@v0.1.5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ develop, release ] 6 | pull_request: 7 | branches: [ develop, release ] 8 | 9 | jobs: 10 | macos: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Xcode version check 15 | run: xcodebuild -version 16 | - name: Check Swift version 17 | run: swift --version 18 | - name: Build 19 | run: swift build 20 | - name: Test 21 | run: swift test 22 | linux: 23 | container: 24 | image: swift:${{ matrix.linux }} 25 | runs-on: ubuntu-latest 26 | strategy: 27 | matrix: 28 | linux: [latest, xenial, focal, amazonlinux2, centos8] 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Check Swift version 32 | run: swift --version 33 | - name: Build 34 | run: swift build 35 | - name: Test 36 | run: swift test --enable-test-discovery --sanitize=thread 37 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | 10 | jobs: 11 | swiftlint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: GitHub Action for SwiftLint 16 | uses: norio-nomura/action-swiftlint@3.1.0 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Apodini .gitignore File 2 | 3 | # Swift Package Manager 4 | Package.resolved 5 | *.xcodeproj 6 | .swiftpm 7 | .build/ 8 | 9 | # Xcode User settings 10 | xcuserdata/ 11 | 12 | # Other files 13 | .DS_Store 14 | 15 | # Fastlane 16 | fastlane/report.xml 17 | fastlane/Preview.html 18 | fastlane/screenshots/**/*.png 19 | fastlane/test_output 20 | -------------------------------------------------------------------------------- /.jazzy.yml: -------------------------------------------------------------------------------- 1 | module: Grebe 2 | author: Apodini 3 | theme: fullwidth 4 | output: ./docs 5 | documentation: ./*.md 6 | author_url: https://github.com/Apodini 7 | github_url: https://github.com/Apodini/Grebe 8 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Apodini SwiftLint file Apodini 2 | 3 | # The whitelist_rules configuration also includes rules that are enabled by default to provide a good overview of all rules. 4 | whitelist_rules: 5 | - anyobject_protocol 6 | # Prefer using AnyObject over class for class-only protocols. 7 | - array_init 8 | # Prefer using Array(seq) over seq.map { $0 } to convert a sequence into an Array. 9 | - block_based_kvo 10 | # Prefer the new block based KVO API with keypaths when using Swift 3.2 or later. 11 | - class_delegate_protocol 12 | # Delegate protocols should be class-only so they can be weakly referenced. 13 | - closing_brace 14 | # Closing brace with closing parenthesis should not have any whitespaces in the middle. 15 | - closure_body_length 16 | # Closure bodies should not span too many lines. 17 | - closure_end_indentation 18 | # Closure end should have the same indentation as the line that started it. 19 | - closure_parameter_position 20 | # Closure parameters should be on the same line as opening brace. 21 | - closure_spacing 22 | # Closure expressions should have a single space inside each brace. 23 | - collection_alignment 24 | # All elements in a collection literal should be vertically aligned 25 | - colon 26 | # Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. 27 | - comma 28 | # There should be no space before and one after any comma. 29 | - compiler_protocol_init 30 | # The initializers declared in compiler protocols such as ExpressibleByArrayLiteral shouldn't be called directly. 31 | - conditional_returns_on_newline 32 | # Conditional statements should always return on the next line 33 | - contains_over_filter_count 34 | # Prefer contains over comparing filter(where:).count to 0. 35 | - contains_over_filter_is_empty 36 | # Prefer contains over using filter(where:).isEmpty 37 | - contains_over_first_not_nil 38 | # Prefer `contains` over `first(where:) != nil` 39 | - contains_over_range_nil_comparison 40 | # Prefer contains over range(of:) != nil and range(of:) == nil 41 | - control_statement 42 | # if, for, guard, switch, while, and catch statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses. 43 | - convenience_type 44 | # Types used for hosting only static members should be implemented as a caseless enum to avoid instantiation. 45 | - cyclomatic_complexity 46 | # Complexity of function bodies should be limited. 47 | - discarded_notification_center_observer 48 | # When registering for a notification using a block, the opaque observer that is returned should be stored so it can be removed later. 49 | - discouraged_direct_init 50 | # Discouraged direct initialization of types that can be harmful. e.g. UIDevice(), Bundle() 51 | - discouraged_optional_boolean 52 | # Prefer non-optional booleans over optional booleans. 53 | # - discouraged_optional_collection # Enable as soon as https://github.com/realm/SwiftLint/issues/2298 is fixed 54 | # Prefer empty collection over optional collection. 55 | - duplicate_imports 56 | # Duplicate Imports 57 | - dynamic_inline 58 | # Avoid using 'dynamic' and '@inline(__always)' together. 59 | - empty_collection_literal 60 | # Prefer checking isEmpty over comparing collection to an empty array or dictionary literal. 61 | - empty_count 62 | # Prefer checking `isEmpty` over comparing `count` to zero. 63 | - empty_enum_arguments 64 | # Arguments can be omitted when matching enums with associated types if they are not used. 65 | - empty_parameters 66 | # Prefer () -> over Void ->. 67 | - empty_parentheses_with_trailing_closure 68 | # When using trailing closures, empty parentheses should be avoided after the method call. 69 | - empty_string 70 | # Prefer checking `isEmpty` over comparing string to an empty string literal. 71 | - empty_xctest_method 72 | # Empty XCTest method should be avoided. 73 | - enum_case_associated_values_count 74 | # Number of associated values in an enum case should be low 75 | - explicit_init 76 | # Explicitly calling .init() should be avoided. 77 | - fatal_error_message 78 | # A fatalError call should have a message. 79 | - file_length 80 | # Files should not span too many lines. 81 | # See file_length below for the exact configuration. 82 | - first_where 83 | # Prefer using ``.first(where:)`` over ``.filter { }.first` in collections. 84 | - flatmap_over_map_reduce 85 | # Prefer flatMap over map followed by reduce([], +). 86 | - for_where 87 | # where clauses are preferred over a single if inside a for. 88 | - force_cast 89 | # Force casts should be avoided. 90 | - force_try 91 | # Force tries should be avoided. 92 | - force_unwrapping 93 | # Force unwrapping should be avoided. 94 | - function_body_length 95 | # Functions bodies should not span too many lines. 96 | # See function_body_length below for the exact configuration. 97 | - function_parameter_count 98 | # Number of function parameters should be low. 99 | # See function_parameter_count below for the exact configuration. 100 | - generic_type_name 101 | # Generic type name should only contain alphanumeric characters, start with an uppercase character and span between 1 and 20 characters in length. 102 | - identical_operands 103 | # Comparing two identical operands is likely a mistake. 104 | - identifier_name 105 | # Identifier names should only contain alphanumeric characters and start with a lowercase character or should only contain capital letters. 106 | # In an exception to the above, variable names may start with a capital letter when they are declared static and immutable. 107 | # Variable names should not be too long or too short. Excluded names are listed below. 108 | - implicit_getter 109 | # Computed read-only properties and subscripts should avoid using the get keyword. 110 | - implicit_return 111 | # Prefer implicit returns in closures. 112 | - implicitly_unwrapped_optional 113 | # Implicitly unwrapped optionals should be avoided when possible. 114 | - inert_defer 115 | # If defer is at the end of its parent scope, it will be executed right where it is anyway. 116 | - is_disjoint 117 | # Prefer using Set.isDisjoint(with:) over Set.intersection(_:).isEmpty. 118 | - joined_default_parameter 119 | # Discouraged explicit usage of the default separator. 120 | - large_tuple 121 | # Tuples shouldn't have too many members. Create a custom type instead. 122 | # See large_tuple below for the exact configuration. 123 | - last_where 124 | # Prefer using .last(where:) over .filter { }.last in collections. 125 | - leading_whitespace 126 | # Files should not contain leading whitespace. 127 | - legacy_cggeometry_functions 128 | # CGGeometry: Struct extension properties and methods are preferred over legacy functions 129 | - legacy_constant 130 | # Struct-scoped constants are preferred over legacy global constants (CGSize, CGRect, NSPoint, ...). 131 | - legacy_constructor 132 | # Swift constructors are preferred over legacy convenience functions (CGPointMake, CGSizeMake, UIOffsetMake, ...). 133 | - legacy_hashing 134 | # Prefer using the hash(into:) function instead of overriding hashValue 135 | - legacy_multiple 136 | # Prefer using the isMultiple(of:) function instead of using the remainder operator (%). 137 | - legacy_nsgeometry_functions 138 | # Struct extension properties and methods are preferred over legacy functions 139 | - legacy_random 140 | # Prefer using type.random(in:) over legacy functions. 141 | - line_length 142 | # Lines should not span too many characters. 143 | # See line_length below for the exact configuration. 144 | - literal_expression_end_indentation 145 | # Array and dictionary literal end should have the same indentation as the line that started it. 146 | - lower_acl_than_parent 147 | # Ensure definitions have a lower access control level than their enclosing parent 148 | - mark 149 | # MARK comment should be in valid format. e.g. '// MARK: ...' or '// MARK: - ...' 150 | - missing_docs 151 | # Declarations should be documented. 152 | - modifier_order 153 | # Modifier order should be consistent. 154 | - multiline_arguments 155 | # Arguments should be either on the same line, or one per line. 156 | - multiline_function_chains 157 | # Chained function calls should be either on the same line, or one per line. 158 | - multiline_literal_brackets 159 | # Multiline literals should have their surrounding brackets in a new line. 160 | - multiline_parameters 161 | # Functions and methods parameters should be either on the same line, or one per line. 162 | - nesting 163 | # Types and statements should only be nested to a certain level deep. 164 | # See nesting below for the exact configuration. 165 | - nimble_operator 166 | # Prefer Nimble operator overloads over free matcher functions. 167 | - no_fallthrough_only 168 | # Fallthroughs can only be used if the case contains at least one other statement. 169 | - no_space_in_method_call 170 | # Don’t add a space between the method name and the parentheses. 171 | - notification_center_detachment 172 | # An object should only remove itself as an observer in deinit. 173 | - nslocalizedstring_key 174 | # Static strings should be used as key in NSLocalizedString in order to genstrings work. 175 | - nsobject_prefer_isequal 176 | # NSObject subclasses should implement isEqual instead of ==. 177 | - object_literal 178 | # Prefer object literals over image and color inits. 179 | - opening_brace 180 | # Opening braces should be preceded by a single space and on the same line as the declaration. 181 | - operator_usage_whitespace 182 | # Operators should be surrounded by a single whitespace when they are being used. 183 | - operator_whitespace 184 | # Operators should be surrounded by a single whitespace when defining them. 185 | - optional_enum_case_matching 186 | # Matching an enum case against an optional enum without ‘?’ is supported on Swift 5.1 and above. 187 | - orphaned_doc_comment 188 | # A doc comment should be attached to a declaration. 189 | - overridden_super_call 190 | # Some overridden methods should always call super 191 | - pattern_matching_keywords 192 | # Combine multiple pattern matching bindings by moving keywords out of tuples. 193 | - prefer_self_type_over_type_of_self 194 | # Prefer Self over type(of: self) when accessing properties or calling methods. 195 | - private_action 196 | # IBActions should be private. 197 | - private_outlet 198 | # IBOutlets should be private to avoid leaking UIKit to higher layers. 199 | - private_over_fileprivate 200 | # Prefer private over fileprivate declarations. 201 | - private_unit_test 202 | # Unit tests marked private are silently skipped. 203 | - prohibited_super_call 204 | # Some methods should not call super ( 205 | # NSFileProviderExtension: providePlaceholder(at:completionHandler:) 206 | # NSTextInput doCommand(by:) 207 | # NSView updateLayer() 208 | # UIViewController loadView()) 209 | - protocol_property_accessors_order 210 | # When declaring properties in protocols, the order of accessors should be get set. 211 | - reduce_boolean 212 | # Prefer using .allSatisfy() or .contains() over reduce(true) or reduce(false) 213 | - reduce_into 214 | # Prefer reduce(into:_:) over reduce(_:_:) for copy-on-write types 215 | - redundant_discardable_let 216 | # Prefer _ = foo() over let _ = foo() when discarding a result from a function. 217 | - redundant_nil_coalescing 218 | # nil coalescing operator is only evaluated if the lhs is nil, coalescing operator with nil as rhs is redundant 219 | - redundant_objc_attribute 220 | # Objective-C attribute (@objc) is redundant in declaration. 221 | - redundant_optional_initialization 222 | # Initializing an optional variable with nil is redundant. 223 | - redundant_set_access_control 224 | # Property setter access level shouldn't be explicit if it's the same as the variable access level. 225 | - redundant_string_enum_value 226 | # String enum values can be omitted when they are equal to the enumcase name. 227 | - redundant_type_annotation 228 | # Variables should not have redundant type annotation 229 | - redundant_void_return 230 | # Returning Void in a function declaration is redundant. 231 | - return_arrow_whitespace 232 | # Return arrow and return type should be separated by a single space or on a separate line. 233 | - shorthand_operator 234 | # Prefer shorthand operators (+=, -=, *=, /=) over doing the operation and assigning. 235 | - single_test_class 236 | # Test files should contain a single QuickSpec or XCTestCase class. 237 | - sorted_first_last 238 | # Prefer using `min()`` or `max()`` over `sorted().first` or `sorted().last` 239 | - statement_position 240 | # Else and catch should be on the same line, one space after the previous declaration. 241 | - static_operator 242 | # Operators should be declared as static functions, not free functions. 243 | - superfluous_disable_command 244 | # SwiftLint ‘disable’ commands are superfluous when the disabled rule would not have triggered a violation in the disabled region. Use “ - ” if you wish to document a command. 245 | - switch_case_alignment 246 | # Case statements should vertically align with their enclosing switch statement, or indented if configured otherwise. 247 | - syntactic_sugar 248 | # Shorthand syntactic sugar should be used, i.e. [Int] instead of Array. 249 | - todo 250 | # TODOs and FIXMEs should be resolved. 251 | - toggle_bool 252 | # Prefer someBool.toggle() over someBool = !someBool. 253 | - trailing_closure 254 | # Trailing closure syntax should be used whenever possible. 255 | - trailing_comma 256 | # Trailing commas in arrays and dictionaries should be avoided/enforced. 257 | - trailing_newline 258 | # Files should have a single trailing newline. 259 | - trailing_semicolon 260 | # Lines should not have trailing semicolons. 261 | - trailing_whitespace 262 | # Lines should not have trailing whitespace. 263 | # Ignored lines are specified below. 264 | - type_body_length 265 | # Type bodies should not span too many lines. 266 | # See large_tuple below for the exact configuration. 267 | - type_name 268 | # Type name should only contain alphanumeric characters, start with an uppercase character and span between 3 and 40 characters in length. 269 | # Excluded types are listed below. 270 | - unavailable_function 271 | # Unimplemented functions should be marked as unavailable. 272 | - unneeded_break_in_switch 273 | # Avoid using unneeded break statements. 274 | - unneeded_parentheses_in_closure_argument 275 | # Parentheses are not needed when declaring closure arguments. 276 | - untyped_error_in_catch 277 | # Catch statements should not declare error variables without type casting. 278 | - unused_capture_list 279 | # Unused reference in a capture list should be removed. 280 | - unused_closure_parameter 281 | # Unused parameter in a closure should be replaced with _. 282 | - unused_control_flow_label 283 | # Unused control flow label should be removed. 284 | - unused_declaration 285 | # Declarations should be referenced at least once within all files linted. 286 | - unused_enumerated 287 | # When the index or the item is not used, .enumerated() can be removed. 288 | - unused_import 289 | # All imported modules should be required to make the file compile. 290 | - unused_optional_binding 291 | # Prefer != nil over let _ = 292 | - unused_setter_value 293 | # Setter value is not used. 294 | - valid_ibinspectable 295 | # @IBInspectable should be applied to variables only, have its type explicit and be of a supported type 296 | - vertical_parameter_alignment 297 | # Function parameters should be aligned vertically if they're in multiple lines in a declaration. 298 | - vertical_parameter_alignment_on_call 299 | # Function parameters should be aligned vertically if they're in multiple lines in a method call. 300 | - vertical_whitespace 301 | # Limit vertical whitespace to a single empty line. 302 | # See vertical_whitespace below for the exact configuration. 303 | - vertical_whitespace_closing_braces 304 | # Don’t include vertical whitespace (empty line) before closing braces. 305 | - vertical_whitespace_opening_braces 306 | # Don’t include vertical whitespace (empty line) after opening braces. 307 | - void_return 308 | # Prefer -> Void over -> (). 309 | - weak_delegate 310 | # Delegates should be weak to avoid reference cycles. 311 | - xctfail_message 312 | # An XCTFail call should include a description of the assertion. 313 | - yoda_condition 314 | # The variable should be placed on the left, the constant on the right of a comparison operator. 315 | 316 | excluded: # paths to ignore during linting. Takes precedence over `included`. 317 | - Carthage 318 | - Pods 319 | - .build 320 | - .swiftpm 321 | - R.generated.swift 322 | - Tests/Grebe-FrameworkTests/Generated 323 | 324 | closure_body_length: # Closure bodies should not span too many lines. 325 | - 35 # warning - default: 20 326 | - 35 # error - default: 100 327 | 328 | enum_case_associated_values_count: # Number of associated values in an enum case should be low 329 | - 5 # warning - default: 5 330 | - 5 # error - default: 6 331 | 332 | file_length: # Files should not span too many lines. 333 | - 500 # warning - default: 400 334 | - 500 # error - default: 1000 335 | 336 | function_body_length: # Functions bodies should not span too many lines. 337 | - 50 # warning - default: 40 338 | - 50 # error - default: 100 339 | 340 | function_parameter_count: # Number of function parameters should be low. 341 | - 5 # warning - default: 5 342 | - 5 # error - default: 8 343 | 344 | identifier_name: 345 | excluded: # excluded names 346 | - id 347 | - ok 348 | - or 349 | 350 | large_tuple: # Tuples shouldn't have too many members. Create a custom type instead. 351 | - 2 # warning - default: 2 352 | - 2 # error - default: 3 353 | 354 | line_length: # Lines should not span too many characters. 355 | warning: 150 # default: 120 356 | error: 150 # default: 200 357 | ignores_comments: true # default: false 358 | ignores_urls: true # default: false 359 | ignores_function_declarations: false # default: false 360 | ignores_interpolated_strings: true # default: false 361 | 362 | nesting: # Types should be nested at most 2 level deep, and statements should be nested at most 5 levels deep. 363 | type_level: 364 | warning: 2 # warning - default: 1 365 | statement_level: 366 | warning: 5 # warning - default: 5 367 | 368 | trailing_closure: 369 | only_single_muted_parameter: true 370 | 371 | type_body_length: # Type bodies should not span too many lines. 372 | - 250 # warning - default: 200 373 | - 250 # error - default: 200 374 | 375 | type_name: 376 | excluded: # excluded names 377 | - ID 378 | 379 | trailing_whitespace: 380 | ignores_empty_lines: true # default: false 381 | ignores_comments: true # default: false 382 | 383 | unused_optional_binding: 384 | ignore_optional_try: true 385 | 386 | vertical_whitespace: # Limit vertical whitespace to a single empty line. 387 | max_empty_lines: 2 # warning - default: 1 388 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Apodini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "grpc-swift", 6 | "repositoryURL": "https://github.com/grpc/grpc-swift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "dca180f7f7f3bdd495d294ca2a463a82b5e33900", 10 | "version": "1.0.0-alpha.13" 11 | } 12 | }, 13 | { 14 | "package": "swift-argument-parser", 15 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 16 | "state": { 17 | "branch": null, 18 | "revision": "3d79b2b5a2e5af52c14e462044702ea7728f5770", 19 | "version": "0.1.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-log", 24 | "repositoryURL": "https://github.com/apple/swift-log.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", 28 | "version": "1.2.0" 29 | } 30 | }, 31 | { 32 | "package": "swift-nio", 33 | "repositoryURL": "https://github.com/apple/swift-nio.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "e876fb37410e0036b98b5361bb18e6854739572b", 37 | "version": "2.16.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-nio-http2", 42 | "repositoryURL": "https://github.com/apple/swift-nio-http2.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "c5d10f4165128c3d0cc0e3c0f0a8ef55947a73a6", 46 | "version": "1.12.2" 47 | } 48 | }, 49 | { 50 | "package": "swift-nio-ssl", 51 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "ae213938e151964aa691f0e902462fbe06baeeb6", 55 | "version": "2.7.1" 56 | } 57 | }, 58 | { 59 | "package": "swift-nio-transport-services", 60 | "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "46cc01e461bfb4001b3ccd8fe016d5b6d354e42a", 64 | "version": "1.4.0" 65 | } 66 | }, 67 | { 68 | "package": "SwiftProtobuf", 69 | "repositoryURL": "https://github.com/apple/swift-protobuf.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "7790acf0a81d08429cb20375bf42a8c7d279c5fe", 73 | "version": "1.8.0" 74 | } 75 | } 76 | ] 77 | }, 78 | "version": 1 79 | } 80 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Grebe", 8 | platforms: [ 9 | .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6) 10 | ], 11 | products: [ 12 | .library(name: "Grebe-Framework", targets: ["Grebe-Framework"]), 13 | .executable(name: "Grebe-Generate", targets: ["Grebe-Generate"]), 14 | .executable(name: "grebe", targets: ["Grebe-CLI"]) 15 | ], 16 | dependencies: [ 17 | .package( 18 | url: "https://github.com/grpc/grpc-swift.git", 19 | from: "1.0.0-alpha.13" 20 | ), 21 | .package( 22 | name: "SwiftProtobuf", 23 | url: "https://github.com/apple/swift-protobuf.git", 24 | from: "1.7.0" 25 | ), 26 | .package( 27 | url: "https://github.com/apple/swift-argument-parser", 28 | .upToNextMinor(from: "0.1.0") 29 | ) 30 | ], 31 | targets: [ 32 | .target( 33 | name: "Grebe-Framework", 34 | dependencies: [ 35 | .product(name: "GRPC", package: "grpc-swift") 36 | ] 37 | ), 38 | .target( 39 | name: "Grebe-Generate", 40 | dependencies: [ 41 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 42 | .product(name: "SwiftProtobufPluginLibrary", package: "SwiftProtobuf") 43 | ] 44 | ), 45 | .target( 46 | name: "Grebe-CLI", 47 | dependencies: [ 48 | .product(name: "ArgumentParser", package: "swift-argument-parser") 49 | ] 50 | ), 51 | .testTarget( 52 | name: "Grebe-FrameworkTests", 53 | dependencies: [ 54 | .target(name: "Grebe-Framework") 55 | ] 56 | ) 57 | ] 58 | ) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grebe 2 | 3 | This repository is intended to improve and simplify the gRPC development workflow for iOS apps and provides a small wrapper to incorporate Swift Combine in gRPC. The Swift Package contains the following parts: 4 | - A Swift Combine gRPC wrapper. Read more about it in [this section](#swift-combine-grpc-wrapper). 5 | - A code generator to generate Swift code using the previously stated library. 6 | - A CLI tool to simplify the development workflow. You can import the library without using the CLI tool. Read more about it in [this section](#building-the-grebe-swift-package) 7 | 8 | ## Installation 9 | You can install the Grebe CLI executable via [Homebrew](https://brew.sh). To install run the following command in your command line: 10 | ```bash 11 | brew install apodini/tap/grebe 12 | ``` 13 | Of course you can simply clone the repository or install it via the [Swift Package Manager](https://swift.org/package-manager/). 14 | 15 | ## Usage 16 | The recommended way to use Grebe is to first define an API using the [Protocol Buffer](https://developers.google.com/protocol-buffers/) language. 17 | 18 | ### Setting up Grebe 19 | To use Grebe you need to install the [Protocol Buffer Compiler](https://github.com/protocolbuffers/protobuf), the [Swift Protobuf Code Generator Plugin](https://github.com/apple/swift-protobuf) and [Swift gRPC](https://github.com/grpc/grpc-swift) plugins to generate the necessary support code. To do all this in one step, run the following command in your command line. Make sure to specify a directory that is part of your `PATH` environment variable. 20 | ```bash 21 | grebe setup -p 22 | ``` 23 | 24 | ### Building the Grebe Swift Package 25 | This step generates a Swift Package that contains the Protocol Buffer support code, the gRPC interface code and the Grebe interface code. It hides the complete `gRPC` implementation and exposes only the methods, services and message types defined in your proto file. You can easily intergrate it into your project via drag and drop. It is not part of the main target and therefore offers a clear public interface. 26 | 27 | After you run the setup command described in the previous step you are ready to build the Swift Package: 28 | 29 | ```bash 30 | grebe generate -p 31 | ``` 32 | 33 | This command will do the following: 34 | 1. Load the latest version of the Grebe code generator (unless otherwise stated) to generate Swift code which projects the service methods defined in your proto file to simple Swift methods using our library. 35 | 2. Invoke the `protoc-gen-swift` and `protoc-gen-swiftgrpc` plugins on your proto file. 36 | 3. Bundle all generated code in a Swift Package. 37 | 38 | #### Parameters 39 | 40 | | Flag | Values | Default | Description | 41 | | -------------------- | -------------- | ---------------- | ------------------------------------------------ | 42 | | `-p`/`--proto` | `String` | `` | The path of your proto file | 43 | | `-d`/`--destination` | `String` | `` | The path of the generated Swift Package | 44 | | `-e`/`--pathDirectory` | `String` | `/usr/local/bin` | Your shell path | 45 | | `-v`/`--version` | `Double` | 1.0 | The version number of the Grebe-Generator Plugin | 46 | | `-g`/`--grebeGenerate` | `true`/`false` | `true` | Wether to generate only Grebe files | 47 | | `-s`/`--swiftGrpcGenerate` | `true`/`false` | `true` | Wether to generate only gRPC-Swift files | 48 | 49 | ### Using the generated Swift Package 50 | Drag the package folder into your Xcode project. Then click the Plus button in the "Link Binary with Libraries" section, locate the package in the modal dialog, select the gray library icon inside the package, and add this one. In all files you would like to use the package import `Grebe_Generated`. 51 | 52 | #### Example 53 | Consider the following protobuf definition for a simple echo service. The service defines one unary RPC. You send one message and it echoes the message back to you. 54 | ```proto 55 | syntax = "proto3"; 56 | 57 | service EchoService { 58 | rpc echo (EchoRequest) returns (EchoResponse); 59 | } 60 | 61 | message EchoRequest { 62 | string message = 1; 63 | } 64 | 65 | message EchoResponse { 66 | string message = 1; 67 | } 68 | ``` 69 | 70 | The code generator will create following Swift file: 71 | ```swift 72 | extension EchoServiceService: GRPCClientInitializable { 73 | func echo( 74 | request: EchoRequest, 75 | callOptions: CallOptions? = defaultCallOptions 76 | ) -> AnyPublisher { 77 | GUnaryCall(request: request, callOptions: callOptions, closure: echo).execute() 78 | } 79 | } 80 | ``` 81 | Now just call the generated method: 82 | ```swift 83 | echo(request: EchoRequest.with { $0.message = "hello"}) 84 | ``` 85 | 86 | ## Swift Combine gRPC Wrapper 87 | This library provides a [Swift-Combine](https://developer.apple.com/documentation/combine) wrapper for [Swift-gRPC](https://github.com/grpc/grpc-swift/tree/nio). It is a generic abstraction 88 | layer above the `nio` layer provided by the `Swift-gRPC` implementation. It supports all four gRPC API styles (Unary, Server Streaming, Client Streaming, and Bidirectional Streaming). 89 | 90 | ### Example 91 | Again consider the following protobuf definition for a simple echo service. 92 | ```proto 93 | syntax = "proto3"; 94 | 95 | service EchoService { 96 | rpc echo (EchoRequest) returns (EchoResponse); 97 | } 98 | 99 | message EchoRequest { 100 | string message = 1; 101 | } 102 | 103 | message EchoResponse { 104 | string message = 1; 105 | } 106 | ``` 107 | 108 | Let's set up the client: 109 | ```swift 110 | let client = GClient(target: .hostAndPort("localhost", 62801)) 111 | ``` 112 | 113 | To call the service, create a `GUnaryCall` and use it's `execute` method. You provide it with a `EchoRequest` and get back a stream `AnyPublisher AnyPublisher { 46 | GUnaryCall(request: request, callOptions: callOptions, closure: echo).execute() 47 | } 48 | } 49 | ``` 50 | Now just call the generated method: 51 | ```swift 52 | echo(request: EchoRequest.with { $0.message = "hello"}) 53 | ``` 54 | """ 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Grebe-CLI/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 17.01.20. 6 | // 7 | 8 | import ArgumentParser 9 | import Foundation 10 | 11 | struct Grebe: ParsableCommand { 12 | static var configuration = CommandConfiguration( 13 | subcommands: [Generate.self, Setup.self], 14 | defaultSubcommand: Generate.self 15 | ) 16 | } 17 | 18 | extension Grebe { 19 | struct Generate: ParsableCommand { 20 | @Option(name: .shortAndLong, help: "Path to the proto file") 21 | var proto: String 22 | 23 | @Option(name: .shortAndLong, help: "Path of the generated Swift Package") 24 | var destination: String 25 | 26 | @Option(name: .shortAndLong, help: "Path to a PATH directory") 27 | var pathDirectory: String? 28 | 29 | @Option(name: .shortAndLong, default: "0.0.5", help: "Version number of Grebe Code Generator") 30 | var version: String 31 | 32 | @Option(name: .shortAndLong, default: true, help: "Generate gRPC-Swift files") 33 | var grebeGenerate: Bool 34 | 35 | @Option(name: .shortAndLong, default: true, help: "Generate Grebe files") 36 | var swiftGrpcGenerate: Bool 37 | 38 | func run() { 39 | let arguments = Arguments( 40 | protoPath: proto, 41 | destinationPath: destination, 42 | executablePath: pathDirectory ?? "/usr/local/bin", 43 | versionNumber: version, 44 | grebeGenerate: grebeGenerate, 45 | grpcGenerate: swiftGrpcGenerate 46 | ) 47 | 48 | do { 49 | try GenerateCommand(arguments: arguments).run() 50 | } catch { 51 | print("An error occurred") 52 | } 53 | } 54 | } 55 | 56 | struct Setup: ParsableCommand { 57 | @Option(name: .shortAndLong, help: "Path to a PATH directory") 58 | var pathDirectory: String? 59 | 60 | func run() { 61 | do { 62 | try SetupCommand( 63 | envPath: pathDirectory ?? "/usr/local/bin" 64 | ).run() 65 | } catch { 66 | print("An error occurred") 67 | } 68 | } 69 | } 70 | } 71 | 72 | Grebe.main() 73 | -------------------------------------------------------------------------------- /Sources/Grebe-Framework/Calls/GBidirectionalStreamingCall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GBidirectionalStreamingCall.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 07.12.19. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import GRPC 11 | import SwiftProtobuf 12 | 13 | /// #### A bidirectional streaming Grebe call. 14 | /// 15 | /// Both sides, the client and the server, send a sequence of messages. The two streams 16 | /// operate independently, so clients and servers can read and write and whatever 17 | /// oder they like: for example, the server could wait to receive all the client messages 18 | /// before writing its responses, or it could alternately read a message then write a 19 | /// message, or some other combination of reads and writes. 20 | /// 21 | /// ##### Example usage of `GBidirectionalStreamingCall` 22 | /// 23 | /// Consider the following protobuf definition for a simple echo service. 24 | /// The service defines one bidirectional streaming RPC. You send a stream of messages and it 25 | /// echoes a stream of messages back to you. 26 | /// 27 | /// ```proto 28 | /// syntax = "proto3"; 29 | /// 30 | /// service EchoService { 31 | /// rpc echo (stream EchoRequest) returns (stream EchoResponse); 32 | /// } 33 | /// 34 | /// message EchoRequest { 35 | /// string message = 1; 36 | /// } 37 | /// 38 | /// message EchoResponse { 39 | /// string message = 1; 40 | /// } 41 | /// ``` 42 | /// 43 | /// You can create a `GBidirectionalStreamingCall` like this: 44 | /// ``` 45 | /// let requests = Publishers.Sequence<[EchoRequest], Error>( 46 | /// sequence: [EchoRequest.with { $0.message = "hello"}, EchoRequest.with { $0.message = "world"}] 47 | /// ).eraseToAnyPublisher() 48 | /// 49 | /// GBidirectionalStreamingCall(request: requests, callOptions: callOptions, closure: echo) 50 | /// ``` 51 | /// 52 | public class GBidirectionalStreamingCall: IGCall { 53 | public typealias CallClosure = ( 54 | _ callOptions: CallOptions?, 55 | _ handler: @escaping (Response) -> Void 56 | ) -> GRPC.BidirectionalStreamingCall 57 | 58 | /// The request message stream for the call. 59 | public var requests: AnyPublisher 60 | public let callClosure: CallClosure 61 | public let callOptions: CallOptions? 62 | 63 | /// Stores all cacellables. 64 | private var cancellables: Set = [] 65 | 66 | /** 67 | Sets up an new bidirectional streaming Grebe call. 68 | 69 | - Parameters: 70 | - request: The request message stream for the call. 71 | - callOptions: Options to use for each service call. 72 | - closure: The closure which contains the executable call. 73 | */ 74 | public init( 75 | requests: AnyPublisher, 76 | callOptions: CallOptions? = nil, 77 | closure: @escaping CallClosure 78 | ) { 79 | self.requests = requests 80 | self.callClosure = closure 81 | self.callOptions = callOptions 82 | } 83 | 84 | /** 85 | Executes the Grebe bidirectional streaming call. 86 | 87 | - Returns: A stream of `Response` elements. The response publisher may fail 88 | with a `GRPCStatus` error. 89 | */ 90 | public func execute() -> AnyPublisher { 91 | let subject = PassthroughSubject() 92 | 93 | let call = callClosure(callOptions) { response in 94 | subject.send(response) 95 | } 96 | 97 | requests 98 | .sink(receiveCompletion: { completion in 99 | switch completion { 100 | case .finished: 101 | call.sendEnd(promise: nil) 102 | case .failure: 103 | _ = call.cancel() 104 | } 105 | }, receiveValue: { message in 106 | call.sendMessage(message, promise: nil) 107 | }) 108 | .store(in: &cancellables) 109 | 110 | call.status.whenSuccess { 111 | subject.send(completion: $0.code == .ok ? .finished : .failure($0)) 112 | } 113 | 114 | return subject.eraseToAnyPublisher() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/Grebe-Framework/Calls/GClientStreamingCall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GClientStreamingCall.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 07.12.19. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import GRPC 11 | import SwiftProtobuf 12 | 13 | /// #### A client streaming Grebe call. 14 | /// 15 | /// The client sends a sequence of request messages to the server. Once the client 16 | /// has finished writing the messages it waits for the server to read them and 17 | /// return its response 18 | /// 19 | /// ##### Example usage of `GClientStreamingCall` 20 | /// 21 | /// Consider the following protobuf definition for a simple echo service. 22 | /// The service defines one client streaming RPC. You send a stream of messages and it 23 | /// sends one messages back to you. 24 | /// 25 | /// ```proto 26 | /// syntax = "proto3"; 27 | /// 28 | /// service EchoService { 29 | /// rpc echo (stream EchoRequest) returns (EchoResponse); 30 | /// } 31 | /// 32 | /// message EchoRequest { 33 | /// string message = 1; 34 | /// } 35 | /// 36 | /// message EchoResponse { 37 | /// string message = 1; 38 | /// } 39 | /// ``` 40 | /// 41 | /// You can create a `GClientStreamingCall` like this: 42 | /// ``` 43 | /// let requests = Publishers.Sequence<[EchoRequest], Error>( 44 | /// sequence: [EchoRequest.with { $0.message = "hello"}, EchoRequest.with { $0.message = "world"}] 45 | /// ).eraseToAnyPublisher() 46 | /// 47 | /// GClientStreamingCall(request: requests, callOptions: callOptions, closure: echo) 48 | /// ``` 49 | /// 50 | public class GClientStreamingCall: IGCall { 51 | public typealias CallClosure = ( 52 | _ callOptions: CallOptions? 53 | ) -> GRPC.ClientStreamingCall 54 | 55 | /// The request message stream for the call. 56 | public var request: AnyPublisher 57 | public let callClosure: CallClosure 58 | public let callOptions: CallOptions? 59 | 60 | /// Stores all cacellables. 61 | private var cancellables: Set = [] 62 | 63 | /** 64 | Sets up an new client streaming Grebe call. 65 | 66 | - Parameters: 67 | - request: The request message stream for the call. 68 | - callOptions: Options to use for each service call. 69 | - closure: The closure which contains the executable call. 70 | */ 71 | public init( 72 | request: AnyPublisher, 73 | callOptions: CallOptions? = nil, 74 | closure: @escaping CallClosure 75 | ) { 76 | self.request = request 77 | self.callClosure = closure 78 | self.callOptions = callOptions 79 | } 80 | 81 | /** 82 | Executes the Grebe client streaming call. 83 | 84 | - Returns: A stream of `Response` elements. The response publisher may fail 85 | with a `GRPCStatus` error. 86 | */ 87 | public func execute() -> AnyPublisher { 88 | let subject = PassthroughSubject() 89 | let call = callClosure(callOptions) 90 | 91 | request 92 | .sink(receiveCompletion: { completion in 93 | switch completion { 94 | case .finished: 95 | call.sendEnd(promise: nil) 96 | case .failure: 97 | _ = call.cancel() 98 | } 99 | }, receiveValue: { message in 100 | call.sendMessage(message, promise: nil) 101 | }) 102 | .store(in: &cancellables) 103 | 104 | call.response.whenSuccess { 105 | subject.send($0) 106 | } 107 | call.status.whenSuccess { 108 | subject.send(completion: $0.code == .ok ? .finished : .failure($0)) 109 | } 110 | 111 | return subject.eraseToAnyPublisher() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/Grebe-Framework/Calls/GServerStreamingCall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GServerStreamingCall.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 07.12.19. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import GRPC 11 | import SwiftProtobuf 12 | 13 | /// #### A server streaming Grebe call. 14 | /// 15 | /// The client sends a request to the server and gets a sequence of response messages back. 16 | /// 17 | /// ##### Example usage of `GServerStreamingCall` 18 | /// 19 | /// Consider the following protobuf definition for a simple echo service. 20 | /// The service defines one server streaming RPC. You send one message and it 21 | /// echoes a stream of messages back to you. 22 | /// 23 | /// ```proto 24 | /// syntax = "proto3"; 25 | /// 26 | /// service EchoService { 27 | /// rpc echo (EchoRequest) returns (stream EchoResponse); 28 | /// } 29 | /// 30 | /// message EchoRequest { 31 | /// string message = 1; 32 | /// } 33 | /// 34 | /// message EchoResponse { 35 | /// string message = 1; 36 | /// } 37 | /// ``` 38 | /// 39 | /// You can create a `GServerStreamingCall` like this: 40 | /// ``` 41 | /// GServerStreamingCall(request: EchoRequest.with { $0.message = "hello"}, closure: echo) 42 | /// ``` 43 | /// 44 | public class GServerStreamingCall: IGCall { 45 | public typealias CallClosure = ( 46 | _ request: Request, 47 | _ callOptions: CallOptions?, 48 | _ handler: @escaping (Response) -> Void 49 | ) -> GRPC.ServerStreamingCall 50 | 51 | /// The request message for the call. 52 | public var request: Request 53 | public let callClosure: CallClosure 54 | public let callOptions: CallOptions? 55 | 56 | /** 57 | Sets up an new server streaming Grebe call. 58 | 59 | - Parameters: 60 | - request: The request message for the call. 61 | - callOptions: Options to use for each service call. 62 | - closure: The closure which contains the executable call. 63 | */ 64 | public init( 65 | request: Request, 66 | callOptions: CallOptions? = nil, 67 | closure: @escaping CallClosure 68 | ) { 69 | self.request = request 70 | self.callClosure = closure 71 | self.callOptions = callOptions 72 | } 73 | 74 | /** 75 | Executes the Grebe server streaming call. 76 | 77 | - Returns: A stream of `Response` elements. The response publisher may fail 78 | with a `GRPCStatus` error. 79 | */ 80 | public func execute() -> AnyPublisher { 81 | let subject = PassthroughSubject() 82 | 83 | let call = callClosure(request, callOptions) { response in 84 | subject.send(response) 85 | } 86 | 87 | call.status.whenSuccess { 88 | subject.send(completion: $0.code == .ok ? .finished : .failure($0)) 89 | } 90 | 91 | return subject.eraseToAnyPublisher() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/Grebe-Framework/Calls/GUnaryCall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GUnaryCall.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 07.12.19. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import GRPC 11 | import SwiftProtobuf 12 | 13 | /// #### A unary Grebe call. 14 | /// 15 | /// The client sends a single request to the server and gets a single response back, just like a normal function call. 16 | /// 17 | /// ##### Example usage of `GUnaryCall` 18 | /// 19 | /// Consider the following protobuf definition for a simple echo service. 20 | /// The service defines one unary RPC. You send one message and it 21 | /// echoes the message back to you. 22 | /// 23 | /// ```proto 24 | /// syntax = "proto3"; 25 | /// 26 | /// service EchoService { 27 | /// rpc echo (EchoRequest) returns (EchoResponse); 28 | /// } 29 | /// 30 | /// message EchoRequest { 31 | /// string message = 1; 32 | /// } 33 | /// 34 | /// message EchoResponse { 35 | /// string message = 1; 36 | /// } 37 | /// ``` 38 | /// 39 | /// You can create a `GUnaryCall` like this: 40 | /// ``` 41 | /// GUnaryCall(request: EchoRequest.with { $0.message = "hello"}, closure: echo) 42 | /// ``` 43 | /// 44 | 45 | public class GUnaryCall: IGCall { 46 | public typealias CallClosure = ( 47 | _ request: Request, 48 | _ callOptions: CallOptions? 49 | ) -> GRPC.UnaryCall 50 | 51 | /// The request message for the call. 52 | public var request: Request 53 | public let callClosure: CallClosure 54 | public let callOptions: CallOptions? 55 | 56 | /** 57 | Sets up an new unary Grebe call. 58 | 59 | - Parameters: 60 | - request: The request message for the call. 61 | - callOptions: Options to use for each service call. 62 | - closure: The closure which contains the executable call. 63 | */ 64 | public init( 65 | request: Request, 66 | callOptions: CallOptions? = nil, 67 | closure: @escaping CallClosure 68 | ) { 69 | self.request = request 70 | self.callClosure = closure 71 | self.callOptions = callOptions 72 | } 73 | 74 | /** 75 | Executes the Grebe unary call. 76 | 77 | - Returns: A stream of `Response` elements. The response publisher may fail 78 | with a `GRPCStatus` error. 79 | */ 80 | public func execute() -> AnyPublisher { 81 | let future = Future { [weak self] promise in 82 | guard let strongself = self else { 83 | return 84 | } 85 | 86 | let call = strongself 87 | .callClosure(strongself.request, strongself.callOptions) 88 | 89 | call.response.whenSuccess { promise(.success($0)) } 90 | call.status.whenSuccess { promise(.failure($0)) } 91 | } 92 | 93 | return future.eraseToAnyPublisher() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/Grebe-Framework/Calls/IGCall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICall.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 07.12.19. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import GRPC 11 | import SwiftProtobuf 12 | import NIO 13 | 14 | /// #### Base protocol for a Grebe call to a gRPC service. 15 | /// 16 | /// gRPC lets you define four kinds of service method: 17 | /// - Unary RPCs (`GUnaryCall`) 18 | /// - Server streaming RPCs (`GServerStreamingCall`) 19 | /// - Client streaming RPCs (`GClientStreamingCall`) 20 | /// - Bidirectional streaming RPCs (`GBidirectionalStreamingCall`) 21 | /// 22 | /// To run an instance of `ICall`, call the `execute` method of the specific call. 23 | /// 24 | public protocol IGCall { 25 | /// The type of the request message for the call. 26 | associatedtype Request: GRPCPayload 27 | /// The type of the response message for the call. 28 | associatedtype Response: GRPCPayload 29 | /// The type of the call closure for the call. 30 | associatedtype CallClosure 31 | 32 | /// The closure which contains the executable call. 33 | var callClosure: CallClosure { get } 34 | /// Options to use for each service call. 35 | var callOptions: CallOptions? { get } 36 | 37 | /// Executes the current call 38 | func execute() -> AnyPublisher 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Grebe-Framework/Client/GClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 07.12.19. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import GRPC 11 | import NIO 12 | import SwiftProtobuf 13 | 14 | /// Implementation of `IGClient`. 15 | public class GClient: IGClient { 16 | public let service: Client 17 | public let group: EventLoopGroup 18 | 19 | public required init(target: ConnectionTarget, callOptions: CallOptions = CallOptions()) { 20 | self.group = PlatformSupport.makeEventLoopGroup(loopCount: 1) 21 | 22 | let config = ClientConnection.Configuration(target: target, eventLoopGroup: group) 23 | let connection = ClientConnection(configuration: config) 24 | service = Client(connection: connection, defaultCallOptions: callOptions) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Grebe-Framework/Client/GRPCClientInitializable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GRPCClientInitializable.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 25.12.19. 6 | // 7 | 8 | import Foundation 9 | import GRPC 10 | 11 | /// Classes conforming to this protocol can create a `GRPCClient` using a `ClientConnection` 12 | /// and `CallOptions`. 13 | public protocol GRPCClientInitializable: GRPCClient { 14 | init(connection: ClientConnection, defaultCallOptions: CallOptions) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Grebe-Framework/Client/IGClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IClient.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 07.12.19. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import GRPC 11 | import NIO 12 | import SwiftProtobuf 13 | 14 | /// #### A Grebe client 15 | /// 16 | /// This client encapsulates a `GRPCClient` that conforms to `GRPCClientInitializable`. 17 | /// The `GRPCClient` is created at initialization. To hide the complexity of a `GRPCClient` 18 | /// the initializer only takes a `ConnectionTarget` and `CallOptions` as parameters. 19 | /// 20 | /// ##### Example Usage 21 | /// ``` 22 | /// let client = GClient(target: .hostAndPort("localhost", 62801)) 23 | /// ``` 24 | public protocol IGClient { 25 | /// A `GRPCClient` that conforms to `GRPCClientInitializable` 26 | associatedtype Client: GRPCClientInitializable 27 | 28 | /// The `GRPCClient` this client is using 29 | var service: Client { get } 30 | 31 | /// The `EventLoopGroup` this client is using. 32 | var group: EventLoopGroup { get } 33 | 34 | /** 35 | Creates a Grebe client 36 | 37 | - Parameters: 38 | - target: The target to connect to. 39 | - callOptions: Options to use for each service call if the user doesn't provide them. 40 | */ 41 | init(target: ConnectionTarget, callOptions: CallOptions) 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Grebe-Generate/CommandLineTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandLineTool.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 14.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | internal final class CommandLineTool { 11 | private let protoPath: String 12 | private let destinationPath: String 13 | 14 | internal init(protoPath: String, destinationPath: String) { 15 | self.protoPath = protoPath 16 | self.destinationPath = destinationPath 17 | } 18 | 19 | internal func run() throws { 20 | let protoName = splitPath(pathname: protoPath).base 21 | let protoString = try String(contentsOfFile: protoPath) 22 | let protoFile = ProtoFile(name: protoName, content: protoString) 23 | let generator = Generator(protoFile) 24 | 25 | try writeFile(name: protoName, content: generator.code) 26 | } 27 | 28 | private func writeFile(name: String, content: String) throws { 29 | let outputFile = outputFileName(name: name, path: destinationPath) 30 | try content.write( 31 | toFile: outputFile, 32 | atomically: true, 33 | encoding: .utf8 34 | ) 35 | } 36 | 37 | private func outputFileName(name: String, path: String) -> String { 38 | let ext = name + "." + "grebe" + ".swift" 39 | return path + "/" + ext 40 | } 41 | 42 | // from apple/swift-protobuf/Sources/protoc-gen-swift/StringUtils.swift 43 | // swiftlint:disable large_tuple 44 | private func splitPath(pathname: String) -> (dir: String, base: String, suffix: String) { 45 | var dir = "" 46 | var base = "" 47 | var suffix = "" 48 | #if swift(>=3.2) 49 | let pathnameChars = pathname 50 | #else 51 | let pathnameChars = pathname.characters 52 | #endif 53 | for char in pathnameChars { 54 | if char == "/" { 55 | dir += base + suffix + String(char) 56 | base = "" 57 | suffix = "" 58 | } else if char == "." { 59 | base += suffix 60 | suffix = String(char) 61 | } else { 62 | suffix += String(char) 63 | } 64 | } 65 | #if swift(>=3.2) 66 | let validSuffix = suffix.isEmpty || suffix.first == "." 67 | #else 68 | let validSuffix = suffix.isEmpty || suffix.characters.first == "." 69 | #endif 70 | if !validSuffix { 71 | base += suffix 72 | suffix = "" 73 | } 74 | return (dir: dir, base: base, suffix: suffix) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Grebe-Generate/Generator-Grebe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generator-Grebe.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 14.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Generator { 11 | internal func printGrebe() { 12 | println() 13 | printGrebeImplementation() 14 | } 15 | 16 | private func printGrebeImplementation() { 17 | println("extension \(serviceClassName): GRPCClientInitializable {") 18 | indent() 19 | 20 | guard let methods = service?.methods else { 21 | return 22 | } 23 | 24 | for method in methods { 25 | self.method = method 26 | println() 27 | switch method.stramingType { 28 | case .unary: 29 | println("public func \(method.name)(request: \(method.request), callOptions: CallOptions? = nil) -> AnyPublisher<\(method.response), GRPCStatus> {") 30 | indent() 31 | println("return GUnaryCall(request: request, callOptions: callOptions ?? defaultCallOptions, closure: \(method.callClosure)).execute()") 32 | 33 | case .serverStreaming: 34 | println("public func \(method.name)(request: \(method.request), callOptions: CallOptions? = nil) -> AnyPublisher<\(method.response), GRPCStatus> {") 35 | indent() 36 | println("return GServerStreamingCall(request: request, callOptions: callOptions ?? defaultCallOptions, closure: \(method.callClosure)).execute()") 37 | 38 | case .clientStreaming: 39 | println("public func \(method.name)(request: AnyPublisher<\(method.request),Error>, callOptions: CallOptions? = nil) -> AnyPublisher<\(method.response), GRPCStatus> {") 40 | indent() 41 | println("return GClientStreamingCall(request: request, callOptions: callOptions ?? defaultCallOptions, closure: \(method.callClosure)).execute()") 42 | 43 | case .bidirectionalStreaming: 44 | println("public func \(method.name)(request: AnyPublisher<\(method.request),Error>, callOptions: CallOptions? = nil) -> AnyPublisher<\(method.response), GRPCStatus> {") 45 | indent() 46 | println("return GBidirectionalStreamingCall(requests: request, callOptions: callOptions ?? defaultCallOptions, closure: \(method.callClosure)).execute()") 47 | } 48 | outdent() 49 | println("}") 50 | } 51 | outdent() 52 | println("}") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Grebe-Generate/Generator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generator.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 14.01.20. 6 | // 7 | 8 | import SwiftProtobufPluginLibrary 9 | 10 | internal class Generator { 11 | private var printer: CodePrinter 12 | internal var file: ProtoFile 13 | internal var service: ProtoService? // context during generation 14 | internal var method: ProtoMethod? // context during generation 15 | 16 | init(_ file: ProtoFile) { 17 | self.file = file 18 | printer = CodePrinter() 19 | 20 | printMain() 21 | } 22 | 23 | var code: String { 24 | printer.content 25 | } 26 | 27 | private func printMain() { 28 | printer.print(""" 29 | // 30 | // DO NOT EDIT. 31 | // 32 | // Generated by the protocol buffer compiler. 33 | // Source: \(file.name).proto 34 | // 35 | \n 36 | """) 37 | 38 | let moduleNames = [ 39 | "Grebe_Framework", 40 | "Combine", 41 | "GRPC" 42 | ] 43 | 44 | for moduleName in moduleNames.sorted() { 45 | println("import \(moduleName)") 46 | } 47 | 48 | for service in file.services { 49 | self.service = service 50 | printGrebe() 51 | } 52 | } 53 | 54 | internal func println(_ text: String = "") { 55 | printer.print(text) 56 | printer.print("\n") 57 | } 58 | 59 | internal func indent() { 60 | printer.indent() 61 | } 62 | 63 | internal func outdent() { 64 | printer.outdent() 65 | } 66 | } 67 | 68 | extension Generator { 69 | internal var serviceClassName: String { 70 | (service?.name ?? "") + "ServiceClient" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Grebe-Generate/ProtoFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtoFile.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 16.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | internal struct ProtoFile { 11 | let name: String 12 | var services = [ProtoService]() 13 | 14 | init(name: String, content: String) { 15 | print("New Proto File: \(name)") 16 | self.name = name 17 | services = content.split(separator: "\n") // Seperate each line 18 | .filter { !$0.starts(with: "//") } // Filter comments out 19 | .dropFirst() // Removes first line: syntax = "proto3"; 20 | .joined(separator: "\n") // Merge to one string 21 | .filter { $0 != "\n" } // Filter line breaks out 22 | .split(separator: "}") // Seperates services and messages 23 | .filter { !$0.starts(with: "message") } // Filter messages out 24 | .map(String.init) // Map to String 25 | .map(ProtoService.init) // Create ProtoService 26 | .compactMap { $0 } // Filter nil objects 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Grebe-Generate/ProtoMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtoMethod.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 16.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | internal struct ProtoMethod { 11 | let name: String 12 | let request: String 13 | let response: String 14 | let stramingType: StreamType 15 | var callClosure: String { name.firstLowercased } 16 | 17 | enum StreamType { 18 | case unary 19 | case clientStreaming 20 | case serverStreaming 21 | case bidirectionalStreaming 22 | } 23 | 24 | init?(content: String) { 25 | var type = StreamType.unary 26 | // Format: rpc Send (stream EchoRequest) returns (stream EchoResponse); 27 | 28 | let removeRpcContent = content.replacingOccurrences(of: "rpc", with: "") // Remove rpc declaration 29 | // Format: Send (stream EchoRequest) returns (stream EchoResponse); 30 | 31 | let components = removeRpcContent.components(separatedBy: "(") // Seperates name, request & response 32 | // Format: Send - stream EchoRequest) returns - stream EchoResponse); 33 | 34 | guard components.count == 3 else { 35 | return nil 36 | } // We proceed only if we got all three 37 | 38 | // Parse Name 39 | self.name = components[0].replacingOccurrences(of: " ", with: "").firstLowercased // Remove spaces 40 | 41 | // Parse Request 42 | // Format: stream EchoRequest) returns 43 | var requestPart = components[1] 44 | if requestPart.contains("stream") { 45 | type = .clientStreaming 46 | requestPart = requestPart.replacingOccurrences(of: "stream ", with: "") 47 | // Format: EchoRequest) returns 48 | } 49 | guard let req = requestPart.components(separatedBy: ")").first else { 50 | return nil 51 | } 52 | self.request = req 53 | 54 | // Parse Response 55 | // Format: stream EchoResponse 56 | var responsePart = String(components[2].dropLast()) 57 | // Format: stream EchoResponse) 58 | if responsePart.contains("stream") { 59 | type = (type == .clientStreaming) ? .bidirectionalStreaming : .serverStreaming 60 | responsePart = responsePart.replacingOccurrences(of: "stream ", with: "") 61 | // Format: EchoResponse 62 | } 63 | self.response = String(responsePart) 64 | self.stramingType = type 65 | 66 | print("Found Method: \(self.name) - request: \(self.request) - response: \(self.response) - type: \(type)") 67 | } 68 | } 69 | 70 | extension StringProtocol { 71 | var firstLowercased: String { 72 | prefix(1).lowercased() + dropFirst() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Grebe-Generate/ProtoService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtoService.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 16.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | internal struct ProtoService { 11 | let name: String 12 | var methods = [ProtoMethod]() 13 | 14 | init?(content: String) { 15 | let array = content.components(separatedBy: "{") // Seperate service name and methods 16 | 17 | // Parse Name 18 | guard let declaration = array.first else { 19 | return nil 20 | } 21 | self.name = declaration 22 | .replacingOccurrences(of: "service", with: "") // Remove the service declaration 23 | .replacingOccurrences(of: " ", with: "") // Remove spaces 24 | 25 | print("\nFound Service: \(self.name)") 26 | 27 | // Parse functions 28 | guard let methodsContent = array.last else { 29 | return 30 | } 31 | 32 | self.methods = methodsContent 33 | .split(separator: ";") // Seperates each method 34 | .map(String.init) // Map to String 35 | .map(ProtoMethod.init) // Create ProtoMethod 36 | .compactMap { $0 } // Filter nil objects 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Grebe-Generate/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 14.01.20. 6 | // 7 | 8 | import ArgumentParser 9 | import Foundation 10 | 11 | struct Generate: ParsableCommand { 12 | @Option(name: .shortAndLong, help: "Path to the proto file") 13 | var protoFilePath: String 14 | 15 | @Option(name: .shortAndLong, help: "Path to the generated Swift file") 16 | var destinationFilePath: String 17 | 18 | func run() { 19 | let tool = CommandLineTool(protoPath: protoFilePath, destinationPath: destinationFilePath) 20 | do { 21 | try tool.run() 22 | } catch { 23 | print("Whoops! An error occurred: \(error)") 24 | } 25 | } 26 | } 27 | 28 | Generate.main() 29 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Call Tests/BaseCallTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseCallTest.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 25.12.19. 6 | // 7 | 8 | import Combine 9 | @testable import Grebe_Framework 10 | import GRPC 11 | import NIO 12 | import XCTest 13 | 14 | class BaseCallTest: XCTestCase { 15 | typealias Request = EchoRequest 16 | typealias Response = EchoResponse 17 | 18 | internal var cancellables: Set = [] 19 | } 20 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Call Tests/BidirectionalStreamingCallTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BidirectionalStreamingCallTests.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 27.12.19. 6 | // 7 | 8 | import Combine 9 | @testable import Grebe_Framework 10 | import GRPC 11 | import NIO 12 | import SwiftProtobuf 13 | import XCTest 14 | 15 | final class BidirectionalStreamingCallTests: BaseCallTest { 16 | var mockClient: BidrectionalStreamingMockClient = BidrectionalStreamingMockClient() 17 | 18 | override func setUp() { 19 | mockClient.mockNetworkCalls = [] 20 | super.setUp() 21 | } 22 | 23 | override func tearDown() { 24 | XCTAssert(mockClient.mockNetworkCalls.isEmpty) 25 | super.tearDown() 26 | } 27 | 28 | func testOk() { 29 | runTestOk( 30 | requests: (0...100).map(EchoRequest.init), 31 | responses: (0...100).map(EchoResponse.init) 32 | ) 33 | } 34 | 35 | func testEmptyResponseStream() { 36 | runTestOk(requests: (0...1).map(EchoRequest.init), responses: []) 37 | } 38 | 39 | func testEmptyRequestStream() { 40 | runTestOk(requests: [], responses: (0...1).map(EchoResponse.init)) 41 | } 42 | 43 | private func runTestOk(requests: [Request], responses: [Response]) { 44 | let bidirectionalStreamingMock = BidirectionalStreamMock( 45 | requests: requests, 46 | responses: responses.map { .success($0) } 47 | ) 48 | 49 | mockClient.mockNetworkCalls = [bidirectionalStreamingMock] 50 | 51 | let responseExpectation = XCTestExpectation(description: "Correct response count") 52 | responseExpectation.expectedFulfillmentCount = responses.count + 1 53 | 54 | let call = GBidirectionalStreamingCall( 55 | requests: bidirectionalStreamingMock.requestStream, 56 | closure: mockClient.test 57 | ) 58 | var receivedResponses = [Response]() 59 | 60 | call.execute() 61 | .sink(receiveCompletion: { 62 | switch $0 { 63 | case .failure(let status): 64 | XCTFail("Unexpected status: " + status.localizedDescription) 65 | case .finished: 66 | responseExpectation.fulfill() 67 | XCTAssertEqual(responses, receivedResponses) 68 | } 69 | }, receiveValue: { message in 70 | receivedResponses.append(message) 71 | responseExpectation.fulfill() 72 | }) 73 | .store(in: &cancellables) 74 | 75 | wait(for: [bidirectionalStreamingMock.expectation, responseExpectation], timeout: 0.1, enforceOrder: true) 76 | } 77 | 78 | private func stream(elements: [T]) -> AnyPublisher { 79 | Publishers.Sequence<[T], E>(sequence: elements).eraseToAnyPublisher() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Call Tests/ClientStreamingCallTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientStreamingCallTests.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 25.12.19. 6 | // 7 | 8 | import Combine 9 | @testable import Grebe_Framework 10 | import GRPC 11 | import NIO 12 | import XCTest 13 | 14 | final class ClientStreamingCallTests: BaseCallTest { 15 | var mockClient: ClientStreamingMockClient = ClientStreamingMockClient() 16 | 17 | override func setUp() { 18 | mockClient.mockNetworkCalls = [] 19 | super.setUp() 20 | } 21 | 22 | override func tearDown() { 23 | XCTAssert(mockClient.mockNetworkCalls.isEmpty) 24 | super.tearDown() 25 | } 26 | 27 | func testOk() { 28 | runTestOk(requests: (0...100).map(EchoRequest.init), response: EchoResponse(id: 1)) 29 | } 30 | 31 | func testEmptyRequestStream() { 32 | runTestOk(requests: [], response: EchoResponse(id: 1)) 33 | } 34 | 35 | private func runTestOk(requests: [Request], response: Response) { 36 | let clientStreamingMock = ClientStreamMock( 37 | requests: requests, 38 | response: .success(response) 39 | ) 40 | 41 | mockClient.mockNetworkCalls = [clientStreamingMock] 42 | 43 | let responseExpectation = XCTestExpectation(description: "Correct Response") 44 | responseExpectation.expectedFulfillmentCount = 2 45 | 46 | let call = GClientStreamingCall( 47 | request: clientStreamingMock.requestStream, 48 | closure: mockClient.test 49 | ) 50 | call.execute() 51 | .sinkUnarySucceed(expectedResponse: response, expectation: responseExpectation) 52 | .store(in: &cancellables) 53 | 54 | wait(for: [clientStreamingMock.expectation, responseExpectation], timeout: 0.1, enforceOrder: true) 55 | } 56 | 57 | func testFailedPrecondition() { 58 | let expectedResponse: GRPCStatus = .init(code: .failedPrecondition, message: nil) 59 | let clientStreamingMock = ClientStreamMock( 60 | requests: (0...1).map(EchoRequest.init), 61 | response: .failure(expectedResponse) 62 | ) 63 | 64 | mockClient.mockNetworkCalls = [clientStreamingMock] 65 | let errorExpectation = XCTestExpectation(description: "Correct Error") 66 | 67 | let call = GClientStreamingCall( 68 | request: clientStreamingMock.requestStream, 69 | closure: mockClient.test 70 | ) 71 | call.execute() 72 | .sinkUnaryFail(expectedResponse: expectedResponse, expectation: errorExpectation) 73 | .store(in: &cancellables) 74 | 75 | wait(for: [clientStreamingMock.expectation, errorExpectation], timeout: 0.1, enforceOrder: true) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Call Tests/ServerStreamingCallTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerStreamingCallTests.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 25.12.19. 6 | // 7 | 8 | import Combine 9 | @testable import Grebe_Framework 10 | import GRPC 11 | import NIO 12 | import XCTest 13 | 14 | final class ServerStreamingCallTests: BaseCallTest { 15 | private var mockClient: ServerStreamingMockClient = ServerStreamingMockClient() 16 | 17 | override func setUp() { 18 | mockClient.mockNetworkCalls = [] 19 | super.setUp() 20 | } 21 | 22 | override func tearDown() { 23 | XCTAssert(mockClient.mockNetworkCalls.isEmpty) 24 | super.tearDown() 25 | } 26 | 27 | func testOk() { 28 | runTestOk(request: EchoRequest(id: 1), responses: (0...1).map(EchoResponse.init)) 29 | } 30 | 31 | func testEmptyResponseStream() { 32 | runTestOk(request: EchoRequest(id: 1), responses: []) 33 | } 34 | 35 | private func runTestOk(request: Request, responses: [Response]) { 36 | let serverStreamingMock = ServerStreamMock( 37 | request: request, 38 | responses: responses.map { .success($0) } 39 | ) 40 | 41 | mockClient.mockNetworkCalls = [serverStreamingMock] 42 | 43 | let responseExpectation = XCTestExpectation(description: "Correct response count") 44 | responseExpectation.expectedFulfillmentCount = responses.count + 1 45 | 46 | let call = GServerStreamingCall(request: request, closure: mockClient.test) 47 | var receivedResponses = [Response]() 48 | call.execute() 49 | .sink( 50 | receiveCompletion: { 51 | switch $0 { 52 | case .failure(let status): 53 | XCTFail("Unexpected status: " + status.localizedDescription) 54 | case .finished: 55 | responseExpectation.fulfill() 56 | XCTAssertEqual(responses, receivedResponses) 57 | } 58 | }, 59 | receiveValue: { message in 60 | receivedResponses.append(message) 61 | responseExpectation.fulfill() 62 | } 63 | ) 64 | .store(in: &cancellables) 65 | 66 | wait(for: [serverStreamingMock.expectation, responseExpectation], timeout: 0.1, enforceOrder: true) 67 | } 68 | 69 | func testFailedPrecondition() { 70 | let expectedRequest = EchoRequest(id: 1) 71 | let errorStatus: GRPCStatus = .init(code: .failedPrecondition, message: nil) 72 | let serverStreamingMock = ServerStreamMock( 73 | request: expectedRequest, 74 | responses: [.failure(errorStatus)] 75 | // responseStream: expectedResponseStream 76 | ) 77 | 78 | mockClient.mockNetworkCalls = [serverStreamingMock] 79 | let errorExpectation = XCTestExpectation(description: "Correct Error") 80 | 81 | let call = GServerStreamingCall(request: expectedRequest, closure: mockClient.test) 82 | call.execute() 83 | .sink( 84 | receiveCompletion: { 85 | switch $0 { 86 | case .failure(let status): 87 | XCTAssertEqual(status, errorStatus) 88 | errorExpectation.fulfill() 89 | case .finished: 90 | XCTFail("Call should fail") 91 | } 92 | }, receiveValue: { _ in 93 | XCTFail("Call should fail") 94 | } 95 | ) 96 | .store(in: &cancellables) 97 | 98 | wait( 99 | for: [serverStreamingMock.expectation, errorExpectation], 100 | timeout: 0.1, 101 | enforceOrder: true 102 | ) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Call Tests/UnaryCallTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnaryCallTests.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 25.12.19. 6 | // 7 | 8 | import Combine 9 | @testable import Grebe_Framework 10 | import GRPC 11 | import NIO 12 | import XCTest 13 | 14 | final class UnaryCallTests: BaseCallTest { 15 | private var mockClient: UnaryMockClient = UnaryMockClient() 16 | 17 | override func setUp() { 18 | mockClient.mockNetworkCalls = [] 19 | super.setUp() 20 | } 21 | 22 | override func tearDown() { 23 | XCTAssert(mockClient.mockNetworkCalls.isEmpty) 24 | super.tearDown() 25 | } 26 | 27 | func testOk() { 28 | let expectedRequest = EchoRequest(id: 1) 29 | let expectedResponse = EchoResponse(id: 1) 30 | let unaryMock = UnaryMock(request: expectedRequest, response: .success(expectedResponse)) 31 | 32 | mockClient.mockNetworkCalls = [unaryMock] 33 | 34 | let responseExpectation = XCTestExpectation(description: "Correct Response") 35 | responseExpectation.expectedFulfillmentCount = 2 36 | 37 | let call = GUnaryCall(request: expectedRequest, closure: mockClient.test) 38 | call.execute() 39 | .sinkUnarySucceed(expectedResponse: expectedResponse, expectation: responseExpectation) 40 | .store(in: &cancellables) 41 | 42 | wait(for: [unaryMock.expectation, responseExpectation], timeout: 0.1, enforceOrder: true) 43 | } 44 | 45 | func testFailedPrecondition() { 46 | let expectedRequest = EchoRequest(id: 1) 47 | let expectedResponse: GRPCStatus = .init(code: .failedPrecondition, message: nil) 48 | let unaryMock = UnaryMock( 49 | request: expectedRequest, 50 | response: .failure(expectedResponse) 51 | ) 52 | 53 | mockClient.mockNetworkCalls = [unaryMock] 54 | let errorExpectation = XCTestExpectation(description: "Correct Error") 55 | 56 | let call = GUnaryCall(request: expectedRequest, closure: mockClient.test) 57 | call.execute() 58 | .sinkUnaryFail(expectedResponse: expectedResponse, expectation: errorExpectation) 59 | .store(in: &cancellables) 60 | 61 | wait(for: [unaryMock.expectation, errorExpectation], timeout: 0.1, enforceOrder: true) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Extensions/Publisher+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 04.02.20. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import SwiftProtobuf 11 | import XCTest 12 | import GRPC 13 | 14 | typealias Response = Message & Equatable 15 | 16 | extension Publisher where Self.Output: Response { 17 | func sinkUnarySucceed(expectedResponse: Self.Output, expectation: XCTestExpectation) -> AnyCancellable { 18 | sink(receiveCompletion: { 19 | switch $0 { 20 | case .failure(let status): 21 | XCTFail("Unexpected status: " + status.localizedDescription) 22 | case .finished: 23 | expectation.fulfill() 24 | } 25 | }, receiveValue: { response in 26 | XCTAssertEqual(response, expectedResponse) 27 | expectation.fulfill() 28 | }) 29 | } 30 | } 31 | 32 | extension Publisher where Self.Failure: GRPCStatus { 33 | func sinkUnaryFail(expectedResponse: Self.Failure, expectation: XCTestExpectation) -> AnyCancellable { 34 | sink(receiveCompletion: { 35 | switch $0 { 36 | case .failure(let status): 37 | XCTAssertEqual(status, expectedResponse) 38 | expectation.fulfill() 39 | case .finished: 40 | XCTFail("Call should fail") 41 | } 42 | }, receiveValue: { _ in 43 | XCTFail("Call should fail") 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Generated/Echo+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Echo+Init.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 10.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension EchoRequest { 11 | init(id: Int) { 12 | self.id = Int32(id) 13 | } 14 | } 15 | 16 | extension EchoResponse { 17 | init(id: Int) { 18 | self.id = Int32(id) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Generated/test_scenarios.grpc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DO NOT EDIT. 3 | // 4 | // Generated by the protocol buffer compiler. 5 | // Source: test_scenarios.proto 6 | // 7 | 8 | // 9 | // Copyright 2018, gRPC Authors All rights reserved. 10 | // 11 | // Licensed under the Apache License, Version 2.0 (the "License"); 12 | // you may not use this file except in compliance with the License. 13 | // You may obtain a copy of the License at 14 | // 15 | // http://www.apache.org/licenses/LICENSE-2.0 16 | // 17 | // Unless required by applicable law or agreed to in writing, software 18 | // distributed under the License is distributed on an "AS IS" BASIS, 19 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | // See the License for the specific language governing permissions and 21 | // limitations under the License. 22 | // 23 | import Foundation 24 | import GRPC 25 | import NIO 26 | import NIOHTTP1 27 | import SwiftProtobuf 28 | 29 | /// Usage: instantiate UnaryMockServiceClient, then call methods of this protocol to make API calls. 30 | internal protocol UnaryMockService { 31 | func ok(_ request: EchoRequest, callOptions: CallOptions?) -> UnaryCall 32 | } 33 | 34 | internal final class UnaryMockServiceClient: GRPCClient, UnaryMockService { 35 | internal let connection: ClientConnection 36 | internal var defaultCallOptions: CallOptions 37 | 38 | /// Creates a client for the UnaryMock service. 39 | /// 40 | /// - Parameters: 41 | /// - connection: `ClientConnection` to the service host. 42 | /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. 43 | internal init(connection: ClientConnection, defaultCallOptions: CallOptions = CallOptions()) { 44 | self.connection = connection 45 | self.defaultCallOptions = defaultCallOptions 46 | } 47 | 48 | /// Asynchronous unary call to Ok. 49 | /// 50 | /// - Parameters: 51 | /// - request: Request to send to Ok. 52 | /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. 53 | /// - Returns: A `UnaryCall` with futures for the metadata, status and response. 54 | internal func ok(_ request: EchoRequest, callOptions: CallOptions? = nil) -> UnaryCall { 55 | self.makeUnaryCall(path: "/UnaryMock/Ok", 56 | request: request, 57 | callOptions: callOptions ?? self.defaultCallOptions) 58 | } 59 | } 60 | 61 | /// Usage: instantiate ServerStreamingMockServiceClient, then call methods of this protocol to make API calls. 62 | internal protocol ServerStreamingMockService { 63 | func ok(_ request: EchoRequest, callOptions: CallOptions?, handler: @escaping (EchoResponse) -> Void) -> ServerStreamingCall 64 | } 65 | 66 | internal final class ServerStreamingMockServiceClient: GRPCClient, ServerStreamingMockService { 67 | internal let connection: ClientConnection 68 | internal var defaultCallOptions: CallOptions 69 | 70 | /// Creates a client for the ServerStreamingMock service. 71 | /// 72 | /// - Parameters: 73 | /// - connection: `ClientConnection` to the service host. 74 | /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. 75 | internal init(connection: ClientConnection, defaultCallOptions: CallOptions = CallOptions()) { 76 | self.connection = connection 77 | self.defaultCallOptions = defaultCallOptions 78 | } 79 | 80 | /// Asynchronous server-streaming call to Ok. 81 | /// 82 | /// - Parameters: 83 | /// - request: Request to send to Ok. 84 | /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. 85 | /// - handler: A closure called when each response is received from the server. 86 | /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. 87 | internal func ok(_ request: EchoRequest, callOptions: CallOptions? = nil, handler: @escaping (EchoResponse) -> Void) -> ServerStreamingCall { 88 | self.makeServerStreamingCall(path: "/ServerStreamingMock/Ok", 89 | request: request, 90 | callOptions: callOptions ?? self.defaultCallOptions, 91 | handler: handler) 92 | } 93 | } 94 | 95 | /// Usage: instantiate ClientStreamingMockServiceClient, then call methods of this protocol to make API calls. 96 | internal protocol ClientStreamingMockService { 97 | func ok(callOptions: CallOptions?) -> ClientStreamingCall 98 | } 99 | 100 | internal final class ClientStreamingMockServiceClient: GRPCClient, ClientStreamingMockService { 101 | internal let connection: ClientConnection 102 | internal var defaultCallOptions: CallOptions 103 | 104 | /// Creates a client for the ClientStreamingMock service. 105 | /// 106 | /// - Parameters: 107 | /// - connection: `ClientConnection` to the service host. 108 | /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. 109 | internal init(connection: ClientConnection, defaultCallOptions: CallOptions = CallOptions()) { 110 | self.connection = connection 111 | self.defaultCallOptions = defaultCallOptions 112 | } 113 | 114 | /// Asynchronous client-streaming call to Ok. 115 | /// 116 | /// Callers should use the `send` method on the returned object to send messages 117 | /// to the server. The caller should send an `.end` after the final message has been sent. 118 | /// 119 | /// - Parameters: 120 | /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. 121 | /// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response. 122 | internal func ok(callOptions: CallOptions? = nil) -> ClientStreamingCall { 123 | self.makeClientStreamingCall(path: "/ClientStreamingMock/Ok", 124 | callOptions: callOptions ?? self.defaultCallOptions) 125 | } 126 | } 127 | 128 | /// Usage: instantiate BidirectionalStreamingMockServiceClient, then call methods of this protocol to make API calls. 129 | internal protocol BidirectionalStreamingMockService { 130 | func ok(callOptions: CallOptions?, handler: @escaping (EchoResponse) -> Void) -> BidirectionalStreamingCall 131 | } 132 | 133 | internal final class BidirectionalStreamingMockServiceClient: GRPCClient, BidirectionalStreamingMockService { 134 | internal let connection: ClientConnection 135 | internal var defaultCallOptions: CallOptions 136 | 137 | /// Creates a client for the BidirectionalStreamingMock service. 138 | /// 139 | /// - Parameters: 140 | /// - connection: `ClientConnection` to the service host. 141 | /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. 142 | internal init(connection: ClientConnection, defaultCallOptions: CallOptions = CallOptions()) { 143 | self.connection = connection 144 | self.defaultCallOptions = defaultCallOptions 145 | } 146 | 147 | /// Asynchronous bidirectional-streaming call to Ok. 148 | /// 149 | /// Callers should use the `send` method on the returned object to send messages 150 | /// to the server. The caller should send an `.end` after the final message has been sent. 151 | /// 152 | /// - Parameters: 153 | /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. 154 | /// - handler: A closure called when each response is received from the server. 155 | /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. 156 | internal func ok(callOptions: CallOptions? = nil, handler: @escaping (EchoResponse) -> Void) -> BidirectionalStreamingCall { 157 | self.makeBidirectionalStreamingCall(path: "/BidirectionalStreamingMock/Ok", 158 | callOptions: callOptions ?? self.defaultCallOptions, 159 | handler: handler) 160 | } 161 | } 162 | 163 | /// To build a server, implement a class that conforms to this protocol. 164 | internal protocol UnaryMockProvider: CallHandlerProvider { 165 | func ok(request: EchoRequest, context: StatusOnlyCallContext) -> EventLoopFuture 166 | } 167 | 168 | extension UnaryMockProvider { 169 | internal var serviceName: String { "UnaryMock" } 170 | 171 | /// Determines, calls and returns the appropriate request handler, depending on the request's method. 172 | /// Returns nil for methods not handled by this service. 173 | internal func handleMethod(_ methodName: String, callHandlerContext: CallHandlerContext) -> GRPCCallHandler? { 174 | switch methodName { 175 | case "Ok": 176 | return UnaryCallHandler(callHandlerContext: callHandlerContext) { context in { request in 177 | self.ok(request: request, context: context) 178 | } 179 | } 180 | 181 | default: return nil 182 | } 183 | } 184 | } 185 | 186 | /// To build a server, implement a class that conforms to this protocol. 187 | internal protocol ServerStreamingMockProvider: CallHandlerProvider { 188 | func ok(request: EchoRequest, context: StreamingResponseCallContext) -> EventLoopFuture 189 | } 190 | 191 | extension ServerStreamingMockProvider { 192 | internal var serviceName: String { "ServerStreamingMock" } 193 | 194 | /// Determines, calls and returns the appropriate request handler, depending on the request's method. 195 | /// Returns nil for methods not handled by this service. 196 | internal func handleMethod(_ methodName: String, callHandlerContext: CallHandlerContext) -> GRPCCallHandler? { 197 | switch methodName { 198 | case "Ok": 199 | return ServerStreamingCallHandler(callHandlerContext: callHandlerContext) { context in { request in 200 | self.ok(request: request, context: context) 201 | } 202 | } 203 | 204 | default: return nil 205 | } 206 | } 207 | } 208 | 209 | /// To build a server, implement a class that conforms to this protocol. 210 | internal protocol ClientStreamingMockProvider: CallHandlerProvider { 211 | func ok(context: UnaryResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> 212 | } 213 | 214 | extension ClientStreamingMockProvider { 215 | internal var serviceName: String { "ClientStreamingMock" } 216 | 217 | /// Determines, calls and returns the appropriate request handler, depending on the request's method. 218 | /// Returns nil for methods not handled by this service. 219 | internal func handleMethod(_ methodName: String, callHandlerContext: CallHandlerContext) -> GRPCCallHandler? { 220 | switch methodName { 221 | case "Ok": 222 | return ClientStreamingCallHandler(callHandlerContext: callHandlerContext) { context in 223 | self.ok(context: context) 224 | } 225 | 226 | default: return nil 227 | } 228 | } 229 | } 230 | 231 | /// To build a server, implement a class that conforms to this protocol. 232 | internal protocol BidirectionalStreamingMockProvider: CallHandlerProvider { 233 | func ok(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> 234 | } 235 | 236 | extension BidirectionalStreamingMockProvider { 237 | internal var serviceName: String { "BidirectionalStreamingMock" } 238 | 239 | /// Determines, calls and returns the appropriate request handler, depending on the request's method. 240 | /// Returns nil for methods not handled by this service. 241 | internal func handleMethod(_ methodName: String, callHandlerContext: CallHandlerContext) -> GRPCCallHandler? { 242 | switch methodName { 243 | case "Ok": 244 | return BidirectionalStreamingCallHandler(callHandlerContext: callHandlerContext) { context in 245 | self.ok(context: context) 246 | } 247 | 248 | default: return nil 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Generated/test_scenarios.pb.swift: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | // 3 | // Generated by the Swift generator plugin for the protocol buffer compiler. 4 | // Source: test_scenarios.proto 5 | // 6 | // For information on using the generated types, please see the documentation: 7 | // https://github.com/apple/swift-protobuf/ 8 | 9 | // Copyright 2019, Vy-Shane Xie 10 | // Licensed under the Apache License, Version 2.0 11 | // 12 | // Scenarios for end to end tests. 13 | 14 | import Foundation 15 | import SwiftProtobuf 16 | 17 | // If the compiler emits an error on this type, it is because this file 18 | // was generated by a version of the `protoc` Swift plug-in that is 19 | // incompatible with the version of SwiftProtobuf to which you are linking. 20 | // Please ensure that your are building against the same version of the API 21 | // that was used to generate this file. 22 | private struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { 23 | struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} 24 | typealias Version = _2 25 | } 26 | 27 | struct EchoRequest { 28 | // SwiftProtobuf.Message conformance is added in an extension below. See the 29 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 30 | // methods supported on all messages. 31 | 32 | var id: Int32 = 0 33 | 34 | var unknownFields = SwiftProtobuf.UnknownStorage() 35 | 36 | init() {} 37 | } 38 | 39 | struct EchoResponse { 40 | // SwiftProtobuf.Message conformance is added in an extension below. See the 41 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 42 | // methods supported on all messages. 43 | 44 | var id: Int32 = 0 45 | 46 | var unknownFields = SwiftProtobuf.UnknownStorage() 47 | 48 | init() {} 49 | } 50 | 51 | // MARK: - Code below here is support for the SwiftProtobuf runtime. 52 | 53 | extension EchoRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 54 | static let protoMessageName: String = "EchoRequest" 55 | static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 56 | 1: .same(proto: "id") 57 | ] 58 | 59 | mutating func decodeMessage(decoder: inout D) throws { 60 | while let fieldNumber = try decoder.nextFieldNumber() { 61 | switch fieldNumber { 62 | case 1: try decoder.decodeSingularInt32Field(value: &self.id) 63 | default: break 64 | } 65 | } 66 | } 67 | 68 | func traverse(visitor: inout V) throws { 69 | if self.id != 0 { 70 | try visitor.visitSingularInt32Field(value: self.id, fieldNumber: 1) 71 | } 72 | try unknownFields.traverse(visitor: &visitor) 73 | } 74 | 75 | static func ==(lhs: EchoRequest, rhs: EchoRequest) -> Bool { 76 | if lhs.id != rhs.id { return false } 77 | if lhs.unknownFields != rhs.unknownFields { return false } 78 | return true 79 | } 80 | } 81 | 82 | extension EchoResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 83 | static let protoMessageName: String = "EchoResponse" 84 | static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 85 | 1: .same(proto: "id") 86 | ] 87 | 88 | mutating func decodeMessage(decoder: inout D) throws { 89 | while let fieldNumber = try decoder.nextFieldNumber() { 90 | switch fieldNumber { 91 | case 1: try decoder.decodeSingularInt32Field(value: &self.id) 92 | default: break 93 | } 94 | } 95 | } 96 | 97 | func traverse(visitor: inout V) throws { 98 | if self.id != 0 { 99 | try visitor.visitSingularInt32Field(value: self.id, fieldNumber: 1) 100 | } 101 | try unknownFields.traverse(visitor: &visitor) 102 | } 103 | 104 | static func ==(lhs: EchoResponse, rhs: EchoResponse) -> Bool { 105 | if lhs.id != rhs.id { return false } 106 | if lhs.unknownFields != rhs.unknownFields { return false } 107 | return true 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Generated/test_scenarios.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019, Vy-Shane Xie 2 | // Licensed under the Apache License, Version 2.0 3 | // 4 | // Scenarios for end to end tests. 5 | 6 | syntax = "proto3"; 7 | 8 | service UnaryMock { 9 | rpc Ok (EchoRequest) returns (EchoResponse); 10 | } 11 | 12 | service ServerStreamingMock { 13 | rpc Ok (EchoRequest) returns (stream EchoResponse); 14 | } 15 | 16 | service ClientStreamingMock { 17 | rpc Ok (stream EchoRequest) returns (EchoResponse); 18 | } 19 | 20 | service BidirectionalStreamingMock { 21 | rpc Ok (stream EchoRequest) returns (stream EchoResponse); 22 | } 23 | 24 | message EchoRequest { 25 | int32 id = 1; 26 | } 27 | 28 | message EchoResponse { 29 | int32 id = 1; 30 | } 31 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Mock Clients/BaseMockClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseMockClient.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 10.01.20. 6 | // 7 | 8 | import Combine 9 | import NIO 10 | import SwiftProtobuf 11 | import XCTest 12 | 13 | @testable import GRPC 14 | 15 | internal class BaseMockClient: GRPCClient { 16 | let channel = EmbeddedChannel() 17 | let connection: ClientConnection 18 | var defaultCallOptions = CallOptions() 19 | var cancellables = Set() 20 | 21 | init() { 22 | let configuration = ClientConnection.Configuration( 23 | target: .socketAddress(.init(sockaddr_un.init())), eventLoopGroup: channel.eventLoop 24 | ) 25 | connection = ClientConnection(channel: channel, configuration: configuration) 26 | } 27 | } 28 | 29 | /// MockInboundHandler 30 | /// Allows us to inject mock responses into the subchannel pipeline setup by a Call. 31 | internal class MockInboundHandler: ChannelInboundHandler { 32 | typealias InboundIn = Any 33 | typealias InboundOut = GRPCClientResponsePart 34 | 35 | private var context: ChannelHandlerContext? 36 | 37 | func handlerAdded(context: ChannelHandlerContext) { 38 | self.context = context 39 | } 40 | 41 | func respondWithMock(_ mock: Result) { 42 | let response: GRPCClientResponsePart 43 | switch mock { 44 | case let .success(success): 45 | response = .message(_Box(success)) 46 | case let .failure(error): 47 | response = .status(error) 48 | } 49 | 50 | context?.fireChannelRead(wrapInboundOut(response)) 51 | } 52 | 53 | func respondWithStatus(_ status: GRPCStatus) { 54 | context?.fireChannelRead(wrapInboundOut(.status(status))) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Mock Clients/BidrectionalStreamingMockClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BidrectionalStreamingMockClient.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 10.01.20. 6 | // 7 | 8 | import GRPC 9 | import NIO 10 | import SwiftProtobuf 11 | import XCTest 12 | 13 | internal final class BidrectionalStreamingMockClient: BaseMockClient { 14 | typealias BidirectionalStreamingMockCall = BidirectionalStreamMock 15 | 16 | var mockNetworkCalls: [BidirectionalStreamingMockCall] = [] 17 | 18 | func test( 19 | callOptions: CallOptions?, 20 | handler: @escaping (Response) -> Void 21 | ) -> BidirectionalStreamingCall { 22 | let networkCall = mockNetworkCalls.removeFirst() 23 | 24 | let call = BidirectionalStreamingCall( 25 | connection: connection, 26 | path: "/ok", 27 | callOptions: defaultCallOptions, 28 | errorDelegate: nil, 29 | handler: handler 30 | ) 31 | channel.embeddedEventLoop.advanceTime(by: .nanoseconds(1)) 32 | 33 | let unaryMockInboundHandler = MockInboundHandler() 34 | call.subchannel 35 | .map { subchannel in 36 | subchannel.pipeline.handler(type: GRPCClientChannelHandler.self).map { clientChannelHandler in 37 | subchannel.pipeline.addHandler(unaryMockInboundHandler, position: .after(clientChannelHandler)) 38 | } 39 | } 40 | .whenSuccess { _ in } 41 | 42 | var expectedRequests: [Request] = [] 43 | var responses = networkCall.responses 44 | 45 | networkCall.requestStream 46 | .sink(receiveCompletion: { [weak self] completion in 47 | XCTAssertEqual(expectedRequests, networkCall.requests) 48 | switch completion { 49 | case .failure: 50 | unaryMockInboundHandler.respondWithStatus(.processingError) 51 | networkCall.expectation.fulfill() 52 | case .finished: 53 | guard expectedRequests == networkCall.requests else { 54 | XCTFail("Could not match the network call to the next MockNetworkCall.") 55 | fatalError("Could not match the network call to the next MockNetworkCall.") 56 | } 57 | networkCall.expectation.fulfill() 58 | 59 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { 60 | // Send remaining reponses 61 | for response in responses { 62 | self?.channel.embeddedEventLoop.advanceTime(by: .nanoseconds(1)) 63 | unaryMockInboundHandler.respondWithMock(response) 64 | } 65 | 66 | self?.channel.embeddedEventLoop.advanceTime(by: .nanoseconds(1)) 67 | unaryMockInboundHandler.respondWithStatus(.ok) 68 | } 69 | } 70 | }, receiveValue: { request in 71 | expectedRequests.append(request) 72 | 73 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { 74 | self.channel.embeddedEventLoop.advanceTime(by: .nanoseconds(1)) 75 | 76 | guard !responses.isEmpty else { 77 | return 78 | } 79 | let response = responses.removeFirst() 80 | unaryMockInboundHandler.respondWithMock(response) 81 | } 82 | }) 83 | .store(in: &cancellables) 84 | 85 | return call 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Mock Clients/CallMocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CallMocks.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 11.02.20. 6 | // 7 | 8 | import Combine 9 | import NIO 10 | import SwiftProtobuf 11 | import XCTest 12 | 13 | @testable import GRPC 14 | 15 | internal protocol HasRequestStream { 16 | associatedtype Request: Message 17 | 18 | var requests: [Request] { get } 19 | var requestStream: AnyPublisher { get } 20 | } 21 | 22 | extension HasRequestStream { 23 | var requestStream: AnyPublisher { 24 | Publishers.Sequence<[Request], Error>(sequence: requests).eraseToAnyPublisher() 25 | } 26 | } 27 | 28 | internal protocol HasResponseStream { 29 | associatedtype Response: Message 30 | 31 | var responses: [Result] { get } 32 | var responseStream: AnyPublisher { get } 33 | } 34 | 35 | extension HasResponseStream { 36 | var responseStream: AnyPublisher { 37 | var sequence = [Response]() 38 | for response in responses { 39 | switch response { 40 | case .success(let message): 41 | sequence.append(message) 42 | case .failure(let status): 43 | return Fail(error: status).eraseToAnyPublisher() 44 | } 45 | } 46 | return Publishers.Sequence<[Response], GRPCStatus>(sequence: sequence).eraseToAnyPublisher() 47 | } 48 | } 49 | 50 | internal struct UnaryMock { 51 | let request: Request 52 | let response: Result 53 | let expectation = XCTestExpectation( 54 | description: "Request matches the expected UnaryMock Request" 55 | ) 56 | } 57 | 58 | internal struct ClientStreamMock: HasRequestStream { 59 | let requests: [Request] 60 | let response: Result 61 | let expectation = XCTestExpectation( 62 | description: "Requests match the expected ClientStreamMock requests" 63 | ) 64 | } 65 | 66 | internal struct ServerStreamMock: HasResponseStream { 67 | let request: Request 68 | let responses: [Result] 69 | let expectation = XCTestExpectation( 70 | description: "Request matches the expected ServerStreamMock request" 71 | ) 72 | } 73 | 74 | internal struct BidirectionalStreamMock 75 | : HasRequestStream, HasResponseStream { 76 | let requests: [Request] 77 | var responses: [Result] 78 | let expectation = XCTestExpectation( 79 | description: "Requests match the expected BidirectionalStreamMock requests" 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Mock Clients/ClientStreamingMockClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientStreamingMockClient.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 10.01.20. 6 | // 7 | 8 | import Combine 9 | import GRPC 10 | import NIO 11 | import SwiftProtobuf 12 | import XCTest 13 | 14 | internal final class ClientStreamingMockClient: BaseMockClient { 15 | typealias ClientStreamingMockCall = ClientStreamMock 16 | 17 | var mockNetworkCalls: [ClientStreamingMockCall] = [] 18 | 19 | func test(callOptions: CallOptions?) -> ClientStreamingCall { 20 | let networkCall = mockNetworkCalls.removeFirst() 21 | 22 | let call = ClientStreamingCall( 23 | connection: connection, 24 | path: "/ok", 25 | callOptions: defaultCallOptions, 26 | errorDelegate: nil 27 | ) 28 | channel.embeddedEventLoop.advanceTime(by: .nanoseconds(1)) 29 | 30 | let unaryMockInboundHandler = MockInboundHandler() 31 | call.subchannel 32 | .map { subchannel in 33 | subchannel.pipeline.handler(type: GRPCClientChannelHandler.self).map { clientChannelHandler in 34 | subchannel.pipeline.addHandler(unaryMockInboundHandler, position: .after(clientChannelHandler)) 35 | } 36 | } 37 | .whenSuccess { _ in } 38 | 39 | var expectedRequests: [Request] = [] 40 | 41 | networkCall.requestStream 42 | .sink(receiveCompletion: { [weak self] completion in 43 | XCTAssertEqual(expectedRequests, networkCall.requests) 44 | switch completion { 45 | case .failure: 46 | unaryMockInboundHandler.respondWithStatus(.processingError) 47 | networkCall.expectation.fulfill() 48 | case .finished: 49 | guard expectedRequests == networkCall.requests else { 50 | XCTFail("Could not match the network call to the next MockNetworkCall.") 51 | fatalError("Could not match the network call to the next MockNetworkCall.") 52 | } 53 | networkCall.expectation.fulfill() 54 | 55 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { 56 | unaryMockInboundHandler.respondWithMock(networkCall.response) 57 | self?.channel.embeddedEventLoop.advanceTime(by: .nanoseconds(1)) 58 | unaryMockInboundHandler.respondWithStatus(.ok) 59 | } 60 | } 61 | }, receiveValue: { request in 62 | expectedRequests.append(request) 63 | }) 64 | .store(in: &cancellables) 65 | 66 | return call 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Mock Clients/ServerStreamingMockClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerStreamingMockClient.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 10.01.20. 6 | // 7 | 8 | import Combine 9 | import GRPC 10 | import NIO 11 | import SwiftProtobuf 12 | import XCTest 13 | 14 | internal final class ServerStreamingMockClient: BaseMockClient { 15 | typealias ServerStreamingMockCall = ServerStreamMock 16 | 17 | var mockNetworkCalls: [ServerStreamingMockCall] = [] 18 | 19 | func test( 20 | _ request: Request, 21 | callOptions: CallOptions?, 22 | handler: @escaping (Response) -> Void 23 | ) -> ServerStreamingCall { 24 | let networkCall = mockNetworkCalls.removeFirst() 25 | 26 | guard networkCall.request == request else { 27 | XCTFail("Could not match the network call to the next MockNetworkCall.") 28 | fatalError("Could not match the network call to the next MockNetworkCall.") 29 | } 30 | networkCall.expectation.fulfill() 31 | 32 | let call = ServerStreamingCall( 33 | connection: connection, 34 | path: "/test", 35 | request: request, 36 | callOptions: defaultCallOptions, 37 | errorDelegate: nil, 38 | handler: handler 39 | ) 40 | 41 | channel.embeddedEventLoop.advanceTime(by: .nanoseconds(1)) 42 | 43 | let unaryMockInboundHandler = MockInboundHandler() 44 | call.subchannel 45 | .map { subchannel in 46 | subchannel.pipeline.handler(type: GRPCClientChannelHandler.self).map { clientChannelHandler in 47 | subchannel.pipeline.addHandler(unaryMockInboundHandler, position: .after(clientChannelHandler)) 48 | } 49 | } 50 | .whenSuccess { _ in } 51 | 52 | channel.embeddedEventLoop.advanceTime(by: .nanoseconds(1)) 53 | 54 | networkCall.responseStream 55 | .sink(receiveCompletion: { completion in 56 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { 57 | switch completion { 58 | case .failure(let status): 59 | unaryMockInboundHandler.respondWithStatus(status) 60 | case .finished: 61 | unaryMockInboundHandler.respondWithStatus(.ok) 62 | } 63 | } 64 | }, receiveValue: { message in 65 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { 66 | unaryMockInboundHandler.respondWithMock(.success(message)) 67 | } 68 | }) 69 | .store(in: &self.cancellables) 70 | 71 | return call 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/Mock Clients/UnaryServiceMockClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnaryServiceMockClient.swift 3 | // 4 | // 5 | // Created by Tim Mewe on 10.01.20. 6 | // 7 | 8 | import NIO 9 | import SwiftProtobuf 10 | import XCTest 11 | 12 | @testable import GRPC 13 | 14 | internal final class UnaryMockClient: BaseMockClient { 15 | typealias UnaryMockCall = UnaryMock 16 | 17 | var mockNetworkCalls: [UnaryMockCall] = [] 18 | 19 | func test(_ request: Request, callOptions: CallOptions?) -> UnaryCall { 20 | let networkCall = mockNetworkCalls.removeFirst() 21 | 22 | // Check if the Request correspons to the expected Response 23 | guard networkCall.request == request else { 24 | XCTFail("Could not match the network call to the next MockNetworkCall.") 25 | fatalError("Could not match the network call to the next MockNetworkCall.") 26 | } 27 | networkCall.expectation.fulfill() 28 | 29 | // Create our UnaryCall and advance the EventLoop to register all nescessary ChannelHanders 30 | let call = UnaryCall( 31 | connection: connection, 32 | path: "/test", 33 | request: request, 34 | callOptions: defaultCallOptions, 35 | errorDelegate: nil 36 | ) 37 | channel.embeddedEventLoop.advanceTime(by: .nanoseconds(1)) 38 | 39 | // Creates a subchannel for handling HTTP2 Streams with the following setup: 40 | // [I] ↓↑ [O] 41 | // GRPCClientChannelHandler ↓↑ GRPCClientChannelHandler [handler0] 42 | // GRPCClientUnaryResponseChannelHandler ↓↑ [handler1] 43 | // UnaryRequestChannelHandler ↓↑ [handler2] 44 | // 45 | // We need to inject our `UnaryMockInboundHandler` after the GRPCClientChannelHandler because a 46 | // GRPCClientChannelHandler has the following Inbound Types: 47 | // public typealias InboundIn = HTTP2Frame 48 | // public typealias InboundOut = GRPCClientResponsePart 49 | // --> We get the subchannel, get the position of the GRPCClientChannelHandler and add our mock handler after that: 50 | let unaryMockInboundHandler = MockInboundHandler() 51 | call.subchannel 52 | .map { subchannel in 53 | subchannel.pipeline.handler(type: GRPCClientChannelHandler.self).map { clientChannelHandler in 54 | subchannel.pipeline.addHandler(unaryMockInboundHandler, position: .after(clientChannelHandler)) 55 | } 56 | } 57 | .whenSuccess { _ in } 58 | channel.embeddedEventLoop.advanceTime(by: .nanoseconds(1)) 59 | 60 | // State after injecting our UnaryMockInboundHandler: 61 | // [I] ↓↑ [O] 62 | // GRPCClientChannelHandler ↓↑ GRPCClientChannelHandler [handler0] 63 | // UnaryMockInboundHandler ↓↑ [handler3] 64 | // GRPCClientUnaryResponseChannelHandler ↓↑ [handler1] 65 | // UnaryRequestChannelHandler ↓↑ [handler2] 66 | 67 | // Trigger our `fireChannelRead` that is going to propagate inbound. 68 | unaryMockInboundHandler.respondWithMock(networkCall.response) 69 | 70 | return call 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/Grebe-FrameworkTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | /// All test cases 5 | public func allTests() -> [XCTestCaseEntry] { 6 | [ 7 | testCase(Grebe_FrameworkTests.allTests) 8 | ] 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import Grebe_FrameworkTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += Grebe_FrameworkTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------