diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5139af9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +### https://raw.github.com/github/gitignore/e6dd3a81e6037cc923e503e372c80326f65ccb25/Global/macos.gitignore + +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### https://raw.github.com/github/gitignore/e6dd3a81e6037cc923e503e372c80326f65ccb25/Global/xcode.gitignore + +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint +UserInterfaceState.xcuserstate + +#Carthage/Build +Carthage + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6904958 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Tatsuya Tanaka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MoreCodable.xcodeproj/project.pbxproj b/MoreCodable.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a6cc6d2 --- /dev/null +++ b/MoreCodable.xcodeproj/project.pbxproj @@ -0,0 +1,508 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 242C3E262030CBE500AAA577 /* URLQueryItemsEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242C3E252030CBE500AAA577 /* URLQueryItemsEncoder.swift */; }; + 242C3E292030D70300AAA577 /* URLQueryItemsEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242C3E272030D68900AAA577 /* URLQueryItemsEncoderTests.swift */; }; + 242C3E2B2030D83600AAA577 /* URLQueryItemsDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242C3E2A2030D83600AAA577 /* URLQueryItemsDecoder.swift */; }; + 242C3E2D2030DDDE00AAA577 /* URLQueryItem+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242C3E2C2030DDDE00AAA577 /* URLQueryItem+.swift */; }; + 242C3E2F2030E28500AAA577 /* URLQueryItemsDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242C3E2E2030E28500AAA577 /* URLQueryItemsDecoderTests.swift */; }; + 2491406D2039DF1D00D3E4CD /* Failable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2491406C2039DF1D00D3E4CD /* Failable.swift */; }; + 2491406F2039DF7B00D3E4CD /* FailableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2491406E2039DF7B00D3E4CD /* FailableTests.swift */; }; + 249140712039E27F00D3E4CD /* StringTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 249140702039E27F00D3E4CD /* StringTo.swift */; }; + 2491407D203C7D5200D3E4CD /* StringToTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2491407C203C7D5200D3E4CD /* StringToTests.swift */; }; + 2491407F203C85A500D3E4CD /* RuleBasedCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2491407E203C85A500D3E4CD /* RuleBasedCodingKey.swift */; }; + 24914081203C89D000D3E4CD /* RuleBasedCodingKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24914080203C89D000D3E4CD /* RuleBasedCodingKeyTests.swift */; }; + 24A4FF4420302322001618E1 /* MoreCodable.h in Headers */ = {isa = PBXBuildFile; fileRef = 24A4FF4220302322001618E1 /* MoreCodable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24A4FF4B203023E6001618E1 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A4FF4A203023E6001618E1 /* Storage.swift */; }; + 24A4FF4D20302407001618E1 /* AnyCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A4FF4C20302407001618E1 /* AnyCodingKey.swift */; }; + 24A4FF4F20302421001618E1 /* DictionaryEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A4FF4E20302421001618E1 /* DictionaryEncoder.swift */; }; + 24A4FF5720302490001618E1 /* DictionaryEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A4FF5620302490001618E1 /* DictionaryEncoderTests.swift */; }; + 24A4FF5920302490001618E1 /* MoreCodable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24A4FF3F20302322001618E1 /* MoreCodable.framework */; }; + 24A4FF6220302B98001618E1 /* InternalFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A4FF6120302B98001618E1 /* InternalFunction.swift */; }; + 24A4FF6420302D41001618E1 /* DictionaryDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A4FF6320302D41001618E1 /* DictionaryDecoder.swift */; }; + 24A4FF6720304D8F001618E1 /* DictionaryDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A4FF6520304D8C001618E1 /* DictionaryDecoderTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 24A4FF5A20302490001618E1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 24A4FF3620302322001618E1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 24A4FF3E20302322001618E1; + remoteInfo = MoreCodable; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 242C3E252030CBE500AAA577 /* URLQueryItemsEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLQueryItemsEncoder.swift; sourceTree = ""; }; + 242C3E272030D68900AAA577 /* URLQueryItemsEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLQueryItemsEncoderTests.swift; sourceTree = ""; }; + 242C3E2A2030D83600AAA577 /* URLQueryItemsDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLQueryItemsDecoder.swift; sourceTree = ""; }; + 242C3E2C2030DDDE00AAA577 /* URLQueryItem+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLQueryItem+.swift"; sourceTree = ""; }; + 242C3E2E2030E28500AAA577 /* URLQueryItemsDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLQueryItemsDecoderTests.swift; sourceTree = ""; }; + 2491406C2039DF1D00D3E4CD /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = ""; }; + 2491406E2039DF7B00D3E4CD /* FailableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailableTests.swift; sourceTree = ""; }; + 249140702039E27F00D3E4CD /* StringTo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringTo.swift; sourceTree = ""; }; + 2491407C203C7D5200D3E4CD /* StringToTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringToTests.swift; sourceTree = ""; }; + 2491407E203C85A500D3E4CD /* RuleBasedCodingKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleBasedCodingKey.swift; sourceTree = ""; }; + 24914080203C89D000D3E4CD /* RuleBasedCodingKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleBasedCodingKeyTests.swift; sourceTree = ""; }; + 24A4FF3F20302322001618E1 /* MoreCodable.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MoreCodable.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 24A4FF4220302322001618E1 /* MoreCodable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MoreCodable.h; sourceTree = ""; }; + 24A4FF4320302322001618E1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 24A4FF4A203023E6001618E1 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; + 24A4FF4C20302407001618E1 /* AnyCodingKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCodingKey.swift; sourceTree = ""; }; + 24A4FF4E20302421001618E1 /* DictionaryEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryEncoder.swift; sourceTree = ""; }; + 24A4FF5420302490001618E1 /* MoreCodableTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MoreCodableTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 24A4FF5620302490001618E1 /* DictionaryEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryEncoderTests.swift; sourceTree = ""; }; + 24A4FF5820302490001618E1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 24A4FF6120302B98001618E1 /* InternalFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalFunction.swift; sourceTree = ""; }; + 24A4FF6320302D41001618E1 /* DictionaryDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryDecoder.swift; sourceTree = ""; }; + 24A4FF6520304D8C001618E1 /* DictionaryDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryDecoderTests.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 24A4FF3B20302322001618E1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24A4FF5120302490001618E1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 24A4FF5920302490001618E1 /* MoreCodable.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 24A4FF3520302321001618E1 = { + isa = PBXGroup; + children = ( + 24A4FF4120302322001618E1 /* Sources */, + 24A4FF5520302490001618E1 /* Tests */, + ); + sourceTree = ""; + }; + 24A4FF4020302322001618E1 /* Products */ = { + isa = PBXGroup; + children = ( + 24A4FF3F20302322001618E1 /* MoreCodable.framework */, + 24A4FF5420302490001618E1 /* MoreCodableTests.xctest */, + ); + name = Products; + path = ..; + sourceTree = ""; + }; + 24A4FF4120302322001618E1 /* Sources */ = { + isa = PBXGroup; + children = ( + 24A4FF4220302322001618E1 /* MoreCodable.h */, + 24A4FF4320302322001618E1 /* Info.plist */, + 24A4FF4A203023E6001618E1 /* Storage.swift */, + 24A4FF4C20302407001618E1 /* AnyCodingKey.swift */, + 24A4FF6120302B98001618E1 /* InternalFunction.swift */, + 24A4FF4E20302421001618E1 /* DictionaryEncoder.swift */, + 24A4FF6320302D41001618E1 /* DictionaryDecoder.swift */, + 242C3E252030CBE500AAA577 /* URLQueryItemsEncoder.swift */, + 242C3E2A2030D83600AAA577 /* URLQueryItemsDecoder.swift */, + 242C3E2C2030DDDE00AAA577 /* URLQueryItem+.swift */, + 2491406C2039DF1D00D3E4CD /* Failable.swift */, + 249140702039E27F00D3E4CD /* StringTo.swift */, + 2491407E203C85A500D3E4CD /* RuleBasedCodingKey.swift */, + ); + path = Sources; + sourceTree = ""; + }; + 24A4FF5520302490001618E1 /* Tests */ = { + isa = PBXGroup; + children = ( + 24A4FF5620302490001618E1 /* DictionaryEncoderTests.swift */, + 24A4FF6520304D8C001618E1 /* DictionaryDecoderTests.swift */, + 242C3E272030D68900AAA577 /* URLQueryItemsEncoderTests.swift */, + 242C3E2E2030E28500AAA577 /* URLQueryItemsDecoderTests.swift */, + 2491406E2039DF7B00D3E4CD /* FailableTests.swift */, + 2491407C203C7D5200D3E4CD /* StringToTests.swift */, + 24914080203C89D000D3E4CD /* RuleBasedCodingKeyTests.swift */, + 24A4FF4020302322001618E1 /* Products */, + 24A4FF5820302490001618E1 /* Info.plist */, + ); + path = Tests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 24A4FF3C20302322001618E1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 24A4FF4420302322001618E1 /* MoreCodable.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 24A4FF3E20302322001618E1 /* MoreCodable */ = { + isa = PBXNativeTarget; + buildConfigurationList = 24A4FF4720302322001618E1 /* Build configuration list for PBXNativeTarget "MoreCodable" */; + buildPhases = ( + 24A4FF3A20302322001618E1 /* Sources */, + 24A4FF3B20302322001618E1 /* Frameworks */, + 24A4FF3C20302322001618E1 /* Headers */, + 24A4FF3D20302322001618E1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MoreCodable; + productName = MoreCodable; + productReference = 24A4FF3F20302322001618E1 /* MoreCodable.framework */; + productType = "com.apple.product-type.framework"; + }; + 24A4FF5320302490001618E1 /* MoreCodableTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 24A4FF5C20302490001618E1 /* Build configuration list for PBXNativeTarget "MoreCodableTests" */; + buildPhases = ( + 24A4FF5020302490001618E1 /* Sources */, + 24A4FF5120302490001618E1 /* Frameworks */, + 24A4FF5220302490001618E1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 24A4FF5B20302490001618E1 /* PBXTargetDependency */, + ); + name = MoreCodableTests; + productName = MoreCodableTests; + productReference = 24A4FF5420302490001618E1 /* MoreCodableTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 24A4FF3620302322001618E1 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0920; + ORGANIZATIONNAME = tattn; + TargetAttributes = { + 24A4FF3E20302322001618E1 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 0920; + ProvisioningStyle = Automatic; + }; + 24A4FF5320302490001618E1 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 24A4FF3920302322001618E1 /* Build configuration list for PBXProject "MoreCodable" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 24A4FF3520302321001618E1; + productRefGroup = 24A4FF4020302322001618E1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 24A4FF3E20302322001618E1 /* MoreCodable */, + 24A4FF5320302490001618E1 /* MoreCodableTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 24A4FF3D20302322001618E1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24A4FF5220302490001618E1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 24A4FF3A20302322001618E1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 24A4FF4F20302421001618E1 /* DictionaryEncoder.swift in Sources */, + 2491407F203C85A500D3E4CD /* RuleBasedCodingKey.swift in Sources */, + 2491406D2039DF1D00D3E4CD /* Failable.swift in Sources */, + 249140712039E27F00D3E4CD /* StringTo.swift in Sources */, + 242C3E2D2030DDDE00AAA577 /* URLQueryItem+.swift in Sources */, + 24A4FF4D20302407001618E1 /* AnyCodingKey.swift in Sources */, + 242C3E2B2030D83600AAA577 /* URLQueryItemsDecoder.swift in Sources */, + 242C3E262030CBE500AAA577 /* URLQueryItemsEncoder.swift in Sources */, + 24A4FF4B203023E6001618E1 /* Storage.swift in Sources */, + 24A4FF6220302B98001618E1 /* InternalFunction.swift in Sources */, + 24A4FF6420302D41001618E1 /* DictionaryDecoder.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24A4FF5020302490001618E1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2491406F2039DF7B00D3E4CD /* FailableTests.swift in Sources */, + 24914081203C89D000D3E4CD /* RuleBasedCodingKeyTests.swift in Sources */, + 24A4FF6720304D8F001618E1 /* DictionaryDecoderTests.swift in Sources */, + 242C3E292030D70300AAA577 /* URLQueryItemsEncoderTests.swift in Sources */, + 242C3E2F2030E28500AAA577 /* URLQueryItemsDecoderTests.swift in Sources */, + 24A4FF5720302490001618E1 /* DictionaryEncoderTests.swift in Sources */, + 2491407D203C7D5200D3E4CD /* StringToTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 24A4FF5B20302490001618E1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 24A4FF3E20302322001618E1 /* MoreCodable */; + targetProxy = 24A4FF5A20302490001618E1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 24A4FF4520302322001618E1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 24A4FF4620302322001618E1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 24A4FF4820302322001618E1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.github.tattn.MoreCodable; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 24A4FF4920302322001618E1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.github.tattn.MoreCodable; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 24A4FF5D20302490001618E1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.github.tattn.MoreCodableTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 24A4FF5E20302490001618E1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.github.tattn.MoreCodableTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 24A4FF3920302322001618E1 /* Build configuration list for PBXProject "MoreCodable" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 24A4FF4520302322001618E1 /* Debug */, + 24A4FF4620302322001618E1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 24A4FF4720302322001618E1 /* Build configuration list for PBXNativeTarget "MoreCodable" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 24A4FF4820302322001618E1 /* Debug */, + 24A4FF4920302322001618E1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 24A4FF5C20302490001618E1 /* Build configuration list for PBXNativeTarget "MoreCodableTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 24A4FF5D20302490001618E1 /* Debug */, + 24A4FF5E20302490001618E1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 24A4FF3620302322001618E1 /* Project object */; +} diff --git a/MoreCodable.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MoreCodable.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..dd58602 --- /dev/null +++ b/MoreCodable.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5aca912 --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +

