├── .codecov.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .ruby-version ├── .swift-version ├── .swiftlint.yml ├── .xcode-version ├── Dangerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Mintfile ├── Package.swift ├── README.md ├── Scripts ├── Bootstrap │ ├── bundler.sh │ ├── common.sh │ ├── congratulations.sh │ ├── gemfile.sh │ ├── homebrew.sh │ ├── macos.sh │ ├── mint.sh │ ├── mintfile.sh │ ├── rbenv.sh │ ├── ruby.sh │ ├── spm.sh │ ├── swift.sh │ ├── swiftenv.sh │ ├── welcome.sh │ └── xcode.sh ├── Helpers │ ├── script-paths.sh │ └── script-run.sh ├── bootstrap.sh └── swiftlint.sh ├── Sources ├── .swiftlint.yml ├── Decoder │ ├── Options │ │ ├── URLQueryDataDecodingStrategy.swift │ │ ├── URLQueryDateDecodingStrategy.swift │ │ ├── URLQueryDecodingOptions.swift │ │ ├── URLQueryKeyDecodingStrategy.swift │ │ └── URLQueryNonConformingFloatDecodingStrategy.swift │ ├── URLQueryDecoder.swift │ ├── URLQueryDeserializer.swift │ ├── URLQueryKeyedDecodingContainer.swift │ ├── URLQuerySingleValueDecodingContainer.swift │ ├── URLQueryUnkeyedDecodingContainer.swift │ └── URLQueryValueDecoding.swift ├── Encoder │ ├── Options │ │ ├── URLQueryArrayEncodingStrategy.swift │ │ ├── URLQueryBoolEncodingStrategy.swift │ │ ├── URLQueryDataEncodingStrategy.swift │ │ ├── URLQueryDateEncodingStrategy.swift │ │ ├── URLQueryEncodingOptions.swift │ │ ├── URLQueryKeyEncodingStrategy.swift │ │ ├── URLQueryNonConformingFloatEncodingStrategy.swift │ │ └── URLQuerySpaceEncodingStrategy.swift │ ├── URLQueryAnyKeyedEncodingContainer.swift │ ├── URLQueryEncoder.swift │ ├── URLQueryKeyedEncodingContainer.swift │ ├── URLQuerySerializer.swift │ ├── URLQuerySingleValueEncodingContainer.swift │ ├── URLQueryUnkeyedEncodingContainer.swift │ ├── URLQueryValueEncoding.swift │ └── URLQueryValueForm.swift ├── Info.plist ├── Tools │ ├── AnyCodingKey.swift │ ├── Character+Extensions.swift │ ├── Collection+Extensions.swift │ ├── Dictionary+Extensions.swift │ ├── Optional+Extensions.swift │ ├── RangeReplaceableCollection+Extensions.swift │ ├── String+Extensions.swift │ └── TimeZone+Extensions.swift ├── URLQueryCoder.h └── Value │ ├── URLQueryValue.swift │ └── URLQueryValueResolver.swift ├── Tests ├── .swiftlint.yml ├── Decoder │ ├── URLQueryDecoderStrategiesTests.swift │ ├── URLQueryDecoderTesting.swift │ └── URLQueryDecoderTests.swift ├── Encoder │ ├── URLQueryEncoderStrategiesTests.swift │ ├── URLQueryEncoderTesting.swift │ └── URLQueryEncoderTests.swift └── Info.plist ├── URLQueryCoder.podspec └── URLQueryCoder.xcodeproj ├── project.pbxproj ├── project.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── xcshareddata └── xcschemes ├── URLQueryCoder iOS.xcscheme ├── URLQueryCoder macOS.xcscheme ├── URLQueryCoder tvOS.xcscheme └── URLQueryCoder watchOS.xcscheme /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "70...100" 5 | 6 | status: 7 | project: 8 | default: 9 | threshold: 1.0% 10 | changes: 11 | default: 12 | threshold: 1.0% 13 | patch: off 14 | 15 | comment: 16 | layout: "flags, files" 17 | behavior: new 18 | 19 | ignore: 20 | - Tests/**/* 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | LC_CTYPE: en_US.UTF-8 13 | LANG: en_US.UTF-8 14 | 15 | jobs: 16 | BuildAndTests: 17 | name: Build & Tests 18 | runs-on: macOS-12 19 | env: 20 | DEVELOPER_DIR: /Applications/Xcode_14.0.1.app/Contents/Developer 21 | XCODE_PROJECT: URLQueryCoder.xcodeproj 22 | IOS_SCHEME: URLQueryCoder iOS 23 | IOS_DESTINATION: OS=16.0,name=iPhone 14 24 | IOS_RESULT_PATH: xcodebuild-ios.xcresult 25 | MACOS_SCHEME: URLQueryCoder macOS 26 | MACOS_DESTINATION: platform=macOS 27 | MACOS_RESULT_PATH: xcodebuild-macos.xcresult 28 | TVOS_SCHEME: URLQueryCoder tvOS 29 | TVOS_DESTINATION: OS=16.0,name=Apple TV 30 | TVOS_RESULT_PATH: xcodebuild-tvos.xcresult 31 | WATCHOS_SCHEME: URLQueryCoder watchOS 32 | WATCHOS_DESTINATION: OS=9.0,name=Apple Watch Series 8 (45mm) 33 | WATCHOS_RESULT_PATH: xcodebuild-watchos.xcresult 34 | SKIP_SWIFTLINT: YES 35 | DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} 36 | steps: 37 | - uses: actions/checkout@v3 38 | - uses: ruby/setup-ruby@v1 39 | - name: Bundler 40 | run: | 41 | gem install bundler 42 | bundle install --without=documentation 43 | - name: Preparation 44 | run: | 45 | set -o pipefail 46 | swift --version 47 | - name: Test iOS 48 | run: | 49 | xcodebuild clean build test \ 50 | -project "$XCODE_PROJECT" \ 51 | -scheme "$IOS_SCHEME" \ 52 | -destination "$IOS_DESTINATION" \ 53 | -resultBundlePath "$IOS_RESULT_PATH" | xcpretty -f `xcpretty-json-formatter` 54 | 55 | bash <(curl -s https://codecov.io/bash) -cF ios -J 'URLQueryCoder' 56 | - name: Test macOS 57 | run: | 58 | xcodebuild clean build test \ 59 | -project "$XCODE_PROJECT" \ 60 | -scheme "$MACOS_SCHEME" \ 61 | -destination "$MACOS_DESTINATION" \ 62 | -resultBundlePath "$MACOS_RESULT_PATH" | xcpretty -f `xcpretty-json-formatter` 63 | 64 | bash <(curl -s https://codecov.io/bash) -cF osx -J 'URLQueryCoder' 65 | - name: Test tvOS 66 | run: | 67 | xcodebuild clean build test \ 68 | -project "$XCODE_PROJECT" \ 69 | -scheme "$TVOS_SCHEME" \ 70 | -destination "$TVOS_DESTINATION" \ 71 | -resultBundlePath "$TVOS_RESULT_PATH" | xcpretty -f `xcpretty-json-formatter` 72 | 73 | bash <(curl -s https://codecov.io/bash) -cF tvos -J 'URLQueryCoder' 74 | - name: Build watchOS 75 | run: | 76 | xcodebuild clean build \ 77 | -project "$XCODE_PROJECT" \ 78 | -scheme "$WATCHOS_SCHEME" \ 79 | -destination "$WATCHOS_DESTINATION" \ 80 | -resultBundlePath "$WATCHOS_RESULT_PATH" | xcpretty -f `xcpretty-json-formatter` 81 | - name: Danger 82 | run: bundle exec danger --remove-previous-comments 83 | 84 | Cocoapods: 85 | name: Cocoapods 86 | runs-on: macOS-latest 87 | steps: 88 | - uses: actions/checkout@v3 89 | - uses: ruby/setup-ruby@v1 90 | - name: Bundler 91 | run: | 92 | gem install bundler 93 | bundle install --without=documentation 94 | - name: Linting 95 | run: bundle exec pod lib lint --skip-tests --allow-warnings 96 | 97 | SPM: 98 | name: Swift Package Manager 99 | runs-on: macOS-latest 100 | steps: 101 | - uses: actions/checkout@v3 102 | - name: Build 103 | run: swift build 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## OS X files 2 | .DS_Store 3 | .DS_Store? 4 | .Trashes 5 | .Spotlight-V100 6 | *.swp 7 | 8 | ## Xcode build files 9 | DerivedData/ 10 | build/ 11 | 12 | ## Xcode private settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.xccheckout 25 | *.moved-aside 26 | *.xcuserstate 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Packages Manager 40 | Packages 41 | .build 42 | .swiftpm 43 | 44 | # CocoaPods 45 | Pods/ 46 | 47 | # Carthage 48 | Carthage/Build 49 | 50 | # fastlane 51 | fastlane/report.xml 52 | fastlane/Preview.html 53 | fastlane/screenshots/**/*.png 54 | fastlane/test_output 55 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.0 2 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.5 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | - Tests 4 | 5 | whitelist_rules: 6 | - anyobject_protocol 7 | - array_init 8 | - attributes 9 | - block_based_kvo 10 | - class_delegate_protocol 11 | - closing_brace 12 | - closure_body_length 13 | - closure_end_indentation 14 | - closure_parameter_position 15 | - closure_spacing 16 | - collection_alignment 17 | - colon 18 | - comma 19 | - compiler_protocol_init 20 | - conditional_returns_on_newline 21 | - contains_over_filter_count 22 | - contains_over_filter_is_empty 23 | - contains_over_first_not_nil 24 | - contains_over_range_nil_comparison 25 | - control_statement 26 | - convenience_type 27 | - custom_rules 28 | - cyclomatic_complexity 29 | - discarded_notification_center_observer 30 | - discouraged_direct_init 31 | - discouraged_object_literal 32 | - duplicate_enum_cases 33 | - duplicate_imports 34 | - dynamic_inline 35 | - empty_count 36 | - empty_enum_arguments 37 | - empty_parameters 38 | - empty_parentheses_with_trailing_closure 39 | - empty_string 40 | - empty_xctest_method 41 | - enum_case_associated_values_count 42 | - explicit_init 43 | - extension_access_modifier 44 | - file_header 45 | - file_length 46 | - file_name 47 | - first_where 48 | - flatmap_over_map_reduce 49 | - for_where 50 | - force_try 51 | - function_body_length 52 | - generic_type_name 53 | - identical_operands 54 | - identifier_name 55 | - implicit_getter 56 | - inert_defer 57 | - is_disjoint 58 | - joined_default_parameter 59 | - large_tuple 60 | - last_where 61 | - leading_whitespace 62 | - legacy_cggeometry_functions 63 | - legacy_constant 64 | - legacy_constructor 65 | - legacy_hashing 66 | - legacy_multiple 67 | - legacy_nsgeometry_functions 68 | - legacy_random 69 | - let_var_whitespace 70 | - line_length 71 | - literal_expression_end_indentation 72 | - lower_acl_than_parent 73 | - mark 74 | - modifier_order 75 | - multiline_arguments 76 | - multiline_arguments_brackets 77 | - multiline_literal_brackets 78 | - multiline_parameters 79 | - multiline_parameters_brackets 80 | - multiple_closures_with_trailing_closure 81 | - nesting 82 | - no_space_in_method_call 83 | - nsobject_prefer_isequal 84 | - number_separator 85 | - opening_brace 86 | - operator_usage_whitespace 87 | - operator_whitespace 88 | - optional_enum_case_matching 89 | - orphaned_doc_comment 90 | - overridden_super_call 91 | - override_in_extension 92 | - pattern_matching_keywords 93 | - prefer_self_type_over_type_of_self 94 | - private_action 95 | - private_outlet 96 | - private_over_fileprivate 97 | - private_unit_test 98 | - prohibited_super_call 99 | - protocol_property_accessors_order 100 | - quick_discouraged_call 101 | - quick_discouraged_focused_test 102 | - quick_discouraged_pending_test 103 | - raw_value_for_camel_cased_codable_enum 104 | - reduce_boolean 105 | - redundant_discardable_let 106 | - redundant_nil_coalescing 107 | - redundant_objc_attribute 108 | - redundant_optional_initialization 109 | - redundant_set_access_control 110 | - redundant_string_enum_value 111 | - redundant_type_annotation 112 | - redundant_void_return 113 | - return_arrow_whitespace 114 | - shorthand_operator 115 | - single_test_class 116 | - sorted_first_last 117 | - statement_position 118 | - static_operator 119 | - superfluous_disable_command 120 | - switch_case_alignment 121 | - switch_case_on_newline 122 | - syntactic_sugar 123 | - todo 124 | - toggle_bool 125 | - trailing_closure 126 | - trailing_comma 127 | - trailing_newline 128 | - trailing_semicolon 129 | - trailing_whitespace 130 | - type_body_length 131 | - type_contents_order 132 | - type_name 133 | - unavailable_function 134 | - unneeded_break_in_switch 135 | - untyped_error_in_catch 136 | - unused_capture_list 137 | - unused_closure_parameter 138 | - unused_control_flow_label 139 | - unused_declaration 140 | - unused_enumerated 141 | - unused_import 142 | - unused_optional_binding 143 | - unused_setter_value 144 | - valid_ibinspectable 145 | - vertical_parameter_alignment 146 | - vertical_parameter_alignment_on_call 147 | - vertical_whitespace 148 | - vertical_whitespace_between_cases 149 | - vertical_whitespace_closing_braces 150 | - void_return 151 | - weak_delegate 152 | - xct_specific_matcher 153 | - xctfail_message 154 | - yoda_condition 155 | 156 | attributes: 157 | always_on_same_line: 158 | - '@IBAction' 159 | - '@IBOutlet' 160 | - '@IBDesignable' 161 | - '@IBInspectable' 162 | - '@GKInspectable' 163 | - '@NSCopying' 164 | - '@NSManaged' 165 | - '@dynamic' 166 | - '@nonobjc' 167 | - '@objc' 168 | - '@objcMembers' 169 | - '@testable' 170 | always_on_line_above: 171 | - '@UIApplicationMain' 172 | - '@NSApplicationMain' 173 | - '@dynamicMemberLookup' 174 | - '@dynamicCallable' 175 | - '@propertyWrapper' 176 | - '@convention' 177 | - '@frozen' 178 | - '@available' 179 | - '@discardableResult' 180 | - '@inlinable' 181 | - '@usableFromInline' 182 | - '@warn_unqualified_access' 183 | - '@requires_stored_property_inits' 184 | 185 | closure_body_length: 186 | warning: 20 187 | error: 200 188 | 189 | collection_alignment: 190 | align_colons: false 191 | 192 | colon: 193 | flexible_right_spacing: true 194 | apply_to_dictionaries: true 195 | 196 | conditional_returns_on_newline: 197 | if_only: false 198 | 199 | cyclomatic_complexity: 200 | warning: 16 201 | error: 160 202 | ignores_case_statements: true 203 | 204 | discouraged_direct_init: 205 | types: 206 | - Bundle 207 | - UIDevice 208 | - AVAudioSession 209 | 210 | discouraged_object_literal: 211 | image_literal: true 212 | color_literal: true 213 | 214 | duplicate_enum_cases: 215 | severity: warning 216 | 217 | dynamic_inline: 218 | severity: warning 219 | 220 | empty_count: 221 | severity: warning 222 | 223 | enum_case_associated_values_count: 224 | warning: 4 225 | error: 40 226 | 227 | file_header: 228 | forbidden_pattern: ".?" 229 | 230 | file_length: 231 | warning: 400 232 | error: 4000 233 | ignore_comment_only_lines: true 234 | 235 | file_name: 236 | excluded: 237 | - main.swift 238 | prefix_pattern: '' 239 | suffix_pattern: '[+][A-z][A-z]+' 240 | nested_type_separator: '' 241 | 242 | force_try: 243 | severity: warning 244 | 245 | function_body_length: 246 | warning: 40 247 | error: 400 248 | 249 | generic_type_name: 250 | min_length: 251 | warning: 3 252 | error: 0 253 | max_length: 254 | warning: 20 255 | error: 200 256 | validates_start_with_lowercase: true 257 | excluded: 258 | - T 259 | - U 260 | - V 261 | 262 | identifier_name: 263 | min_length: 264 | warning: 2 265 | error: 1 266 | max_length: 267 | warning: 40 268 | error: 400 269 | validates_start_with_lowercase: true 270 | excluded: 271 | - a 272 | - r 273 | - g 274 | - b 275 | - i 276 | - j 277 | - x 278 | - y 279 | - z 280 | - w 281 | 282 | large_tuple: 283 | warning: 3 284 | error: 8 285 | 286 | line_length: 287 | warning: 120 288 | error: 1200 289 | ignores_urls: false 290 | ignores_function_declarations: false 291 | ignores_comments: false 292 | ignores_interpolated_strings: false 293 | 294 | modifier_order: 295 | preferred_modifier_order: 296 | - acl 297 | - setterACL 298 | - override 299 | - owned 300 | - mutators 301 | - final 302 | - typeMethods 303 | - required 304 | - convenience 305 | - lazy 306 | - dynamic 307 | 308 | multiline_arguments: 309 | first_argument_location: next_line 310 | only_enforce_after_first_closure_on_first_line: false 311 | 312 | nesting: 313 | type_level: 314 | warning: 1 315 | error: 10 316 | statement_level: 317 | warning: 4 318 | error: 40 319 | 320 | number_separator: 321 | minimum_length: 5 322 | minimum_fraction_length: 5 323 | exclude_ranges: [] 324 | 325 | overridden_super_call: 326 | included: 327 | - '*' 328 | excluded: [] 329 | 330 | private_outlet: 331 | allow_private_set: false 332 | 333 | private_over_fileprivate: 334 | validate_extensions: true 335 | 336 | prohibited_super_call: 337 | included: 338 | - '*' 339 | excluded: [] 340 | 341 | shorthand_operator: 342 | severity: warning 343 | 344 | statement_position: 345 | statement_mode: default 346 | 347 | switch_case_alignment: 348 | indented_cases: false 349 | 350 | trailing_closure: 351 | only_single_muted_parameter: false 352 | 353 | trailing_comma: 354 | mandatory_comma: false 355 | 356 | trailing_whitespace: 357 | ignores_empty_lines: false 358 | ignores_comments: false 359 | 360 | type_contents_order: 361 | order: 362 | - associated_type 363 | - type_alias 364 | - subtype 365 | - case 366 | - type_property 367 | - type_method 368 | - ib_outlet 369 | - ib_inspectable 370 | - instance_property 371 | - initializer 372 | - ib_action 373 | - other_method 374 | - view_life_cycle_method 375 | - subscript 376 | 377 | type_body_length: 378 | warning: 400 379 | error: 4000 380 | 381 | type_name: 382 | min_length: 383 | warning: 3 384 | error: 0 385 | max_length: 386 | warning: 50 387 | error: 500 388 | validates_start_with_lowercase: true 389 | 390 | unused_optional_binding: 391 | ignore_optional_try: false 392 | 393 | unused_declaration: 394 | severity: warning 395 | include_public_and_open: false 396 | 397 | vertical_whitespace: 398 | max_empty_lines: 1 399 | 400 | warning_threshold: 100 401 | -------------------------------------------------------------------------------- /.xcode-version: -------------------------------------------------------------------------------- 1 | 14.1 2 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | def report_xcode_summary(platform:) 2 | path = "xcodebuild-#{platform.downcase}.xcresult" 3 | 4 | xcode_summary.ignores_warnings = false 5 | xcode_summary.inline_mode = true 6 | 7 | xcode_summary.report(path) 8 | end 9 | 10 | warn('This pull request is marked as Work in Progress. DO NOT MERGE!') if github.pr_title.include? "[WIP]" 11 | 12 | swiftlint.lint_all_files = true 13 | swiftlint.lint_files(fail_on_error: true, inline_mode: true) 14 | 15 | report_xcode_summary(platform: "iOS") 16 | report_xcode_summary(platform: "macOS") 17 | report_xcode_summary(platform: "tvOS") 18 | report_xcode_summary(platform: "watchOS") 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'xcode-install' 4 | 5 | gem 'cocoapods' 6 | gem 'xcpretty' 7 | gem 'xcpretty-json-formatter' 8 | 9 | gem "danger" 10 | gem 'danger-xcode_summary' 11 | gem 'danger-swiftlint' 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | activesupport (6.1.7) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 1.6, < 2) 9 | minitest (>= 5.1) 10 | tzinfo (~> 2.0) 11 | zeitwerk (~> 2.3) 12 | addressable (2.8.1) 13 | public_suffix (>= 2.0.2, < 6.0) 14 | algoliasearch (1.27.5) 15 | httpclient (~> 2.8, >= 2.8.3) 16 | json (>= 1.5.1) 17 | artifactory (3.0.15) 18 | atomos (0.1.3) 19 | aws-eventstream (1.2.0) 20 | aws-partitions (1.655.0) 21 | aws-sdk-core (3.166.0) 22 | aws-eventstream (~> 1, >= 1.0.2) 23 | aws-partitions (~> 1, >= 1.651.0) 24 | aws-sigv4 (~> 1.5) 25 | jmespath (~> 1, >= 1.6.1) 26 | aws-sdk-kms (1.59.0) 27 | aws-sdk-core (~> 3, >= 3.165.0) 28 | aws-sigv4 (~> 1.1) 29 | aws-sdk-s3 (1.117.1) 30 | aws-sdk-core (~> 3, >= 3.165.0) 31 | aws-sdk-kms (~> 1) 32 | aws-sigv4 (~> 1.4) 33 | aws-sigv4 (1.5.2) 34 | aws-eventstream (~> 1, >= 1.0.2) 35 | babosa (1.0.4) 36 | claide (1.1.0) 37 | claide-plugins (0.9.2) 38 | cork 39 | nap 40 | open4 (~> 1.3) 41 | cocoapods (1.11.3) 42 | addressable (~> 2.8) 43 | claide (>= 1.0.2, < 2.0) 44 | cocoapods-core (= 1.11.3) 45 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 46 | cocoapods-downloader (>= 1.4.0, < 2.0) 47 | cocoapods-plugins (>= 1.0.0, < 2.0) 48 | cocoapods-search (>= 1.0.0, < 2.0) 49 | cocoapods-trunk (>= 1.4.0, < 2.0) 50 | cocoapods-try (>= 1.1.0, < 2.0) 51 | colored2 (~> 3.1) 52 | escape (~> 0.0.4) 53 | fourflusher (>= 2.3.0, < 3.0) 54 | gh_inspector (~> 1.0) 55 | molinillo (~> 0.8.0) 56 | nap (~> 1.0) 57 | ruby-macho (>= 1.0, < 3.0) 58 | xcodeproj (>= 1.21.0, < 2.0) 59 | cocoapods-core (1.11.3) 60 | activesupport (>= 5.0, < 7) 61 | addressable (~> 2.8) 62 | algoliasearch (~> 1.0) 63 | concurrent-ruby (~> 1.1) 64 | fuzzy_match (~> 2.0.4) 65 | nap (~> 1.0) 66 | netrc (~> 0.11) 67 | public_suffix (~> 4.0) 68 | typhoeus (~> 1.0) 69 | cocoapods-deintegrate (1.0.5) 70 | cocoapods-downloader (1.6.3) 71 | cocoapods-plugins (1.0.0) 72 | nap 73 | cocoapods-search (1.0.1) 74 | cocoapods-trunk (1.6.0) 75 | nap (>= 0.8, < 2.0) 76 | netrc (~> 0.11) 77 | cocoapods-try (1.2.0) 78 | colored (1.2) 79 | colored2 (3.1.2) 80 | commander (4.6.0) 81 | highline (~> 2.0.0) 82 | concurrent-ruby (1.1.10) 83 | cork (0.3.0) 84 | colored2 (~> 3.1) 85 | danger (9.0.0) 86 | claide (~> 1.0) 87 | claide-plugins (>= 0.9.2) 88 | colored2 (~> 3.1) 89 | cork (~> 0.1) 90 | faraday (>= 0.9.0, < 2.0) 91 | faraday-http-cache (~> 2.0) 92 | git (~> 1.7) 93 | kramdown (~> 2.3) 94 | kramdown-parser-gfm (~> 1.0) 95 | no_proxy_fix 96 | octokit (~> 5.0) 97 | terminal-table (>= 1, < 4) 98 | danger-plugin-api (1.0.0) 99 | danger (> 2.0) 100 | danger-swiftlint (0.30.2) 101 | danger 102 | rake (> 10) 103 | thor (~> 0.19) 104 | danger-xcode_summary (1.2.0) 105 | danger-plugin-api (~> 1.0) 106 | xcresult (~> 0.2) 107 | declarative (0.0.20) 108 | digest-crc (0.6.4) 109 | rake (>= 12.0.0, < 14.0.0) 110 | domain_name (0.5.20190701) 111 | unf (>= 0.0.5, < 1.0.0) 112 | dotenv (2.8.1) 113 | emoji_regex (3.2.3) 114 | escape (0.0.4) 115 | ethon (0.15.0) 116 | ffi (>= 1.15.0) 117 | excon (0.93.1) 118 | faraday (1.10.2) 119 | faraday-em_http (~> 1.0) 120 | faraday-em_synchrony (~> 1.0) 121 | faraday-excon (~> 1.1) 122 | faraday-httpclient (~> 1.0) 123 | faraday-multipart (~> 1.0) 124 | faraday-net_http (~> 1.0) 125 | faraday-net_http_persistent (~> 1.0) 126 | faraday-patron (~> 1.0) 127 | faraday-rack (~> 1.0) 128 | faraday-retry (~> 1.0) 129 | ruby2_keywords (>= 0.0.4) 130 | faraday-cookie_jar (0.0.7) 131 | faraday (>= 0.8.0) 132 | http-cookie (~> 1.0.0) 133 | faraday-em_http (1.0.0) 134 | faraday-em_synchrony (1.0.0) 135 | faraday-excon (1.1.0) 136 | faraday-http-cache (2.4.1) 137 | faraday (>= 0.8) 138 | faraday-httpclient (1.0.1) 139 | faraday-multipart (1.0.4) 140 | multipart-post (~> 2) 141 | faraday-net_http (1.0.1) 142 | faraday-net_http_persistent (1.2.0) 143 | faraday-patron (1.0.0) 144 | faraday-rack (1.0.0) 145 | faraday-retry (1.0.3) 146 | faraday_middleware (1.2.0) 147 | faraday (~> 1.0) 148 | fastimage (2.2.6) 149 | fastlane (2.210.1) 150 | CFPropertyList (>= 2.3, < 4.0.0) 151 | addressable (>= 2.8, < 3.0.0) 152 | artifactory (~> 3.0) 153 | aws-sdk-s3 (~> 1.0) 154 | babosa (>= 1.0.3, < 2.0.0) 155 | bundler (>= 1.12.0, < 3.0.0) 156 | colored 157 | commander (~> 4.6) 158 | dotenv (>= 2.1.1, < 3.0.0) 159 | emoji_regex (>= 0.1, < 4.0) 160 | excon (>= 0.71.0, < 1.0.0) 161 | faraday (~> 1.0) 162 | faraday-cookie_jar (~> 0.0.6) 163 | faraday_middleware (~> 1.0) 164 | fastimage (>= 2.1.0, < 3.0.0) 165 | gh_inspector (>= 1.1.2, < 2.0.0) 166 | google-apis-androidpublisher_v3 (~> 0.3) 167 | google-apis-playcustomapp_v1 (~> 0.1) 168 | google-cloud-storage (~> 1.31) 169 | highline (~> 2.0) 170 | json (< 3.0.0) 171 | jwt (>= 2.1.0, < 3) 172 | mini_magick (>= 4.9.4, < 5.0.0) 173 | multipart-post (~> 2.0.0) 174 | naturally (~> 2.2) 175 | optparse (~> 0.1.1) 176 | plist (>= 3.1.0, < 4.0.0) 177 | rubyzip (>= 2.0.0, < 3.0.0) 178 | security (= 0.1.3) 179 | simctl (~> 1.6.3) 180 | terminal-notifier (>= 2.0.0, < 3.0.0) 181 | terminal-table (>= 1.4.5, < 2.0.0) 182 | tty-screen (>= 0.6.3, < 1.0.0) 183 | tty-spinner (>= 0.8.0, < 1.0.0) 184 | word_wrap (~> 1.0.0) 185 | xcodeproj (>= 1.13.0, < 2.0.0) 186 | xcpretty (~> 0.3.0) 187 | xcpretty-travis-formatter (>= 0.0.3) 188 | ffi (1.15.5) 189 | fourflusher (2.3.1) 190 | fuzzy_match (2.0.4) 191 | gh_inspector (1.1.3) 192 | git (1.12.0) 193 | addressable (~> 2.8) 194 | rchardet (~> 1.8) 195 | google-apis-androidpublisher_v3 (0.30.0) 196 | google-apis-core (>= 0.9.1, < 2.a) 197 | google-apis-core (0.9.1) 198 | addressable (~> 2.5, >= 2.5.1) 199 | googleauth (>= 0.16.2, < 2.a) 200 | httpclient (>= 2.8.1, < 3.a) 201 | mini_mime (~> 1.0) 202 | representable (~> 3.0) 203 | retriable (>= 2.0, < 4.a) 204 | rexml 205 | webrick 206 | google-apis-iamcredentials_v1 (0.16.0) 207 | google-apis-core (>= 0.9.1, < 2.a) 208 | google-apis-playcustomapp_v1 (0.12.0) 209 | google-apis-core (>= 0.9.1, < 2.a) 210 | google-apis-storage_v1 (0.19.0) 211 | google-apis-core (>= 0.9.0, < 2.a) 212 | google-cloud-core (1.6.0) 213 | google-cloud-env (~> 1.0) 214 | google-cloud-errors (~> 1.0) 215 | google-cloud-env (1.6.0) 216 | faraday (>= 0.17.3, < 3.0) 217 | google-cloud-errors (1.3.0) 218 | google-cloud-storage (1.44.0) 219 | addressable (~> 2.8) 220 | digest-crc (~> 0.4) 221 | google-apis-iamcredentials_v1 (~> 0.1) 222 | google-apis-storage_v1 (~> 0.19.0) 223 | google-cloud-core (~> 1.6) 224 | googleauth (>= 0.16.2, < 2.a) 225 | mini_mime (~> 1.0) 226 | googleauth (1.3.0) 227 | faraday (>= 0.17.3, < 3.a) 228 | jwt (>= 1.4, < 3.0) 229 | memoist (~> 0.16) 230 | multi_json (~> 1.11) 231 | os (>= 0.9, < 2.0) 232 | signet (>= 0.16, < 2.a) 233 | highline (2.0.3) 234 | http-cookie (1.0.5) 235 | domain_name (~> 0.5) 236 | httpclient (2.8.3) 237 | i18n (1.12.0) 238 | concurrent-ruby (~> 1.0) 239 | jmespath (1.6.1) 240 | json (2.6.2) 241 | jwt (2.5.0) 242 | kramdown (2.4.0) 243 | rexml 244 | kramdown-parser-gfm (1.1.0) 245 | kramdown (~> 2.0) 246 | memoist (0.16.2) 247 | mini_magick (4.11.0) 248 | mini_mime (1.1.2) 249 | minitest (5.16.3) 250 | molinillo (0.8.0) 251 | multi_json (1.15.0) 252 | multipart-post (2.0.0) 253 | nanaimo (0.3.0) 254 | nap (1.1.0) 255 | naturally (2.2.1) 256 | netrc (0.11.0) 257 | no_proxy_fix (0.1.2) 258 | octokit (5.6.1) 259 | faraday (>= 1, < 3) 260 | sawyer (~> 0.9) 261 | open4 (1.3.4) 262 | optparse (0.1.1) 263 | os (1.1.4) 264 | plist (3.6.0) 265 | public_suffix (4.0.7) 266 | rake (13.0.6) 267 | rchardet (1.8.0) 268 | representable (3.2.0) 269 | declarative (< 0.1.0) 270 | trailblazer-option (>= 0.1.1, < 0.2.0) 271 | uber (< 0.2.0) 272 | retriable (3.1.2) 273 | rexml (3.2.5) 274 | rouge (2.0.7) 275 | ruby-macho (2.5.1) 276 | ruby2_keywords (0.0.5) 277 | rubyzip (2.3.2) 278 | sawyer (0.9.2) 279 | addressable (>= 2.3.5) 280 | faraday (>= 0.17.3, < 3) 281 | security (0.1.3) 282 | signet (0.17.0) 283 | addressable (~> 2.8) 284 | faraday (>= 0.17.5, < 3.a) 285 | jwt (>= 1.5, < 3.0) 286 | multi_json (~> 1.10) 287 | simctl (1.6.8) 288 | CFPropertyList 289 | naturally 290 | terminal-notifier (2.0.0) 291 | terminal-table (1.8.0) 292 | unicode-display_width (~> 1.1, >= 1.1.1) 293 | thor (0.20.3) 294 | trailblazer-option (0.1.2) 295 | tty-cursor (0.7.1) 296 | tty-screen (0.8.1) 297 | tty-spinner (0.9.3) 298 | tty-cursor (~> 0.7) 299 | typhoeus (1.4.0) 300 | ethon (>= 0.9.0) 301 | tzinfo (2.0.5) 302 | concurrent-ruby (~> 1.0) 303 | uber (0.1.0) 304 | unf (0.1.4) 305 | unf_ext 306 | unf_ext (0.0.8.2) 307 | unicode-display_width (1.8.0) 308 | webrick (1.7.0) 309 | word_wrap (1.0.0) 310 | xcode-install (2.8.1) 311 | claide (>= 0.9.1) 312 | fastlane (>= 2.1.0, < 3.0.0) 313 | xcodeproj (1.22.0) 314 | CFPropertyList (>= 2.3.3, < 4.0) 315 | atomos (~> 0.1.3) 316 | claide (>= 1.0.2, < 2.0) 317 | colored2 (~> 3.1) 318 | nanaimo (~> 0.3.0) 319 | rexml (~> 3.2.4) 320 | xcpretty (0.3.0) 321 | rouge (~> 2.0.7) 322 | xcpretty-json-formatter (0.1.1) 323 | xcpretty (~> 0.2, >= 0.0.7) 324 | xcpretty-travis-formatter (1.0.1) 325 | xcpretty (~> 0.2, >= 0.0.7) 326 | xcresult (0.2.1) 327 | zeitwerk (2.6.4) 328 | 329 | PLATFORMS 330 | ruby 331 | 332 | DEPENDENCIES 333 | cocoapods 334 | danger 335 | danger-swiftlint 336 | danger-xcode_summary 337 | xcode-install 338 | xcpretty 339 | xcpretty-json-formatter 340 | 341 | BUNDLED WITH 342 | 2.3.21 343 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Almaz Ibragimov 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 | -------------------------------------------------------------------------------- /Mintfile: -------------------------------------------------------------------------------- 1 | realm/SwiftLint@0.49.1 2 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "URLQueryCoder", 6 | products: [ 7 | .library( 8 | name: "URLQueryCoder", 9 | targets: ["URLQueryCoder"] 10 | ) 11 | ], 12 | targets: [ 13 | .target( 14 | name: "URLQueryCoder", 15 | path: "Sources" 16 | ), 17 | .testTarget( 18 | name: "URLQueryCoderTests", 19 | dependencies: ["URLQueryCoder"], 20 | path: "Tests" 21 | ) 22 | ], 23 | swiftLanguageVersions: [.v5] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # URLQueryCoder 2 | [![Build Status](https://github.com/almazrafi/URLQueryCoder/workflows/CI/badge.svg?branch=main)](https://github.com/almazrafi/URLQueryCoder/actions) 3 | [![Codecov](https://codecov.io/gh/almazrafi/URLQueryCoder/branch/main/graph/badge.svg?token=DRFk9RWXWB)](https://codecov.io/gh/almazrafi/URLQueryCoder) 4 | [![Cocoapods](https://img.shields.io/cocoapods/v/URLQueryCoder)](http://cocoapods.org/pods/URLQueryCoder) 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-Compatible-brightgreen.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | [![SPM compatible](https://img.shields.io/badge/SPM-Compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager/) 7 | [![Platforms](https://img.shields.io/cocoapods/p/URLQueryCoder)](https://developer.apple.com/discover/) 8 | [![Xcode](https://img.shields.io/badge/Xcode-13-blue)](https://developer.apple.com/xcode) 9 | [![Swift](https://img.shields.io/badge/Swift-5.5-orange)](https://swift.org) 10 | [![License](https://img.shields.io/github/license/almazrafi/URLQueryCoder)](https://opensource.org/licenses/MIT) 11 | 12 | ## Requirements 13 | - iOS 12.0+ / macOS 10.14+ / watchOS 5.0+ / tvOS 12.0+ 14 | - Xcode 13.0+ 15 | - Swift 5.5+ 16 | 17 | ## Usage 18 | ```swift 19 | struct User: Codable { 20 | var id: Int 21 | var name: String 22 | } 23 | 24 | // Encode to URL query 25 | let user = User(id: 123, name: "Neo") 26 | let query = try URLQueryEncoder().encode(user) 27 | 28 | // Decode from URL query 29 | let query = "id=123&name=Neo" 30 | let user = try URLQueryDecoder().decode(User.self, from: query) 31 | ``` 32 | 33 | ## Installation 34 | ### CocoaPods 35 | [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: 36 | ``` bash 37 | $ gem install cocoapods 38 | ``` 39 | 40 | To integrate URLQueryCoder into your Xcode project using [CocoaPods](http://cocoapods.org), specify it in your `Podfile`: 41 | ``` ruby 42 | platform :ios, '12.0' 43 | use_frameworks! 44 | 45 | target '' do 46 | pod 'URLQueryCoder' 47 | end 48 | ``` 49 | 50 | Finally run the following command: 51 | ``` bash 52 | $ pod install 53 | ``` 54 | 55 | ### Carthage 56 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. You can install Carthage with Homebrew using the following command: 57 | ``` bash 58 | $ brew update 59 | $ brew install carthage 60 | ``` 61 | 62 | To integrate URLQueryCoder into your Xcode project using Carthage, specify it in your `Cartfile`: 63 | ``` ogdl 64 | github "almazrafi/URLQueryCoder" ~> 1.1.0 65 | ``` 66 | 67 | Finally run `carthage update` to build the framework and drag the built `URLQueryCoder.framework` into your Xcode project. 68 | 69 | ### Swift Package Manager 70 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies. 71 | 72 | To integrate URLQueryCoder into your Xcode project using Swift Package Manager, 73 | add the following as a dependency to your `Package.swift`: 74 | ``` swift 75 | .package(url: "https://github.com/almazrafi/URLQueryCoder.git", from: "1.1.0") 76 | ``` 77 | and then specify `"URLQueryCoder"` as a dependency of the Target in which you wish to use URLQueryCoder. 78 | 79 | Here's an example `Package.swift`: 80 | ``` swift 81 | // swift-tools-version:5.5 82 | import PackageDescription 83 | 84 | let package = Package( 85 | name: "MyPackage", 86 | products: [ 87 | .library(name: "MyPackage", targets: ["MyPackage"]) 88 | ], 89 | dependencies: [ 90 | .package(url: "https://github.com/almazrafi/URLQueryCoder.git", from: "1.1.0") 91 | ], 92 | targets: [ 93 | .target(name: "MyPackage", dependencies: ["URLQueryCoder"]) 94 | ] 95 | ) 96 | ``` 97 | 98 | ## Communication 99 | - If you need help, open an issue. 100 | - If you found a bug, open an issue. 101 | - If you have a feature request, open an issue. 102 | - If you want to contribute, submit a pull request. 103 | 104 | ## License 105 | URLQueryCoder is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 106 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/bundler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly arguments=$@ 4 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 5 | 6 | source "${script_path}/common.sh" 7 | 8 | echo "Checking ${bundler_style}Bundler${default_style} installation:" 9 | 10 | if [[ "$(uname -m)" == "arm64" ]]; then 11 | eval "$(/opt/homebrew/bin/brew shellenv)" 12 | fi 13 | 14 | eval "$(rbenv init -)" 15 | 16 | if rbenv which bundler &> /dev/null; then 17 | if [[ " ${arguments[*]} " == *" ${update_flag} "* ]]; then 18 | echo " Bundler already installed. Updating..." 19 | assert_failure 'gem update bundler' 20 | else 21 | echo " Bundler already installed." 22 | fi 23 | else 24 | echo " Bundler not found. Installing..." 25 | assert_failure 'gem install bundler' 26 | fi 27 | 28 | echo "" 29 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "${script_path}" ]; then 4 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 5 | fi 6 | 7 | readonly helpers_path="${script_path}/../Helpers" 8 | 9 | source "${helpers_path}/script-paths.sh" 10 | 11 | readonly default_style='\033[0m' 12 | readonly warning_style='\033[33m' 13 | readonly error_style='\033[31m' 14 | 15 | readonly macos_style='\033[38;5;99m' 16 | readonly xcode_style='\033[38;5;75m' 17 | readonly homebrew_style='\033[38;5;208m' 18 | readonly rbenv_style='\033[38;5;43m' 19 | readonly ruby_style='\033[38;5;89m' 20 | readonly bundler_style='\033[38;5;45m' 21 | readonly swiftenv_style='\033[38;5;226m' 22 | readonly swift_style='\033[38;5;208m' 23 | readonly spm_style='\033[38;5;202m' 24 | readonly mint_style='\033[0;38;5;77m' 25 | readonly congratulations_style='\033[38;5;48m' 26 | 27 | readonly update_flag='--update' 28 | readonly verify_flag='--verify' 29 | 30 | failure() { 31 | echo "${error_style}Fatal error:${default_style} '$1' failed with exit code $2" 32 | exit 1 33 | } 34 | 35 | warning() { 36 | echo "${warning_style}Warning:${default_style} '$1' failed with exit code $2" 37 | } 38 | 39 | assert_failure() { 40 | eval $1 2>&1 | sed -e "s/^/ /" 41 | 42 | local exit_code=${PIPESTATUS[0]} 43 | 44 | if [ $exit_code -ne 0 ]; then 45 | failure "$1" $exit_code 46 | fi 47 | } 48 | 49 | assert_warning() { 50 | eval $1 2>&1 | sed -e "s/^/ /" 51 | 52 | local exit_code=${PIPESTATUS[0]} 53 | 54 | if [ $exit_code -ne 0 ]; then 55 | warning "$1" $exit_code 56 | fi 57 | } 58 | 59 | brew_install_if_needed() { 60 | local options=$@ 61 | local formulae=$1 62 | 63 | if [[ "$(uname -m)" == "arm64" ]]; then 64 | eval "$(/opt/homebrew/bin/brew shellenv)" 65 | fi 66 | 67 | if brew ls --versions "${formulae}" &> /dev/null; then 68 | if [[ " ${options[*]} " == *" ${update_flag} "* ]]; then 69 | echo " ${formulae} already installed. Updating..." 70 | 71 | brew_outdated=$(brew outdated 2> /dev/null) 72 | brew_outdated_exit_code=$? 73 | 74 | if [ $brew_outdated_exit_code -ne 0 ]; then 75 | echo " Failed to find outdated formulae." 76 | warning 'brew outdated' $brew_outdated_exit_code 77 | else 78 | if [[ $brew_outdated == *"${formulae}"* ]]; then 79 | assert_failure 'brew upgrade ${formulae}' 80 | else 81 | echo " Already up-to-date." 82 | fi 83 | fi 84 | else 85 | echo " ${formulae} already installed." 86 | fi 87 | else 88 | echo " ${formulae} not found. Installing..." 89 | assert_failure 'brew install "${formulae}"' 90 | fi 91 | } 92 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/congratulations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 4 | 5 | source "${script_path}/common.sh" 6 | 7 | echo "" 8 | echo "${congratulations_style}Congratulations!${default_style} Setting up the development environment successfully completed 🥳" 9 | echo "" 10 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/gemfile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly arguments=$@ 4 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 5 | 6 | source "${script_path}/common.sh" 7 | 8 | if [[ "$(uname -m)" == "arm64" ]]; then 9 | eval "$(/opt/homebrew/bin/brew shellenv)" 10 | fi 11 | 12 | eval "$(rbenv init -)" 13 | 14 | export SDKROOT=$(xcrun --sdk macosx --show-sdk-path) 15 | 16 | if [[ " ${arguments[*]} " == *" ${update_flag} "* ]]; then 17 | echo "Updating ${ruby_style}Ruby gems${default_style} specified in Gemfile..." 18 | assert_failure '(cd "${root_path}" && bundle update)' 19 | else 20 | echo "Installing ${ruby_style}Ruby gems${default_style} specified in Gemfile..." 21 | assert_failure '(cd "${root_path}" && bundle install)' 22 | fi 23 | 24 | echo "" 25 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/homebrew.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly arguments=$@ 4 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 5 | 6 | source "${script_path}/common.sh" 7 | 8 | readonly shell_init_line="eval \"\$(/opt/homebrew/bin/brew shellenv)\"" 9 | 10 | setup_shell() { 11 | local shell_profile_path=$1 12 | 13 | if [[ ! -f "${shell_profile_path}" ]]; then 14 | > "${shell_profile_path}" 15 | fi 16 | 17 | if [[ $(grep -L "${shell_init_line}" "${shell_profile_path}") ]]; then 18 | echo "${shell_init_line}" >> "${shell_profile_path}" 19 | fi 20 | } 21 | 22 | echo "Checking ${homebrew_style}Homebrew${default_style} installation:" 23 | 24 | if which -s brew; then 25 | if [[ " ${arguments[*]} " == *" ${update_flag} "* ]]; then 26 | echo " Homebrew already installed. Updating..." 27 | assert_failure 'brew update' 28 | else 29 | echo " Homebrew already installed." 30 | fi 31 | else 32 | echo " Homebrew not found. Installing..." 33 | assert_failure 'bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' 34 | fi 35 | 36 | if [[ "$(uname -m)" == "arm64" ]]; then 37 | setup_shell "${HOME}/.zshrc" 38 | eval "$(/opt/homebrew/bin/brew shellenv)" 39 | fi 40 | 41 | if [[ " ${arguments[*]} " == *" ${verify_flag} "* ]]; then 42 | echo "" 43 | echo " Verifying that Homebrew is properly set up..." 44 | 45 | assert_warning 'brew doctor' 46 | fi 47 | 48 | echo "" 49 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 4 | 5 | source "${script_path}/common.sh" 6 | 7 | plain_version() { 8 | echo "$@" | awk -F. '{ printf("%d%03d%03d%03d", $1,$2,$3,$4); }' 9 | } 10 | 11 | echo "Checking ${macos_style}macOS${default_style} version:" 12 | 13 | readonly macos_required_version='11.3.0' 14 | readonly macos_version=$(/usr/bin/sw_vers -productVersion 2>&1) 15 | 16 | if [ "$(plain_version ${macos_version})" -lt "$(plain_version ${macos_required_version})" ]; then 17 | echo " ${error_style}Your macOS version (${macos_version}) is older then required version (${macos_required_version}). Exiting...${default_style}" 18 | exit 1 19 | else 20 | echo " Your macOS version: ${macos_version}" 21 | fi 22 | 23 | echo "" 24 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/mint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly arguments=$@ 4 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 5 | 6 | source "${script_path}/common.sh" 7 | 8 | readonly shell_mint_path_line="export MINT_PATH=\"\$HOME/.mint\"" 9 | readonly shell_mint_link_path_line="export MINT_LINK_PATH=\"\$MINT_PATH/bin\"" 10 | 11 | setup_shell() { 12 | local shell_profile_path=$1 13 | 14 | if [[ ! -f "${shell_profile_path}" ]]; then 15 | > "${shell_profile_path}" 16 | fi 17 | 18 | if [[ $(grep -L "${shell_mint_path_line}" "${shell_profile_path}") ]]; then 19 | echo "${shell_mint_path_line}" >> "${shell_profile_path}" 20 | fi 21 | 22 | if [[ $(grep -L "${shell_mint_link_path_line}" "${shell_profile_path}") ]]; then 23 | echo "${shell_mint_link_path_line}" >> "${shell_profile_path}" 24 | fi 25 | } 26 | 27 | echo "Checking ${mint_style}Mint${default_style} installation:" 28 | 29 | brew_install_if_needed mint "$arguments" 30 | setup_shell "${HOME}/.zshrc" 31 | 32 | if [[ -f "${HOME}/.bash_profile" ]]; then 33 | setup_shell "${HOME}/.bash_profile" 34 | fi 35 | 36 | echo "" 37 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/mintfile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 4 | 5 | source "${script_path}/common.sh" 6 | source "${helpers_path}/script-run.sh" 7 | 8 | echo "Installing ${swift_style}Swift tools${default_style} specified in Mintfile..." 9 | 10 | if [[ "$(uname -m)" == "arm64" ]]; then 11 | eval "$(/opt/homebrew/bin/brew shellenv)" 12 | fi 13 | 14 | assert_failure '(cd "${root_path}" && mint bootstrap)' 15 | 16 | echo "" 17 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/rbenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly arguments=$@ 4 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 5 | 6 | source "${script_path}/common.sh" 7 | 8 | readonly shell_init_line="if which rbenv > /dev/null; then eval \"\$(rbenv init -)\"; fi" 9 | readonly doctor_url='https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-doctor' 10 | readonly doctor_temp_path="${script_path}/rbenv_doctor" 11 | 12 | setup_shell() { 13 | local shell_profile_path=$1 14 | 15 | if [[ ! -f "${shell_profile_path}" ]]; then 16 | > "${shell_profile_path}" 17 | fi 18 | 19 | if [[ $(grep -L "${shell_init_line}" "${shell_profile_path}") ]]; then 20 | echo "${shell_init_line}" >> "${shell_profile_path}" 21 | fi 22 | } 23 | 24 | cleanup() { 25 | rm -rf $doctor_temp_path; 26 | } 27 | 28 | trap cleanup EXIT 29 | 30 | echo "Checking ${rbenv_style}rbenv${default_style} installation:" 31 | 32 | brew_install_if_needed rbenv 33 | setup_shell "${HOME}/.zshrc" 34 | 35 | if [[ -f "${HOME}/.bash_profile" ]]; then 36 | setup_shell "${HOME}/.bash_profile" 37 | fi 38 | 39 | eval "$(rbenv init -)" 40 | 41 | if [[ " ${arguments[*]} " == *" ${verify_flag} "* ]]; then 42 | echo "" 43 | echo " Verifying that rbenv is properly set up..." 44 | curl -fsSL "${doctor_url}" > "${doctor_temp_path}" 2> /dev/null 45 | rbenv_doctor_exit_code=$? 46 | 47 | if [ "${rbenv_doctor_exit_code}" -ne 0 ]; then 48 | echo " Failed to load rbenv-doctor script." 49 | warning 'curl -fsSL "${doctor_url}"' "${rbenv_doctor_exit_code}" 50 | else 51 | chmod a+x "${doctor_temp_path}" 52 | assert_warning 'bash "${doctor_temp_path}"' 53 | fi 54 | fi 55 | 56 | echo "" 57 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/ruby.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 4 | 5 | source "${script_path}/common.sh" 6 | 7 | echo "Checking ${ruby_style}Ruby${default_style} version:" 8 | 9 | if [[ "$(uname -m)" == "arm64" ]]; then 10 | eval "$(/opt/homebrew/bin/brew shellenv)" 11 | fi 12 | 13 | eval "$(rbenv init -)" 14 | 15 | readonly ruby_required_version=$(cat "${root_path}"/.ruby-version) 16 | readonly ruby_versions=($(rbenv versions 2>&1)) 17 | readonly ruby_install_flags="-Wno-error=implicit-function-declaration" 18 | 19 | if [[ " ${ruby_versions[@]} " =~ " ${ruby_required_version} " ]]; then 20 | echo " Required Ruby version ($ruby_required_version) already installed." 21 | else 22 | echo " Required Ruby version ($ruby_required_version) not found. Installing..." 23 | 24 | export SDKROOT=$(xcrun --sdk macosx --show-sdk-path) 25 | 26 | assert_failure '(cd "${root_path}" && RUBY_CFLAGS="${ruby_install_flags}" rbenv install $ruby_required_version)' 27 | assert_warning '(cd "${root_path}" && rbenv rehash)' 28 | fi 29 | 30 | echo "" 31 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/spm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly arguments=$@ 4 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 5 | 6 | source "${script_path}/common.sh" 7 | 8 | if [[ "$(uname -m)" == "arm64" ]]; then 9 | eval "$(/opt/homebrew/bin/brew shellenv)" 10 | fi 11 | 12 | eval "$(swiftenv init -)" 13 | 14 | if [[ " ${arguments[*]} " == *" ${update_flag} "* ]]; then 15 | echo "Updating ${spm_style}Swift packages${default_style} specified in Package.swift..." 16 | assert_failure '(cd "${root_path}" && swift package update)' 17 | else 18 | echo "Resolving ${spm_style}Swift packages${default_style} specified in Package.swift..." 19 | assert_failure '(cd "${root_path}" && swift package resolve)' 20 | fi 21 | 22 | echo "" 23 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/swift.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 4 | 5 | source "${script_path}/common.sh" 6 | 7 | echo "Checking ${swift_style}Swift${default_style} version:" 8 | 9 | if [[ "$(uname -m)" == "arm64" ]]; then 10 | eval "$(/opt/homebrew/bin/brew shellenv)" 11 | fi 12 | 13 | eval "$(swiftenv init -)" 14 | 15 | readonly swift_required_version=$(cat "${root_path}"/.swift-version) 16 | readonly swift_versions=($(swiftenv versions 2>&1)) 17 | 18 | if [[ " ${swift_versions[@]} " =~ " ${swift_required_version} " ]]; then 19 | echo " Required Swift version ($swift_required_version) already installed." 20 | else 21 | echo " Required Swift version ($swift_required_version) not found. Installing..." 22 | assert_failure '(cd "${root_path}" && swiftenv install $swift_required_version)' 23 | assert_warning '(cd "${root_path}" && swiftenv rehash)' 24 | fi 25 | 26 | echo "" 27 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/swiftenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly arguments=$@ 4 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 5 | 6 | source "${script_path}/common.sh" 7 | 8 | readonly shell_init_line='if which swiftenv > /dev/null; then eval "$(swiftenv init -)"; fi' 9 | 10 | setup_shell() { 11 | local shell_profile_path=$1 12 | 13 | if [[ ! -f "${shell_profile_path}" ]]; then 14 | > "${shell_profile_path}" 15 | fi 16 | 17 | if [[ $(grep -L "${shell_init_line}" "${shell_profile_path}") ]]; then 18 | echo "${shell_init_line}" >> "${shell_profile_path}" 19 | fi 20 | } 21 | 22 | echo "Checking ${swiftenv_style}swiftenv${default_style} installation:" 23 | 24 | brew_install_if_needed kylef/formulae/swiftenv "$arguments" 25 | setup_shell "${HOME}/.zshrc" 26 | 27 | if [[ -f "${HOME}/.bash_profile" ]]; then 28 | setup_shell "${HOME}/.bash_profile" 29 | fi 30 | 31 | eval "$(swiftenv init -)" 32 | 33 | echo "" 34 | -------------------------------------------------------------------------------- /Scripts/Bootstrap/welcome.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 4 | 5 | source "${script_path}/common.sh" 6 | 7 | echo "${default_style}" 8 | echo "This script will set up your development environment." 9 | echo "This might take a few minutes. Please don't interrupt the script." 10 | echo "" -------------------------------------------------------------------------------- /Scripts/Bootstrap/xcode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | while [[ "$#" -gt 0 ]]; do 6 | case $1 in 7 | --sudo-password) sudo_password="${2}"; shift ;; 8 | *) echo "Unknown parameter passed: $1"; exit 1 ;; 9 | esac 10 | shift 11 | done 12 | 13 | if [ -n "${sudo_password}" ]; then 14 | echo "${sudo_password}" | sudo -S -E "$0" "$@" 15 | exit $? 16 | fi 17 | 18 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 19 | 20 | source "${script_path}/common.sh" 21 | 22 | echo "Checking ${xcode_style}Xcode${default_style} installation:" 23 | 24 | if [[ "$(uname -m)" == "arm64" ]]; then 25 | eval "$(/opt/homebrew/bin/brew shellenv)" 26 | fi 27 | 28 | eval "$(rbenv init -)" 29 | 30 | readonly xcode_required_version=$(cat "${root_path}"/.xcode-version) 31 | readonly xcode_version=($(bundle exec xcversion selected 2> /dev/null | sed -n 's/Xcode \(.*\)/\1/p')) 32 | 33 | if [[ "$xcode_version" == "$xcode_required_version" ]]; then 34 | echo " Required Xcode version ($xcode_required_version) already installed." 35 | else 36 | echo " Required Xcode version ($xcode_required_version) not found. Installing..." 37 | 38 | bundle exec xcversion update 39 | bundle exec xcversion install ${xcode_required_version} 40 | 41 | echo " Selecting Xcode version..." 42 | 43 | bundle exec xcversion select "${xcode_required_version}" 44 | sudo xcodebuild -license accept 45 | fi 46 | 47 | echo "" 48 | -------------------------------------------------------------------------------- /Scripts/Helpers/script-paths.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "${helpers_path}" ]; then 4 | readonly helpers_path="$( cd "$( dirname "$0" )" && pwd )" 5 | fi 6 | 7 | readonly tools_path="$( cd "${helpers_path}/../" && pwd )" 8 | readonly root_path="$( cd "${tools_path}/../" && pwd )" 9 | -------------------------------------------------------------------------------- /Scripts/Helpers/script-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export MINT_PATH="$HOME/.mint" 4 | export MINT_LINK_PATH="$MINT_PATH/bin" 5 | 6 | if [[ -f "/opt/homebrew/bin/brew" ]]; then 7 | eval "$(/opt/homebrew/bin/brew shellenv)" 8 | fi 9 | 10 | run() { 11 | if [ -z "${root_path}" ]; then 12 | if [ -z "${helpers_path}" ]; then 13 | readonly helpers_path="$( cd "$( dirname "$0" )" && pwd )" 14 | fi 15 | 16 | source "${helpers_path}/script-paths.sh" 17 | fi 18 | 19 | if which mint >/dev/null; then 20 | (cd "${root_path}" && mint run "$@") 21 | else 22 | echo "error: Mint does not exist" 23 | exit 1 24 | fi 25 | } 26 | -------------------------------------------------------------------------------- /Scripts/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | readonly script_path="$( cd "$( dirname "$0" )" && pwd )" 6 | readonly bootstrap_path="${script_path}/Bootstrap" 7 | 8 | "${bootstrap_path}/welcome.sh" 9 | "${bootstrap_path}/macos.sh" 10 | 11 | "${bootstrap_path}/homebrew.sh" --update --verify 12 | "${bootstrap_path}/rbenv.sh" --update --verify 13 | "${bootstrap_path}/ruby.sh" 14 | 15 | "${bootstrap_path}/bundler.sh" --update 16 | "${bootstrap_path}/gemfile.sh" 17 | 18 | "${bootstrap_path}/xcode.sh" 19 | 20 | "${bootstrap_path}/swiftenv.sh" --update 21 | "${bootstrap_path}/swift.sh" --update 22 | 23 | "${bootstrap_path}/spm.sh" 24 | 25 | "${bootstrap_path}/mint.sh" --update 26 | "${bootstrap_path}/mintfile.sh" 27 | 28 | "${bootstrap_path}/congratulations.sh" 29 | -------------------------------------------------------------------------------- /Scripts/swiftlint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ "${SKIP_SWIFTLINT}" == "YES" ]]; then 4 | exit 0 5 | fi 6 | 7 | readonly helpers_path="$( cd "$( dirname "$0" )" && pwd )/Helpers" 8 | 9 | source "${helpers_path}/script-run.sh" 10 | run swiftlint --quiet || true 11 | -------------------------------------------------------------------------------- /Sources/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - extension_access_modifier 3 | 4 | opt_in_rules: 5 | - explicit_acl 6 | - explicit_top_level_acl 7 | -------------------------------------------------------------------------------- /Sources/Decoder/Options/URLQueryDataDecodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The strategies for decoding raw data. 4 | public enum URLQueryDataDecodingStrategy { 5 | 6 | /// The strategy that encodes data using the encoding specified by the data instance itself. 7 | case deferredToData 8 | 9 | /// The strategy that decodes data using Base 64 decoding. 10 | case base64 11 | 12 | /// The strategy that decodes data using a user-defined function. 13 | case custom((_ decoder: Decoder) throws -> Data) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Decoder/Options/URLQueryDateDecodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The strategies available for formatting dates when decoding them from URL. 4 | public enum URLQueryDateDecodingStrategy { 5 | 6 | /// The strategy that uses formatting from the Date structure. 7 | case deferredToDate 8 | 9 | /// The strategy that decodes dates in terms of seconds since midnight UTC on January 1st, 1970. 10 | case secondsSince1970 11 | 12 | /// The strategy that decodes dates in terms of milliseconds since midnight UTC on January 1st, 1970. 13 | case millisecondsSince1970 14 | 15 | /// The strategy that formats dates according to the ISO 8601 standard. 16 | @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) 17 | case iso8601 18 | 19 | /// The strategy that defers formatting settings to a supplied date formatter. 20 | case formatted(DateFormatter) 21 | 22 | /// The strategy that formats custom dates by calling a user-defined function. 23 | case custom((_ decoder: Decoder) throws -> Date) 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Decoder/Options/URLQueryDecodingOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal struct URLQueryDecodingOptions { 4 | 5 | internal let dateDecodingStrategy: URLQueryDateDecodingStrategy 6 | internal let dataDecodingStrategy: URLQueryDataDecodingStrategy 7 | internal let nonConformingFloatDecodingStrategy: URLQueryNonConformingFloatDecodingStrategy 8 | internal let keyDecodingStrategy: URLQueryKeyDecodingStrategy 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Decoder/Options/URLQueryKeyDecodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The values that determine how to decode a type’s coding keys from URL query keys. 4 | public enum URLQueryKeyDecodingStrategy { 5 | 6 | /// A key decoding strategy that doesn’t change key names during decoding. 7 | case useDefaultKeys 8 | 9 | /// A key decoding strategy defined by the closure you supply. 10 | case custom((_ codingPath: [CodingKey]) -> CodingKey) 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Decoder/Options/URLQueryNonConformingFloatDecodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The strategies for encoding nonconforming floating-point numbers, 4 | /// also known as IEEE 754 exceptional values. 5 | public enum URLQueryNonConformingFloatDecodingStrategy { 6 | 7 | /// The strategy that throws an error upon decoding an exceptional floating-point value. 8 | case `throw` 9 | 10 | /// The strategy that decodes exceptional floating-point values from a specified string representation. 11 | case convertFromString( 12 | positiveInfinity: String, 13 | negativeInfinity: String, 14 | nan: String 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Decoder/URLQueryDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct URLQueryDecoder { 4 | 5 | public static let `default` = URLQueryDecoder() 6 | 7 | public var dateDecodingStrategy: URLQueryDateDecodingStrategy 8 | public var dataDecodingStrategy: URLQueryDataDecodingStrategy 9 | public var nonConformingFloatDecodingStrategy: URLQueryNonConformingFloatDecodingStrategy 10 | public var keyDecodingStrategy: URLQueryKeyDecodingStrategy 11 | public var userInfo: [CodingUserInfoKey: Any] 12 | 13 | public init( 14 | dateDecodingStrategy: URLQueryDateDecodingStrategy = .deferredToDate, 15 | dataDecodingStrategy: URLQueryDataDecodingStrategy = .base64, 16 | nonConformingFloatDecodingStrategy: URLQueryNonConformingFloatDecodingStrategy = .throw, 17 | keyDecodingStrategy: URLQueryKeyDecodingStrategy = .useDefaultKeys, 18 | userInfo: [CodingUserInfoKey: Any] = [:] 19 | ) { 20 | self.dateDecodingStrategy = dateDecodingStrategy 21 | self.dataDecodingStrategy = dataDecodingStrategy 22 | self.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy 23 | self.keyDecodingStrategy = keyDecodingStrategy 24 | self.userInfo = userInfo 25 | } 26 | 27 | public func decode( 28 | _ type: T.Type, 29 | from query: String 30 | ) throws -> T { 31 | let options = URLQueryDecodingOptions( 32 | dateDecodingStrategy: dateDecodingStrategy, 33 | dataDecodingStrategy: dataDecodingStrategy, 34 | nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy, 35 | keyDecodingStrategy: keyDecodingStrategy 36 | ) 37 | 38 | let deserializer = URLQueryDeserializer() 39 | let value = try deserializer.deserialize(query) 40 | 41 | let decoder = URLQuerySingleValueDecodingContainer( 42 | value: value, 43 | options: options, 44 | userInfo: userInfo, 45 | codingPath: [] 46 | ) 47 | 48 | return try T(from: decoder) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Decoder/URLQueryDeserializer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal final class URLQueryDeserializer { 4 | 5 | internal init() { } 6 | 7 | private func deserializeKey(_ key: Substring) throws -> [String] { 8 | guard let percentDecodedKey = key.removingPercentEncoding else { 9 | let context = DecodingError.Context( 10 | codingPath: [], 11 | debugDescription: "The key '\(key)' contains an invalid percent-encoding sequence." 12 | ) 13 | 14 | throw DecodingError.dataCorrupted(context) 15 | } 16 | 17 | return try percentDecodedKey 18 | .split(separator: .leftSquareBracket) 19 | .enumerated() 20 | .map { index, part in 21 | if part.last == .rightSquareBracket { 22 | return String(part.dropLast()) 23 | } else if key.first != .leftSquareBracket, index == .zero { 24 | return String(part) 25 | } 26 | 27 | let context = DecodingError.Context( 28 | codingPath: [], 29 | debugDescription: "The key '\(key)' does not contain a closing separator ']'." 30 | ) 31 | 32 | throw DecodingError.dataCorrupted(context) 33 | } 34 | } 35 | 36 | private func deserializeStringValue( 37 | _ value: Substring?, 38 | path: [String] 39 | ) throws -> URLQueryValue? { 40 | guard let value = value else { 41 | return nil 42 | } 43 | 44 | guard let decodedValue = value.removingPercentEncoding else { 45 | throw DecodingError.dataCorrupted( 46 | DecodingError.Context( 47 | codingPath: [], 48 | debugDescription: "Unable to remove percent encoding for '\(value)' at the '\(path)' key." 49 | ) 50 | ) 51 | } 52 | 53 | return .string(String(decodedValue)) 54 | } 55 | 56 | private func deserializeArrayValue( 57 | _ value: Substring?, 58 | at keyIndex: Int, 59 | of path: [String], 60 | for array: [Int: URLQueryValue] 61 | ) throws -> URLQueryValue { 62 | let key = path[keyIndex] 63 | 64 | guard let index = key.isEmpty ? array.count : Int(key) else { 65 | let context = DecodingError.Context( 66 | codingPath: [], 67 | debugDescription: "The key '\(path)' does not contain index of the '\(key)' array." 68 | ) 69 | 70 | throw DecodingError.dataCorrupted(context) 71 | } 72 | 73 | guard let value = try deserializeValue(value, at: keyIndex + 1, of: path, for: array[index]) else { 74 | return .array(array) 75 | } 76 | 77 | return .array(array.updatingValue(value, forKey: index)) 78 | } 79 | 80 | private func deserializeDictionaryValue( 81 | _ value: Substring?, 82 | at keyIndex: Int, 83 | of path: [String], 84 | for dictionary: [String: URLQueryValue] 85 | ) throws -> URLQueryValue { 86 | let key = path[keyIndex] 87 | 88 | guard let value = try deserializeValue(value, at: keyIndex + 1, of: path, for: dictionary[key]) else { 89 | return .dictionary(dictionary) 90 | } 91 | 92 | return .dictionary(dictionary.updatingValue(value, forKey: key)) 93 | } 94 | 95 | private func deserializeValue( 96 | _ value: Substring?, 97 | at keyIndex: Int, 98 | of path: [String], 99 | for component: URLQueryValue? 100 | ) throws -> URLQueryValue? { 101 | switch component { 102 | case nil where keyIndex >= path.count: 103 | return try deserializeStringValue(value, path: path) 104 | 105 | case nil where path[keyIndex].isEmpty || Int(path[keyIndex]) != nil: 106 | return try deserializeArrayValue( 107 | value, 108 | at: keyIndex, 109 | of: path, 110 | for: [:] 111 | ) 112 | 113 | case nil: 114 | return try deserializeDictionaryValue( 115 | value, 116 | at: keyIndex, 117 | of: path, 118 | for: [:] 119 | ) 120 | 121 | case .array(let array) where keyIndex < path.count: 122 | return try deserializeArrayValue( 123 | value, 124 | at: keyIndex, 125 | of: path, 126 | for: array 127 | ) 128 | 129 | case .dictionary(let dictionary) where keyIndex < path.count: 130 | return try deserializeDictionaryValue( 131 | value, 132 | at: keyIndex, 133 | of: path, 134 | for: dictionary 135 | ) 136 | 137 | default: 138 | let context = DecodingError.Context( 139 | codingPath: [], 140 | debugDescription: "The key '\(path)' is used for values of different types." 141 | ) 142 | 143 | throw DecodingError.dataCorrupted(context) 144 | } 145 | } 146 | 147 | private func deserializeFragment( 148 | _ fragment: Substring, 149 | for query: URLQueryValue? 150 | ) throws -> URLQueryValue? { 151 | let keyValue = fragment.split( 152 | separator: .equals, 153 | maxSplits: 1, 154 | omittingEmptySubsequences: false 155 | ) 156 | 157 | let path = try deserializeKey(keyValue[0]) 158 | let value = keyValue[safe: 1] 159 | 160 | return try deserializeValue( 161 | value, 162 | at: .zero, 163 | of: path, 164 | for: query 165 | ) 166 | } 167 | 168 | internal func deserialize(_ query: String) throws -> URLQueryValue { 169 | try query 170 | .replacingOccurrences( 171 | of: String.urlPlusReplacedSpace, 172 | with: String.urlPercentEscapedSpace 173 | ) 174 | .split(separator: .ampersand) 175 | .reduce(nil) { query, fragment in 176 | try deserializeFragment(fragment, for: query) 177 | } ?? .dictionary([:]) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Sources/Decoder/URLQueryKeyedDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal final class URLQueryKeyedDecodingContainer: URLQueryValueDecoding { 4 | 5 | internal let dictionary: [String: URLQueryValue] 6 | internal let options: URLQueryDecodingOptions 7 | internal let userInfo: [CodingUserInfoKey: Any] 8 | internal let codingPath: [CodingKey] 9 | 10 | internal init( 11 | dictionary: [String: URLQueryValue], 12 | options: URLQueryDecodingOptions, 13 | userInfo: [CodingUserInfoKey: Any], 14 | codingPath: [CodingKey] 15 | ) { 16 | switch options.keyDecodingStrategy { 17 | case .useDefaultKeys: 18 | self.dictionary = dictionary 19 | 20 | case let .custom(closure): 21 | let keysAndValues = dictionary.map { key, value in 22 | (closure(codingPath.appending(AnyCodingKey(key))).stringValue, value) 23 | } 24 | 25 | self.dictionary = Dictionary(keysAndValues) { $1 } 26 | } 27 | 28 | self.options = options 29 | self.userInfo = userInfo 30 | self.codingPath = codingPath 31 | } 32 | 33 | private func value(forKey key: Key, of type: T.Type = T.self) throws -> T { 34 | let anyValue = dictionary[key.stringValue] 35 | 36 | guard let value = anyValue as? T else { 37 | throw DecodingError.invalidValue( 38 | anyValue, 39 | forKey: key, 40 | at: codingPath.appending(key), 41 | expectation: type 42 | ) 43 | } 44 | 45 | return value 46 | } 47 | 48 | private func superDecoder(forAnyKey key: CodingKey) throws -> Decoder { 49 | let decoder = URLQuerySingleValueDecodingContainer( 50 | value: dictionary[key.stringValue], 51 | options: options, 52 | userInfo: userInfo, 53 | codingPath: codingPath.appending(key) 54 | ) 55 | 56 | return decoder 57 | } 58 | } 59 | 60 | extension URLQueryKeyedDecodingContainer: KeyedDecodingContainerProtocol { 61 | 62 | internal var allKeys: [Key] { 63 | dictionary.keys.compactMap { Key(stringValue: $0) } 64 | } 65 | 66 | internal func contains(_ key: Key) -> Bool { 67 | dictionary.contains { $0.key == key.stringValue } 68 | } 69 | 70 | internal func decodeNil(forKey key: Key) throws -> Bool { 71 | decodeNil(try value(forKey: key)) 72 | } 73 | 74 | internal func decode(_ type: String.Type, forKey key: Key) throws -> String { 75 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 76 | } 77 | 78 | internal func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { 79 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 80 | } 81 | 82 | internal func decode(_ type: Int.Type, forKey key: Key) throws -> Int { 83 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 84 | } 85 | 86 | internal func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { 87 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 88 | } 89 | 90 | internal func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { 91 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 92 | } 93 | 94 | internal func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { 95 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 96 | } 97 | 98 | internal func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { 99 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 100 | } 101 | 102 | internal func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { 103 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 104 | } 105 | 106 | internal func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { 107 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 108 | } 109 | 110 | internal func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { 111 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 112 | } 113 | 114 | internal func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { 115 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 116 | } 117 | 118 | internal func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { 119 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 120 | } 121 | 122 | internal func decode(_ type: Double.Type, forKey key: Key) throws -> Double { 123 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 124 | } 125 | 126 | internal func decode(_ type: Float.Type, forKey key: Key) throws -> Float { 127 | try decodeValue(try value(forKey: key), at: codingPath.appending(key)) 128 | } 129 | 130 | internal func decode(_ type: T.Type, forKey key: Key) throws -> T { 131 | try decodeValue(try value(forKey: key), at: codingPath.appending(key), of: type) 132 | } 133 | 134 | internal func nestedContainer( 135 | keyedBy keyType: NestedKey.Type, 136 | forKey key: Key 137 | ) throws -> KeyedDecodingContainer { 138 | try superDecoder(forAnyKey: key).container(keyedBy: keyType) 139 | } 140 | 141 | internal func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { 142 | try superDecoder(forAnyKey: key).unkeyedContainer() 143 | } 144 | 145 | internal func superDecoder(forKey key: Key) throws -> Decoder { 146 | try superDecoder(forAnyKey: key) 147 | } 148 | 149 | internal func superDecoder() throws -> Decoder { 150 | try superDecoder(forAnyKey: AnyCodingKey.super) 151 | } 152 | } 153 | 154 | private extension DecodingError { 155 | 156 | static func invalidValue( 157 | _ value: Any?, 158 | forKey key: Key, 159 | at codingPath: [CodingKey], 160 | expectation: Any.Type 161 | ) -> Self { 162 | switch value { 163 | case let value?: 164 | let context = Context( 165 | codingPath: codingPath, 166 | debugDescription: "Expected to decode \(expectation) but found \(type(of: value)) instead." 167 | ) 168 | 169 | return .typeMismatch(expectation, context) 170 | 171 | case nil: 172 | let context = Context( 173 | codingPath: codingPath, 174 | debugDescription: "No value associated with key \(key.stringValue)." 175 | ) 176 | 177 | return .keyNotFound(key, context) 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Sources/Decoder/URLQuerySingleValueDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal final class URLQuerySingleValueDecodingContainer: URLQueryValueDecoding { 4 | 5 | internal let value: URLQueryValue? 6 | internal let options: URLQueryDecodingOptions 7 | internal let userInfo: [CodingUserInfoKey: Any] 8 | internal let codingPath: [CodingKey] 9 | 10 | internal init( 11 | value: URLQueryValue?, 12 | options: URLQueryDecodingOptions, 13 | userInfo: [CodingUserInfoKey: Any], 14 | codingPath: [CodingKey] 15 | ) { 16 | self.value = value 17 | self.options = options 18 | self.userInfo = userInfo 19 | self.codingPath = codingPath 20 | } 21 | } 22 | 23 | extension URLQuerySingleValueDecodingContainer: SingleValueDecodingContainer { 24 | 25 | internal func decodeNil() -> Bool { 26 | decodeNil(value) 27 | } 28 | 29 | internal func decode(_ type: String.Type) throws -> String { 30 | try decodeValue(value, at: codingPath) 31 | } 32 | 33 | internal func decode(_ type: Bool.Type) throws -> Bool { 34 | try decodeValue(value, at: codingPath) 35 | } 36 | 37 | internal func decode(_ type: Int.Type) throws -> Int { 38 | try decodeValue(value, at: codingPath) 39 | } 40 | 41 | internal func decode(_ type: Int8.Type) throws -> Int8 { 42 | try decodeValue(value, at: codingPath) 43 | } 44 | 45 | internal func decode(_ type: Int16.Type) throws -> Int16 { 46 | try decodeValue(value, at: codingPath) 47 | } 48 | 49 | internal func decode(_ type: Int32.Type) throws -> Int32 { 50 | try decodeValue(value, at: codingPath) 51 | } 52 | 53 | internal func decode(_ type: Int64.Type) throws -> Int64 { 54 | try decodeValue(value, at: codingPath) 55 | } 56 | 57 | internal func decode(_ type: UInt.Type) throws -> UInt { 58 | try decodeValue(value, at: codingPath) 59 | } 60 | 61 | internal func decode(_ type: UInt8.Type) throws -> UInt8 { 62 | try decodeValue(value, at: codingPath) 63 | } 64 | 65 | internal func decode(_ type: UInt16.Type) throws -> UInt16 { 66 | try decodeValue(value, at: codingPath) 67 | } 68 | 69 | internal func decode(_ type: UInt32.Type) throws -> UInt32 { 70 | try decodeValue(value, at: codingPath) 71 | } 72 | 73 | internal func decode(_ type: UInt64.Type) throws -> UInt64 { 74 | try decodeValue(value, at: codingPath) 75 | } 76 | 77 | internal func decode(_ type: Double.Type) throws -> Double { 78 | try decodeValue(value, at: codingPath) 79 | } 80 | 81 | internal func decode(_ type: Float.Type) throws -> Float { 82 | try decodeValue(value, at: codingPath) 83 | } 84 | 85 | internal func decode(_ type: T.Type) throws -> T { 86 | try decodeValue(value, at: codingPath, of: type) 87 | } 88 | } 89 | 90 | extension URLQuerySingleValueDecodingContainer: Decoder { 91 | 92 | internal func container(keyedBy keyType: Key.Type) throws -> KeyedDecodingContainer { 93 | guard let dictionary = value?.dictionary else { 94 | throw DecodingError.keyedContainerTypeMismatch(at: codingPath, value: value) 95 | } 96 | 97 | let container = URLQueryKeyedDecodingContainer( 98 | dictionary: dictionary, 99 | options: options, 100 | userInfo: userInfo, 101 | codingPath: codingPath 102 | ) 103 | 104 | return KeyedDecodingContainer(container) 105 | } 106 | 107 | internal func unkeyedContainer() throws -> UnkeyedDecodingContainer { 108 | guard let array = value?.array else { 109 | throw DecodingError.unkeyedContainerTypeMismatch(at: codingPath, value: value) 110 | } 111 | 112 | return URLQueryUnkeyedDecodingContainer( 113 | array: array, 114 | options: options, 115 | userInfo: userInfo, 116 | codingPath: codingPath 117 | ) 118 | } 119 | 120 | internal func singleValueContainer() throws -> SingleValueDecodingContainer { 121 | self 122 | } 123 | } 124 | 125 | private extension DecodingError { 126 | 127 | static func keyedContainerTypeMismatch( 128 | at codingPath: [CodingKey], 129 | value: URLQueryValue? 130 | ) -> Self { 131 | let debugDescription: String 132 | 133 | switch value { 134 | case .string: 135 | debugDescription = "Expected to decode a dictionary but found string instead." 136 | 137 | case .array: 138 | debugDescription = "Expected to decode a dictionary but found array instead." 139 | 140 | case .dictionary: 141 | debugDescription = "Cannot get keyed decoding container." 142 | 143 | case nil: 144 | debugDescription = "Cannot get keyed decoding container -- found null value instead." 145 | } 146 | 147 | return .typeMismatch([String: Any].self, Context(codingPath: codingPath, debugDescription: debugDescription)) 148 | } 149 | 150 | static func unkeyedContainerTypeMismatch( 151 | at codingPath: [CodingKey], 152 | value: URLQueryValue? 153 | ) -> Self { 154 | let debugDescription: String 155 | 156 | switch value { 157 | case .string: 158 | debugDescription = "Expected to decode an array but found string instead." 159 | 160 | case .dictionary: 161 | debugDescription = "Expected to decode an array but found dictionary instead." 162 | 163 | case .array: 164 | debugDescription = "Cannot get unkeyed decoding container." 165 | 166 | case nil: 167 | debugDescription = "Cannot get unkeyed decoding container -- found null value instead." 168 | } 169 | 170 | let context = Context( 171 | codingPath: codingPath, 172 | debugDescription: debugDescription 173 | ) 174 | 175 | return .typeMismatch([Any].self, context) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Sources/Decoder/URLQueryUnkeyedDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal final class URLQueryUnkeyedDecodingContainer: URLQueryValueDecoding { 4 | 5 | internal let array: [URLQueryValue?] 6 | internal let options: URLQueryDecodingOptions 7 | internal let userInfo: [CodingUserInfoKey: Any] 8 | internal let codingPath: [CodingKey] 9 | 10 | internal private(set) var currentIndex = 0 11 | 12 | internal var currentCodingPath: [CodingKey] { 13 | codingPath.appending(AnyCodingKey(currentIndex)) 14 | } 15 | 16 | internal init( 17 | array: [Int: URLQueryValue], 18 | options: URLQueryDecodingOptions, 19 | userInfo: [CodingUserInfoKey: Any], 20 | codingPath: [CodingKey] 21 | ) { 22 | self.array = array 23 | .keys 24 | .max() 25 | .map { 0...Int($0) }? 26 | .map { array[$0] } ?? [] 27 | 28 | self.options = options 29 | self.userInfo = userInfo 30 | self.codingPath = codingPath 31 | } 32 | 33 | private func popNextValue() throws -> URLQueryValue? { 34 | guard currentIndex < array.count else { 35 | let errorContext = DecodingError.Context( 36 | codingPath: currentCodingPath, 37 | debugDescription: "Unkeyed container is at end." 38 | ) 39 | 40 | throw DecodingError.valueNotFound(Any.self, errorContext) 41 | } 42 | 43 | defer { 44 | currentIndex += 1 45 | } 46 | 47 | return array[currentIndex] 48 | } 49 | } 50 | 51 | extension URLQueryUnkeyedDecodingContainer: UnkeyedDecodingContainer { 52 | 53 | internal var count: Int? { 54 | array.count 55 | } 56 | 57 | internal var isAtEnd: Bool { 58 | currentIndex == count 59 | } 60 | 61 | internal func decodeNil() throws -> Bool { 62 | decodeNil(try popNextValue()) 63 | } 64 | 65 | internal func decode(_ type: String.Type) throws -> String { 66 | try decodeValue(try popNextValue(), at: currentCodingPath) 67 | } 68 | 69 | internal func decode(_ type: Bool.Type) throws -> Bool { 70 | try decodeValue(try popNextValue(), at: currentCodingPath) 71 | } 72 | 73 | internal func decode(_ type: Int.Type) throws -> Int { 74 | try decodeValue(try popNextValue(), at: currentCodingPath) 75 | } 76 | 77 | internal func decode(_ type: Int8.Type) throws -> Int8 { 78 | try decodeValue(try popNextValue(), at: currentCodingPath) 79 | } 80 | 81 | internal func decode(_ type: Int16.Type) throws -> Int16 { 82 | try decodeValue(try popNextValue(), at: currentCodingPath) 83 | } 84 | 85 | internal func decode(_ type: Int32.Type) throws -> Int32 { 86 | try decodeValue(try popNextValue(), at: currentCodingPath) 87 | } 88 | 89 | internal func decode(_ type: Int64.Type) throws -> Int64 { 90 | try decodeValue(try popNextValue(), at: currentCodingPath) 91 | } 92 | 93 | internal func decode(_ type: UInt.Type) throws -> UInt { 94 | try decodeValue(try popNextValue(), at: currentCodingPath) 95 | } 96 | 97 | internal func decode(_ type: UInt8.Type) throws -> UInt8 { 98 | try decodeValue(try popNextValue(), at: currentCodingPath) 99 | } 100 | 101 | internal func decode(_ type: UInt16.Type) throws -> UInt16 { 102 | try decodeValue(try popNextValue(), at: currentCodingPath) 103 | } 104 | 105 | internal func decode(_ type: UInt32.Type) throws -> UInt32 { 106 | try decodeValue(try popNextValue(), at: currentCodingPath) 107 | } 108 | 109 | internal func decode(_ type: UInt64.Type) throws -> UInt64 { 110 | try decodeValue(try popNextValue(), at: currentCodingPath) 111 | } 112 | 113 | internal func decode(_ type: Double.Type) throws -> Double { 114 | try decodeValue(try popNextValue(), at: currentCodingPath) 115 | } 116 | 117 | internal func decode(_ type: Float.Type) throws -> Float { 118 | try decodeValue(try popNextValue(), at: currentCodingPath) 119 | } 120 | 121 | internal func decode(_ type: T.Type) throws -> T { 122 | try decodeValue(try popNextValue(), at: currentCodingPath, of: type) 123 | } 124 | 125 | internal func nestedContainer( 126 | keyedBy keyType: NestedKey.Type 127 | ) throws -> KeyedDecodingContainer { 128 | try superDecoder().container(keyedBy: keyType) 129 | } 130 | 131 | internal func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { 132 | try superDecoder().unkeyedContainer() 133 | } 134 | 135 | internal func superDecoder() throws -> Decoder { 136 | URLQuerySingleValueDecodingContainer( 137 | value: try popNextValue(), 138 | options: options, 139 | userInfo: userInfo, 140 | codingPath: currentCodingPath 141 | ) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/Decoder/URLQueryValueDecoding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal protocol URLQueryValueDecoding { 4 | 5 | var options: URLQueryDecodingOptions { get } 6 | var userInfo: [CodingUserInfoKey: Any] { get } 7 | } 8 | 9 | extension URLQueryValueDecoding { 10 | 11 | private func decodeString( 12 | _ value: URLQueryValue?, 13 | at codingPath: [CodingKey] 14 | ) throws -> String { 15 | guard let value = value?.string else { 16 | throw DecodingError.invalidValue( 17 | value, 18 | of: String.self, 19 | at: codingPath 20 | ) 21 | } 22 | 23 | return value 24 | } 25 | 26 | private func decodePrimitiveValue( 27 | _ value: URLQueryValue?, 28 | at codingPath: [CodingKey] 29 | ) throws -> T { 30 | guard let value = T(try decodeString(value, at: codingPath)) else { 31 | throw DecodingError.invalidValue(value, of: T.self, at: codingPath) 32 | } 33 | 34 | return value 35 | } 36 | 37 | private func decodeNonPrimitiveValue( 38 | _ value: URLQueryValue?, 39 | of type: T.Type = T.self, 40 | at codingPath: [CodingKey] 41 | ) throws -> T { 42 | let decoder = URLQuerySingleValueDecodingContainer( 43 | value: value, 44 | options: options, 45 | userInfo: userInfo, 46 | codingPath: codingPath 47 | ) 48 | 49 | return try T(from: decoder) 50 | } 51 | 52 | private func decodeCustomizedValue( 53 | _ value: URLQueryValue?, 54 | of type: T.Type = T.self, 55 | at codingPath: [CodingKey], 56 | using closure: (_ decoder: Decoder) throws -> T 57 | ) throws -> T { 58 | let decoder = URLQuerySingleValueDecodingContainer( 59 | value: value, 60 | options: options, 61 | userInfo: userInfo, 62 | codingPath: codingPath 63 | ) 64 | 65 | return try closure(decoder) 66 | } 67 | 68 | private func decodeBooleanValue( 69 | _ value: URLQueryValue?, 70 | at codingPath: [CodingKey] 71 | ) throws -> Bool { 72 | switch try decodeString(value, at: codingPath).lowercased() { 73 | case .urlQueryNumericTrue, .urlQueryLiteralTrue: 74 | return true 75 | 76 | case .urlQueryNumericFalse, .urlQueryLiteralFalse: 77 | return false 78 | 79 | default: 80 | throw DecodingError.invalidValue(value, of: Bool.self, at: codingPath) 81 | } 82 | } 83 | 84 | private func decodeFloatingPointValue( 85 | _ value: URLQueryValue?, 86 | of type: T.Type = T.self, 87 | at codingPath: [CodingKey] 88 | ) throws -> T { 89 | let rawValue = try decodeString(value, at: codingPath) 90 | 91 | if let value = T(rawValue) { 92 | return value 93 | } 94 | 95 | switch options.nonConformingFloatDecodingStrategy { 96 | case let .convertFromString(positiveInfinity, _, _) where rawValue == positiveInfinity: 97 | return T.infinity 98 | 99 | case let .convertFromString(_, negativeInfinity, _) where rawValue == negativeInfinity: 100 | return -T.infinity 101 | 102 | case let .convertFromString(_, _, nan) where rawValue == nan: 103 | return T.nan 104 | 105 | case .convertFromString, .throw: 106 | throw DecodingError.invalidValue(value, of: T.self, at: codingPath) 107 | } 108 | } 109 | 110 | private func decodeDate(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> Date { 111 | switch options.dateDecodingStrategy { 112 | case .deferredToDate: 113 | return try decodeNonPrimitiveValue(value, at: codingPath) 114 | 115 | case .secondsSince1970: 116 | return Date(timeIntervalSince1970: try decodeFloatingPointValue(value, at: codingPath)) 117 | 118 | case .millisecondsSince1970: 119 | return Date(timeIntervalSince1970: try decodeFloatingPointValue(value, at: codingPath) / 1000.0) 120 | 121 | case .iso8601: 122 | guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { 123 | fatalError("ISO8601DateFormatter is unavailable on this platform.") 124 | } 125 | 126 | let formattedDate = try decodeString(value, at: codingPath) 127 | 128 | guard let date = ISO8601DateFormatter().date(from: formattedDate) else { 129 | let errorContext = DecodingError.Context( 130 | codingPath: codingPath, 131 | debugDescription: "Expected date string to be ISO8601-formatted." 132 | ) 133 | 134 | throw DecodingError.dataCorrupted(errorContext) 135 | } 136 | 137 | return date 138 | 139 | case .formatted(let dateFormatter): 140 | let formattedDate = try decodeString(value, at: codingPath) 141 | 142 | guard let date = dateFormatter.date(from: formattedDate) else { 143 | let errorContext = DecodingError.Context( 144 | codingPath: codingPath, 145 | debugDescription: "Date string does not match format expected by formatter." 146 | ) 147 | 148 | throw DecodingError.dataCorrupted(errorContext) 149 | } 150 | 151 | return date 152 | 153 | case .custom(let closure): 154 | return try decodeCustomizedValue(value, at: codingPath, using: closure) 155 | } 156 | } 157 | 158 | private func decodeData(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> Data { 159 | switch options.dataDecodingStrategy { 160 | case .deferredToData: 161 | return try decodeNonPrimitiveValue(value, at: codingPath) 162 | 163 | case .base64: 164 | let base64EncodedString = try decodeString(value, at: codingPath) 165 | 166 | guard let data = Data(base64Encoded: base64EncodedString) else { 167 | let errorContext = DecodingError.Context( 168 | codingPath: codingPath, 169 | debugDescription: "Encountered Data is not valid Base64." 170 | ) 171 | 172 | throw DecodingError.dataCorrupted(errorContext) 173 | } 174 | 175 | return data 176 | 177 | case .custom(let closure): 178 | return try decodeCustomizedValue(value, at: codingPath, using: closure) 179 | } 180 | } 181 | 182 | private func decodeURL(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> URL { 183 | guard let url = URL(string: try decodeString(value, at: codingPath)) else { 184 | let errorContext = DecodingError.Context( 185 | codingPath: codingPath, 186 | debugDescription: "String is not valid URL." 187 | ) 188 | 189 | throw DecodingError.dataCorrupted(errorContext) 190 | } 191 | 192 | return url 193 | } 194 | } 195 | 196 | extension URLQueryValueDecoding { 197 | 198 | internal func decodeNil(_ value: URLQueryValue?) -> Bool { 199 | value.isNil 200 | } 201 | 202 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> String { 203 | try decodeString(value, at: codingPath) 204 | } 205 | 206 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> Bool { 207 | try decodeBooleanValue(value, at: codingPath) 208 | } 209 | 210 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> Int { 211 | try decodePrimitiveValue(value, at: codingPath) 212 | } 213 | 214 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> Int8 { 215 | try decodePrimitiveValue(value, at: codingPath) 216 | } 217 | 218 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> Int16 { 219 | try decodePrimitiveValue(value, at: codingPath) 220 | } 221 | 222 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> Int32 { 223 | try decodePrimitiveValue(value, at: codingPath) 224 | } 225 | 226 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> Int64 { 227 | try decodePrimitiveValue(value, at: codingPath) 228 | } 229 | 230 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> UInt { 231 | try decodePrimitiveValue(value, at: codingPath) 232 | } 233 | 234 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> UInt8 { 235 | try decodePrimitiveValue(value, at: codingPath) 236 | } 237 | 238 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> UInt16 { 239 | try decodePrimitiveValue(value, at: codingPath) 240 | } 241 | 242 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> UInt32 { 243 | try decodePrimitiveValue(value, at: codingPath) 244 | } 245 | 246 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> UInt64 { 247 | try decodePrimitiveValue(value, at: codingPath) 248 | } 249 | 250 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> Double { 251 | try decodeFloatingPointValue(value, at: codingPath) 252 | } 253 | 254 | internal func decodeValue(_ value: URLQueryValue?, at codingPath: [CodingKey]) throws -> Float { 255 | try decodeFloatingPointValue(value, at: codingPath) 256 | } 257 | 258 | internal func decodeValue( 259 | _ value: URLQueryValue?, 260 | at codingPath: [CodingKey], 261 | of type: T.Type 262 | ) throws -> T { 263 | switch T.self { 264 | case is URL.Type: 265 | return try decodeURL(value, at: codingPath) as! T 266 | 267 | case is Date.Type: 268 | return try decodeDate(value, at: codingPath) as! T 269 | 270 | case is Data.Type: 271 | return try decodeData(value, at: codingPath) as! T 272 | 273 | default: 274 | return try decodeNonPrimitiveValue(value, at: codingPath) 275 | } 276 | } 277 | } 278 | 279 | private extension DecodingError { 280 | 281 | static func invalidValue( 282 | _ component: URLQueryValue?, 283 | of type: Any.Type, 284 | at codingPath: [CodingKey] 285 | ) -> DecodingError { 286 | let componentDescription = component == nil 287 | ? "nil" 288 | : "invalid" 289 | 290 | let context = DecodingError.Context( 291 | codingPath: codingPath, 292 | debugDescription: "Expected to decode \(type) but the value is \(componentDescription)." 293 | ) 294 | 295 | return .typeMismatch(type, context) 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /Sources/Encoder/Options/URLQueryArrayEncodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum URLQueryArrayEncodingStrategy { 4 | 5 | case enumerated 6 | case unenumerated 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Encoder/Options/URLQueryBoolEncodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum URLQueryBoolEncodingStrategy { 4 | 5 | case numeric 6 | case literal 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Encoder/Options/URLQueryDataEncodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum URLQueryDataEncodingStrategy { 4 | 5 | case deferredToData 6 | case base64 7 | case custom((_ data: Data, _ encoder: Encoder) throws -> Void) 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Encoder/Options/URLQueryDateEncodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum URLQueryDateEncodingStrategy { 4 | 5 | case deferredToDate 6 | 7 | case millisecondsSince1970 8 | case secondsSince1970 9 | 10 | @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) 11 | case iso8601 12 | 13 | case formatted(DateFormatter) 14 | case custom((_ date: Date, _ encoder: Encoder) throws -> Void) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Encoder/Options/URLQueryEncodingOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal struct URLQueryEncodingOptions { 4 | 5 | internal let dateEncodingStrategy: URLQueryDateEncodingStrategy 6 | internal let dataEncodingStrategy: URLQueryDataEncodingStrategy 7 | internal let nonConformingFloatEncodingStrategy: URLQueryNonConformingFloatEncodingStrategy 8 | internal let boolEncodingStrategy: URLQueryBoolEncodingStrategy 9 | internal let arrayEncodingStrategy: URLQueryArrayEncodingStrategy 10 | internal let spaceEncodingStrategy: URLQuerySpaceEncodingStrategy 11 | internal let keyEncodingStrategy: URLQueryKeyEncodingStrategy 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Encoder/Options/URLQueryKeyEncodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum URLQueryKeyEncodingStrategy { 4 | 5 | case useDefaultKeys 6 | case custom((_ codingPath: [CodingKey]) -> CodingKey) 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Encoder/Options/URLQueryNonConformingFloatEncodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum URLQueryNonConformingFloatEncodingStrategy { 4 | 5 | case `throw` 6 | case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String) 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Encoder/Options/URLQuerySpaceEncodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum URLQuerySpaceEncodingStrategy { 4 | 5 | case percentEscaped 6 | case plusReplaced 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Encoder/URLQueryAnyKeyedEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal final class URLQueryAnyKeyedEncodingContainer { 4 | 5 | private var dictionary: [String: URLQueryValueForm] = [:] 6 | 7 | internal let options: URLQueryEncodingOptions 8 | internal let userInfo: [CodingUserInfoKey: Any] 9 | internal let codingPath: [CodingKey] 10 | 11 | internal init( 12 | options: URLQueryEncodingOptions, 13 | userInfo: [CodingUserInfoKey: Any], 14 | codingPath: [CodingKey] 15 | ) { 16 | self.options = options 17 | self.userInfo = userInfo 18 | self.codingPath = codingPath 19 | } 20 | 21 | private func encodeKey(_ key: Key) -> String { 22 | switch options.keyEncodingStrategy { 23 | case .useDefaultKeys: 24 | return key.stringValue 25 | 26 | case let .custom(closure): 27 | return closure(codingPath.appending(key)).stringValue 28 | } 29 | } 30 | 31 | internal func collectValue(_ encodedValue: URLQueryValueForm, forKey key: Key) { 32 | dictionary[encodeKey(key)] = encodedValue 33 | } 34 | 35 | internal func nestedContainer( 36 | keyedBy keyType: NestedKey.Type, 37 | forKey key: Key 38 | ) -> URLQueryAnyKeyedEncodingContainer { 39 | if case let .resolver(container as URLQueryAnyKeyedEncodingContainer) = dictionary[encodeKey(key)] { 40 | return container 41 | } 42 | 43 | let container = URLQueryAnyKeyedEncodingContainer( 44 | options: options, 45 | userInfo: userInfo, 46 | codingPath: codingPath.appending(key) 47 | ) 48 | 49 | collectValue(.resolver(container), forKey: key) 50 | 51 | return container 52 | } 53 | 54 | internal func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { 55 | if case let .resolver(container as URLQueryUnkeyedEncodingContainer) = dictionary[encodeKey(key)] { 56 | return container 57 | } 58 | 59 | let container = URLQueryUnkeyedEncodingContainer( 60 | options: options, 61 | userInfo: userInfo, 62 | codingPath: codingPath.appending(key) 63 | ) 64 | 65 | collectValue(.resolver(container), forKey: key) 66 | 67 | return container 68 | } 69 | 70 | internal func superEncoder(forKey key: Key) -> Encoder { 71 | if case let .resolver(container as URLQuerySingleValueEncodingContainer) = dictionary[encodeKey(key)] { 72 | return container 73 | } 74 | 75 | let encoder = URLQuerySingleValueEncodingContainer( 76 | options: options, 77 | userInfo: userInfo, 78 | codingPath: codingPath.appending(key) 79 | ) 80 | 81 | collectValue(.resolver(encoder), forKey: key) 82 | 83 | return encoder 84 | } 85 | } 86 | 87 | extension URLQueryAnyKeyedEncodingContainer: URLQueryValueResolver { 88 | 89 | internal func resolveValue() -> URLQueryValue? { 90 | .dictionary(dictionary.compactMapValues { $0.resolveValue() }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Encoder/URLQueryEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class URLQueryEncoder { 4 | 5 | public static let `default` = URLQueryEncoder() 6 | 7 | public var dateEncodingStrategy: URLQueryDateEncodingStrategy 8 | public var dataEncodingStrategy: URLQueryDataEncodingStrategy 9 | public var nonConformingFloatEncodingStrategy: URLQueryNonConformingFloatEncodingStrategy 10 | public var boolEncodingStrategy: URLQueryBoolEncodingStrategy 11 | public var arrayEncodingStrategy: URLQueryArrayEncodingStrategy 12 | public var spaceEncodingStrategy: URLQuerySpaceEncodingStrategy 13 | public var keyEncodingStrategy: URLQueryKeyEncodingStrategy 14 | public var userInfo: [CodingUserInfoKey: Any] 15 | 16 | public init( 17 | dateEncodingStrategy: URLQueryDateEncodingStrategy = .deferredToDate, 18 | dataEncodingStrategy: URLQueryDataEncodingStrategy = .base64, 19 | nonConformingFloatEncodingStrategy: URLQueryNonConformingFloatEncodingStrategy = .throw, 20 | boolEncodingStrategy: URLQueryBoolEncodingStrategy = .literal, 21 | arrayEncodingStrategy: URLQueryArrayEncodingStrategy = .enumerated, 22 | spaceEncodingStrategy: URLQuerySpaceEncodingStrategy = .percentEscaped, 23 | keyEncodingStrategy: URLQueryKeyEncodingStrategy = .useDefaultKeys, 24 | userInfo: [CodingUserInfoKey: Any] = [:] 25 | ) { 26 | self.dateEncodingStrategy = dateEncodingStrategy 27 | self.dataEncodingStrategy = dataEncodingStrategy 28 | self.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy 29 | self.boolEncodingStrategy = boolEncodingStrategy 30 | self.arrayEncodingStrategy = arrayEncodingStrategy 31 | self.spaceEncodingStrategy = spaceEncodingStrategy 32 | self.keyEncodingStrategy = keyEncodingStrategy 33 | self.userInfo = userInfo 34 | } 35 | 36 | public func encode(_ value: T) throws -> String { 37 | let options = URLQueryEncodingOptions( 38 | dateEncodingStrategy: dateEncodingStrategy, 39 | dataEncodingStrategy: dataEncodingStrategy, 40 | nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy, 41 | boolEncodingStrategy: boolEncodingStrategy, 42 | arrayEncodingStrategy: arrayEncodingStrategy, 43 | spaceEncodingStrategy: spaceEncodingStrategy, 44 | keyEncodingStrategy: keyEncodingStrategy 45 | ) 46 | 47 | let encoder = URLQuerySingleValueEncodingContainer( 48 | options: options, 49 | userInfo: userInfo, 50 | codingPath: [] 51 | ) 52 | 53 | try value.encode(to: encoder) 54 | 55 | guard case let .dictionary(urlEncodedForm) = encoder.resolveValue() else { 56 | let errorContext = EncodingError.Context( 57 | codingPath: [], 58 | debugDescription: "Root component cannot be encoded in URL" 59 | ) 60 | 61 | throw EncodingError.invalidValue(value, errorContext) 62 | } 63 | 64 | let serializer = URLQuerySerializer( 65 | arrayEncodingStrategy: arrayEncodingStrategy, 66 | spaceEncodingStrategy: spaceEncodingStrategy 67 | ) 68 | 69 | return serializer.serialize(urlEncodedForm) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Encoder/URLQueryKeyedEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal final class URLQueryKeyedEncodingContainer: URLQueryValueEncoding { 4 | 5 | internal let container: URLQueryAnyKeyedEncodingContainer 6 | 7 | internal var options: URLQueryEncodingOptions { 8 | container.options 9 | } 10 | 11 | internal var userInfo: [CodingUserInfoKey: Any] { 12 | container.userInfo 13 | } 14 | 15 | internal var codingPath: [CodingKey] { 16 | container.codingPath 17 | } 18 | 19 | internal init(container: URLQueryAnyKeyedEncodingContainer) { 20 | self.container = container 21 | } 22 | } 23 | 24 | extension URLQueryKeyedEncodingContainer: KeyedEncodingContainerProtocol { 25 | 26 | internal func encodeNil(forKey key: Key) throws { 27 | container.collectValue(encodeNil(), forKey: key) 28 | } 29 | 30 | internal func encode(_ value: String, forKey key: Key) throws { 31 | container.collectValue(encodeValue(value), forKey: key) 32 | } 33 | 34 | internal func encode(_ value: Bool, forKey key: Key) throws { 35 | container.collectValue(encodeValue(value), forKey: key) 36 | } 37 | 38 | internal func encode(_ value: Int, forKey key: Key) throws { 39 | container.collectValue(encodeValue(value), forKey: key) 40 | } 41 | 42 | internal func encode(_ value: Int8, forKey key: Key) throws { 43 | container.collectValue(encodeValue(value), forKey: key) 44 | } 45 | 46 | internal func encode(_ value: Int16, forKey key: Key) throws { 47 | container.collectValue(encodeValue(value), forKey: key) 48 | } 49 | 50 | internal func encode(_ value: Int32, forKey key: Key) throws { 51 | container.collectValue(encodeValue(value), forKey: key) 52 | } 53 | 54 | internal func encode(_ value: Int64, forKey key: Key) throws { 55 | container.collectValue(encodeValue(value), forKey: key) 56 | } 57 | 58 | internal func encode(_ value: UInt, forKey key: Key) throws { 59 | container.collectValue(encodeValue(value), forKey: key) 60 | } 61 | 62 | internal func encode(_ value: UInt8, forKey key: Key) throws { 63 | container.collectValue(encodeValue(value), forKey: key) 64 | } 65 | 66 | internal func encode(_ value: UInt16, forKey key: Key) throws { 67 | container.collectValue(encodeValue(value), forKey: key) 68 | } 69 | 70 | internal func encode(_ value: UInt32, forKey key: Key) throws { 71 | container.collectValue(encodeValue(value), forKey: key) 72 | } 73 | 74 | internal func encode(_ value: UInt64, forKey key: Key) throws { 75 | container.collectValue(encodeValue(value), forKey: key) 76 | } 77 | 78 | internal func encode(_ value: Double, forKey key: Key) throws { 79 | container.collectValue(try encodeValue(value, at: codingPath.appending(key)), forKey: key) 80 | } 81 | 82 | internal func encode(_ value: Float, forKey key: Key) throws { 83 | container.collectValue(try encodeValue(value, at: codingPath.appending(key)), forKey: key) 84 | } 85 | 86 | internal func encode(_ value: T, forKey key: Key) throws { 87 | container.collectValue(try encodeValue(value, at: codingPath.appending(key)), forKey: key) 88 | } 89 | 90 | internal func nestedContainer( 91 | keyedBy keyType: NestedKey.Type, 92 | forKey key: Key 93 | ) -> KeyedEncodingContainer { 94 | let container = self.container.nestedContainer(keyedBy: keyType, forKey: key) 95 | 96 | return KeyedEncodingContainer( 97 | URLQueryKeyedEncodingContainer(container: container) 98 | ) 99 | } 100 | 101 | internal func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { 102 | container.nestedUnkeyedContainer(forKey: key) 103 | } 104 | 105 | internal func superEncoder(forKey key: Key) -> Encoder { 106 | container.superEncoder(forKey: key) 107 | } 108 | 109 | internal func superEncoder() -> Encoder { 110 | container.superEncoder(forKey: AnyCodingKey.super) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Encoder/URLQuerySerializer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal final class URLQuerySerializer { 4 | 5 | internal let arrayEncodingStrategy: URLQueryArrayEncodingStrategy 6 | internal let spaceEncodingStrategy: URLQuerySpaceEncodingStrategy 7 | 8 | internal init( 9 | arrayEncodingStrategy: URLQueryArrayEncodingStrategy, 10 | spaceEncodingStrategy: URLQuerySpaceEncodingStrategy 11 | ) { 12 | self.arrayEncodingStrategy = arrayEncodingStrategy 13 | self.spaceEncodingStrategy = spaceEncodingStrategy 14 | } 15 | 16 | private func escapeString(_ string: String) -> String { 17 | var allowedCharacters = CharacterSet.urlQueryAllowed 18 | 19 | allowedCharacters.remove(charactersIn: .urlQueryDelimiters) 20 | allowedCharacters.insert(charactersIn: .space) 21 | 22 | let escapedString = string.addingPercentEncoding(withAllowedCharacters: allowedCharacters) ?? string 23 | 24 | switch spaceEncodingStrategy { 25 | case .percentEscaped: 26 | return escapedString.replacingOccurrences( 27 | of: String.space, 28 | with: String.urlPercentEscapedSpace 29 | ) 30 | 31 | case .plusReplaced: 32 | return escapedString.replacingOccurrences( 33 | of: String.space, 34 | with: String.urlPlusReplacedSpace 35 | ) 36 | } 37 | } 38 | 39 | private func serializeString(_ string: String, at key: String) -> String { 40 | "\(key)=\(escapeString(string))" 41 | } 42 | 43 | private func serializeArray(_ array: [Int: URLQueryValue], at key: String) -> String { 44 | let array = array.sorted { lhs, rhs in 45 | lhs.key < rhs.key 46 | } 47 | 48 | switch arrayEncodingStrategy { 49 | case .enumerated: 50 | return array 51 | .map { serializeValue($0.value, key: "\(key)[\($0.key)]") } 52 | .joined(separator: .urlQuerySeparator) 53 | 54 | case .unenumerated: 55 | return array 56 | .map { serializeValue($0.value, key: "\(key)[]") } 57 | .joined(separator: .urlQuerySeparator) 58 | } 59 | } 60 | 61 | private func serializeDictionary(_ dictionary: [String: URLQueryValue], at key: String) -> String { 62 | dictionary 63 | .map { serializeValue($0.value, key: "\(key)[\(escapeString($0.key))]") } 64 | .joined(separator: .urlQuerySeparator) 65 | } 66 | 67 | private func serializeValue(_ value: URLQueryValue, key: String) -> String { 68 | switch value { 69 | case let .string(string): 70 | return serializeString(string, at: key) 71 | 72 | case let .array(array): 73 | return serializeArray(array, at: key) 74 | 75 | case let .dictionary(dictionary): 76 | return serializeDictionary(dictionary, at: key) 77 | } 78 | } 79 | 80 | internal func serialize(_ query: [String: URLQueryValue]) -> String { 81 | query 82 | .map { serializeValue($0.value, key: escapeString($0.key)) } 83 | .joined(separator: .urlQuerySeparator) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Encoder/URLQuerySingleValueEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal final class URLQuerySingleValueEncodingContainer: URLQueryValueEncoding { 4 | 5 | private var value: URLQueryValueForm? 6 | 7 | internal let options: URLQueryEncodingOptions 8 | internal let userInfo: [CodingUserInfoKey: Any] 9 | internal let codingPath: [CodingKey] 10 | 11 | internal init( 12 | options: URLQueryEncodingOptions, 13 | userInfo: [CodingUserInfoKey: Any], 14 | codingPath: [CodingKey] 15 | ) { 16 | self.options = options 17 | self.userInfo = userInfo 18 | self.codingPath = codingPath 19 | } 20 | 21 | private func collectValue(_ encodedValue: URLQueryValueForm, for value: Any?) throws { 22 | guard self.value == nil else { 23 | let errorContext = EncodingError.Context( 24 | codingPath: codingPath, 25 | debugDescription: "Single value container already has encoded value." 26 | ) 27 | 28 | throw EncodingError.invalidValue(value as Any, errorContext) 29 | } 30 | 31 | self.value = encodedValue 32 | } 33 | } 34 | 35 | extension URLQuerySingleValueEncodingContainer: SingleValueEncodingContainer { 36 | 37 | internal func encodeNil() throws { 38 | try collectValue(encodeNil(), for: nil) 39 | } 40 | 41 | internal func encode(_ value: String) throws { 42 | try collectValue(encodeValue(value), for: value) 43 | } 44 | 45 | internal func encode(_ value: Bool) throws { 46 | try collectValue(encodeValue(value), for: value) 47 | } 48 | 49 | internal func encode(_ value: Int) throws { 50 | try collectValue(encodeValue(value), for: value) 51 | } 52 | 53 | internal func encode(_ value: Int8) throws { 54 | try collectValue(encodeValue(value), for: value) 55 | } 56 | 57 | internal func encode(_ value: Int16) throws { 58 | try collectValue(encodeValue(value), for: value) 59 | } 60 | 61 | internal func encode(_ value: Int32) throws { 62 | try collectValue(encodeValue(value), for: value) 63 | } 64 | 65 | internal func encode(_ value: Int64) throws { 66 | try collectValue(encodeValue(value), for: value) 67 | } 68 | 69 | internal func encode(_ value: UInt) throws { 70 | try collectValue(encodeValue(value), for: value) 71 | } 72 | 73 | internal func encode(_ value: UInt8) throws { 74 | try collectValue(encodeValue(value), for: value) 75 | } 76 | 77 | internal func encode(_ value: UInt16) throws { 78 | try collectValue(encodeValue(value), for: value) 79 | } 80 | 81 | internal func encode(_ value: UInt32) throws { 82 | try collectValue(encodeValue(value), for: value) 83 | } 84 | 85 | internal func encode(_ value: UInt64) throws { 86 | try collectValue(encodeValue(value), for: value) 87 | } 88 | 89 | internal func encode(_ value: Double) throws { 90 | try collectValue(try encodeValue(value, at: codingPath), for: value) 91 | } 92 | 93 | internal func encode(_ value: Float) throws { 94 | try collectValue(try encodeValue(value, at: codingPath), for: value) 95 | } 96 | 97 | internal func encode(_ value: T) throws { 98 | try collectValue(try encodeValue(value, at: codingPath), for: value) 99 | } 100 | } 101 | 102 | extension URLQuerySingleValueEncodingContainer: Encoder { 103 | 104 | internal func container(keyedBy keyType: Key.Type) -> KeyedEncodingContainer { 105 | if case let .resolver(container as URLQueryAnyKeyedEncodingContainer) = value { 106 | return KeyedEncodingContainer( 107 | URLQueryKeyedEncodingContainer(container: container) 108 | ) 109 | } 110 | 111 | let container = URLQueryAnyKeyedEncodingContainer( 112 | options: options, 113 | userInfo: userInfo, 114 | codingPath: codingPath 115 | ) 116 | 117 | value = .resolver(container) 118 | 119 | return KeyedEncodingContainer( 120 | URLQueryKeyedEncodingContainer(container: container) 121 | ) 122 | } 123 | 124 | internal func unkeyedContainer() -> UnkeyedEncodingContainer { 125 | if case let .resolver(container as URLQueryUnkeyedEncodingContainer) = value { 126 | return container 127 | } 128 | 129 | let container = URLQueryUnkeyedEncodingContainer( 130 | options: options, 131 | userInfo: userInfo, 132 | codingPath: codingPath 133 | ) 134 | 135 | value = .resolver(container) 136 | 137 | return container 138 | } 139 | 140 | internal func singleValueContainer() -> SingleValueEncodingContainer { 141 | self 142 | } 143 | } 144 | 145 | extension URLQuerySingleValueEncodingContainer: URLQueryValueResolver { 146 | 147 | internal func resolveValue() -> URLQueryValue? { 148 | value?.resolveValue() 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Sources/Encoder/URLQueryUnkeyedEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal final class URLQueryUnkeyedEncodingContainer: URLQueryValueEncoding { 4 | 5 | private var array: [URLQueryValueForm] = [] 6 | 7 | internal let options: URLQueryEncodingOptions 8 | internal let userInfo: [CodingUserInfoKey: Any] 9 | internal let codingPath: [CodingKey] 10 | 11 | internal var currentCodingPath: [CodingKey] { 12 | codingPath.appending(AnyCodingKey(count)) 13 | } 14 | 15 | internal init( 16 | options: URLQueryEncodingOptions, 17 | userInfo: [CodingUserInfoKey: Any], 18 | codingPath: [CodingKey] 19 | ) { 20 | self.options = options 21 | self.userInfo = userInfo 22 | self.codingPath = codingPath 23 | } 24 | 25 | private func collectValue(_ encodedValue: URLQueryValueForm) { 26 | array.append(encodedValue) 27 | } 28 | } 29 | 30 | extension URLQueryUnkeyedEncodingContainer: UnkeyedEncodingContainer { 31 | 32 | internal var count: Int { 33 | array.count 34 | } 35 | 36 | internal func encodeNil() throws { 37 | collectValue(encodeNil()) 38 | } 39 | 40 | internal func encode(_ value: String) throws { 41 | collectValue(encodeValue(value)) 42 | } 43 | 44 | internal func encode(_ value: Bool) throws { 45 | collectValue(encodeValue(value)) 46 | } 47 | 48 | internal func encode(_ value: Int) throws { 49 | collectValue(encodeValue(value)) 50 | } 51 | 52 | internal func encode(_ value: Int8) throws { 53 | collectValue(encodeValue(value)) 54 | } 55 | 56 | internal func encode(_ value: Int16) throws { 57 | collectValue(encodeValue(value)) 58 | } 59 | 60 | internal func encode(_ value: Int32) throws { 61 | collectValue(encodeValue(value)) 62 | } 63 | 64 | internal func encode(_ value: Int64) throws { 65 | collectValue(encodeValue(value)) 66 | } 67 | 68 | internal func encode(_ value: UInt) throws { 69 | collectValue(encodeValue(value)) 70 | } 71 | 72 | internal func encode(_ value: UInt8) throws { 73 | collectValue(encodeValue(value)) 74 | } 75 | 76 | internal func encode(_ value: UInt16) throws { 77 | collectValue(encodeValue(value)) 78 | } 79 | 80 | internal func encode(_ value: UInt32) throws { 81 | collectValue(encodeValue(value)) 82 | } 83 | 84 | internal func encode(_ value: UInt64) throws { 85 | collectValue(encodeValue(value)) 86 | } 87 | 88 | internal func encode(_ value: Double) throws { 89 | collectValue(try encodeValue(value, at: currentCodingPath)) 90 | } 91 | 92 | internal func encode(_ value: Float) throws { 93 | collectValue(try encodeValue(value, at: currentCodingPath)) 94 | } 95 | 96 | internal func encode(_ value: T) throws { 97 | collectValue(try encodeValue(value, at: currentCodingPath)) 98 | } 99 | 100 | internal func nestedContainer( 101 | keyedBy keyType: NestedKey.Type 102 | ) -> KeyedEncodingContainer { 103 | let container = URLQueryAnyKeyedEncodingContainer( 104 | options: options, 105 | userInfo: userInfo, 106 | codingPath: currentCodingPath 107 | ) 108 | 109 | collectValue(.resolver(container)) 110 | 111 | return KeyedEncodingContainer( 112 | URLQueryKeyedEncodingContainer(container: container) 113 | ) 114 | } 115 | 116 | internal func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { 117 | let container = URLQueryUnkeyedEncodingContainer( 118 | options: options, 119 | userInfo: userInfo, 120 | codingPath: currentCodingPath 121 | ) 122 | 123 | collectValue(.resolver(container)) 124 | 125 | return container 126 | } 127 | 128 | internal func superEncoder() -> Encoder { 129 | let encoder = URLQuerySingleValueEncodingContainer( 130 | options: options, 131 | userInfo: userInfo, 132 | codingPath: currentCodingPath 133 | ) 134 | 135 | collectValue(.resolver(encoder)) 136 | 137 | return encoder 138 | } 139 | } 140 | 141 | extension URLQueryUnkeyedEncodingContainer: URLQueryValueResolver { 142 | 143 | internal func resolveValue() -> URLQueryValue? { 144 | let array = array 145 | .enumerated() 146 | .map { (key: $0, value: $1.resolveValue()) } 147 | .reduce(into: [:]) { result, keyValue in 148 | result[keyValue.key] = keyValue.value 149 | } 150 | 151 | return .array(array) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/Encoder/URLQueryValueEncoding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal protocol URLQueryValueEncoding { 4 | 5 | var options: URLQueryEncodingOptions { get } 6 | var userInfo: [CodingUserInfoKey: Any] { get } 7 | } 8 | 9 | extension URLQueryValueEncoding { 10 | 11 | private func encodeString(_ value: String) -> URLQueryValueForm { 12 | .value(.string(value)) 13 | } 14 | 15 | private func encodePrimitiveValue( 16 | _ value: T 17 | ) -> URLQueryValueForm { 18 | encodeString(String(value)) 19 | } 20 | 21 | private func encodeNonPrimitiveValue( 22 | _ value: T, 23 | at codingPath: [CodingKey] 24 | ) throws -> URLQueryValueForm { 25 | let encoder = URLQuerySingleValueEncodingContainer( 26 | options: options, 27 | userInfo: userInfo, 28 | codingPath: codingPath 29 | ) 30 | 31 | try value.encode(to: encoder) 32 | 33 | return .value(encoder.resolveValue()) 34 | } 35 | 36 | private func encodeCustomizedValue( 37 | _ value: T, 38 | at codingPath: [CodingKey], 39 | closure: (_ value: T, _ encoder: Encoder) throws -> Void 40 | ) throws -> URLQueryValueForm { 41 | let encoder = URLQuerySingleValueEncodingContainer( 42 | options: options, 43 | userInfo: userInfo, 44 | codingPath: codingPath 45 | ) 46 | 47 | try closure(value, encoder) 48 | 49 | return .value(encoder.resolveValue()) 50 | } 51 | 52 | private func encodeBooleanValue(_ value: Bool) -> URLQueryValueForm { 53 | switch options.boolEncodingStrategy { 54 | case .numeric: 55 | return encodeString(value ? .urlQueryNumericTrue : .urlQueryNumericFalse) 56 | 57 | case .literal: 58 | return encodeString(value ? .urlQueryLiteralTrue : .urlQueryLiteralFalse) 59 | } 60 | } 61 | 62 | private func encodeFloatingPointValue( 63 | _ value: T, 64 | at codingPath: [CodingKey] 65 | ) throws -> URLQueryValueForm { 66 | if value.isFinite { 67 | return encodePrimitiveValue(value) 68 | } 69 | 70 | switch options.nonConformingFloatEncodingStrategy { 71 | case let .convertToString(positiveInfinity, _, _) where value == T.infinity: 72 | return encodeString(positiveInfinity) 73 | 74 | case let .convertToString(_, negativeInfinity, _) where value == -T.infinity: 75 | return encodeString(negativeInfinity) 76 | 77 | case let .convertToString(_, _, nan): 78 | return encodeString(nan) 79 | 80 | case .throw: 81 | throw EncodingError.invalidFloatingPointValue(value, at: codingPath) 82 | } 83 | } 84 | 85 | private func encodeDate(_ date: Date, at codingPath: [CodingKey]) throws -> URLQueryValueForm { 86 | switch options.dateEncodingStrategy { 87 | case .deferredToDate: 88 | return try encodeNonPrimitiveValue(date, at: codingPath) 89 | 90 | case .millisecondsSince1970: 91 | return try encodeFloatingPointValue(date.timeIntervalSince1970 * 1000.0, at: codingPath) 92 | 93 | case .secondsSince1970: 94 | return try encodeFloatingPointValue(date.timeIntervalSince1970, at: codingPath) 95 | 96 | case .iso8601: 97 | guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { 98 | fatalError("ISO8601DateFormatter is unavailable on this platform.") 99 | } 100 | 101 | let formattedDate = ISO8601DateFormatter.string( 102 | from: date, 103 | timeZone: .iso8601, 104 | formatOptions: .withInternetDateTime 105 | ) 106 | 107 | return encodeString(formattedDate) 108 | 109 | case let .formatted(dateFormatter): 110 | return encodeString(dateFormatter.string(from: date)) 111 | 112 | case let .custom(closure): 113 | return try encodeCustomizedValue(date, at: codingPath, closure: closure) 114 | } 115 | } 116 | 117 | private func encodeData(_ data: Data, at codingPath: [CodingKey]) throws -> URLQueryValueForm { 118 | switch options.dataEncodingStrategy { 119 | case .deferredToData: 120 | return try encodeNonPrimitiveValue(data, at: codingPath) 121 | 122 | case .base64: 123 | return encodeString(data.base64EncodedString()) 124 | 125 | case let .custom(closure): 126 | return try encodeCustomizedValue(data, at: codingPath, closure: closure) 127 | } 128 | } 129 | 130 | private func encodeURL(_ url: URL) -> URLQueryValueForm { 131 | encodeString(url.absoluteString) 132 | } 133 | } 134 | 135 | extension URLQueryValueEncoding { 136 | 137 | internal func encodeNil() -> URLQueryValueForm { 138 | .value(nil) 139 | } 140 | 141 | internal func encodeValue(_ value: String) -> URLQueryValueForm { 142 | encodeString(value) 143 | } 144 | 145 | internal func encodeValue(_ value: Bool) -> URLQueryValueForm { 146 | encodeBooleanValue(value) 147 | } 148 | 149 | internal func encodeValue(_ value: Int) -> URLQueryValueForm { 150 | encodePrimitiveValue(value) 151 | } 152 | 153 | internal func encodeValue(_ value: Int8) -> URLQueryValueForm { 154 | encodePrimitiveValue(value) 155 | } 156 | 157 | internal func encodeValue(_ value: Int16) -> URLQueryValueForm { 158 | encodePrimitiveValue(value) 159 | } 160 | 161 | internal func encodeValue(_ value: Int32) -> URLQueryValueForm { 162 | encodePrimitiveValue(value) 163 | } 164 | 165 | internal func encodeValue(_ value: Int64) -> URLQueryValueForm { 166 | encodePrimitiveValue(value) 167 | } 168 | 169 | internal func encodeValue(_ value: UInt) -> URLQueryValueForm { 170 | encodePrimitiveValue(value) 171 | } 172 | 173 | internal func encodeValue(_ value: UInt8) -> URLQueryValueForm { 174 | encodePrimitiveValue(value) 175 | } 176 | 177 | internal func encodeValue(_ value: UInt16) -> URLQueryValueForm { 178 | encodePrimitiveValue(value) 179 | } 180 | 181 | internal func encodeValue(_ value: UInt32) -> URLQueryValueForm { 182 | encodePrimitiveValue(value) 183 | } 184 | 185 | internal func encodeValue(_ value: UInt64) -> URLQueryValueForm { 186 | encodePrimitiveValue(value) 187 | } 188 | 189 | internal func encodeValue(_ value: Double, at codingPath: [CodingKey]) throws -> URLQueryValueForm { 190 | try encodeFloatingPointValue(value, at: codingPath) 191 | } 192 | 193 | internal func encodeValue(_ value: Float, at codingPath: [CodingKey]) throws -> URLQueryValueForm { 194 | try encodeFloatingPointValue(value, at: codingPath) 195 | } 196 | 197 | internal func encodeValue( 198 | _ value: T, 199 | at codingPath: [CodingKey] 200 | ) throws -> URLQueryValueForm { 201 | switch value { 202 | case let url as URL: 203 | return encodeURL(url) 204 | 205 | case let date as Date: 206 | return try encodeDate(date, at: codingPath) 207 | 208 | case let data as Data: 209 | return try encodeData(data, at: codingPath) 210 | 211 | default: 212 | return try encodeNonPrimitiveValue(value, at: codingPath) 213 | } 214 | } 215 | } 216 | 217 | private extension EncodingError { 218 | 219 | static func invalidFloatingPointValue(_ value: T, at codingPath: [CodingKey]) -> EncodingError { 220 | let valueDescription: String 221 | 222 | switch value { 223 | case T.infinity: 224 | valueDescription = "\(T.self).infinity" 225 | 226 | case -T.infinity: 227 | valueDescription = "-\(T.self).infinity" 228 | 229 | default: 230 | valueDescription = "\(T.self).nan" 231 | } 232 | 233 | let debugDescription = """ 234 | Unable to encode \(valueDescription) directly in URL query. 235 | Use URLQueryNonConformingFloatEncodingStrategy.convertToString to specify how the value should be encoded. 236 | """ 237 | 238 | return .invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription)) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Sources/Encoder/URLQueryValueForm.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal indirect enum URLQueryValueForm { 4 | 5 | case value(URLQueryValue?) 6 | case resolver(URLQueryValueResolver) 7 | 8 | internal func resolveValue() -> URLQueryValue? { 9 | switch self { 10 | case .value(let value): 11 | return value 12 | 13 | case .resolver(let resolver): 14 | return resolver.resolveValue() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/Tools/AnyCodingKey.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal struct AnyCodingKey: CodingKey { 4 | 5 | internal static let `super` = AnyCodingKey("super") 6 | 7 | internal let stringValue: String 8 | internal let intValue: Int? 9 | 10 | internal init(_ stringValue: String) { 11 | self.stringValue = stringValue 12 | self.intValue = nil 13 | } 14 | 15 | internal init(_ intValue: Int) { 16 | self.stringValue = "\(intValue)" 17 | self.intValue = intValue 18 | } 19 | 20 | internal init?(stringValue: String) { 21 | self.init(stringValue) 22 | } 23 | 24 | internal init?(intValue: Int) { 25 | self.init(intValue) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Tools/Character+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Character { 4 | 5 | internal static let leftSquareBracket = Self("[") 6 | internal static let rightSquareBracket = Self("]") 7 | internal static let ampersand = Self("&") 8 | internal static let equals = Self("=") 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Tools/Collection+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Collection { 4 | 5 | internal subscript(safe index: Index) -> Iterator.Element? { 6 | indices.contains(index) ? self[index] : nil 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Tools/Dictionary+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Dictionary { 4 | 5 | internal func updatingValue(_ value: Value, forKey key: Key) -> Self { 6 | var dictionary = self 7 | 8 | dictionary.updateValue(value, forKey: key) 9 | 10 | return dictionary 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Tools/Optional+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Optional { 4 | 5 | internal var isNil: Bool { 6 | self == nil 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Tools/RangeReplaceableCollection+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension RangeReplaceableCollection { 4 | 5 | internal func appending(contentsOf collection: T) -> Self where Self.Element == T.Element { 6 | self + collection 7 | } 8 | 9 | internal func appending(_ element: Element) -> Self { 10 | appending(contentsOf: [element]) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Tools/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | 5 | internal static let space = " " 6 | 7 | internal static let urlPercentEscapedSpace = "%20" 8 | internal static let urlPlusReplacedSpace = "+" 9 | internal static let urlPathSeparator = "/" 10 | 11 | internal static let urlQueryDelimiters = ":#[]@!$&'()*+,;=" 12 | internal static let urlQuerySeparator = "&" 13 | 14 | internal static let urlQueryNumericFalse = "0" 15 | internal static let urlQueryNumericTrue = "1" 16 | 17 | internal static let urlQueryLiteralFalse = "false" 18 | internal static let urlQueryLiteralTrue = "true" 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Tools/TimeZone+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension TimeZone { 4 | 5 | internal static let iso8601 = TimeZone(secondsFromGMT: 0)! 6 | } 7 | -------------------------------------------------------------------------------- /Sources/URLQueryCoder.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | FOUNDATION_EXPORT double URLQueryCoderVersionNumber; 4 | FOUNDATION_EXPORT const unsigned char URLQueryCoderVersionString[]; 5 | -------------------------------------------------------------------------------- /Sources/Value/URLQueryValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal enum URLQueryValue { 4 | 5 | case string(String) 6 | indirect case array([Int: URLQueryValue]) 7 | indirect case dictionary([String: URLQueryValue]) 8 | 9 | internal var string: String? { 10 | switch self { 11 | case let .string(string): 12 | return string 13 | 14 | default: 15 | return nil 16 | } 17 | } 18 | 19 | internal var array: [Int: URLQueryValue]? { 20 | switch self { 21 | case let .array(array): 22 | return array 23 | 24 | default: 25 | return nil 26 | } 27 | } 28 | 29 | internal var dictionary: [String: URLQueryValue]? { 30 | switch self { 31 | case let .dictionary(dictionary): 32 | return dictionary 33 | 34 | default: 35 | return nil 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Value/URLQueryValueResolver.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal protocol URLQueryValueResolver { 4 | 5 | func resolveValue() -> URLQueryValue? 6 | } 7 | -------------------------------------------------------------------------------- /Tests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - closure_body_length 3 | - file_length 4 | - force_try 5 | - function_body_length 6 | - trailing_closure 7 | - type_body_length 8 | - nesting 9 | -------------------------------------------------------------------------------- /Tests/Decoder/URLQueryDecoderStrategiesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import URLQueryCoder 4 | 5 | final class URLQueryDecoderStrategiesTests: XCTestCase, URLQueryDecoderTesting { 6 | 7 | private(set) var decoder: URLQueryDecoder! 8 | 9 | func testThatDecoderSucceedsWhenDecodingStructUsingDefaultKeys() { 10 | struct DecodableStruct: Decodable, Equatable { 11 | let foo: Int 12 | let bar: Int 13 | } 14 | 15 | decoder.keyDecodingStrategy = .useDefaultKeys 16 | 17 | let query = "foo=123&bar=456" 18 | 19 | let expectedValue = DecodableStruct( 20 | foo: 123, 21 | bar: 456 22 | ) 23 | 24 | assertDecoderSucceeds( 25 | decoding: DecodableStruct.self, 26 | from: query, 27 | expecting: expectedValue 28 | ) 29 | } 30 | 31 | func testThatDecoderSucceedsWhenDecodingStructUsingCustomFunctionForKeys() { 32 | struct DecodableStruct: Decodable, Equatable { 33 | let foo: Bool 34 | let bar: Bool 35 | } 36 | 37 | decoder.keyDecodingStrategy = .custom { codingPath in 38 | if let codingKey = codingPath.last?.stringValue.components(separatedBy: ".").first { 39 | return AnyCodingKey(codingKey) 40 | } else { 41 | return AnyCodingKey("unknown") 42 | } 43 | } 44 | 45 | let query = "foo.value=true&bar.value=false" 46 | 47 | let expectedValue = DecodableStruct( 48 | foo: true, 49 | bar: false 50 | ) 51 | 52 | assertDecoderSucceeds( 53 | decoding: DecodableStruct.self, 54 | from: query, 55 | expecting: expectedValue 56 | ) 57 | } 58 | 59 | func testThatDecoderSucceedsWhenDecodingDate() { 60 | decoder.dateDecodingStrategy = .deferredToDate 61 | 62 | let date = Date(timeIntervalSinceReferenceDate: 123_456.789) 63 | let query = "foobar=\(date.timeIntervalSinceReferenceDate)" 64 | let expectedValue = ["foobar": date] 65 | 66 | assertDecoderSucceeds( 67 | decoding: [String: Date].self, 68 | from: query, 69 | expecting: expectedValue 70 | ) 71 | } 72 | 73 | func testThatDecoderFailsWhenDecodingInvalidDate() { 74 | decoder.dateDecodingStrategy = .deferredToDate 75 | 76 | let query = "foobar=qwe" 77 | 78 | assertDecoderFails(decoding: [String: Date].self, from: query) { error in 79 | switch error { 80 | case let DecodingError.typeMismatch(type, _) where type is Double.Type: 81 | return true 82 | 83 | default: 84 | return false 85 | } 86 | } 87 | } 88 | 89 | func testThatDecoderSucceedsWhenDecodingDateFromMillisecondsSince1970() { 90 | decoder.dateDecodingStrategy = .millisecondsSince1970 91 | 92 | let date = Date(timeIntervalSince1970: 123_456.789) 93 | let query = "foobar=\(date.timeIntervalSince1970 * 1000)" 94 | let expectedValue = ["foobar": date] 95 | 96 | assertDecoderSucceeds( 97 | decoding: [String: Date].self, 98 | from: query, 99 | expecting: expectedValue 100 | ) 101 | } 102 | 103 | func testThatDecoderSucceedsWhenDecodingDateFromSecondsSince1970() { 104 | decoder.dateDecodingStrategy = .secondsSince1970 105 | 106 | let date = Date(timeIntervalSince1970: 123_456.789) 107 | let query = "foobar=\(date.timeIntervalSince1970)" 108 | let expectedValue = ["foobar": date] 109 | 110 | assertDecoderSucceeds( 111 | decoding: [String: Date].self, 112 | from: query, 113 | expecting: expectedValue 114 | ) 115 | } 116 | 117 | @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) 118 | func testThatDecoderSucceedsWhenDecodingDateFromISO8601Format() { 119 | decoder.dateDecodingStrategy = .iso8601 120 | 121 | let dateFormatter = ISO8601DateFormatter() 122 | 123 | dateFormatter.timeZone = .iso8601 124 | 125 | let date = dateFormatter.date(from: "1970-01-02T10:17:36Z")! 126 | let query = "foobar=\(dateFormatter.string(from: date))" 127 | let expectedValue = ["foobar": date] 128 | 129 | assertDecoderSucceeds( 130 | decoding: [String: Date].self, 131 | from: query, 132 | expecting: expectedValue 133 | ) 134 | } 135 | 136 | @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) 137 | func testThatDecoderFailsWhenDecodingInvalidDateFromISO8601Format() { 138 | decoder.dateDecodingStrategy = .iso8601 139 | 140 | let query = "foobar=qwe" 141 | 142 | assertDecoderFails(decoding: [String: Date].self, from: query) { error in 143 | switch error { 144 | case DecodingError.dataCorrupted: 145 | return true 146 | 147 | default: 148 | return false 149 | } 150 | } 151 | } 152 | 153 | func testThatDecoderSucceedsWhenDecodingDateUsingFormatter() { 154 | let dateFormatter = DateFormatter() 155 | 156 | dateFormatter.dateFormat = "yyyy-MM-dd" 157 | dateFormatter.timeZone = .iso8601 158 | 159 | decoder.dateDecodingStrategy = .formatted(dateFormatter) 160 | 161 | let date = dateFormatter.date(from: "1970-01-02")! 162 | let query = "foobar=\(dateFormatter.string(from: date))" 163 | let expectedValue = ["foobar": date] 164 | 165 | assertDecoderSucceeds( 166 | decoding: [String: Date].self, 167 | from: query, 168 | expecting: expectedValue 169 | ) 170 | } 171 | 172 | func testThatDecoderFailsWhenDecodingInvalidDateUsingFormatter() { 173 | let dateFormatter = DateFormatter() 174 | 175 | dateFormatter.dateFormat = "yyyy-MM-dd" 176 | dateFormatter.timeZone = .iso8601 177 | 178 | decoder.dateDecodingStrategy = .formatted(dateFormatter) 179 | 180 | let query = "foobar=qwe" 181 | 182 | assertDecoderFails(decoding: [String: Date].self, from: query) { error in 183 | switch error { 184 | case DecodingError.dataCorrupted: 185 | return true 186 | 187 | default: 188 | return false 189 | } 190 | } 191 | } 192 | 193 | func testThatDecoderSucceedsWhenDecodingDateUsingCustomFunction() { 194 | decoder.dateDecodingStrategy = .custom { decoder in 195 | let container = try decoder.singleValueContainer() 196 | 197 | guard let timeIntervalSince1970 = TimeInterval(try container.decode(String.self)) else { 198 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date") 199 | } 200 | 201 | return Date(timeIntervalSince1970: timeIntervalSince1970) 202 | } 203 | 204 | let date = Date(timeIntervalSince1970: 123_456.789) 205 | let query = "foobar=123456.789" 206 | let expectedValue = ["foobar": date] 207 | 208 | assertDecoderSucceeds( 209 | decoding: [String: Date].self, 210 | from: query, 211 | expecting: expectedValue 212 | ) 213 | } 214 | 215 | func testThatDecoderSucceedsWhenDecodingData() { 216 | decoder.dataDecodingStrategy = .deferredToData 217 | 218 | let query = "foobar[]=1&foobar[]=2&foobar[]=3" 219 | let expectedValue = ["foobar": Data([1, 2, 3])] 220 | 221 | assertDecoderSucceeds( 222 | decoding: [String: Data].self, 223 | from: query, 224 | expecting: expectedValue 225 | ) 226 | } 227 | 228 | func testThatDecoderFailsWhenDecodingInvalidData() { 229 | decoder.dataDecodingStrategy = .deferredToData 230 | 231 | let query = "foobar=qwe" 232 | 233 | assertDecoderFails(decoding: [String: Data].self, from: query) { error in 234 | switch error { 235 | case DecodingError.typeMismatch: 236 | return true 237 | 238 | default: 239 | return false 240 | } 241 | } 242 | } 243 | 244 | func testThatDecoderSucceedsWhenDecodingDataToBase64() { 245 | decoder.dataDecodingStrategy = .base64 246 | 247 | let data = Data([1, 2, 3]) 248 | let query = "foobar=\(data.base64EncodedString())" 249 | let expectedValue = ["foobar": data] 250 | 251 | assertDecoderSucceeds( 252 | decoding: [String: Data].self, 253 | from: query, 254 | expecting: expectedValue 255 | ) 256 | } 257 | 258 | func testThatDecoderFailsWhenDecodingInvalidDataToBase64() { 259 | decoder.dataDecodingStrategy = .base64 260 | 261 | let query = "foobar=123" 262 | 263 | assertDecoderFails(decoding: [String: Data].self, from: query) { error in 264 | switch error { 265 | case DecodingError.dataCorrupted: 266 | return true 267 | 268 | default: 269 | return false 270 | } 271 | } 272 | } 273 | 274 | func testThatDecoderSucceedsWhenDecodingDataUsingCustomFunction() { 275 | decoder.dataDecodingStrategy = .custom { decoder in 276 | let container = try decoder.singleValueContainer() 277 | let string = try container.decode(String.self) 278 | 279 | let bytes = string 280 | .components(separatedBy: ",") 281 | .compactMap { UInt8($0) } 282 | 283 | return Data(bytes) 284 | } 285 | 286 | let query = "foobar=1,2,3" 287 | let expectedValue = ["foobar": Data([1, 2, 3])] 288 | 289 | assertDecoderSucceeds( 290 | decoding: [String: Data].self, 291 | from: query, 292 | expecting: expectedValue 293 | ) 294 | } 295 | 296 | func testThatDecoderFailsWhenDecodingInvalidDataUsingCustomFunction() { 297 | decoder.dataDecodingStrategy = .custom { decoder in 298 | let container = try decoder.singleValueContainer() 299 | 300 | return Data([try container.decode(UInt8.self)]) 301 | } 302 | 303 | let query = "foobar=qwe" 304 | 305 | assertDecoderFails(decoding: [String: Data].self, from: query) { error in 306 | switch error { 307 | case let DecodingError.typeMismatch(type, _) where type is UInt8.Type: 308 | return true 309 | 310 | default: 311 | return false 312 | } 313 | } 314 | } 315 | 316 | func testThatDecoderFailsWhenDecodingNonConformingFloat() { 317 | decoder.nonConformingFloatDecodingStrategy = .throw 318 | 319 | let query = "foo=∞&bar=-∞&baz=¬" 320 | 321 | assertDecoderFails(decoding: [String: Float].self, from: query) { error in 322 | switch error { 323 | case DecodingError.typeMismatch: 324 | return true 325 | 326 | default: 327 | return false 328 | } 329 | } 330 | } 331 | 332 | func testThatDecoderSucceedsWhenDecodingNonConformingFloatFromString() { 333 | decoder.nonConformingFloatDecodingStrategy = .convertFromString( 334 | positiveInfinity: "∞", 335 | negativeInfinity: "-∞", 336 | nan: "¬" 337 | ) 338 | 339 | let query = "foo=∞&bar=-∞&baz=¬" 340 | 341 | let expectedValue: [String: Float] = [ 342 | "foo": Float.infinity, 343 | "bar": -Float.infinity, 344 | "baz": Float.nan 345 | ] 346 | 347 | assertDecoderSucceeds( 348 | decoding: [String: Float].self, 349 | from: query, 350 | expecting: expectedValue 351 | ) 352 | } 353 | 354 | func testThatDecoderFailsWhenDecodingNonConformingFloatFromInvalidString() { 355 | decoder.nonConformingFloatDecodingStrategy = .convertFromString( 356 | positiveInfinity: "∞", 357 | negativeInfinity: "-∞", 358 | nan: "¬" 359 | ) 360 | 361 | let query = "foobar=qwe" 362 | 363 | assertDecoderFails(decoding: [String: Float].self, from: query) { error in 364 | switch error { 365 | case let DecodingError.typeMismatch(type, _) where type is Float.Type: 366 | return true 367 | 368 | default: 369 | return false 370 | } 371 | } 372 | } 373 | 374 | override func setUp() { 375 | super.setUp() 376 | 377 | decoder = URLQueryDecoder() 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /Tests/Decoder/URLQueryDecoderTesting.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import URLQueryCoder 4 | 5 | protocol URLQueryDecoderTesting { 6 | 7 | var decoder: URLQueryDecoder! { get } 8 | } 9 | 10 | extension URLQueryDecoderTesting { 11 | 12 | func assertDecoderSucceeds( 13 | decoding valueType: [Key: Value].Type, 14 | from query: String, 15 | expecting expectedValue: [Key: Value], 16 | file: StaticString = #file, 17 | line: UInt = #line 18 | ) { 19 | do { 20 | let value = try decoder.decode(valueType, from: query) 21 | 22 | XCTAssertEqual( 23 | NSDictionary(dictionary: value), 24 | NSDictionary(dictionary: expectedValue), 25 | file: file, 26 | line: line 27 | ) 28 | } catch { 29 | XCTFail("Test encountered unexpected error: \(error)", file: file, line: line) 30 | } 31 | } 32 | 33 | func assertDecoderSucceeds( 34 | decoding valueType: T.Type, 35 | from query: String, 36 | expecting expectedValue: T, 37 | file: StaticString = #file, 38 | line: UInt = #line 39 | ) { 40 | do { 41 | let value = try decoder.decode(valueType, from: query) 42 | 43 | XCTAssertEqual(value, expectedValue, file: file, line: line) 44 | } catch { 45 | XCTFail("Test encountered unexpected error: \(error)", file: file, line: line) 46 | } 47 | } 48 | 49 | func assertDecoderFails( 50 | decoding valueType: T.Type, 51 | from query: String, 52 | file: StaticString = #file, 53 | line: UInt = #line, 54 | errorValidation: (_ error: Error) -> Bool 55 | ) { 56 | do { 57 | _ = try decoder.decode(valueType, from: query) 58 | 59 | XCTFail("Test encountered unexpected behavior", file: file, line: line) 60 | } catch { 61 | if !errorValidation(error) { 62 | XCTFail("Test encountered unexpected error: \(error)", file: file, line: line) 63 | } 64 | } 65 | } 66 | } 67 | 68 | extension String { 69 | 70 | internal var urlQueryEncoded: String? { 71 | var allowedCharacters = CharacterSet.urlQueryAllowed 72 | 73 | allowedCharacters.remove(charactersIn: ":#[]@!$&'()*+,;=") 74 | 75 | return addingPercentEncoding(withAllowedCharacters: allowedCharacters) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/Encoder/URLQueryEncoderStrategiesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import URLQueryCoder 4 | 5 | final class URLQueryEncoderStrategiesTests: XCTestCase, URLQueryEncoderTesting { 6 | 7 | private(set) var encoder: URLQueryEncoder! 8 | 9 | func testThatEncoderSucceedsWhenEncodingStructUsingDefaultKeys() { 10 | struct EncodableStruct: Encodable { 11 | let foo = 123 12 | let bar = 456 13 | } 14 | 15 | encoder.keyEncodingStrategy = .useDefaultKeys 16 | 17 | let expectedQuery = "foo=123&bar=456" 18 | 19 | assertEncoderSucceeds( 20 | encoding: EncodableStruct(), 21 | expecting: expectedQuery 22 | ) 23 | } 24 | 25 | func testThatEncoderSucceedsWhenEncodingStructUsingCustomFunctionForKeys() { 26 | struct EncodableStruct: Encodable { 27 | let foo = true 28 | let bar = false 29 | } 30 | 31 | encoder.keyEncodingStrategy = .custom { codingPath in 32 | codingPath.last.map { AnyCodingKey("\($0.stringValue).value") } ?? AnyCodingKey("unknown") 33 | } 34 | 35 | let expectedQuery = "foo.value=true&bar.value=false" 36 | 37 | assertEncoderSucceeds( 38 | encoding: EncodableStruct(), 39 | expecting: expectedQuery 40 | ) 41 | } 42 | 43 | func testThatEncoderSucceedsWhenEncodingDate() { 44 | encoder.dateEncodingStrategy = .deferredToDate 45 | 46 | let date = Date(timeIntervalSinceReferenceDate: 123_456.789) 47 | let value = ["foobar": date] 48 | let expectedQuery = "foobar=\(date.timeIntervalSinceReferenceDate)" 49 | 50 | assertEncoderSucceeds( 51 | encoding: value, 52 | expecting: expectedQuery 53 | ) 54 | } 55 | 56 | func testThatEncoderSucceedsWhenEncodingDateToMillisecondsSince1970() { 57 | encoder.dateEncodingStrategy = .millisecondsSince1970 58 | 59 | let date = Date(timeIntervalSinceReferenceDate: 123_456.789) 60 | let value = ["foobar": date] 61 | let expectedQuery = "foobar=\(date.timeIntervalSince1970 * 1000)" 62 | 63 | assertEncoderSucceeds( 64 | encoding: value, 65 | expecting: expectedQuery 66 | ) 67 | } 68 | 69 | func testThatEncoderSucceedsWhenEncodingDateToSecondsSince1970() { 70 | encoder.dateEncodingStrategy = .secondsSince1970 71 | 72 | let date = Date(timeIntervalSinceReferenceDate: 123_456.789) 73 | let value = ["foobar": date] 74 | let expectedQuery = "foobar=\(date.timeIntervalSince1970)" 75 | 76 | assertEncoderSucceeds( 77 | encoding: value, 78 | expecting: expectedQuery 79 | ) 80 | } 81 | 82 | @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) 83 | func testThatEncoderSucceedsWhenEncodingDateToISO8601Format() { 84 | encoder.dateEncodingStrategy = .iso8601 85 | 86 | let dateString = "1970-01-02T10:17:36Z" 87 | let dateFormatter = ISO8601DateFormatter() 88 | 89 | dateFormatter.timeZone = .iso8601 90 | 91 | let date = dateFormatter.date(from: dateString)! 92 | let value = ["foobar": date] 93 | let expectedQuery = "foobar=\(dateString.urlQueryEncoded!)" 94 | 95 | assertEncoderSucceeds( 96 | encoding: value, 97 | expecting: expectedQuery 98 | ) 99 | } 100 | 101 | func testThatEncoderSucceedsWhenEncodingDateUsingFormatter() { 102 | let dateFormatter = DateFormatter() 103 | 104 | dateFormatter.dateFormat = "yyyy-MM-dd" 105 | dateFormatter.timeZone = .iso8601 106 | 107 | encoder.dateEncodingStrategy = .formatted(dateFormatter) 108 | 109 | let date = Date(timeIntervalSinceReferenceDate: 123_456.789) 110 | let value = ["foobar": date] 111 | let expectedQuery = "foobar=\(dateFormatter.string(from: date).urlQueryEncoded!)" 112 | 113 | assertEncoderSucceeds( 114 | encoding: value, 115 | expecting: expectedQuery 116 | ) 117 | } 118 | 119 | func testThatEncoderSucceedsWhenEncodingDateUsingCustomFunction() { 120 | encoder.dateEncodingStrategy = .custom { date, encoder in 121 | var container = encoder.singleValueContainer() 122 | 123 | try container.encode("\(date.timeIntervalSince1970)") 124 | } 125 | 126 | let date = Date(timeIntervalSinceReferenceDate: 123.456) 127 | let value = ["foobar": date] 128 | let expectedQuery = "foobar=\(date.timeIntervalSince1970)" 129 | 130 | assertEncoderSucceeds( 131 | encoding: value, 132 | expecting: expectedQuery 133 | ) 134 | } 135 | 136 | func testThatEncoderSucceedsWhenEncodingData() { 137 | encoder.dataEncodingStrategy = .deferredToData 138 | 139 | let data = Data([1, 2, 3]) 140 | let value = ["foobar": data] 141 | let expectedQuery = "foobar[0]=1&foobar[1]=2&foobar[2]=3" 142 | 143 | assertEncoderSucceeds( 144 | encoding: value, 145 | expecting: expectedQuery 146 | ) 147 | } 148 | 149 | func testThatEncoderSucceedsWhenEncodingDataToBase64() { 150 | encoder.dataEncodingStrategy = .base64 151 | 152 | let data = Data([1, 2, 3]) 153 | let value = ["foobar": data] 154 | let expectedQuery = "foobar=\(data.base64EncodedString())" 155 | 156 | assertEncoderSucceeds( 157 | encoding: value, 158 | expecting: expectedQuery 159 | ) 160 | } 161 | 162 | func testThatEncoderSucceedsWhenEncodingDataUsingCustomFunction() { 163 | encoder.dataEncodingStrategy = .custom { data, encoder in 164 | var container = encoder.singleValueContainer() 165 | 166 | let string = data 167 | .map { "\($0)" } 168 | .joined(separator: ", ") 169 | 170 | try container.encode(string) 171 | } 172 | 173 | let data = Data([1, 2, 3]) 174 | let value = ["foobar": data] 175 | let expectedQuery = "foobar=\("1, 2, 3".urlQueryEncoded!)" 176 | 177 | assertEncoderSucceeds( 178 | encoding: value, 179 | expecting: expectedQuery 180 | ) 181 | } 182 | 183 | func testThatEncoderFailsWhenEncodingPositiveInfinityFloat() { 184 | encoder.nonConformingFloatEncodingStrategy = .throw 185 | 186 | let number = Float.infinity 187 | let value = ["foobar": number] 188 | 189 | assertEncoderFails(encoding: value) { error in 190 | switch error { 191 | case let EncodingError.invalidValue(invalidValue as Float, _): 192 | return invalidValue == number 193 | 194 | default: 195 | return false 196 | } 197 | } 198 | } 199 | 200 | func testThatEncoderFailsWhenEncodingNegativeInfinityFloat() { 201 | encoder.nonConformingFloatEncodingStrategy = .throw 202 | 203 | let number = -Float.infinity 204 | let value = ["foobar": number] 205 | 206 | assertEncoderFails(encoding: value) { error in 207 | switch error { 208 | case let EncodingError.invalidValue(invalidValue as Float, _): 209 | return invalidValue == number 210 | 211 | default: 212 | return false 213 | } 214 | } 215 | } 216 | 217 | func testThatEncoderFailsWhenEncodingNanFloat() { 218 | encoder.nonConformingFloatEncodingStrategy = .throw 219 | 220 | let value = ["foobar": Float.nan] 221 | 222 | assertEncoderFails(encoding: value) { error in 223 | switch error { 224 | case let EncodingError.invalidValue(invalidValue as Float, _): 225 | return invalidValue.isNaN 226 | 227 | default: 228 | return false 229 | } 230 | } 231 | } 232 | 233 | func testThatEncoderSucceedsWhenEncodingNonConformingFloatToString() { 234 | let positiveInfinity = "+∞" 235 | let negativeInfinity = "-∞" 236 | let nan = "¬" 237 | 238 | encoder.nonConformingFloatEncodingStrategy = .convertToString( 239 | positiveInfinity: positiveInfinity, 240 | negativeInfinity: negativeInfinity, 241 | nan: nan 242 | ) 243 | 244 | let value = [ 245 | "foo": Float.infinity, 246 | "bar": -Float.infinity, 247 | "baz": Float.nan 248 | ] 249 | 250 | let expectedQuery = """ 251 | foo=\(positiveInfinity.urlQueryEncoded!)&\ 252 | bar=\(negativeInfinity.urlQueryEncoded!)&\ 253 | baz=\(nan.urlQueryEncoded!) 254 | """ 255 | 256 | assertEncoderSucceeds( 257 | encoding: value, 258 | expecting: expectedQuery 259 | ) 260 | } 261 | 262 | func testThatEncoderSucceedsWhenEncodingEnumeratedArray() { 263 | encoder.arrayEncodingStrategy = .enumerated 264 | 265 | let value: [String: [Int]] = [ 266 | "foo": [1, 2, 3] 267 | ] 268 | 269 | let expectedQuery = "foo[0]=1&foo[1]=2&foo[2]=3" 270 | 271 | assertEncoderSucceeds( 272 | encoding: value, 273 | expecting: expectedQuery 274 | ) 275 | } 276 | 277 | func testThatEncoderSucceedsWhenEncodingUnenumeratedArray() { 278 | encoder.arrayEncodingStrategy = .unenumerated 279 | 280 | let value: [String: [Int]] = [ 281 | "foo": [1, 2, 3] 282 | ] 283 | 284 | let expectedQuery = "foo[]=1&foo[]=2&foo[]=3" 285 | 286 | assertEncoderSucceeds( 287 | encoding: value, 288 | expecting: expectedQuery 289 | ) 290 | } 291 | 292 | func testThatEncoderSucceedsWhenEncodingLiteralBool() { 293 | encoder.boolEncodingStrategy = .literal 294 | 295 | let value: [String: Bool] = [ 296 | "foo": true, 297 | "bar": false 298 | ] 299 | 300 | let expectedQuery = "foo=true&bar=false" 301 | 302 | assertEncoderSucceeds( 303 | encoding: value, 304 | expecting: expectedQuery 305 | ) 306 | } 307 | 308 | func testThatEncoderSucceedsWhenEncodingNumericBool() { 309 | encoder.boolEncodingStrategy = .numeric 310 | 311 | let value: [String: Bool] = [ 312 | "foo": true, 313 | "bar": false 314 | ] 315 | 316 | let expectedQuery = "foo=1&bar=0" 317 | 318 | assertEncoderSucceeds( 319 | encoding: value, 320 | expecting: expectedQuery 321 | ) 322 | } 323 | 324 | func testThatEncoderSucceedsWhenEncodingPercentEscapedString() { 325 | encoder.spaceEncodingStrategy = .percentEscaped 326 | 327 | let value = ["foobar": "qwe asd"] 328 | let expectedQuery = "foobar=qwe%20asd" 329 | 330 | assertEncoderSucceeds( 331 | encoding: value, 332 | expecting: expectedQuery 333 | ) 334 | } 335 | 336 | func testThatEncoderSucceedsWhenEncodingPlusReplacedString() { 337 | encoder.spaceEncodingStrategy = .plusReplaced 338 | 339 | let value = ["foobar": "qwe asd"] 340 | let expectedQuery = "foobar=qwe+asd" 341 | 342 | assertEncoderSucceeds( 343 | encoding: value, 344 | expecting: expectedQuery 345 | ) 346 | } 347 | 348 | override func setUp() { 349 | super.setUp() 350 | 351 | encoder = URLQueryEncoder() 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /Tests/Encoder/URLQueryEncoderTesting.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import URLQueryCoder 3 | 4 | protocol URLQueryEncoderTesting { 5 | 6 | var encoder: URLQueryEncoder! { get } 7 | } 8 | 9 | extension URLQueryEncoderTesting { 10 | 11 | func assertEncoderSucceeds( 12 | encoding value: T, 13 | expecting expectedQuery: String, 14 | file: StaticString = #file, 15 | line: UInt = #line 16 | ) { 17 | let expectedQueryComponents = expectedQuery 18 | .components(separatedBy: "&") 19 | .sorted() 20 | 21 | do { 22 | let query = try encoder.encode(value) 23 | 24 | let queryComponents = query 25 | .components(separatedBy: "&") 26 | .sorted() 27 | 28 | XCTAssertEqual( 29 | expectedQueryComponents, 30 | queryComponents, 31 | file: file, 32 | line: line 33 | ) 34 | } catch { 35 | XCTFail("Test encountered unexpected error: \(error)") 36 | } 37 | } 38 | 39 | func assertEncoderFails( 40 | encoding value: T, 41 | file: StaticString = #file, 42 | line: UInt = #line, 43 | errorValidation: (_ error: Error) -> Bool 44 | ) { 45 | do { 46 | _ = try encoder.encode(value) 47 | 48 | XCTFail("Test encountered unexpected behavior") 49 | } catch { 50 | if !errorValidation(error) { 51 | XCTFail("Test encountered unexpected error: \(error)", file: file, line: line) 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/Encoder/URLQueryEncoderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import URLQueryCoder 4 | 5 | final class URLQueryEncoderTests: XCTestCase, URLQueryEncoderTesting { 6 | 7 | private(set) var encoder: URLQueryEncoder! 8 | 9 | func testThatEncoderSucceedsWhenEncodingEmptyDictionary() { 10 | let value: [String: String] = [:] 11 | let expectedQuery = "" 12 | 13 | assertEncoderSucceeds( 14 | encoding: value, 15 | expecting: expectedQuery 16 | ) 17 | } 18 | 19 | func testThatEncoderSucceedsWhenEncodingStringToBoolDictionary() { 20 | let value = [ 21 | "foo": true, 22 | "bar": false 23 | ] 24 | 25 | let expectedQuery = "foo=true&bar=false" 26 | 27 | assertEncoderSucceeds( 28 | encoding: value, 29 | expecting: expectedQuery 30 | ) 31 | } 32 | 33 | func testThatEncoderSucceedsWhenEncodingStringToIntDictionary() { 34 | let value = [ 35 | "foo": 123, 36 | "bar": -456 37 | ] 38 | 39 | let expectedQuery = "foo=123&bar=-456" 40 | 41 | assertEncoderSucceeds( 42 | encoding: value, 43 | expecting: expectedQuery 44 | ) 45 | } 46 | 47 | func testThatEncoderSucceedsWhenEncodingStringToInt8Dictionary() { 48 | let value: [String: Int8] = [ 49 | "foo": 12, 50 | "bar": -34 51 | ] 52 | 53 | let expectedQuery = "foo=12&bar=-34" 54 | 55 | assertEncoderSucceeds( 56 | encoding: value, 57 | expecting: expectedQuery 58 | ) 59 | } 60 | 61 | func testThatEncoderSucceedsWhenEncodingStringToInt16Dictionary() { 62 | let value: [String: Int16] = [ 63 | "foo": 123, 64 | "bar": -456 65 | ] 66 | 67 | let expectedQuery = "foo=123&bar=-456" 68 | 69 | assertEncoderSucceeds( 70 | encoding: value, 71 | expecting: expectedQuery 72 | ) 73 | } 74 | 75 | func testThatEncoderSucceedsWhenEncodingStringToInt32Dictionary() { 76 | let value: [String: Int32] = [ 77 | "foo": 123, 78 | "bar": -456 79 | ] 80 | 81 | let expectedQuery = "foo=123&bar=-456" 82 | 83 | assertEncoderSucceeds( 84 | encoding: value, 85 | expecting: expectedQuery 86 | ) 87 | } 88 | 89 | func testThatEncoderSucceedsWhenEncodingStringToInt64Dictionary() { 90 | let value: [String: Int64] = [ 91 | "foo": 123, 92 | "bar": -456 93 | ] 94 | 95 | let expectedQuery = "foo=123&bar=-456" 96 | 97 | assertEncoderSucceeds( 98 | encoding: value, 99 | expecting: expectedQuery 100 | ) 101 | } 102 | 103 | func testThatEncoderSucceedsWhenEncodingStringToUIntDictionary() { 104 | let value: [String: UInt] = [ 105 | "foo": 123, 106 | "bar": 456 107 | ] 108 | 109 | let expectedQuery = "foo=123&bar=456" 110 | 111 | assertEncoderSucceeds( 112 | encoding: value, 113 | expecting: expectedQuery 114 | ) 115 | } 116 | 117 | func testThatEncoderSucceedsWhenEncodingStringToUInt8Dictionary() { 118 | let value: [String: UInt8] = [ 119 | "foo": 12, 120 | "bar": 34 121 | ] 122 | 123 | let expectedQuery = "foo=12&bar=34" 124 | 125 | assertEncoderSucceeds( 126 | encoding: value, 127 | expecting: expectedQuery 128 | ) 129 | } 130 | 131 | func testThatEncoderSucceedsWhenEncodingStringToUInt16Dictionary() { 132 | let value: [String: UInt16] = [ 133 | "foo": 123, 134 | "bar": 456 135 | ] 136 | 137 | let expectedQuery = "foo=123&bar=456" 138 | 139 | assertEncoderSucceeds( 140 | encoding: value, 141 | expecting: expectedQuery 142 | ) 143 | } 144 | 145 | func testThatEncoderSucceedsWhenEncodingStringToUInt32Dictionary() { 146 | let value: [String: UInt32] = [ 147 | "foo": 123, 148 | "bar": 456 149 | ] 150 | 151 | let expectedQuery = "foo=123&bar=456" 152 | 153 | assertEncoderSucceeds( 154 | encoding: value, 155 | expecting: expectedQuery 156 | ) 157 | } 158 | 159 | func testThatEncoderSucceedsWhenEncodingStringToUInt64Dictionary() { 160 | let value: [String: UInt64] = [ 161 | "foo": 123, 162 | "bar": 456 163 | ] 164 | 165 | let expectedQuery = "foo=123&bar=456" 166 | 167 | assertEncoderSucceeds( 168 | encoding: value, 169 | expecting: expectedQuery 170 | ) 171 | } 172 | 173 | func testThatEncoderSucceedsWhenEncodingStringToDoubleDictionary() { 174 | let value = [ 175 | "foo": 1.23, 176 | "bar": -45.6 177 | ] 178 | 179 | let expectedQuery = "foo=1.23&bar=-45.6" 180 | 181 | assertEncoderSucceeds( 182 | encoding: value, 183 | expecting: expectedQuery 184 | ) 185 | } 186 | 187 | func testThatEncoderSucceedsWhenEncodingStringToFloatDictionary() { 188 | let value: [String: Float] = [ 189 | "foo": 1.23, 190 | "bar": -45.6 191 | ] 192 | 193 | let expectedQuery = "foo=1.23&bar=-45.6" 194 | 195 | assertEncoderSucceeds( 196 | encoding: value, 197 | expecting: expectedQuery 198 | ) 199 | } 200 | 201 | func testThatEncoderSucceedsWhenEncodingStringToStringDictionary() { 202 | let value = [ 203 | "foo": "qwe", 204 | "bar": "asd" 205 | ] 206 | 207 | let expectedQuery = "foo=qwe&bar=asd" 208 | 209 | assertEncoderSucceeds( 210 | encoding: value, 211 | expecting: expectedQuery 212 | ) 213 | } 214 | 215 | func testThatEncoderSucceedsWhenEncodingStringToURLDictionary() { 216 | let foo = "https://www.swift.org/getting-started#swift-version" 217 | let bar = "https://getsupport.apple.com/?locale=en_US&caller=sfaq&PRKEYS=PF9" 218 | 219 | let value = [ 220 | "foo": URL(string: foo)!, 221 | "bar": URL(string: bar)! 222 | ] 223 | 224 | let expectedQuery = "foo=\(foo.urlQueryEncoded!)&bar=\(bar.urlQueryEncoded!)" 225 | 226 | assertEncoderSucceeds( 227 | encoding: value, 228 | expecting: expectedQuery 229 | ) 230 | } 231 | 232 | func testThatEncoderSucceedsWhenEncodingStringToArrayDictionary() { 233 | let value: [String: [Int?]] = [ 234 | "foo": [1, 2, 3], 235 | "bar": [4, nil, 6] 236 | ] 237 | 238 | let expectedQuery = "foo[0]=1&foo[1]=2&foo[2]=3&bar[0]=4&bar[2]=6" 239 | 240 | assertEncoderSucceeds( 241 | encoding: value, 242 | expecting: expectedQuery 243 | ) 244 | } 245 | 246 | func testThatEncoderSucceedsWhenEncodingNestedStringToIntDictionary() { 247 | let value = [ 248 | "foo": [ 249 | "bar": 123, 250 | "baz": -456 251 | ] 252 | ] 253 | 254 | let expectedQuery = "foo[bar]=123&foo[baz]=-456" 255 | 256 | assertEncoderSucceeds( 257 | encoding: value, 258 | expecting: expectedQuery 259 | ) 260 | } 261 | 262 | func testThatEncoderSucceedsWhenEncodingNestedArrayOfStringToIntDictionaries() { 263 | let value = [ 264 | "foo": [ 265 | [ 266 | "bar": 123, 267 | "baz": 456 268 | ] 269 | ] 270 | ] 271 | 272 | let expectedQuery = "foo[0][bar]=123&foo[0][baz]=456" 273 | 274 | assertEncoderSucceeds( 275 | encoding: value, 276 | expecting: expectedQuery 277 | ) 278 | } 279 | 280 | func testThatEncoderSucceedsWhenEncodingEmptyStruct() { 281 | struct EncodableStruct: Encodable { } 282 | 283 | let expectedQuery = "" 284 | 285 | assertEncoderSucceeds( 286 | encoding: EncodableStruct(), 287 | expecting: expectedQuery 288 | ) 289 | } 290 | 291 | func testThatEncoderSucceedsWhenEncodingStructWithMultipleProperties() { 292 | struct EncodableStruct: Encodable { 293 | let foo = true 294 | let bar: Int? = 123 295 | let baz: Int? = nil 296 | let bat = "qwe" 297 | } 298 | 299 | let expectedQuery = "foo=true&bar=123&bat=qwe" 300 | 301 | assertEncoderSucceeds( 302 | encoding: EncodableStruct(), 303 | expecting: expectedQuery 304 | ) 305 | } 306 | 307 | func testThatEncoderSucceedsWhenEncodingStructWithNestedStruct() { 308 | struct EncodableStruct: Encodable { 309 | struct NestedStruct: Encodable { 310 | let bar = 123 311 | let baz = 456 312 | } 313 | 314 | let foo = NestedStruct() 315 | } 316 | 317 | let expectedQuery = "foo[bar]=123&foo[baz]=456" 318 | 319 | assertEncoderSucceeds( 320 | encoding: EncodableStruct(), 321 | expecting: expectedQuery 322 | ) 323 | } 324 | 325 | func testThatEncoderSucceedsWhenEncodingStructWithNestedEnum() { 326 | struct EncodableStruct: Encodable { 327 | enum NestedEnum: String, Encodable { 328 | case qwe 329 | case asd 330 | } 331 | 332 | let foo = NestedEnum.qwe 333 | let bar = NestedEnum.asd 334 | } 335 | 336 | let expectedQuery = "foo=qwe&bar=asd" 337 | 338 | assertEncoderSucceeds( 339 | encoding: EncodableStruct(), 340 | expecting: expectedQuery 341 | ) 342 | } 343 | 344 | func testThatEncoderSucceedsWhenEncodingStructInSeparateKeyedContainers() { 345 | struct EncodableStruct: Encodable { 346 | enum CodingKeys: String, CodingKey { 347 | case foo 348 | case bar 349 | } 350 | 351 | let foo = 123 352 | let bar = 456 353 | 354 | func encode(to encoder: Encoder) throws { 355 | var fooContainer = encoder.container(keyedBy: CodingKeys.self) 356 | var barContainer = encoder.container(keyedBy: CodingKeys.self) 357 | 358 | try fooContainer.encode(foo, forKey: .foo) 359 | try barContainer.encode(bar, forKey: .bar) 360 | } 361 | } 362 | 363 | let expectedQuery = "foo=123&bar=456" 364 | 365 | assertEncoderSucceeds( 366 | encoding: EncodableStruct(), 367 | expecting: expectedQuery 368 | ) 369 | } 370 | 371 | func testThatEncoderSucceedsWhenEncodingStructInSeparateUnkeyedContainers() { 372 | struct EncodableStruct: Encodable { 373 | enum CodingKeys: String, CodingKey { 374 | case foo 375 | } 376 | 377 | let bar = 123 378 | let baz = 456 379 | 380 | func encode(to encoder: Encoder) throws { 381 | var container = encoder.container(keyedBy: CodingKeys.self) 382 | 383 | var barContainer = container.nestedUnkeyedContainer(forKey: .foo) 384 | var bazContainer = container.nestedUnkeyedContainer(forKey: .foo) 385 | 386 | try barContainer.encode(bar) 387 | try bazContainer.encode(baz) 388 | } 389 | } 390 | 391 | let expectedQuery = "foo[0]=123&foo[1]=456" 392 | 393 | assertEncoderSucceeds( 394 | encoding: EncodableStruct(), 395 | expecting: expectedQuery 396 | ) 397 | } 398 | 399 | func testThatEncoderSucceedsWhenEncodingStructInSeparateKeyedContainersOfUnkeyedContainer() { 400 | struct EncodableStruct: Encodable { 401 | enum CodingKeys: String, CodingKey { 402 | case foo 403 | } 404 | 405 | enum BarCodingKeys: String, CodingKey { 406 | case bar 407 | } 408 | 409 | enum BazCodingKeys: String, CodingKey { 410 | case baz 411 | } 412 | 413 | let bar = 123 414 | let baz = 456 415 | 416 | func encode(to encoder: Encoder) throws { 417 | var container = encoder.container(keyedBy: CodingKeys.self) 418 | var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .foo) 419 | 420 | var barContainer = unkeyedContainer.nestedContainer(keyedBy: BarCodingKeys.self) 421 | var bazContainer = unkeyedContainer.nestedContainer(keyedBy: BazCodingKeys.self) 422 | 423 | try barContainer.encode(bar, forKey: .bar) 424 | try bazContainer.encode(baz, forKey: .baz) 425 | } 426 | } 427 | 428 | let expectedQuery = "foo[0][bar]=123&foo[1][baz]=456" 429 | 430 | assertEncoderSucceeds( 431 | encoding: EncodableStruct(), 432 | expecting: expectedQuery 433 | ) 434 | } 435 | 436 | func testThatEncoderSucceedsWhenEncodingStructInSeparateNestedKeyedContainers() { 437 | struct EncodableStruct: Encodable { 438 | enum CodingKeys: String, CodingKey { 439 | case foo 440 | } 441 | 442 | struct NestedStruct { 443 | enum CodingKeys: String, CodingKey { 444 | case bar 445 | case baz 446 | } 447 | 448 | let bar = 123 449 | let baz = 456 450 | } 451 | 452 | let foo = NestedStruct() 453 | 454 | func encode(to encoder: Encoder) throws { 455 | var container = encoder.container(keyedBy: CodingKeys.self) 456 | 457 | var barContainer = container.nestedContainer(keyedBy: NestedStruct.CodingKeys.self, forKey: .foo) 458 | var bazContainer = container.nestedContainer(keyedBy: NestedStruct.CodingKeys.self, forKey: .foo) 459 | 460 | try barContainer.encode(foo.bar, forKey: .bar) 461 | try bazContainer.encode(foo.baz, forKey: .baz) 462 | } 463 | } 464 | 465 | let expectedQuery = "foo[bar]=123&foo[baz]=456" 466 | 467 | assertEncoderSucceeds( 468 | encoding: EncodableStruct(), 469 | expecting: expectedQuery 470 | ) 471 | } 472 | 473 | func testThatEncoderSucceedsWhenEncodingStructUsingSuperEncoder() { 474 | struct EncodableStruct: Encodable { 475 | enum CodingKeys: String, CodingKey { 476 | case foo 477 | case bar 478 | case baz 479 | case bat 480 | } 481 | 482 | let foo = "qwe" 483 | let bar = "asd" 484 | let baz = 123 485 | let bat = 456 486 | 487 | func encode(to encoder: Encoder) throws { 488 | var container = encoder.container(keyedBy: CodingKeys.self) 489 | 490 | try container.encode(baz, forKey: .baz) 491 | try container.encode(bat, forKey: .bat) 492 | 493 | let superEncoder = container.superEncoder() 494 | var superContainer = superEncoder.container(keyedBy: CodingKeys.self) 495 | 496 | try superContainer.encode(foo, forKey: .foo) 497 | try superContainer.encode(bar, forKey: .bar) 498 | } 499 | } 500 | 501 | let expectedQuery = "baz=123&bat=456&super[foo]=qwe&super[bar]=asd" 502 | 503 | assertEncoderSucceeds( 504 | encoding: EncodableStruct(), 505 | expecting: expectedQuery 506 | ) 507 | } 508 | 509 | func testThatEncoderSucceedsWhenEncodingStructUsingSuperEncoderForKeys() { 510 | struct EncodableStruct: Encodable { 511 | enum CodingKeys: String, CodingKey { 512 | case foo 513 | case bar 514 | case baz 515 | case bat 516 | } 517 | 518 | let foo = "qwe" 519 | let bar = "asd" 520 | let baz = 123 521 | let bat = 456 522 | 523 | func encode(to encoder: Encoder) throws { 524 | var container = encoder.container(keyedBy: CodingKeys.self) 525 | 526 | try container.encode(baz, forKey: .baz) 527 | try container.encode(bat, forKey: .bat) 528 | 529 | let fooSuperEncoder = container.superEncoder(forKey: .foo) 530 | let barSuperEncoder = container.superEncoder(forKey: .bar) 531 | 532 | var fooContainer = fooSuperEncoder.container(keyedBy: CodingKeys.self) 533 | var barContainer = barSuperEncoder.container(keyedBy: CodingKeys.self) 534 | 535 | try fooContainer.encode(foo, forKey: .foo) 536 | try barContainer.encode(bar, forKey: .bar) 537 | } 538 | } 539 | 540 | let expectedQuery = "baz=123&bat=456&foo[foo]=qwe&bar[bar]=asd" 541 | 542 | assertEncoderSucceeds( 543 | encoding: EncodableStruct(), 544 | expecting: expectedQuery 545 | ) 546 | } 547 | 548 | func testThatEncoderSucceedsWhenEncodingStructUsingSuperEncoderOfNestedUnkeyedContainer() { 549 | struct EncodableStruct: Encodable { 550 | enum CodingKeys: String, CodingKey { 551 | case baz 552 | } 553 | 554 | let foo = "qwe" 555 | let bar = "asd" 556 | let baz = 123 557 | let bat = 456 558 | 559 | func encode(to encoder: Encoder) throws { 560 | var container = encoder.container(keyedBy: CodingKeys.self) 561 | 562 | var bazContainer = container.nestedUnkeyedContainer(forKey: .baz) 563 | var batContainer = bazContainer.nestedUnkeyedContainer() 564 | 565 | try bazContainer.encode(baz) 566 | try batContainer.encode(bat) 567 | 568 | let bazSuperEncoder = bazContainer.superEncoder() 569 | 570 | var fooContainer = bazSuperEncoder.unkeyedContainer() 571 | var barContainer = bazSuperEncoder.unkeyedContainer() 572 | 573 | try fooContainer.encode(foo) 574 | try barContainer.encode(bar) 575 | } 576 | } 577 | 578 | let expectedQuery = "baz[0][0]=456&baz[1]=123&baz[2][0]=qwe&baz[2][1]=asd" 579 | 580 | assertEncoderSucceeds( 581 | encoding: EncodableStruct(), 582 | expecting: expectedQuery 583 | ) 584 | } 585 | 586 | func testThatEncoderSucceedsWhenEncodingSubclass() { 587 | class EncodableClass: Encodable { 588 | let foo = "qwe" 589 | let bar = "asd" 590 | } 591 | 592 | class EncodableSubclass: EncodableClass { 593 | enum CodingKeys: String, CodingKey { 594 | case baz 595 | case bat 596 | } 597 | 598 | let baz = 123 599 | let bat = 456 600 | 601 | override func encode(to encoder: Encoder) throws { 602 | try super.encode(to: encoder) 603 | 604 | var container = encoder.container(keyedBy: CodingKeys.self) 605 | 606 | try container.encode(baz, forKey: .baz) 607 | try container.encode(bat, forKey: .bat) 608 | } 609 | } 610 | 611 | let expectedQuery = "foo=qwe&bar=asd&baz=123&bat=456" 612 | 613 | assertEncoderSucceeds( 614 | encoding: EncodableSubclass(), 615 | expecting: expectedQuery 616 | ) 617 | } 618 | 619 | func testThatEncoderFailsWhenEncodingArray() { 620 | let value = [1, 2, 3] 621 | 622 | assertEncoderFails(encoding: value) { error in 623 | switch error { 624 | case let EncodingError.invalidValue(invalidValue as [Int], _): 625 | return invalidValue == value 626 | 627 | default: 628 | return false 629 | } 630 | } 631 | } 632 | 633 | func testThatEncoderFailsWhenEncodingSingleValue() { 634 | let value = 123 635 | 636 | assertEncoderFails(encoding: value) { error in 637 | switch error { 638 | case let EncodingError.invalidValue(invalidValue as Int, _): 639 | return invalidValue == value 640 | 641 | default: 642 | return false 643 | } 644 | } 645 | } 646 | 647 | func testThatEncoderFailsWhenEncodingMultipleSingleValuesForKey() { 648 | struct EncodableStruct: Encodable { 649 | let foo = 123 650 | let bar = 456 651 | 652 | func encode(to encoder: Encoder) throws { 653 | var container = encoder.singleValueContainer() 654 | 655 | try container.encode(foo) 656 | try container.encode(bar) 657 | } 658 | } 659 | 660 | let value = EncodableStruct() 661 | 662 | assertEncoderFails(encoding: value) { error in 663 | switch error { 664 | case let EncodingError.invalidValue(invalidValue as Int, _): 665 | return invalidValue == value.bar 666 | 667 | default: 668 | return false 669 | } 670 | } 671 | } 672 | 673 | func testThatEncoderFailsWhenEncodingWithMultipleSingleValueContainers() { 674 | struct EncodableStruct: Encodable { 675 | let foo = 123 676 | let bar = 456 677 | 678 | func encode(to encoder: Encoder) throws { 679 | var fooContainer = encoder.singleValueContainer() 680 | var barContainer = encoder.singleValueContainer() 681 | 682 | try fooContainer.encode(foo) 683 | try barContainer.encode(bar) 684 | } 685 | } 686 | 687 | let value = EncodableStruct() 688 | 689 | assertEncoderFails(encoding: value) { error in 690 | switch error { 691 | case let EncodingError.invalidValue(invalidValue as Int, _): 692 | return invalidValue == value.bar 693 | 694 | default: 695 | return false 696 | } 697 | } 698 | } 699 | 700 | override func setUp() { 701 | super.setUp() 702 | 703 | encoder = URLQueryEncoder() 704 | } 705 | } 706 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /URLQueryCoder.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "URLQueryCoder" 3 | spec.version = "1.1.0" 4 | spec.summary = "Swift Encoder and Decoder for URL query" 5 | 6 | spec.homepage = "https://github.com/almazrafi/URLQueryCoder" 7 | spec.license = { :type => 'MIT', :file => 'LICENSE' } 8 | spec.author = { "Almaz Ibragimov" => "almazrafi@gmail.com" } 9 | spec.source = { :git => "https://github.com/almazrafi/URLQueryCoder.git", :tag => "#{spec.version}" } 10 | 11 | spec.swift_version = '5.5' 12 | spec.requires_arc = true 13 | spec.source_files = 'Sources/**/*.swift' 14 | 15 | spec.ios.frameworks = 'Foundation' 16 | spec.ios.deployment_target = "12.0" 17 | 18 | spec.osx.frameworks = 'Foundation' 19 | spec.osx.deployment_target = "10.14" 20 | 21 | spec.watchos.frameworks = 'Foundation' 22 | spec.watchos.deployment_target = "5.0" 23 | 24 | spec.tvos.frameworks = 'Foundation' 25 | spec.tvos.deployment_target = "12.0" 26 | end 27 | -------------------------------------------------------------------------------- /URLQueryCoder.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /URLQueryCoder.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /URLQueryCoder.xcodeproj/xcshareddata/xcschemes/URLQueryCoder iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /URLQueryCoder.xcodeproj/xcshareddata/xcschemes/URLQueryCoder macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /URLQueryCoder.xcodeproj/xcshareddata/xcschemes/URLQueryCoder tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /URLQueryCoder.xcodeproj/xcshareddata/xcschemes/URLQueryCoder watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | --------------------------------------------------------------------------------