MoreCodable

+ +
MoreCodable expands the possibilities of "Codable".
+ + + +
+ + +# Installation + +## Carthage + +```ruby +github "tattn/MoreCodable" +``` + + +# Feature + +## DictionaryEncoder / DictionaryDecoder + +```swift +struct User: Codable { + let id: Int + let name: String +} + +let encoder = DictionaryEncoder() +let user = User(id: 123, name: "tattn") +let dictionary: [String: Any] = try! encoder.encode(user) // => {"id": 123, "name": "tattn"} +``` + +```swift +let decoder = DictionaryDecoder() +let user = try decoder.decode(User.self, from: dictionary) +``` + +## URLQueryItemsEncoder / URLQueryItemsDecoder + +```swift +struct Parameter: Codable { + let query: String + let offset: Int + let limit: Int +} +let parameter = Parameter(query: "ねこ", offset: 10, limit: 20) +let encoder = URLQueryItemsEncoder() +let params: [URLQueryItem] = try! encoder.encode(parameter) + +var components = URLComponents(string: "https://example.com") +components?.queryItems = params +components?.url // https://example.com?query=%E3%81%AD%E3%81%93&offset=10&limit=20 +``` + +```swift +let decoder = URLQueryItemsDecoder() +let parameter = try decoder.decode(Parameter.self, from: params) +``` + +## RuleBasedCodingKey + +```swift +struct User: Codable { + let userId: String + let name: String + + enum CodingKeys: String, RuleBasedCodingKey { + case userId + case name + + func codingKeyRule(key: String) -> String { + return key.uppercased() // custom rule + } + } +} + +let json = """ +{"USERID": "abc", "NAME": "tattn"} +""".data(using: .utf8)! + +let user = try! JSONDecoder().decode(User.self, from: json) // => User(userId: "abc", name: "tattn") +``` + +### SnakeCaseCodingKey + +```swift +struct User: Codable { + let userId: String + let name: String + + enum CodingKeys: String, SnakeCaseCodingKey { + case userId + case name + } +} + +let json = """ +{"user_id": "abc", "name": "tattn"} +""".data(using: .utf8)! + +let user = try! JSONDecoder().decode(User.self, from: json) // ok +``` + +### UpperCamelCaseCodingKey + +```swift +struct User: Codable { + let userId: String + let name: String + + enum CodingKeys: String, UpperCamelCaseCodingKey { + case userId + case name + } +} + +let json = """ +{"UserId": "abc", "Name": "tattn"} +""".data(using: .utf8)! + +let user = try! JSONDecoder().decode(User.self, from: json) // ok +``` + +## Failable + +```swift +let json = """ +[ + {"name": "Taro", "age": 20}, + {"name": "Hanako"} +] +""".data(using: .utf8)! // Hanako has no "age" + +struct User: Codable { + let name: String + let age: Int +} + +let users = try! JSONDecoder().decode([Failable].self, + from: json) + +// success +XCTAssertEqual(users[0].value?.name, "Taro") +XCTAssertEqual(users[0].value?.age, 20) +XCTAssertNil(users[1].value) +``` + +## StringTo + +```swift +let json = """ +{ + "int": "100", + "articleId": "abc" +} +""".data(using: .utf8)! + +struct Root: Codable { + let int: StringTo + let articleId: StringTo + + struct ArticleId: LosslessStringConvertible, Codable { + var description: String + + init?(_ description: String) { + self.description = description + } + } +} + +let root = try! JSONDecoder().decode(Root.self, from: json) + +// success +XCTAssertEqual(root.int.value, 100) +XCTAssertEqual(root.articleId.value.description, "abc") +``` + +# Contributing + +1. Fork it! +2. Create your feature branch: `git checkout -b my-new-feature` +3. Commit your changes: `git commit -am 'Add some feature'` +4. Push to the branch: `git push origin my-new-feature` +5. Submit a pull request :D + +# License + +MoreCodable is released under the MIT license. See LICENSE for details. diff --git a/Sources/AnyCodingKey.swift b/Sources/AnyCodingKey.swift new file mode 100644 index 0000000..eebe763 --- /dev/null +++ b/Sources/AnyCodingKey.swift @@ -0,0 +1,31 @@ +// +// AnyCodingKey.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180211. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +struct AnyCodingKey : CodingKey { + public var stringValue: String + public var intValue: Int? + + public init?(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + public init?(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + init(index: Int) { + self.stringValue = "Index \(index)" + self.intValue = index + } + + static let `super` = AnyCodingKey(stringValue: "super")! +} diff --git a/Sources/DictionaryDecoder.swift b/Sources/DictionaryDecoder.swift new file mode 100644 index 0000000..50375ce --- /dev/null +++ b/Sources/DictionaryDecoder.swift @@ -0,0 +1,280 @@ +// +// DictionaryDecoder.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180211. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +open class DictionaryDecoder: Decoder { + open var codingPath: [CodingKey] + open var userInfo: [CodingUserInfoKey: Any] = [:] + private var storage = Storage() + + public init() { + codingPath = [] + } + + public init(container: Any, codingPath: [CodingKey] = []) { + storage.push(container: container) + self.codingPath = codingPath + } + + open func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + let container = try lastContainer(forType: [String: Any].self) + return KeyedDecodingContainer(KeyedContainer(decoder: self, codingPath: [], container: container)) + } + + open func unkeyedContainer() throws -> UnkeyedDecodingContainer { + let container = try lastContainer(forType: [Any].self) + return UnkeyedContanier(decoder: self, container: container) + } + + open func singleValueContainer() throws -> SingleValueDecodingContainer { + return SingleValueContanier(decoder: self) + } + + private func unbox(_ value: Any, as type: T.Type) throws -> T { + return try unbox(value, as: type, codingPath: codingPath) + } + + private func unbox(_ value: Any, as type: T.Type, codingPath: [CodingKey]) throws -> T { + let description = "Expected to decode \(type) but found \(Swift.type(of: value)) instead." + let error = DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: description)) + do { + return try castOrThrow(T.self, value, error: error) + } catch { + storage.push(container: value) + defer { _ = storage.popContainer() } + return try T(from: self) + } + } + + private func lastContainer(forType type: T.Type) throws -> T { + guard let value = storage.last else { + let description = "Expected \(type) but found nil value instead." + let error = DecodingError.Context(codingPath: codingPath, debugDescription: description) + throw DecodingError.valueNotFound(type, error) + } + return try unbox(value, as: T.self) + } + + private func notFound(key: CodingKey) -> DecodingError { + let error = DecodingError.Context(codingPath: codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").") + return DecodingError.keyNotFound(key, error) + } +} + +extension DictionaryDecoder { + open func decode(_ type: T.Type, from container: Any) throws -> T { + storage.push(container: container) + return try unbox(container, as: T.self) + } +} + +extension DictionaryDecoder { + private class KeyedContainer: KeyedDecodingContainerProtocol { + private var decoder: DictionaryDecoder + private(set) var codingPath: [CodingKey] + private var container: [String: Any] + + init(decoder: DictionaryDecoder, codingPath: [CodingKey], container: [String: Any]) { + self.decoder = decoder + self.codingPath = codingPath + self.container = container + } + + var allKeys: [Key] { return container.keys.flatMap { Key(stringValue: $0) } } + func contains(_ key: Key) -> Bool { return container[key.stringValue] != nil } + + private func find(forKey key: CodingKey) throws -> Any { + return try container.tryValue(forKey: key.stringValue, error: decoder.notFound(key: key)) + } + + func _decode(_ type: T.Type, forKey key: Key) throws -> T { + let value = try find(forKey: key) + decoder.codingPath.append(key) + defer { decoder.codingPath.removeLast() } + return try decoder.unbox(value, as: T.self) + } + + func decodeNil(forKey key: Key) throws -> Bool { throw decoder.notFound(key: key) } + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { return try _decode(type, forKey: key) } + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { return try _decode(type, forKey: key) } + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { return try _decode(type, forKey: key) } + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { return try _decode(type, forKey: key) } + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { return try _decode(type, forKey: key) } + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { return try _decode(type, forKey: key) } + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { return try _decode(type, forKey: key) } + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { return try _decode(type, forKey: key) } + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { return try _decode(type, forKey: key) } + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { return try _decode(type, forKey: key) } + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { return try _decode(type, forKey: key) } + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { return try _decode(type, forKey: key) } + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { return try _decode(type, forKey: key) } + func decode(_ type: String.Type, forKey key: Key) throws -> String { return try _decode(type, forKey: key) } + func decode(_ type: T.Type, forKey key: Key) throws -> T { return try _decode(type, forKey: key) } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + decoder.codingPath.append(key) + defer { decoder.codingPath.removeLast() } + + let value = try find(forKey: key) + let dictionary = try decoder.unbox(value, as: [String: Any].self) + return KeyedDecodingContainer(KeyedContainer(decoder: decoder, codingPath: [], container: dictionary)) + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + decoder.codingPath.append(key) + defer { decoder.codingPath.removeLast() } + + let value = try find(forKey: key) + let array = try decoder.unbox(value, as: [Any].self) + return UnkeyedContanier(decoder: decoder, container: array) + } + + func _superDecoder(forKey key: CodingKey = AnyCodingKey.super) throws -> Decoder { + decoder.codingPath.append(key) + defer { decoder.codingPath.removeLast() } + + let value = try find(forKey: key) + return DictionaryDecoder(container: value, codingPath: decoder.codingPath) + } + + func superDecoder() throws -> Decoder { + return try _superDecoder() + } + + func superDecoder(forKey key: Key) throws -> Decoder { + return try _superDecoder(forKey: key) + } + } + + private class UnkeyedContanier: UnkeyedDecodingContainer { + private var decoder: DictionaryDecoder + private(set) var codingPath: [CodingKey] + private var container: [Any] + + var count: Int? { return container.count } + var isAtEnd: Bool { return currentIndex >= count! } + + private(set) var currentIndex: Int + private var currentCodingPath: [CodingKey] { return decoder.codingPath + [AnyCodingKey(index: currentIndex)] } + + init(decoder: DictionaryDecoder, container: [Any]) { + self.decoder = decoder + self.codingPath = decoder.codingPath + self.container = container + currentIndex = 0 + } + + private func checkIndex(_ type: T.Type) throws { + if isAtEnd { + let error = DecodingError.Context(codingPath: currentCodingPath, debugDescription: "container is at end.") + throw DecodingError.valueNotFound(T.self, error) + } + } + + func _decode(_ type: T.Type) throws -> T { + try checkIndex(type) + + decoder.codingPath.append(AnyCodingKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let value = try decoder.unbox(container[currentIndex], as: T.self) + + defer { currentIndex += 1 } + return try decoder.unbox(value, as: T.self) + } + + func decodeNil() throws -> Bool { + try checkIndex(Any?.self) + return false + } + func decode(_ type: Bool.Type) throws -> Bool { return try _decode(type) } + func decode(_ type: Int.Type) throws -> Int { return try _decode(type) } + func decode(_ type: Int8.Type) throws -> Int8 { return try _decode(type) } + func decode(_ type: Int16.Type) throws -> Int16 { return try _decode(type) } + func decode(_ type: Int32.Type) throws -> Int32 { return try _decode(type) } + func decode(_ type: Int64.Type) throws -> Int64 { return try _decode(type) } + func decode(_ type: UInt.Type) throws -> UInt { return try _decode(type) } + func decode(_ type: UInt8.Type) throws -> UInt8 { return try _decode(type) } + func decode(_ type: UInt16.Type) throws -> UInt16 { return try _decode(type) } + func decode(_ type: UInt32.Type) throws -> UInt32 { return try _decode(type) } + func decode(_ type: UInt64.Type) throws -> UInt64 { return try _decode(type) } + func decode(_ type: Float.Type) throws -> Float { return try _decode(type) } + func decode(_ type: Double.Type) throws -> Double { return try _decode(type) } + func decode(_ type: String.Type) throws -> String { return try _decode(type) } + func decode(_ type: T.Type) throws -> T { return try _decode(type) } + + func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer { + decoder.codingPath.append(AnyCodingKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + try checkIndex(UnkeyedContanier.self) + + let value = container[currentIndex] + let dictionary = try castOrThrow([String: Any].self, value) + + currentIndex += 1 + return KeyedDecodingContainer(KeyedContainer(decoder: decoder, codingPath: [], container: dictionary)) + } + + func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + decoder.codingPath.append(AnyCodingKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + try checkIndex(UnkeyedContanier.self) + + let value = container[currentIndex] + let array = try castOrThrow([Any].self, value) + + currentIndex += 1 + return UnkeyedContanier(decoder: decoder, container: array) + } + + func superDecoder() throws -> Decoder { + decoder.codingPath.append(AnyCodingKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + try checkIndex(UnkeyedContanier.self) + + let value = container[currentIndex] + currentIndex += 1 + return DictionaryDecoder(container: value, codingPath: decoder.codingPath) + } + } + + private class SingleValueContanier: SingleValueDecodingContainer { + private var decoder: DictionaryDecoder + private(set) var codingPath: [CodingKey] + + init(decoder: DictionaryDecoder) { + self.decoder = decoder + self.codingPath = decoder.codingPath + } + + func _decode(_ type: T.Type) throws -> T { + return try decoder.lastContainer(forType: type) + } + + func decodeNil() -> Bool { return decoder.storage.last == nil } + func decode(_ type: Bool.Type) throws -> Bool { return try _decode(type) } + func decode(_ type: Int.Type) throws -> Int { return try _decode(type) } + func decode(_ type: Int8.Type) throws -> Int8 { return try _decode(type) } + func decode(_ type: Int16.Type) throws -> Int16 { return try _decode(type) } + func decode(_ type: Int32.Type) throws -> Int32 { return try _decode(type) } + func decode(_ type: Int64.Type) throws -> Int64 { return try _decode(type) } + func decode(_ type: UInt.Type) throws -> UInt { return try _decode(type) } + func decode(_ type: UInt8.Type) throws -> UInt8 { return try _decode(type) } + func decode(_ type: UInt16.Type) throws -> UInt16 { return try _decode(type) } + func decode(_ type: UInt32.Type) throws -> UInt32 { return try _decode(type) } + func decode(_ type: UInt64.Type) throws -> UInt64 { return try _decode(type) } + func decode(_ type: Float.Type) throws -> Float { return try _decode(type) } + func decode(_ type: Double.Type) throws -> Double { return try _decode(type) } + func decode(_ type: String.Type) throws -> String { return try _decode(type) } + func decode(_ type: T.Type) throws -> T { return try _decode(type) } + } +} diff --git a/Sources/DictionaryEncoder.swift b/Sources/DictionaryEncoder.swift new file mode 100644 index 0000000..2195087 --- /dev/null +++ b/Sources/DictionaryEncoder.swift @@ -0,0 +1,184 @@ +// +// DictionaryEncoder.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180211. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +open class DictionaryEncoder: Encoder { + open var codingPath: [CodingKey] = [] + open var userInfo: [CodingUserInfoKey: Any] = [:] + private var storage = Storage() + + public init() {} + + open func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { + return KeyedEncodingContainer(KeyedContainer(encoder: self, codingPath: codingPath)) + } + + open func unkeyedContainer() -> UnkeyedEncodingContainer { + return UnkeyedContanier(encoder: self, codingPath: codingPath) + } + + open func singleValueContainer() -> SingleValueEncodingContainer { + return UnkeyedContanier(encoder: self, codingPath: codingPath) + } + + private func box(_ value: T) throws -> Any { + try value.encode(to: self) + return storage.popContainer() + } +} + +extension DictionaryEncoder { + open func encode(_ value: T) throws -> [String: Any] { + do { + return try castOrThrow([String: Any].self, try box(value)) + } catch (let error) { + throw EncodingError.invalidValue(value, + EncodingError.Context(codingPath: [], + debugDescription: "Top-evel \(T.self) did not encode any values.", + underlyingError: error) + ) + } + } +} + +extension DictionaryEncoder { + private class KeyedContainer: KeyedEncodingContainerProtocol { + private var encoder: DictionaryEncoder + private(set) var codingPath: [CodingKey] + private var storage: Storage + + init(encoder: DictionaryEncoder, codingPath: [CodingKey]) { + self.encoder = encoder + self.codingPath = codingPath + self.storage = encoder.storage + + storage.push(container: [:] as [String: Any]) + } + + deinit { + guard let dictionary = storage.popContainer() as? [String: Any] else { + assertionFailure(); return + } + storage.push(container: dictionary) + } + + private func set(_ value: Any, forKey key: String) { + guard var dictionary = storage.popContainer() as? [String: Any] else { assertionFailure(); return } + dictionary[key] = value + storage.push(container: dictionary) + } + + func encodeNil(forKey key: Key) throws {} + func encode(_ value: Bool, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Int, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Int8, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Int16, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Int32, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Int64, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: UInt, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: UInt8, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: UInt16, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: UInt32, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: UInt64, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Float, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Double, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: String, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: T, forKey key: Key) throws { + encoder.codingPath.append(key) + defer { encoder.codingPath.removeLast() } + set(try encoder.box(value), forKey: key.stringValue) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { + codingPath.append(key) + defer { codingPath.removeLast() } + return KeyedEncodingContainer(KeyedContainer(encoder: encoder, codingPath: codingPath)) + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + codingPath.append(key) + defer { codingPath.removeLast() } + return UnkeyedContanier(encoder: encoder, codingPath: codingPath) + } + + func superEncoder() -> Encoder { + return encoder + } + + func superEncoder(forKey key: Key) -> Encoder { + return encoder + } + } + + private class UnkeyedContanier: UnkeyedEncodingContainer, SingleValueEncodingContainer { + var encoder: DictionaryEncoder + private(set) var codingPath: [CodingKey] + private var storage: Storage + var count: Int { return (storage.last as? [Any])?.count ?? 0 } + + init(encoder: DictionaryEncoder, codingPath: [CodingKey]) { + self.encoder = encoder + self.codingPath = codingPath + self.storage = encoder.storage + + storage.push(container: [] as [Any]) + } + + deinit { + guard let array = storage.popContainer() as? [Any] else { + assertionFailure(); return + } + storage.push(container: array) + } + + private func push(_ value: Any) { + guard var array = storage.popContainer() as? [Any] else { assertionFailure(); return } + array.append(value) + storage.push(container: array) + } + + func encodeNil() throws {} + func encode(_ value: Bool) throws {} + func encode(_ value: Int) throws { push(value) } + func encode(_ value: Int8) throws { push(value) } + func encode(_ value: Int16) throws { push(value) } + func encode(_ value: Int32) throws { push(value) } + func encode(_ value: Int64) throws { push(value) } + func encode(_ value: UInt) throws { push(value) } + func encode(_ value: UInt8) throws { push(value) } + func encode(_ value: UInt16) throws { push(value) } + func encode(_ value: UInt32) throws { push(value) } + func encode(_ value: UInt64) throws { push(value) } + func encode(_ value: Float) throws { push(value) } + func encode(_ value: Double) throws { push(value) } + func encode(_ value: String) throws { push(value) } + func encode(_ value: T) throws { + encoder.codingPath.append(AnyCodingKey(index: count)) + defer { encoder.codingPath.removeLast() } + push(try encoder.box(value)) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { + codingPath.append(AnyCodingKey(index: count)) + defer { codingPath.removeLast() } + return KeyedEncodingContainer(KeyedContainer(encoder: encoder, codingPath: codingPath)) + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + codingPath.append(AnyCodingKey(index: count)) + defer { codingPath.removeLast() } + return UnkeyedContanier(encoder: encoder, codingPath: codingPath) + + } + + func superEncoder() -> Encoder { + return encoder + } + } +} diff --git a/Sources/Failable.swift b/Sources/Failable.swift new file mode 100644 index 0000000..0191186 --- /dev/null +++ b/Sources/Failable.swift @@ -0,0 +1,31 @@ +// +// Failable.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180219. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +public struct Failable: Codable { + public let value: Wrapped? + + public init(from decoder: Decoder) throws { + do { + let container = try decoder.singleValueContainer() + value = try container.decode(Wrapped.self) + } catch { + value = nil + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + if let value = self.value { + try container.encode(value) + } else { + try container.encodeNil() + } + } +} diff --git a/Sources/Info.plist b/Sources/Info.plist new file mode 100644 index 0000000..1007fd9 --- /dev/null +++ b/Sources/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Sources/InternalFunction.swift b/Sources/InternalFunction.swift new file mode 100644 index 0000000..4f0d5fb --- /dev/null +++ b/Sources/InternalFunction.swift @@ -0,0 +1,40 @@ +// +// InternalFunction.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180211. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +public enum MoreCodableError: Error { + case cast + case unwrapped + case tryValue +} + +func castOrThrow(_ resultType: T.Type, _ object: Any, error: Error = MoreCodableError.cast) throws -> T { + guard let returnValue = object as? T else { + throw error + } + + return returnValue +} + +extension Optional { + func unwrapOrThrow(error: Error = MoreCodableError.unwrapped) throws -> Wrapped { + guard let unwrapped = self else { + throw error + } + + return unwrapped + } +} + +extension Dictionary { + func tryValue(forKey key: Key, error: Error = MoreCodableError.tryValue) throws -> Value { + guard let value = self[key] else { throw error } + return value + } +} diff --git a/Sources/MoreCodable.h b/Sources/MoreCodable.h new file mode 100644 index 0000000..1900b2c --- /dev/null +++ b/Sources/MoreCodable.h @@ -0,0 +1,19 @@ +// +// MoreCodable.h +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180211. +// Copyright © 2018年 tattn. All rights reserved. +// + +#import + +//! Project version number for MoreCodable. +FOUNDATION_EXPORT double MoreCodableVersionNumber; + +//! Project version string for MoreCodable. +FOUNDATION_EXPORT const unsigned char MoreCodableVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Sources/RuleBasedCodingKey.swift b/Sources/RuleBasedCodingKey.swift new file mode 100644 index 0000000..c6a85f4 --- /dev/null +++ b/Sources/RuleBasedCodingKey.swift @@ -0,0 +1,38 @@ +// +// RuleBasedCodingKey.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180221. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +public protocol RuleBasedCodingKey: CodingKey { + func codingKeyRule(key: String) -> String +} + +public extension RuleBasedCodingKey where Self: RawRepresentable, Self.RawValue == String { + var stringValue: String { + return codingKeyRule(key: rawValue) + } +} + +public protocol SnakeCaseCodingKey: RuleBasedCodingKey {} + +public extension SnakeCaseCodingKey { + func codingKeyRule(key: String) -> String { + return key.replacingOccurrences(of: "([A-Z])", + with: "_$1", + options: .regularExpression, + range: key.startIndex.. String { + return key.prefix(1).uppercased() + key.dropFirst() + } +} diff --git a/Sources/Storage.swift b/Sources/Storage.swift new file mode 100644 index 0000000..e1f0e78 --- /dev/null +++ b/Sources/Storage.swift @@ -0,0 +1,31 @@ +// +// Storage.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180211. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +final class Storage { + private(set) var containers: [Any] = [] + + var count: Int { + return containers.count + } + + var last: Any? { + return containers.last + } + + func push(container: Any) { + containers.append(container) + } + + @discardableResult + func popContainer() -> Any { + precondition(containers.count > 0, "Empty container stack.") + return containers.popLast()! + } +} diff --git a/Sources/StringTo.swift b/Sources/StringTo.swift new file mode 100644 index 0000000..b4eff3e --- /dev/null +++ b/Sources/StringTo.swift @@ -0,0 +1,32 @@ +// +// StringTo.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180219. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +public struct StringTo: Codable { + public let value: T + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let stringValue = try container.decode(String.self) + + guard let value = T(stringValue) else { + throw DecodingError.dataCorrupted( + .init(codingPath: decoder.codingPath, + debugDescription: "The string cannot cast to \(T.self).") + ) + } + + self.value = value + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(value.description) + } +} diff --git a/Sources/URLQueryItem+.swift b/Sources/URLQueryItem+.swift new file mode 100644 index 0000000..6596ac5 --- /dev/null +++ b/Sources/URLQueryItem+.swift @@ -0,0 +1,29 @@ +// +// URLQueryItem+.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180212. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +extension URLQueryItem: Codable { + enum CodingKeys: String, CodingKey { + case name + case value + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let name = try container.decode(String.self, forKey: .name) + let value = try container.decode(String.self, forKey: .value) + self.init(name: name, value: value) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(value, forKey: .value) + } +} diff --git a/Sources/URLQueryItemsDecoder.swift b/Sources/URLQueryItemsDecoder.swift new file mode 100644 index 0000000..c865d96 --- /dev/null +++ b/Sources/URLQueryItemsDecoder.swift @@ -0,0 +1,260 @@ +// +// URLQueryItemsDecoder.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180212. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +open class URLQueryItemsDecoder: Decoder { + open var codingPath: [CodingKey] + open var userInfo: [CodingUserInfoKey: Any] = [:] + private var storage = Storage() + + public init() { + codingPath = [] + } + + public init(container: Any, codingPath: [CodingKey] = []) { + storage.push(container: container) + self.codingPath = codingPath + } + + open func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + let container = try lastContainer(forType: [URLQueryItem].self) + return KeyedDecodingContainer(KeyedContainer(decoder: self, codingPath: [], container: container)) + } + + open func unkeyedContainer() throws -> UnkeyedDecodingContainer { + let container = try lastContainer(forType: [URLQueryItem].self) + return UnkeyedContanier(decoder: self, container: container) + } + + open func singleValueContainer() throws -> SingleValueDecodingContainer { + return SingleValueContanier(decoder: self) + } + + private func unbox(_ value: Any, as type: T.Type) throws -> T { + return try unbox(value, as: type, codingPath: codingPath) + } + + private func unbox(_ value: Any, as type: T.Type, codingPath: [CodingKey]) throws -> T { + let description = "Expected to decode \(type) but found \(Swift.type(of: value)) instead." + let error = DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: description)) + do { + return try castOrThrow(T.self, value, error: error) + } catch { + storage.push(container: value) + defer { _ = storage.popContainer() } + return try T(from: self) + } + } + + private func lastContainer(forType type: T.Type) throws -> T { + guard let value = storage.last else { + let description = "Expected \(type) but found nil value instead." + let error = DecodingError.Context(codingPath: codingPath, debugDescription: description) + throw DecodingError.valueNotFound(type, error) + } + return try unbox(value, as: T.self) + } + + private func notFound(key: CodingKey) -> DecodingError { + let error = DecodingError.Context(codingPath: codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").") + return DecodingError.keyNotFound(key, error) + } +} + +extension URLQueryItemsDecoder { + open func decode(_ type: T.Type, from container: [URLQueryItem]) throws -> T { + storage.push(container: container) + return try T(from: self) + } +} + +extension URLQueryItemsDecoder { + private class KeyedContainer: KeyedDecodingContainerProtocol { + private var decoder: URLQueryItemsDecoder + private(set) var codingPath: [CodingKey] + private var container: [URLQueryItem] + + init(decoder: URLQueryItemsDecoder, codingPath: [CodingKey], container: [URLQueryItem]) { + self.decoder = decoder + self.codingPath = codingPath + self.container = container + } + + var allKeys: [Key] { return container.flatMap { Key(stringValue: $0.name) } } + func contains(_ key: Key) -> Bool { return container.contains(where: { $0.name == key.stringValue }) } + + private func find(forKey key: CodingKey) throws -> URLQueryItem { + return try container.first(where: { $0.name == key.stringValue }) + .unwrapOrThrow(error: decoder.notFound(key: key)) + } + + func _decode(_ type: T.Type, forKey key: Key) throws -> T { + let value = try find(forKey: key) + decoder.codingPath.append(key) + defer { decoder.codingPath.removeLast() } + return try T.init(try value.value.unwrapOrThrow()).unwrapOrThrow() + } + + func _decode(_ type: T.Type, forKey key: Key) throws -> T { + let value = try find(forKey: key) + decoder.codingPath.append(key) + defer { decoder.codingPath.removeLast() } + return try decoder.unbox(value, as: T.self) + } + + func decodeNil(forKey key: Key) throws -> Bool { throw decoder.notFound(key: key) } + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { return try _decode(type, forKey: key) } + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { return try _decode(type, forKey: key) } + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { return try _decode(type, forKey: key) } + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { return try _decode(type, forKey: key) } + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { return try _decode(type, forKey: key) } + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { return try _decode(type, forKey: key) } + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { return try _decode(type, forKey: key) } + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { return try _decode(type, forKey: key) } + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { return try _decode(type, forKey: key) } + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { return try _decode(type, forKey: key) } + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { return try _decode(type, forKey: key) } + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { return try _decode(type, forKey: key) } + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { return try _decode(type, forKey: key) } + func decode(_ type: String.Type, forKey key: Key) throws -> String { return try _decode(type, forKey: key) } + func decode(_ type: T.Type, forKey key: Key) throws -> T { return try _decode(type, forKey: key) } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + fatalError("unreachable") + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + fatalError("unreachable") + } + + func _superDecoder(forKey key: CodingKey = AnyCodingKey.super) throws -> Decoder { + decoder.codingPath.append(key) + defer { decoder.codingPath.removeLast() } + + let value = try find(forKey: key) + return URLQueryItemsDecoder(container: value, codingPath: decoder.codingPath) + } + + func superDecoder() throws -> Decoder { + return try _superDecoder() + } + + func superDecoder(forKey key: Key) throws -> Decoder { + return try _superDecoder(forKey: key) + } + } + + private class UnkeyedContanier: UnkeyedDecodingContainer { + private var decoder: URLQueryItemsDecoder + private(set) var codingPath: [CodingKey] + private var container: [URLQueryItem] + + var count: Int? { return container.count } + var isAtEnd: Bool { return currentIndex >= count! } + + private(set) var currentIndex: Int + private var currentCodingPath: [CodingKey] { return decoder.codingPath + [AnyCodingKey(index: currentIndex)] } + + init(decoder: URLQueryItemsDecoder, container: [URLQueryItem]) { + self.decoder = decoder + self.codingPath = decoder.codingPath + self.container = container + currentIndex = 0 + } + + private func checkIndex(_ type: T.Type) throws { + if isAtEnd { + let error = DecodingError.Context(codingPath: currentCodingPath, debugDescription: "container is at end.") + throw DecodingError.valueNotFound(T.self, error) + } + } + + func _decode(_ type: T.Type) throws -> T { + try checkIndex(type) + + decoder.codingPath.append(AnyCodingKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let value = try decoder.unbox(try container[currentIndex].value.unwrapOrThrow(), as: T.self) + + defer { currentIndex += 1 } + return try decoder.unbox(value, as: T.self) + } + + func decodeNil() throws -> Bool { + try checkIndex(Any?.self) + return false + } + func decode(_ type: Bool.Type) throws -> Bool { return try _decode(type) } + func decode(_ type: Int.Type) throws -> Int { return try _decode(type) } + func decode(_ type: Int8.Type) throws -> Int8 { return try _decode(type) } + func decode(_ type: Int16.Type) throws -> Int16 { return try _decode(type) } + func decode(_ type: Int32.Type) throws -> Int32 { return try _decode(type) } + func decode(_ type: Int64.Type) throws -> Int64 { return try _decode(type) } + func decode(_ type: UInt.Type) throws -> UInt { return try _decode(type) } + func decode(_ type: UInt8.Type) throws -> UInt8 { return try _decode(type) } + func decode(_ type: UInt16.Type) throws -> UInt16 { return try _decode(type) } + func decode(_ type: UInt32.Type) throws -> UInt32 { return try _decode(type) } + func decode(_ type: UInt64.Type) throws -> UInt64 { return try _decode(type) } + func decode(_ type: Float.Type) throws -> Float { return try _decode(type) } + func decode(_ type: Double.Type) throws -> Double { return try _decode(type) } + func decode(_ type: String.Type) throws -> String { return try _decode(type) } + func decode(_ type: T.Type) throws -> T { return try _decode(type) } + + func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer { + fatalError("unreachable") + } + + func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + fatalError("unreachable") + } + + func superDecoder() throws -> Decoder { + decoder.codingPath.append(AnyCodingKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + try checkIndex(UnkeyedContanier.self) + + let value = container[currentIndex] + currentIndex += 1 + return URLQueryItemsDecoder(container: value, codingPath: decoder.codingPath) + } + } + + private class SingleValueContanier: SingleValueDecodingContainer { + private var decoder: URLQueryItemsDecoder + private(set) var codingPath: [CodingKey] + + init(decoder: URLQueryItemsDecoder) { + self.decoder = decoder + self.codingPath = decoder.codingPath + } + + func _decode(_ type: T.Type) throws -> T { + return try decoder.lastContainer(forType: type) + } + + func decodeNil() -> Bool { return decoder.storage.last == nil } + func decode(_ type: Bool.Type) throws -> Bool { return try _decode(type) } + func decode(_ type: Int.Type) throws -> Int { return try _decode(type) } + func decode(_ type: Int8.Type) throws -> Int8 { return try _decode(type) } + func decode(_ type: Int16.Type) throws -> Int16 { return try _decode(type) } + func decode(_ type: Int32.Type) throws -> Int32 { return try _decode(type) } + func decode(_ type: Int64.Type) throws -> Int64 { return try _decode(type) } + func decode(_ type: UInt.Type) throws -> UInt { return try _decode(type) } + func decode(_ type: UInt8.Type) throws -> UInt8 { return try _decode(type) } + func decode(_ type: UInt16.Type) throws -> UInt16 { return try _decode(type) } + func decode(_ type: UInt32.Type) throws -> UInt32 { return try _decode(type) } + func decode(_ type: UInt64.Type) throws -> UInt64 { return try _decode(type) } + func decode(_ type: Float.Type) throws -> Float { return try _decode(type) } + func decode(_ type: Double.Type) throws -> Double { return try _decode(type) } + func decode(_ type: String.Type) throws -> String { return try _decode(type) } + func decode(_ type: T.Type) throws -> T { return try _decode(type) } + } +} diff --git a/Sources/URLQueryItemsEncoder.swift b/Sources/URLQueryItemsEncoder.swift new file mode 100644 index 0000000..fca1fd0 --- /dev/null +++ b/Sources/URLQueryItemsEncoder.swift @@ -0,0 +1,170 @@ +// +// URLQueryItemsEncoder.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180212. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +open class URLQueryItemsEncoder: Encoder { + open var codingPath: [CodingKey] = [] + open var userInfo: [CodingUserInfoKey: Any] = [:] + private var storage = Storage() + + public init() {} + + open func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { + return KeyedEncodingContainer(KeyedContainer(encoder: self, codingPath: codingPath)) + } + + open func unkeyedContainer() -> UnkeyedEncodingContainer { + return UnkeyedContanier(encoder: self, codingPath: codingPath) + } + + open func singleValueContainer() -> SingleValueEncodingContainer { + return UnkeyedContanier(encoder: self, codingPath: codingPath) + } + + private func box(_ value: T) throws -> Any { + try value.encode(to: self) + return storage.popContainer() + } +} + +extension URLQueryItemsEncoder { + open func encode(_ value: T) throws -> [URLQueryItem] { + do { + return try castOrThrow([URLQueryItem].self, try box(value)) + } catch (let error) { + throw EncodingError.invalidValue(value, + EncodingError.Context(codingPath: [], + debugDescription: "Top-evel \(T.self) did not encode any values.", + underlyingError: error) + ) + } + } +} + +extension URLQueryItemsEncoder { + private class KeyedContainer: KeyedEncodingContainerProtocol { + private var encoder: URLQueryItemsEncoder + private(set) var codingPath: [CodingKey] + private var storage: Storage + + init(encoder: URLQueryItemsEncoder, codingPath: [CodingKey]) { + self.encoder = encoder + self.codingPath = codingPath + self.storage = encoder.storage + + storage.push(container: [URLQueryItem]()) + } + + private func set(_ value: Any, forKey key: String) { + guard var queryItems = storage.popContainer() as? [URLQueryItem] else { assertionFailure(); return } + queryItems.append(URLQueryItem(name: key, value: String(describing: value))) + storage.push(container: queryItems) + } + + func encodeNil(forKey key: Key) throws {} + func encode(_ value: Bool, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Int, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Int8, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Int16, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Int32, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Int64, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: UInt, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: UInt8, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: UInt16, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: UInt32, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: UInt64, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Float, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: Double, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: String, forKey key: Key) throws { set(value, forKey: key.stringValue) } + func encode(_ value: T, forKey key: Key) throws { + encoder.codingPath.append(key) + defer { encoder.codingPath.removeLast() } + set(try encoder.box(value), forKey: key.stringValue) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { + codingPath.append(key) + defer { codingPath.removeLast() } + return KeyedEncodingContainer(KeyedContainer(encoder: encoder, codingPath: codingPath)) + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + codingPath.append(key) + defer { codingPath.removeLast() } + return UnkeyedContanier(encoder: encoder, codingPath: codingPath) + } + + func superEncoder() -> Encoder { + return encoder + } + + func superEncoder(forKey key: Key) -> Encoder { + return encoder + } + } + + private class UnkeyedContanier: UnkeyedEncodingContainer, SingleValueEncodingContainer { + var encoder: URLQueryItemsEncoder + private(set) var codingPath: [CodingKey] + private var storage: Storage + var count: Int { return (storage.last as? [Any])?.count ?? 0 } + + init(encoder: URLQueryItemsEncoder, codingPath: [CodingKey]) { + self.encoder = encoder + self.codingPath = codingPath + self.storage = encoder.storage + + storage.push(container: [] as [String]) + } + + private func push(_ value: Any) { + guard var array = storage.popContainer() as? [String] else { assertionFailure(); return } + array.append(String(describing: value)) + storage.push(container: array) + } + + func encodeNil() throws {} + func encode(_ value: Bool) throws {} + func encode(_ value: Int) throws { push(value) } + func encode(_ value: Int8) throws { push(value) } + func encode(_ value: Int16) throws { push(value) } + func encode(_ value: Int32) throws { push(value) } + func encode(_ value: Int64) throws { push(value) } + func encode(_ value: UInt) throws { push(value) } + func encode(_ value: UInt8) throws { push(value) } + func encode(_ value: UInt16) throws { push(value) } + func encode(_ value: UInt32) throws { push(value) } + func encode(_ value: UInt64) throws { push(value) } + func encode(_ value: Float) throws { push(value) } + func encode(_ value: Double) throws { push(value) } + func encode(_ value: String) throws { push(value) } + func encode(_ value: T) throws { + encoder.codingPath.append(AnyCodingKey(index: count)) + defer { encoder.codingPath.removeLast() } + push(try encoder.box(value)) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { + codingPath.append(AnyCodingKey(index: count)) + defer { codingPath.removeLast() } + return KeyedEncodingContainer(KeyedContainer(encoder: encoder, codingPath: codingPath)) + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + codingPath.append(AnyCodingKey(index: count)) + defer { codingPath.removeLast() } + return UnkeyedContanier(encoder: encoder, codingPath: codingPath) + + } + + func superEncoder() -> Encoder { + return encoder + } + } +} diff --git a/Tests/DictionaryDecoderTests.swift b/Tests/DictionaryDecoderTests.swift new file mode 100644 index 0000000..83a6412 --- /dev/null +++ b/Tests/DictionaryDecoderTests.swift @@ -0,0 +1,40 @@ +// +// DictionaryDecoderTests.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180211. +// Copyright © 2018年 tattn. All rights reserved. +// + +import XCTest +import MoreCodable + +class DictionaryDecoderTests: XCTestCase { + + var decoder = DictionaryDecoder() + + override func setUp() { + super.setUp() + decoder = DictionaryDecoder() + } + + override func tearDown() { + super.tearDown() + } + + func testDecodeSimpleModel() throws { + struct User: Codable { + let name: String + let age: Int + } + + let dictionary: [String: Any] = [ + "name": "Tatsuya Tanaka", + "age": 24 + ] + let user = try decoder.decode(User.self, from: dictionary) + XCTAssertEqual(user.name, dictionary["name"] as? String) + XCTAssertEqual(user.age, dictionary["age"] as? Int) + } + +} diff --git a/Tests/DictionaryEncoderTests.swift b/Tests/DictionaryEncoderTests.swift new file mode 100644 index 0000000..1dd5a46 --- /dev/null +++ b/Tests/DictionaryEncoderTests.swift @@ -0,0 +1,80 @@ +// +// DictionaryEncoderTests.swift +// MoreCodableTests +// +// Created by Tatsuya Tanaka on 20180211. +// Copyright © 2018年 tattn. All rights reserved. +// + +import XCTest +import MoreCodable + +class DictionaryEncoderTests: XCTestCase { + + var encoder = DictionaryEncoder() + + struct User: Codable { + let name: String + let age: Int + } + + override func setUp() { + super.setUp() + encoder = DictionaryEncoder() + } + + override func tearDown() { + super.tearDown() + } + + func testEncodeSimpleModel() throws { + let user = User(name: "Tatsuya Tanaka", age: 24) + let dictionary = try encoder.encode(user) + XCTAssertEqual(user.name, dictionary["name"] as? String) + XCTAssertEqual(user.age, dictionary["age"] as? Int) + XCTAssertEqual(dictionary.keys.count, 2) + } + + func testEncodeNestedModel() throws { + struct Article: Codable { + let title: String + let author: User + } + + let user = User(name: "Tatsuya Tanaka", age: 24) + let article = Article(title: "Swift Tips", author: user) + let dictionary = try encoder.encode(article) + XCTAssertEqual(article.title, dictionary["title"] as? String) + XCTAssertEqual(dictionary.keys.count, 2) + + let dictionaryAuthor = dictionary["author"] as! [String: Any] + XCTAssertEqual(article.author.name, dictionaryAuthor["name"] as? String) + XCTAssertEqual(article.author.age, dictionaryAuthor["age"] as? Int) + XCTAssertEqual(dictionaryAuthor.keys.count, 2) + } + + func testEncodeOptionalValue() throws { + struct Model: Codable { + let int: Int? + let string: String? + let double: Double? + } + let seeds: [(model: Model, count: Int)] = [ + (model: Model(int: nil, string: nil, double: nil), count: 0), + (model: Model(int: 100, string: nil, double: nil), count: 1), + (model: Model(int: nil, string: "a", double: nil), count: 1), + (model: Model(int: nil, string: nil, double: 0.5), count: 1), + (model: Model(int: 100, string: "a", double: nil), count: 2), + (model: Model(int: nil, string: "a", double: 0.5), count: 2), + (model: Model(int: 100, string: nil, double: 0.5), count: 2), + (model: Model(int: 100, string: "a", double: 0.5), count: 3), + ] + for seed in seeds { + let dictionary = try encoder.encode(seed.model) + XCTAssertEqual(seed.model.int, dictionary["int"] as? Int) + XCTAssertEqual(seed.model.string, dictionary["string"] as? String) + XCTAssertEqual(seed.model.double, dictionary["double"] as? Double) + XCTAssertEqual(dictionary.keys.count, seed.count) + } + } +} diff --git a/Tests/FailableTests.swift b/Tests/FailableTests.swift new file mode 100644 index 0000000..1e52d4d --- /dev/null +++ b/Tests/FailableTests.swift @@ -0,0 +1,60 @@ +// +// FailableTests.swift +// MoreCodableTests +// +// Created by Tatsuya Tanaka on 20180219. +// Copyright © 2018年 tattn. All rights reserved. +// + +import XCTest +import MoreCodable + +class FailableTests: XCTestCase { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testFailableArray() { + let json = """ +[ + {"name": "Taro", "age": 20}, + {"name": "Hanako", "age": "にゃーん"} +] +""".data(using: .utf8)! + + struct User: Codable { + let name: String + let age: Int + } + + let users = try! JSONDecoder().decode([Failable].self, + from: json) + + XCTAssertEqual(users[0].value?.name, "Taro") + XCTAssertEqual(users[0].value?.age, 20) + XCTAssertNil(users[1].value) + } + + func testFailableURL() { + let json = """ +{"url": "https://foo.com", "url2": "invalid url string"} +""".data(using: .utf8)! + + struct Model: Codable { + let url: Failable + let url2: Failable + } + + let model = try! JSONDecoder().decode(Model.self, + from: json) + + XCTAssertEqual(model.url.value?.absoluteString, "https://foo.com") + XCTAssertNil(model.url2.value) + } + +} diff --git a/Tests/Info.plist b/Tests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/Tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Tests/RuleBasedCodingKeyTests.swift b/Tests/RuleBasedCodingKeyTests.swift new file mode 100644 index 0000000..15cc258 --- /dev/null +++ b/Tests/RuleBasedCodingKeyTests.swift @@ -0,0 +1,92 @@ +// +// RuleBasedCodingKeyTests.swift +// MoreCodableTests +// +// Created by Tatsuya Tanaka on 20180221. +// Copyright © 2018年 tattn. All rights reserved. +// + +import XCTest +import MoreCodable + +class RuleBasedCodingKeyTests: XCTestCase { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testCustomRule() { + struct User: Codable { + let userId: String + let name: String + + enum CodingKeys: String, RuleBasedCodingKey { + case userId + case name + + func codingKeyRule(key: String) -> String { + return key.uppercased() + } + } + } + + let json = """ +{"USERID": "abc", "NAME": "tattn"} +""".data(using: .utf8)! + + let decoder = JSONDecoder() + let user = try! decoder.decode(User.self, from: json) + + XCTAssertEqual(user.userId, "abc") + XCTAssertEqual(user.name, "tattn") + } + + func testSnakeCase() { + struct User: Codable { + let userId: String + let name: String + + enum CodingKeys: String, SnakeCaseCodingKey { + case userId + case name + } + } + + let json = """ +{"user_id": "abc", "name": "Tatsuya Tanaka"} +""".data(using: .utf8)! + + let decoder = JSONDecoder() + let user = try! decoder.decode(User.self, from: json) + + XCTAssertEqual(user.userId, "abc") + XCTAssertEqual(user.name, "Tatsuya Tanaka") + } + + func testUpperCamelCase() { + struct User: Codable { + let userId: String + let name: String + + enum CodingKeys: String, UpperCamelCaseCodingKey { + case userId + case name + } + } + + let json = """ +{"UserId": "abc", "Name": "Tatsuya Tanaka"} +""".data(using: .utf8)! + + let decoder = JSONDecoder() + let user = try! decoder.decode(User.self, from: json) + + XCTAssertEqual(user.userId, "abc") + XCTAssertEqual(user.name, "Tatsuya Tanaka") + } + +} diff --git a/Tests/StringToTests.swift b/Tests/StringToTests.swift new file mode 100644 index 0000000..1bdfb96 --- /dev/null +++ b/Tests/StringToTests.swift @@ -0,0 +1,50 @@ +// +// StringToTests.swift +// MoreCodableTests +// +// Created by Tatsuya Tanaka on 20180221. +// Copyright © 2018年 tattn. All rights reserved. +// + +import XCTest +@testable import MoreCodable + +class StringToTests: XCTestCase { + + let json = """ +{ + "int": "100", + "articleId": "abc" +} +""".data(using: .utf8)! + + struct Root: Codable { + let int: StringTo + let articleId: StringTo + + struct ArticleId: LosslessStringConvertible, Codable { + var description: String + + init?(_ description: String) { + self.description = description + } + } + } + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testExample() { + let decoder = JSONDecoder() + let root = try! decoder.decode(Root.self, from: json) + XCTAssertEqual(root.int.value, 100) + XCTAssertEqual(root.articleId.value.description, "abc") + } + +} + diff --git a/Tests/URLQueryItemsDecoderTests.swift b/Tests/URLQueryItemsDecoderTests.swift new file mode 100644 index 0000000..76e9c58 --- /dev/null +++ b/Tests/URLQueryItemsDecoderTests.swift @@ -0,0 +1,42 @@ +// +// URLQueryItemsDecoderTests.swift +// MoreCodableTests +// +// Created by Tatsuya Tanaka on 20180212. +// Copyright © 2018年 tattn. All rights reserved. +// + +import XCTest +import MoreCodable + +class URLQueryItemsDecoderTests: XCTestCase { + + var decoder = URLQueryItemsDecoder() + + override func setUp() { + super.setUp() + decoder = URLQueryItemsDecoder() + } + + override func tearDown() { + super.tearDown() + } + + func testDecodeSimpleParameter() throws { + struct Parameter: Codable { + let string: String + let int: Int + let double: Double + } + let params: [URLQueryItem] = [ + URLQueryItem(name: "string", value: "abc"), + URLQueryItem(name: "int", value: "123"), + URLQueryItem(name: "double", value: Double.pi.description) + ] + let parameter = try decoder.decode(Parameter.self, from: params) + + XCTAssertEqual(parameter.string, params[0].value) + XCTAssertEqual(parameter.int.description, params[1].value) + XCTAssertEqual(parameter.double.description, params[2].value) + } +} diff --git a/Tests/URLQueryItemsEncoderTests.swift b/Tests/URLQueryItemsEncoderTests.swift new file mode 100644 index 0000000..edb9cce --- /dev/null +++ b/Tests/URLQueryItemsEncoderTests.swift @@ -0,0 +1,44 @@ +// +// URLQueryItemsEncoderTests.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180212. +// Copyright © 2018年 tattn. All rights reserved. +// + +import XCTest +import MoreCodable + +class URLQueryItemsEncoderTests: XCTestCase { + + var encoder = URLQueryItemsEncoder() + + override func setUp() { + super.setUp() + encoder = URLQueryItemsEncoder() + } + + override func tearDown() { + super.tearDown() + } + + func testEncodeSimpleParameter() throws { + struct Parameter: Codable { + let query: String + let offset: Int + let limit: Int + } + let parameter = Parameter(query: "ねこ", offset: 10, limit: 20) + let params: [URLQueryItem] = try encoder.encode(parameter) + XCTAssertEqual("query", params[0].name) + XCTAssertEqual(parameter.query, params[0].value) + XCTAssertEqual("offset", params[1].name) + XCTAssertEqual(parameter.offset.description, params[1].value) + XCTAssertEqual("limit", params[2].name) + XCTAssertEqual(parameter.limit.description, params[2].value) + + var components = URLComponents(string: "https://example.com") + components?.queryItems = params + XCTAssertEqual(components?.url?.absoluteString, "https://example.com?query=%E3%81%AD%E3%81%93&offset=10&limit=20") + } +}