diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d860a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +xcuserdata +*.pbxuser +*.perspectivev3 +*.mode1v3 +.DS_Store +*.xccheckout + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6914b67 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.0 + +This is the first public release of the Lift library. diff --git a/LICENCE.md b/LICENCE.md new file mode 100644 index 0000000..e1c9a8d --- /dev/null +++ b/LICENCE.md @@ -0,0 +1,19 @@ +**Copyright (c) 2016 - 2017, iZettle AB** +**All rights reserved.** + +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/Lift.podspec b/Lift.podspec new file mode 100644 index 0000000..25b8906 --- /dev/null +++ b/Lift.podspec @@ -0,0 +1,17 @@ +Pod::Spec.new do |s| + s.name = "Lift" + s.version = "1.0.0" + s.summary = "Working with JSON-like structures" + s.description = <<-DESC + Lift is a Swift library for generating and extracting values into and out of JSON-like structures. + DESC + s.homepage = "https://github.com/iZettle/Lift" + s.license = { :type => "MIT", :file => "LICENSE.md" } + s.author = { 'iZettle AB' => 'hello@izettle.com' } + + s.osx.deployment_target = "10.9" + s.ios.deployment_target = "9.0" + + s.source = { :git => "https://github.com/iZettle/Lift", :tag => "#{s.version}" } + s.source_files = "Lift/*.{swift}" +end diff --git a/Lift.xcodeproj/project.pbxproj b/Lift.xcodeproj/project.pbxproj new file mode 100644 index 0000000..99ac029 --- /dev/null +++ b/Lift.xcodeproj/project.pbxproj @@ -0,0 +1,509 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 21E1D3A91D9410F000A91CA0 /* Lift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 21E1D39F1D9410F000A91CA0 /* Lift.framework */; }; + 21E1D3BD1D94118800A91CA0 /* Jar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E1D3B91D94118800A91CA0 /* Jar.swift */; }; + 21E1D3BE1D94118800A91CA0 /* Jar+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E1D3BA1D94118800A91CA0 /* Jar+Additions.swift */; }; + 21E1D3BF1D94118800A91CA0 /* JarElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E1D3BB1D94118800A91CA0 /* JarElement.swift */; }; + 21E1D3C01D94118800A91CA0 /* JarElement+Primitives.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E1D3BC1D94118800A91CA0 /* JarElement+Primitives.swift */; }; + B30713531E571D9F0032EB39 /* JarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35977911E57087000FB4ABF /* JarTests.swift */; }; + CDDE32951EC3400200867A95 /* IntegerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDE32941EC3400200867A95 /* IntegerTests.swift */; }; + F6F4E6FA1E9263600013050A /* Jar+Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F4E6F91E9263600013050A /* Jar+Context.swift */; }; + F6F4E6FC1E92642B0013050A /* Jar+Expressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F4E6FB1E92642B0013050A /* Jar+Expressible.swift */; }; + F6F4E6FE1E9264480013050A /* Jar+Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F4E6FD1E9264480013050A /* Jar+Object.swift */; }; + F6F4E7001E9264FA0013050A /* LiftError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F4E6FF1E9264FA0013050A /* LiftError.swift */; }; + F6F4E7021E9266150013050A /* ValueForKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F4E7011E9266150013050A /* ValueForKey.swift */; }; + F6F4E7041E9267670013050A /* Jar+Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F4E7031E9267670013050A /* Jar+Dictionary.swift */; }; + F6F4E7061E9267AD0013050A /* Jar+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F4E7051E9267AD0013050A /* Jar+Array.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 21E1D3AA1D9410F000A91CA0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 21E1D3961D9410F000A91CA0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 21E1D39E1D9410F000A91CA0; + remoteInfo = Lift; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 21E1D39F1D9410F000A91CA0 /* Lift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Lift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 21E1D3A31D9410F000A91CA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 21E1D3A81D9410F000A91CA0 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 21E1D3B91D94118800A91CA0 /* Jar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Jar.swift; sourceTree = ""; }; + 21E1D3BA1D94118800A91CA0 /* Jar+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Jar+Additions.swift"; sourceTree = ""; }; + 21E1D3BB1D94118800A91CA0 /* JarElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JarElement.swift; sourceTree = ""; }; + 21E1D3BC1D94118800A91CA0 /* JarElement+Primitives.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JarElement+Primitives.swift"; sourceTree = ""; }; + 21E1D3C51D94122D00A91CA0 /* Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Framework.xcconfig; path = ../../Configurations/Framework.xcconfig; sourceTree = ""; }; + 21E1D3C61D94122D00A91CA0 /* FrameworkTest.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = FrameworkTest.xcconfig; path = ../../Configurations/FrameworkTest.xcconfig; sourceTree = ""; }; + B359778F1E57087000FB4ABF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B35977911E57087000FB4ABF /* JarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JarTests.swift; sourceTree = ""; }; + CDDE32941EC3400200867A95 /* IntegerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegerTests.swift; sourceTree = ""; }; + F6AD1F091E2CD0820082CF27 /* LICENCE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENCE.md; sourceTree = ""; }; + F6AD1F0B1E2CD1150082CF27 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + F6AD1F0C1E2CD14E0082CF27 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; + F6AD20241E2FAED20082CF27 /* Lift.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Lift.podspec; sourceTree = ""; }; + F6B704411FC6B1FD00808AE5 /* lift-logo.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = "lift-logo.svg"; sourceTree = ""; }; + F6F4E6F91E9263600013050A /* Jar+Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Jar+Context.swift"; sourceTree = ""; }; + F6F4E6FB1E92642B0013050A /* Jar+Expressible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Jar+Expressible.swift"; sourceTree = ""; }; + F6F4E6FD1E9264480013050A /* Jar+Object.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Jar+Object.swift"; sourceTree = ""; }; + F6F4E6FF1E9264FA0013050A /* LiftError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiftError.swift; sourceTree = ""; }; + F6F4E7011E9266150013050A /* ValueForKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueForKey.swift; sourceTree = ""; }; + F6F4E7031E9267670013050A /* Jar+Dictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Jar+Dictionary.swift"; sourceTree = ""; }; + F6F4E7051E9267AD0013050A /* Jar+Array.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Jar+Array.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 21E1D39B1D9410F000A91CA0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 21E1D3A51D9410F000A91CA0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 21E1D3A91D9410F000A91CA0 /* Lift.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 21E1D3951D9410F000A91CA0 = { + isa = PBXGroup; + children = ( + F6AD1F0C1E2CD14E0082CF27 /* CHANGELOG.md */, + F6AD1F091E2CD0820082CF27 /* LICENCE.md */, + F6AD1F0B1E2CD1150082CF27 /* README.md */, + F6B704411FC6B1FD00808AE5 /* lift-logo.svg */, + F6AD20241E2FAED20082CF27 /* Lift.podspec */, + 21E1D3C41D9411FE00A91CA0 /* Dependencies */, + 21E1D3A11D9410F000A91CA0 /* Lift */, + 21E1D3A01D9410F000A91CA0 /* Products */, + B359778E1E57087000FB4ABF /* Tests */, + ); + sourceTree = ""; + }; + 21E1D3A01D9410F000A91CA0 /* Products */ = { + isa = PBXGroup; + children = ( + 21E1D39F1D9410F000A91CA0 /* Lift.framework */, + 21E1D3A81D9410F000A91CA0 /* Tests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 21E1D3A11D9410F000A91CA0 /* Lift */ = { + isa = PBXGroup; + children = ( + 21E1D3B91D94118800A91CA0 /* Jar.swift */, + F6F4E7051E9267AD0013050A /* Jar+Array.swift */, + F6F4E6F91E9263600013050A /* Jar+Context.swift */, + F6F4E7031E9267670013050A /* Jar+Dictionary.swift */, + F6F4E6FB1E92642B0013050A /* Jar+Expressible.swift */, + F6F4E6FD1E9264480013050A /* Jar+Object.swift */, + 21E1D3BA1D94118800A91CA0 /* Jar+Additions.swift */, + F6F4E6FF1E9264FA0013050A /* LiftError.swift */, + 21E1D3BB1D94118800A91CA0 /* JarElement.swift */, + 21E1D3BC1D94118800A91CA0 /* JarElement+Primitives.swift */, + F6F4E7011E9266150013050A /* ValueForKey.swift */, + 21E1D3A31D9410F000A91CA0 /* Info.plist */, + ); + path = Lift; + sourceTree = ""; + }; + 21E1D3C41D9411FE00A91CA0 /* Dependencies */ = { + isa = PBXGroup; + children = ( + 21E1D3C51D94122D00A91CA0 /* Framework.xcconfig */, + 21E1D3C61D94122D00A91CA0 /* FrameworkTest.xcconfig */, + ); + name = Dependencies; + sourceTree = ""; + }; + B359778E1E57087000FB4ABF /* Tests */ = { + isa = PBXGroup; + children = ( + B359778F1E57087000FB4ABF /* Info.plist */, + B35977901E57087000FB4ABF /* LiftTests */, + ); + path = Tests; + sourceTree = ""; + }; + B35977901E57087000FB4ABF /* LiftTests */ = { + isa = PBXGroup; + children = ( + B35977911E57087000FB4ABF /* JarTests.swift */, + CDDE32941EC3400200867A95 /* IntegerTests.swift */, + ); + path = LiftTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 21E1D39C1D9410F000A91CA0 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 21E1D39E1D9410F000A91CA0 /* Lift */ = { + isa = PBXNativeTarget; + buildConfigurationList = 21E1D3B31D9410F000A91CA0 /* Build configuration list for PBXNativeTarget "Lift" */; + buildPhases = ( + 21E1D39A1D9410F000A91CA0 /* Sources */, + 21E1D39B1D9410F000A91CA0 /* Frameworks */, + 21E1D39C1D9410F000A91CA0 /* Headers */, + 21E1D39D1D9410F000A91CA0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Lift; + productName = Lift; + productReference = 21E1D39F1D9410F000A91CA0 /* Lift.framework */; + productType = "com.apple.product-type.framework"; + }; + 21E1D3A71D9410F000A91CA0 /* Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 21E1D3B61D9410F000A91CA0 /* Build configuration list for PBXNativeTarget "Tests" */; + buildPhases = ( + 21E1D3A41D9410F000A91CA0 /* Sources */, + 21E1D3A51D9410F000A91CA0 /* Frameworks */, + 21E1D3A61D9410F000A91CA0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 21E1D3AB1D9410F000A91CA0 /* PBXTargetDependency */, + ); + name = Tests; + productName = LiftTests; + productReference = 21E1D3A81D9410F000A91CA0 /* Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 21E1D3961D9410F000A91CA0 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0820; + LastUpgradeCheck = 0900; + ORGANIZATIONNAME = iZettle; + TargetAttributes = { + 21E1D39E1D9410F000A91CA0 = { + CreatedOnToolsVersion = 8.0; + DevelopmentTeam = G94FN7ACW4; + LastSwiftMigration = 0900; + ProvisioningStyle = Manual; + }; + 21E1D3A71D9410F000A91CA0 = { + CreatedOnToolsVersion = 8.0; + DevelopmentTeam = G94FN7ACW4; + LastSwiftMigration = 0900; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 21E1D3991D9410F000A91CA0 /* Build configuration list for PBXProject "Lift" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 21E1D3951D9410F000A91CA0; + productRefGroup = 21E1D3A01D9410F000A91CA0 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 21E1D39E1D9410F000A91CA0 /* Lift */, + 21E1D3A71D9410F000A91CA0 /* Tests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 21E1D39D1D9410F000A91CA0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 21E1D3A61D9410F000A91CA0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 21E1D39A1D9410F000A91CA0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F6F4E7061E9267AD0013050A /* Jar+Array.swift in Sources */, + F6F4E6FC1E92642B0013050A /* Jar+Expressible.swift in Sources */, + F6F4E7021E9266150013050A /* ValueForKey.swift in Sources */, + 21E1D3C01D94118800A91CA0 /* JarElement+Primitives.swift in Sources */, + 21E1D3BD1D94118800A91CA0 /* Jar.swift in Sources */, + F6F4E7001E9264FA0013050A /* LiftError.swift in Sources */, + 21E1D3BE1D94118800A91CA0 /* Jar+Additions.swift in Sources */, + F6F4E6FA1E9263600013050A /* Jar+Context.swift in Sources */, + F6F4E6FE1E9264480013050A /* Jar+Object.swift in Sources */, + 21E1D3BF1D94118800A91CA0 /* JarElement.swift in Sources */, + F6F4E7041E9267670013050A /* Jar+Dictionary.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 21E1D3A41D9410F000A91CA0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CDDE32951EC3400200867A95 /* IntegerTests.swift in Sources */, + B30713531E571D9F0032EB39 /* JarTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 21E1D3AB1D9410F000A91CA0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 21E1D39E1D9410F000A91CA0 /* Lift */; + targetProxy = 21E1D3AA1D9410F000A91CA0 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 21E1D3B11D9410F000A91CA0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + 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; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 21E1D3B21D9410F000A91CA0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + 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 = gnu99; + 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; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 21E1D3B41D9410F000A91CA0 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 21E1D3C51D94122D00A91CA0 /* Framework.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Lift/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks @loader_path/../Frameworks @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.iZettle.Lift; + PRODUCT_NAME = Lift; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos watchsimulator watchos appletvos appletvsimulator"; + SWIFT_VERSION = 4.0; + }; + name = Debug; + }; + 21E1D3B51D9410F000A91CA0 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 21E1D3C51D94122D00A91CA0 /* Framework.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Lift/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks @loader_path/../Frameworks @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.iZettle.Lift; + PRODUCT_NAME = Lift; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos watchsimulator watchos appletvos appletvsimulator"; + SWIFT_VERSION = 4.0; + }; + name = Release; + }; + 21E1D3B71D9410F000A91CA0 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 21E1D3C61D94122D00A91CA0 /* FrameworkTest.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + APPLICATION_EXTENSION_API_ONLY = NO; + INFOPLIST_FILE = Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks @executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.iZettle.LiftTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos watchsimulator watchos appletvos appletvsimulator"; + SWIFT_VERSION = 4.0; + }; + name = Debug; + }; + 21E1D3B81D9410F000A91CA0 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 21E1D3C61D94122D00A91CA0 /* FrameworkTest.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + APPLICATION_EXTENSION_API_ONLY = NO; + INFOPLIST_FILE = Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks @executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.iZettle.LiftTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos watchsimulator watchos appletvos appletvsimulator"; + SWIFT_VERSION = 4.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 21E1D3991D9410F000A91CA0 /* Build configuration list for PBXProject "Lift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 21E1D3B11D9410F000A91CA0 /* Debug */, + 21E1D3B21D9410F000A91CA0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 21E1D3B31D9410F000A91CA0 /* Build configuration list for PBXNativeTarget "Lift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 21E1D3B41D9410F000A91CA0 /* Debug */, + 21E1D3B51D9410F000A91CA0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 21E1D3B61D9410F000A91CA0 /* Build configuration list for PBXNativeTarget "Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 21E1D3B71D9410F000A91CA0 /* Debug */, + 21E1D3B81D9410F000A91CA0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 21E1D3961D9410F000A91CA0 /* Project object */; +} diff --git a/Lift.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Lift.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Lift.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Lift.xcodeproj/xcshareddata/xcschemes/Lift.xcscheme b/Lift.xcodeproj/xcshareddata/xcschemes/Lift.xcscheme new file mode 100644 index 0000000..b73543f --- /dev/null +++ b/Lift.xcodeproj/xcshareddata/xcschemes/Lift.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Lift/Info.plist b/Lift/Info.plist new file mode 100644 index 0000000..7cf5349 --- /dev/null +++ b/Lift/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Lift + 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/Lift/Jar+Additions.swift b/Lift/Jar+Additions.swift new file mode 100644 index 0000000..35e17f4 --- /dev/null +++ b/Lift/Jar+Additions.swift @@ -0,0 +1,68 @@ +// +// Jar+Additions.swift +// Lift +// +// Created by Måns Bernhardt on 2016-05-23. +// Copyright © 2016 iZettle. All rights reserved. +// + +import Foundation + +public extension Data { + /// Construct a JSON string `Data` from a `Jar` + init(json jar: Jar, prettyPrinted: Bool = true) throws { + let any = try jar.asAny() + + if any is [Any] || any is [String: Any] { + self = try JSONSerialization.data(withJSONObject: try jar.asAny(), options: prettyPrinted ? .prettyPrinted : []) + } else if any is Null { + self = try jar.assertNotNil("null".data(using: .utf8)) + } else if let n = any as? NSNumber, String(cString: n.objCType) == "c" { + self = try jar.assertNotNil("\((any as? Bool) ?? any)".data(using: .utf8)) + } else { + self = try jar.assertNotNil("\(any)".data(using: .utf8)) + } + } +} + +public extension String { + /// Construct a JSON `String` from a `Jar` + init(json jar: Jar, prettyPrinted: Bool = true) throws { + let data = try Data(json: jar, prettyPrinted: prettyPrinted) + self = try jar.assertNotNil(String(data: data, encoding: .utf8)) + } +} + +public extension Jar { + /// Construct a `Jar` from the content of the `json` data + init(json data: Data) throws { + try self.init(unchecked: JSONSerialization.jsonObject(with: data, options: .allowFragments)) + } + + /// Construct a `Jar` from the content of the `json` string + init(json string: String) throws { + try self.init(json: string.data(using: String.Encoding.utf8).assertNotNil("Not an UTF8 string")) + } + + /// Construct a `Jar` from the JSON at `url` + init(json url: URL) throws { + let data = try Data(contentsOf: url) + try self.init(json: data) + } +} + +extension Jar: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + do { + return try String(json: self, prettyPrinted: true) + } catch { + return error.localizedDescription + } + } + + public var debugDescription: String { + return description + } +} + +extension UserDefaults: MutatingValueForKey { } diff --git a/Lift/Jar+Array.swift b/Lift/Jar+Array.swift new file mode 100644 index 0000000..bcb2753 --- /dev/null +++ b/Lift/Jar+Array.swift @@ -0,0 +1,110 @@ +// +// Jar+Array.swift +// Lift +// +// Created by Måns Bernhardt on 2017-04-03. +// Copyright © 2017 iZettle. All rights reserved. +// + +import Foundation + + +public extension Jar { + /// Returns the wrapped value if it's an array and it is successfully converted + var array: [Any]? { + return (try? object.asAny(context)) as? [Any] + } + + /// Set the element at `index` to a value conforming to `JarRepresentable` + /// - Note: Setting a value where `self` is not an array or if the index is out of bounds will trap. + /// - Note: The getter is typically never used. Instead use the subscript overload that returns a `Jar` + subscript(index: Int) -> JarRepresentable { + get { + return self[index] as Jar + } + set { + arrayReplace(at: index, with: { [ try newValue.asJar(using: $0).asAny() ] }) + } + } + + /// Extract the element at `index` and return it in a `Jar` + /// When a value is lifted out of the returned jar it might throw if `self` is not an array or if the access was out of bounds. + /// - Note: Setting a value where `self` is not an array or if the index is out of bounds will trap. + /// - Note: The setter is typically never used. Instead use the subscript overload that takes a `JarRepresentable` + subscript(index: Int) -> Jar { + get { + let key: () -> String = { self.key() + "[\(index)]" } + + switch object { + case .error, .none, .null: + return Jar(object: object, context: context, key: self.key) + case .array: + let array = self.array! + guard index >= array.startIndex && index < array.endIndex else { + return Jar(object: .error(LiftError("Index out of bounds", key: "", context: self)), context: context, key: key) + } + return Jar(object: Object(array[index]), context: context, key: key) + default: + return Jar(object: .error(LiftError("Not an array", key: "", context: self)), context: context, key: self.key) + } + } + set { + arrayReplace(at: index, with: { _ in [ try newValue.asAny() ] }) + } + } + + /// Appends a `jar` to `self` if `self` is an array or set `self` to an array holding `jar` if not + mutating func append(_ value: JarRepresentable) { + arrayReplace(at: nil, with: { [ try value.asJar(using: $0).asAny() ] }) + } + + /// Appends a `jar` to `self` if `self` is an array or set `self` to an array holding `jar` if not + mutating func append(_ jar: Jar) { + append(jar as JarRepresentable) + } +} + + +/// Lift an array value out of a Jar +public postfix func ^(jar: Jar) throws -> [T] where T.To == T { + return try jar.assertNotNil(jar.array, "Not an array").enumerated().map { i, any in + let itemJar = Jar(object: Jar.Object(any), context: jar.context, key: { jar.key() + "[\(i)]" }) + return try T.lift(from: itemJar) + } +} + +/// Lift an optional array value out of a Jar +public postfix func ^(jar: Jar) throws -> [T]? where T.To == T { + return try jar.map { try $0^ } +} + +public extension Jar { + /// Lifts an array of type `[T]` and applies `transform` to it's elements + func map(_ transform: (T) throws -> O) throws -> [O] where T.To == T { + return try (self^).map(transform) + } + + /// Lifts an array of type `[T]`, and if not nil, applies `transform` to it's elements + func map(_ transform: (T) throws -> O) throws -> [O]? where T.To == T { + return try map { try $0.map(transform) } + } +} + +private extension Jar { + mutating func arrayReplace(at range: Range?, with toAny: @escaping ToAny) { + switch object { + case let .array(ops): + object = .array(ops + [ (range, toAny) ]) + case .none: + object = .array([ (nil, toAny) ]) + case let .primitive(val): + object = .array([ (nil, val), (nil, toAny) ]) + default: + object = .error(LiftError("Not an array", context: self)) + } + } + + mutating func arrayReplace(at index: Int, with toAny: @escaping ToAny) { + arrayReplace(at: Range(uncheckedBounds: (index, index)), with: toAny) + } +} diff --git a/Lift/Jar+Context.swift b/Lift/Jar+Context.swift new file mode 100644 index 0000000..9a45825 --- /dev/null +++ b/Lift/Jar+Context.swift @@ -0,0 +1,87 @@ +// +// Jar+Context.swift +// Lift +// +// Created by Måns Bernhardt on 2017-04-03. +// Copyright © 2017 iZettle. All rights reserved. +// + +import Foundation + +/// Conforming types can be added to a Jar.Context to pass additional data not provided in the Jar value itself. +public protocol JarContextValue { + var context: Jar.Context { get } +} + +public extension Jar { + /// Context is a type that a `Jar` can carry around holding context information needed that is not provided by the JSON itself. + /// It can also be used to customize the behaviour of encoding and decoding of a type. E.g. `Date`'s conformance to JarElement will use the DateFormatter if any in provided context to encode and decodes dates. + struct Context { + fileprivate var vals = [String: Any]() + + init(key: String, value: Any) { + vals = [ key: value ] + } + + init(value: Any) { + self.init(key: String(reflecting: type(of: value)), value: value) + } + + public init(_ vals: [JarContextValue?] = []) { + for val in vals.flatMap({ $0 }) { + self.vals[String(reflecting: type(of: val))] = val + } + } + + public init(_ vals: JarContextValue?...) { + self.init(vals) + } + } + + /// Creates a union between `self` context and `val`, where `val` context values will be replacing the same context value's in `self`'s context if they already exists + func union(context val: JarContextValue?) -> Jar { + var jar = self + jar.context.formUnion(val) + return jar + } +} + +public extension Jar.Context { + /// Mutating version of `union` + public mutating func formUnion(_ context: JarContextValue?) { + for (key, val) in context?.context.vals ?? [:] { + self.vals[key] = val + } + } + + /// Creates a union between `self` and `val`, where `val`'s context values will be replacing the same context value's in `self` if they already exists + public func union(_ val: JarContextValue?) -> Jar.Context { + var c = self + c.formUnion(val) + return c + } + + /// Get a value of a certain type out of the context or throw if it does not exists + public func get(_ type: T.Type = T.self) throws -> T { + return try (vals[String(reflecting: type)] as? T).assertNotNil("The Jar context does not contain any value of type: \(type)") + } + + /// Get a value of a certain type out of the context or return nil if it does not exists + public func get(_ type: T?.Type = T?.self) -> T? { + return vals[String(reflecting: T.self)].map { $0 as! T } + } +} + +extension Jar.Context: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: JarContextValue?...) { + self.init(elements) + } +} + +public extension JarContextValue { + var context: Jar.Context { return Jar.Context(self) } +} + +extension Jar.Context: JarContextValue { + public var context: Jar.Context { return self } +} diff --git a/Lift/Jar+Dictionary.swift b/Lift/Jar+Dictionary.swift new file mode 100644 index 0000000..d0ce696 --- /dev/null +++ b/Lift/Jar+Dictionary.swift @@ -0,0 +1,117 @@ +// +// Jar+Dictionary.swift +// Lift +// +// Created by Måns Bernhardt on 2017-04-03. +// Copyright © 2017 iZettle. All rights reserved. +// + +import Foundation + +public extension Jar { + /// Returns the wrapped value if it's a dictionary and it is successfully converted + var dictionary: [String: Any]? { + return (try? object.asAny(context)) as? [String: Any] + } + + /// Set the element at `key` to a value conforming to `JarRepresentable` + /// - Note: Setting a value where `self` is not a dictionary will update the wrapped value to become a dictionry holding the value. + /// - Note: The getter is typically never used. Instead use the subscript overload the returns a `Jar` + /// - Note: To set an array, dictionary or optional, pass a `Jar` wrapping the valus such as `Jar(["key": 5])` + subscript(key: String) -> JarRepresentable? { + get { + return dictionary?[key].map(_Droppable.init) + } + set { + if let val = newValue { + dictionaryAppend({ [ key: try val.asJar(using: $0).asAny() ] }) + } else { + dictionaryAppend({ _ in [ key: Object.RemoveElement() ] }) + } + } + } + + /// Access the element at `key` and return it in a `Jar` + /// When a value is lifted out of the jar it might throw if `self` is not a dictionary or if the key is missing and the lift is non-optional + /// - Note: Setting a value where `self` is not a dictionary will update the wrapped to become a dictionry holding the value. + /// - Note: The setter is typically never used. Instead use the subscript overload the takes a `JarRepresentable` + subscript(key: String) -> Jar { + get { + let _key: () -> String = { + let k = self.key() + return k + (k.isEmpty ? "" : ".") + key + } + + switch object { + case .error, .none, .null: + return Jar(object: object, context: context, key: self.key) + case .dictionary: + return Jar(object: Object(dictionary?[key]), context: context, key: _key) + default: + return Jar(object: .error(LiftError("Not a dictionary", key: "", context: self)), context: context, key: _key) + } } + set { + dictionaryAppend({ _ in [ key: try newValue.asAny() ] }) + } + } + + + /// Returns a new jar containing the union of self and `value` + /// - Note: If both `self` and `value` contains the same key, `value`'s will be used + func union(_ value: JarRepresentable) -> Jar { + var new = self + new.formUnion(value) + return new + } + + /// Updates self to be the union between self and `value` + /// - Note: If both `self` and `value` contains the same key, `value`'s will be used + mutating func formUnion(_ value: JarRepresentable) { + dictionaryAppend({ try value.asJar(using: $0).asAny() }) + } +} + +/// Lift a dictionary value out of a Jar +public postfix func ^(jar: Jar) throws -> [K:V] where V.To == V, K.To == K, K: Hashable { + let dictionary = try jar.assertNotNil(jar.dictionary, "Not a dictionary") + let keys: [K] = try Jar(unchecked: Array(dictionary.keys)).union(context: jar.context)^ + let values: [V] = try Jar(unchecked: Array(dictionary.values)).union(context: jar.context)^ + var mappedDictionary = [K:V]() + for (key, value) in zip(keys, values) { + mappedDictionary[key] = value + } + return mappedDictionary +} + +/// Lift an optional dictionary value out of a Jar +public postfix func ^(jar: Jar) throws -> [K:V]? where V.To == V, K.To == K, K: Hashable { + return try jar.map { try $0^ } +} + +public extension Jar { + /// Lifts a dictionary of type `[K:V]` and applies `transform` to it + func map(_ transform: ([K:V]) throws -> O) throws -> O where V.To == V, K.To == K { + return try transform(self^) + } + + /// Lifts a dictionary of type `[K:V]`, and if not nil, applies `transform` to it + func map(_ transform: ([K:V]) throws -> O) throws -> O? where V.To == V, K.To == K { + return try map { try $0.map(transform) } + } +} + +private extension Jar { + mutating func dictionaryAppend(_ toAny: @escaping ToAny) { + switch object { + case let .dictionary(dicts): + object = .dictionary(dicts + [ toAny ]) + case .none: + object = .dictionary([ toAny ]) + case let .primitive(val): + object = .dictionary([ val, toAny ]) + default: + object = .error(LiftError("Not a dictionary", context: self)) + } + } +} + diff --git a/Lift/Jar+Expressible.swift b/Lift/Jar+Expressible.swift new file mode 100644 index 0000000..05bfb71 --- /dev/null +++ b/Lift/Jar+Expressible.swift @@ -0,0 +1,59 @@ +// +// Jar+Expressible.swift +// Lift +// +// Created by Måns Bernhardt on 2016-05-23. +// Copyright © 2016 iZettle. All rights reserved. +// + +import Foundation + +extension Jar: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, JarRepresentable)...) { + var dict = [String: JarRepresentable]() + for (key, value) in elements { + dict[key] = value + } + self.init(dict) + } +} + +extension Jar: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: JarRepresentable...) { + object = .array([ (nil, { context in + try elements.flatMap { try $0.asJar(using: context).object.optionallyUnwrap(context).flatMap { $0 } } + })]) + } +} + +extension Jar: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: Bool) { + self.init(object: .primitive(value.asJar)) + } +} + +extension Jar: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int64) { + self.init(object: .primitive(value.asJar)) + } +} + +extension Jar: ExpressibleByFloatLiteral { + public init(floatLiteral value: Double) { + self.init(object: .primitive(value.asJar)) + } +} + +extension Jar: ExpressibleByStringLiteral { + public init(unicodeScalarLiteral value: String) { + self.init(object: .primitive(value.asJar)) + } + + public init(extendedGraphemeClusterLiteral value: String) { + self.init(object: .primitive(value.asJar)) + } + + public init(stringLiteral value: String) { + self.init(object: .primitive(value.asJar)) + } +} diff --git a/Lift/Jar+Object.swift b/Lift/Jar+Object.swift new file mode 100644 index 0000000..ec8f5d7 --- /dev/null +++ b/Lift/Jar+Object.swift @@ -0,0 +1,104 @@ +// +// Jar+Object.swift +// Lift +// +// Created by Måns Bernhardt on 2016-05-23. +// Copyright © 2016 iZettle. All rights reserved. +// + +import Foundation + + +extension Jar { + init(object: Object, context: Context = [], key: @escaping () -> String = { "" }) { + self.object = object + self.context = context + self.key = key + } + + /// Internal representation to avoid repetitive value conversions as well as handling error and absent states + enum Object { + case none(context: () -> String) /// Contains no value (nil), such as when subscripting using a key into a Jar return a Jar representing a absence of a value + case null + case primitive(ToAny) /// Typically Bool, Integer, Floating Point or String + case dictionary([ToAny]) // an array of where all toAny much return a [String: Any] and will be applied left to right + case array([(Range?, ToAny)]) // replace range with, unless range == nil where we should append. ToAny have to evaluate to [Any] + case error(Error) /// We already know something went wrong such as a conversion that will result in lifting the value out will throw. + + init(_ any: Any?) { + switch any { + case nil: + self = .none(context: { "" }) + case is Null: + self = .null + case let dictionary as [String: Any]: + self = .dictionary([{ _ in dictionary}]) + case let array as [Any]: + self = .array([(nil, { context in array })]) + case let object?: + self = .primitive { _ in object } + } + } + + /// Used by .dictionary mark removal of items. + struct RemoveElement {} + + func optionallyUnwrap(_ context: Context) throws -> Any? { + switch self { + case .none: return nil + case .null: return Lift.null + case let .dictionary(toAnys): + var result = [String: Any]() + for toAny in toAnys { + let any = try toAny(context) + guard let dict = try toAny(context) as? [String: Any] else { + throw LiftError("Not a dictionary: \(any)") + } + for (key, val) in dict { + result[key] = val is RemoveElement ? nil : val + } + } + return result + case let .array(ops): + var result = [Any]() + for (range, toAny) in ops { + let any = try toAny(context) + guard let array = any as? [Any] else { + throw LiftError("Not an array: \(any)") + } + if let range = range { + guard range.lowerBound >= result.startIndex && range.upperBound < result.endIndex else { + throw LiftError("Index out of bounds") + } + result.replaceSubrange(range, with: array) + } else { + result += array + } + } + return result + case let .primitive(val): + let v = try val(context) + if let jar = v as? Jar { + return try jar.object.unwrap(context) + } else { + return v + } + case let .error(error): throw error + } + } + + func unwrap(_ context: Context) throws -> Any { + if case let .none(context) = self { + throw LiftError("Value missing", key: "", context: context) + } + return try optionallyUnwrap(context)! + } + + func asAny(_ context: Context) throws -> Any? { + if case .error = self { return nil } + return try optionallyUnwrap(context) + } + } + + typealias ToAny = (Context) throws -> Any +} diff --git a/Lift/Jar.swift b/Lift/Jar.swift new file mode 100644 index 0000000..ee9359c --- /dev/null +++ b/Lift/Jar.swift @@ -0,0 +1,235 @@ +// +// Liftable.swift +// Lift +// +// Created by Måns Bernhardt on 2016-05-23. +// Copyright © 2016 iZettle. All rights reserved. +// + +import Foundation + +/// `Jar` is used to access heterogenous objects such as JSON, plists and user defaults in a type safe manner. +/// +/// To lift values out of a `Jar` the 'lift' operator ^ is used, for example: +/// +/// let jar = Jar(unchecked: any) +/// let int: Int = try json["val"]^ +/// +/// For a value to be liftable it must conform to `Liftable` or `JarConvertible` +public struct Jar { + internal var object: Object + internal var key: () -> String = { "" } // Capturing of the current key used for lazy evalution (to avoid a peformance hit for happy cases) + public var context: Context = [] +} + +public extension Jar { + /// Creates an empty instance not containing any value. + init() { + object = .none(context: { "" }) + } + + /// Creates an instance wrapping a primitive `value` conforming to `JarRepresentable` + init(_ value: JarRepresentable) { + object = .primitive({ try value.asJar(using: $0).asAny() }) + } + + /// Creates an instance wrapping a dictionary with values conforming to `JarRepresentable` + init(_ dictionary: [S: T]) { + object = .dictionary([{ context in + var result = [String: Any]() + for (key, val) in dictionary { + result[key.description] = try val.asJar(using: context).object.optionallyUnwrap(context) + } + return result + }]) + } + + /// Creates an instance wrapping a dictionary with values conforming to `JarRepresentable` + init(_ dictionary: [S: JarRepresentable]) { + object = .dictionary([{ context in + var result = [String: Any]() + for (key, val) in dictionary { + result[key.description] = try val.asJar(using: context).object.optionallyUnwrap(context) + } + return result + }]) + } + + /// Creates an instance wrapping an array with values conforming to `JarRepresentable` + init(_ elements: S) where S.Iterator.Element: JarRepresentable { + object = .array([ (nil, { context in + try elements.map { $0.asJar(using: context) }.flatMap { + return try $0.object.optionallyUnwrap($0.context).flatMap { $0 } + } + })]) + } + + /// Creates an instance wrapping an optional value conforming to `JarRepresentable` + init(_ value: Optional) { + switch value { + case let val?: + self.init(val) + case nil: + self.init() + } + } + + /// Creates a `Jar` from an unknown `Any` value that might come from deserializing JSON etc. + /// All leaf nodes that are `JarRepresentable` will be dropped down to `Any` + /// - Throws: Unless `value` conform to `JarRepresentable`, or if the `value` is an array with elements conforming to `JarRepresentable`, or if `value` is a dictionary with `String` keys and values conforming to `JarRepresentable` + init(checked value: Any?, context: Context = []) throws { + self.object = try Object(value.map { try convert($0, context: context) }) + } + + /// Creates a `Jar` from an unknown `Any` value that might come from deserializing JSON etc. + /// The content of `value` is not checked if it is `JarRepresentable` until first access. + /// This is typically more efficient, espcially when you know your sources. + /// - Postcondition: all leaf nodes are primitive values and not custom type so no need to dropping is required. + init(unchecked value: Any?) { + self.object = Object(value) + } + + /// Creates a `Jar` that will throw `error` when it's value is lifted. + init(error: Error) { + self.object = .error(error) + } +} + +public extension Jar { + /// Return the `Any` representation of the `Jar`'s wrapped value. + /// - Throws: If the Jar is empty or contains an error. + func asAny() throws -> Any { + do { + return try object.unwrap(context) + } catch let error as LiftError { + throw LiftError(error: error, key: self.key(), context: self) + } + } +} + +/// Null type for explicit marking of nulls in jar's +public typealias Null = NSNull + +/// null value for explicit marking of nulls in jar's +public let null = Null() + +public extension Jar { + /// Lifts a value of type `T` and applies `transform` to it + func map(_ transform: (T) throws -> O) throws -> O where T.To == T { + let value: T = try self^ + do { + return try transform(value) + } catch let error as LiftError { + throw LiftError(error: error, key: self.key(), context: self) + } + } + + /// Lifts a value of type `T` if not nil and applies `transform` to it + func map(_ transform: (T) throws -> O) throws -> O? where T.To == T { + return try map { try $0.map(transform) } + } +} + + +postfix operator ^ + +/// Lift a value out of a Jar +public postfix func ^(jar: Jar) throws -> T where T.To == T { + return try T.lift(from: jar) +} + +/// Lift an optional value out of a Jar +public postfix func ^(jar: Jar) throws -> T? where T.To == T { + return try jar.map { try $0^ } +} + +extension Jar: JarConvertible, JarRepresentable { + public init(jar: Jar) throws { + object = jar.object + context = jar.context + key = jar.key + } + + public var jar: Jar { + return self + } +} + +extension Jar { + func map(_ f: (Jar) throws -> U) throws -> U? { + switch self.object { + case .none: return nil + case let .error(error as LiftError): throw LiftError(error: error, key: self.key(), context: self) + case let .error(error): throw error + // If we don't explicty try to extract null and the value is null just return nil + case .null where !(null is U): return nil + default: return try f(self) + } + } +} + +extension Jar { + var contextDescription: String { + switch object { + case .none, .error: + return "" + default: + do { + return try String(json: self, prettyPrinted: false)//) ?? (self.toAny(context).map { "\($0)" } ?? "null") + } catch let error as LiftError { + return error.description + } catch { + return error.localizedDescription + } + } + } +} + +struct _Droppable: JarRepresentable { + let any: Any + + var jar: Jar { + return Jar(unchecked: any) + } +} + +/// Convert object hierarchy where all objects conform JarRepresentable to their "raw" prepresentation +private func convert(_ object: Any, context: Jar.Context) throws -> Any { + switch object { + case let val as Null: + return val + case let dictionary as [String: Any]: + var dict = [String: Any]() + for (key, value) in dictionary { + dict[key] = try convert(value, context: context) + } + return dict as Any + case let array as [Any]: + return try array.map { try convert($0, context: context) } + case let val as JarRepresentable: + return try val.asJar(using: context).object.unwrap(context) + default: + throw LiftError("Value does not conform to JarRepresentable", key: "", context: { "\(object)" }) + } +} + +private extension Jar { + var optional: Jar? { + if case .none = object { return nil } + return self + } + + func unwrap() throws -> Any { + return try object.unwrap(context) + } + + var objectName: String { + if case .error = object { return "error" } + do { + guard let val = try object.asAny(context) else { return "nil" } + return try val is Null ? "null" : "\((val as? Bool) ?? asAny() as Any)" + } catch { + return error.localizedDescription + } + } +} diff --git a/Lift/JarElement+Primitives.swift b/Lift/JarElement+Primitives.swift new file mode 100644 index 0000000..1de548b --- /dev/null +++ b/Lift/JarElement+Primitives.swift @@ -0,0 +1,234 @@ +// +// JarElement+Primitives.swift +// Lift +// +// Created by Måns Bernhardt on 2016-05-23. +// Copyright © 2016 iZettle. All rights reserved. +// + +import Foundation + + +extension String: JarConvertible, JarRepresentable { + public init(jar: Jar) throws { + switch try jar.asAny() { + case let val as String: self = val + case let val as NSNumber: self = val.description + default: throw jar.assertionFailedToConvert(to: String.self) + } + } + + public var jar: Jar { + return Jar(unchecked: self) + } +} + +extension NSString: JarRepresentable { + public var jar: Jar { + return Jar(unchecked: self) + } +} + +extension Bool: JarElement { + public init(jar: Jar) throws { + self = try jar.convert() + } + + public var jar: Jar { + return Jar(unchecked: NSNumber(value: self)) + } +} + +extension Int: JarElement { + public init(jar: Jar) throws { + self = try jar.assertFitsIn(Int(exactly: jar.int64())) + } + + public var jar: Jar { + return Jar(unchecked: NSNumber(value: self)) + } +} + +extension Int16: JarElement { + public init(jar: Jar) throws { + self = try jar.assertFitsIn(Int16(exactly: jar.int64())) + } + + public var jar: Jar { + return Jar(unchecked: NSNumber(value: self)) + } +} + +extension Int32: JarElement { + public init(jar: Jar) throws { + self = try jar.assertFitsIn(Int32(exactly: jar.int64())) + } + + public var jar: Jar { + return Jar(unchecked: NSNumber(value: self)) + } +} + +extension Int64: JarElement { + public init(jar: Jar) throws { + self = try jar.int64() + } + + public var jar: Jar { + return Jar(unchecked: NSNumber(value: self)) + } +} + +extension Double: JarElement { + public init(jar: Jar) throws { + self = try jar.convert() + } + + public var jar: Jar { + return Jar(unchecked: NSNumber(value: self)) + } +} + +extension Float: JarElement { + public init(jar: Jar) throws { + self = try jar.convert() + } + + public var jar: Jar { + return Jar(unchecked: NSNumber(value: self)) + } +} + +extension NSNumber: JarRepresentable { + public var jar: Jar { + switch self { + case is NSDecimalNumber: + return Jar(description) // use string representation to not lose precision + default: + return Jar(unchecked: self) + } + } +} + +extension Null: Liftable, JarRepresentable { + public static func lift(from jar: Jar) throws -> NSNull { + switch try jar.asAny() { + case let v as NSNull: + return v + default: + throw jar.assertionFailure("Value not convertible to NSNull") + } + } + + public var jar: Jar { + return Jar(unchecked: self) + } +} + +extension UUID: JarElement { + public init(jar: Jar) throws { + self = try jar.assertNotNil(UUID(uuidString: jar^), "Invalid UUID string") + } + + public var jar: Jar { + return Jar(uuidString) + } +} + +extension NSDecimalNumber: Liftable { + public static func lift(from jar: Jar) throws -> NSDecimalNumber { + switch try jar.asAny() { + case let v as NSDecimalNumber: + return v + case let v as NSNumber: + return NSDecimalNumber(string: v.description) + case let v as String: + return NSDecimalNumber(string: v) + default: + throw jar.assertionFailedToConvert(to: self) + } + } +} + +public extension DateFormatter { + @nonobjc static let iso8601: DateFormatter = { + let f = DateFormatter() + f.locale = Locale(identifier: "en_US_POSIX") + f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" + return f; + }() +} + +extension DateFormatter: JarContextValue {} + +extension Date: JarConvertible, JarRepresentableWithContext { + public init(jar: Jar) throws { + let formatter: DateFormatter = jar.context.get() ?? .iso8601 + self = try jar.assertNotNil(formatter.date(from: jar^), "Date failed to convert using formatter with dateFormat: \(formatter.dateFormat)") + } + + public func asJar(using context: Jar.Context) -> Jar { + let formatter: DateFormatter = context.get() ?? .iso8601 + return Jar(formatter.string(from: self)) + } +} + +extension URL: JarElement { + public init(jar: Jar) throws { + self = try jar.assertNotNil(URL(string: jar^), "Invalid URL") + } + + public var jar: Jar { + return Jar(absoluteString) + } +} + +public extension RawRepresentable where Self: JarElement, RawValue: JarElement, RawValue == RawValue.To { + init(jar: Jar) throws { + let value: RawValue = try jar^ + self = try jar.assertNotNil(Self(rawValue: value), "Could not find case matching raw value \(value) for enum \(Self.self)") + } + + var jar: Jar { + return Jar(rawValue) + } +} + +private extension Jar { + func convert() throws -> T { + guard let val = try (asAny() as? T) else { + throw jar.assertionFailedToConvert(to: T.self) + } + return val + } +} + +// Should be updated when new Swift Integers have been released. +private extension Jar { + func int64() throws -> Int64 { + switch try asAny() { + case let v as Int: + return Int64(v) + case let v as Int64: + return v + case let v as NSNumber: + guard let val = Int64(v.stringValue) else { throw assertionFailedToConvert(to: type(of: Int64.self)) } + return val + case let v as String: + guard let val = Int64(v) else { throw assertionFailedToConvert(to: type(of: Int64.self)) } + return val + default: + throw assertionFailedToConvert(to: type(of: Int64.self)) + } + } +} + +private extension Jar { + func assertionFailedToConvert(to type: T.Type) -> LiftError { + return assertionFailure("Value `\(contextDescription)` is not convertible to \(type)") + } + + func assertFitsIn(_ val: T?) throws -> T { + return try assertNotNil(val, "Value `\(contextDescription)` does not fit in \(T.self)") + } +} diff --git a/Lift/JarElement.swift b/Lift/JarElement.swift new file mode 100644 index 0000000..6d42add --- /dev/null +++ b/Lift/JarElement.swift @@ -0,0 +1,64 @@ + +// +// JarElement.swift +// Lift +// +// Created by Måns Bernhardt on 2016-05-23. +// Copyright © 2016 iZettle. All rights reserved. +// + +import Foundation + +/// Class types conforming to `JarConvertible` can by lifted out of a `Jar` using the ^ operator +/// Value types are recommended to use the more convenient `JarConvertible` instead +public protocol Liftable { + associatedtype To + static func lift(from jar: Jar) throws -> To +} + +/// Value types conforming to `JarConvertible` can by lifted out of a `Jar` using the ^ operator +public protocol JarConvertible: Liftable { + /// Construct `Self` from the content of the `jar` + init(jar: Jar) throws +} + +public extension JarConvertible where Self == To { + static func lift(from jar: Jar) throws -> Self { + return try Self(jar: jar) + } +} + +/// Conforming types can be expressed as a `Jar` +public protocol JarRepresentable { + /// Construct a `Jar` representing `self` + var jar: Jar { get } +} + +/// JarRepresentable & JarConvertible +public typealias JarElement = JarConvertible & JarRepresentable + +/// If you need access to the `Jar.Context` to be represented as a Jar conform to this protocol instead of JarRepresentable +public protocol JarRepresentableWithContext: JarRepresentable { + func asJar(using context: Jar.Context) -> Jar +} + +public extension JarRepresentableWithContext { + var jar: Jar { + return asJar(using: []) + } +} + +public extension JarRepresentable { + func asJar(using context: Jar.Context) -> Jar { + if let ctxSelf = self as? JarRepresentableWithContext { + return ctxSelf.asJar(using: context) + } + return jar.union(context: context) + } + + func asJar(using contextValues: JarContextValue?...) -> Jar { + return asJar(using: Jar.Context(contextValues)) + } +} + + diff --git a/Lift/LiftError.swift b/Lift/LiftError.swift new file mode 100644 index 0000000..a1a31ea --- /dev/null +++ b/Lift/LiftError.swift @@ -0,0 +1,93 @@ +// +// LiftError.swift +// Lift +// +// Created by Måns Bernhardt on 2017-04-03. +// Copyright © 2017 iZettle. All rights reserved. +// + +import Foundation + +/// `LiftError` holds besides a description of the Error the `key` (or path) to where the error occured. +public struct LiftError: Error, CustomStringConvertible { + /// The description of the error + public let description: String + + /// The key (path) to where the error occured. Helpful for debugging. + public let key: String + + /// The context jar. Helpful for debugging. + public var context: String { return _context() } + fileprivate let _context: () -> String +} + + +public extension LiftError { + /// Creates an instance with a `description`. + public init(_ description: String) { + self.init(description, key: "", context: { "" }) + } +} + +extension LiftError: CustomNSError { + public static var errorDomain: String { return "com.izettle.lift" } + public var errorUserInfo: [String : Any] { + return [NSLocalizedDescriptionKey: "LiftError(description: \(description), key: \(key), context: \(context))"] + } +} + +public extension Jar { + /// Will throw a `LiftError` using `self` to construct the error's key and context + func assertionFailure(_ description: @autoclosure () -> String = "Assertion failure") -> LiftError { + return LiftError(description(), context: self) + } + + /// Will throw a `LiftError` if `condition` is false using `self` to construct the error's key and context + func assert(_ condition: @autoclosure () throws -> Bool, _ description: @autoclosure () -> String = "Assertion failure") throws { + if try !condition() { + throw LiftError(description(), context: self) + } + } + + /// Will throw a `LiftError` if `val` is nil using `self` to construct the error's key and context + func assertNotNil(_ val: T?, _ description: @autoclosure () -> String = "Expected value missing") throws -> T { + switch val { + case let val?: + return val + case nil: + throw assertionFailure(description) + } + } +} + +extension Optional { + /// Will try to unwrap the `self` and throw a `LiftError` using `description` if unsuccessful + func assertNotNil(_ description: @autoclosure () -> String = "Expected value missing") throws -> Wrapped { + switch self { + case nil: + throw LiftError(description()) + case let val?: + return val + } + } +} + +extension LiftError { + init(error: LiftError, key: String, context jar: Jar) { + self.key = key.isEmpty ? error.key : key + ((error.key.isEmpty || error.key.hasPrefix("[")) ? error.key : ( "." + error.key)) + description = error.description + _context = { error.context.isEmpty ? jar.contextDescription : error.context } + } + + init(_ description: String, key: String, context: @escaping () -> String) { + self.key = key + self.description = description + _context = context + } + + init(_ description: String, key: String? = nil, context jar: Jar) { + self.init(description, key: key ?? jar.key(), context: { jar.contextDescription }) + } +} + + diff --git a/Lift/ValueForKey.swift b/Lift/ValueForKey.swift new file mode 100644 index 0000000..2169dcc --- /dev/null +++ b/Lift/ValueForKey.swift @@ -0,0 +1,72 @@ +// +// ValueForKey.swift +// Lift +// +// Created by Måns Bernhardt on 2016-05-23. +// Copyright © 2016 iZettle. All rights reserved. +// + +import Foundation + +/// Abstracts the access of `Any` for a `key` +/// Conforming types such `UserDefaults`' will allow lifting values out of them: +/// +/// let value: Int = UserDefaults.standard["value"]^ +public protocol ValueForKey { + func value(forKey key: String) -> Any? +} + +/// Abstracts the mutable access of `Any` for a `key` +/// Use this allow lifting and dropping of values out and into types such as UserDefaults +/// +/// UserDefaults.standard["value"] = 5 +public protocol MutatingValueForKey: ValueForKey { + mutating func set(_ value: Any?, forKey key: String) +} + +public extension ValueForKey { + subscript(key: String) -> JarRepresentable? { + return value(forKey: key).map(_Droppable.init) + } +} + +public extension MutatingValueForKey { + subscript(key: String) -> JarRepresentable? { + get { return value(forKey: key).map(_Droppable.init) } + set { try! set(newValue?.asJar(using: []).asAny(), forKey: key) } + } +} + +public extension MutatingValueForKey where Self: AnyObject { + subscript(key: String) -> JarRepresentable? { + get { return value(forKey: key).map(_Droppable.init) } + nonmutating set { + var s = self + try! s.set(newValue?.asJar(using: []).asAny(), forKey: key) + } + } +} + +public extension ValueForKey { + subscript(key: String) -> Jar { + return Jar(object: Jar.Object(value(forKey: key)), key: { key }) + } +} + +public extension MutatingValueForKey { + subscript(key: String) -> Jar { + get { return Jar(object: Jar.Object(value(forKey: key)), key: { key }) } + set { try! set(newValue.asAny(), forKey: key) } + } +} + +public extension MutatingValueForKey where Self: AnyObject { + subscript(key: String) -> Jar { + get { return Jar(object: Jar.Object(value(forKey: key)), key: { key }) } + nonmutating set { + var s = self + try! s.set(newValue.object.asAny([]), forKey: key) + } + } +} + diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..65489f2 --- /dev/null +++ b/Package.swift @@ -0,0 +1,5 @@ +import PackageDescription + +let package = Package( + name: "Lift" +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..31c66ae --- /dev/null +++ b/README.md @@ -0,0 +1,643 @@ +

+ +

+ +[![Build Status](https://travis-ci.org/iZettle/Lift.svg?branch=master)](https://travis-ci.org/iZettle/Lift) +[![Plaforms](https://img.shields.io/badge/platform-%20iOS%20|%20macOS%20|%20tvOS%20|%20linux-gray.svg)](https://img.shields.io/badge/platform-%20iOS%20|%20macOS%20|%20tvOS%20|%20linux-gray.svg) +[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +[![Swift Package Manager Compatible](https://img.shields.io/badge/SwiftPM-Compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) + +Lift is a Swift library for generating and extracting values into and out of JSON-like data structures. Lift was carefully designed to meet the following requirements: + +- Use easy and intuitive syntax using subscripting. +- Be extendable for use with your custom types. +- Support of retroactive modeling/conformance. +- Don't enforce how to structure your data models. +- Be type safe and explicit about errors. +- Work with any key value structured data such as p-lists and user defaults. +- Provide detailed errors and support custom validation. +- Use value semantics for the `Jar` container. + +### Example usage + +Lift is simple, yet powerful. Let's see how to use it with a custom type: + +```swift +struct User { + let name: String + let age: Int +} +``` + +Just conform to `JarElement` to let Lift know how to transform your type: + +```swift +extension User: JarElement { + init(jar: Jar) throws { + name = try jar["name"]^ + age = try jar["age"]^ + } + + var jar: Jar { + return ["name": name, "age": age] + } +} +``` + +Then given some JSON, you can now construct a `Jar` and extract users from it using the lift operator `^`: + +```swift +let json = "[{\"name\": \"Adam\", \"age\": 25}, {\"name\": \"Eve\", \"age\": 20}]" +let jar = try Jar(json: json) +var users: [User] = try jar^ +``` + +And it's as easy to move you model values back to JSON: + +```swift +users.append(User(name: "Junior", age: 2)) +let newJson = try String(json: Jar(users), prettyPrinted: true) +``` + +Lift will even work with other JSON-like structured data such as p-lists and `UserDefaults`: + +```swift +let users [User] = try UserDefaults.standard["users"]^ +``` + +Check the [Usage](#usage) section for more information and examples. + +### Contents: + +- [Requirements](#requirements) +- [Installation](#installation) +- [Usage](#usage) + - [Introduction](#introduction) + - [JSON Serialization](#json-serialization) + - [Generating JSON](#generating-json) + - [Modifying JSON](#modifiying-json) + - [Arrays](#arrays) + - [Missing values](#missing-values) + - [Heterogenuos values](#heterogenuos-values) + - [Transformation of values](#transformation-of-values) + - [Beyond JSON](#beyond-json) + - [Handling custom types](#handling-custom-types) + - [Model structure](#model-structure) + - [Handling errors](#handling-errors) + +## Requirements + +- Xcode `9.1+` +- Swift 4.0 +- Platforms: + * iOS `9.0+` + * macOS `10.11+` + * tvOS `9.0+` + * watchOS `2.0+` + * Linux + + +## Installation + +#### [Carthage](https://github.com/Carthage/Carthage) + +```shell +github "iZettle/Lift" >= 1.0 +``` + +#### [Cocoa Pods](https://github.com/CocoaPods/CocoaPods) + +```ruby +platform :ios, '9.0' +use_frameworks! + +target 'Your App Target' do + pod 'Lift', '~> 1.0' +end +``` + +#### [Swift Package Manager](https://github.com/apple/swift-package-manager) + +```swift +import PackageDescription + +let package = Package( + name: "Your Package Name", + dependencies: [ + .Package(url: "https://github.com/iZettle/Lift.git", + majorVersion: 1) + ] +) +``` + +## Note on `Codable` + +Swift 4 introduced `Codable` with the "promise" to have solved working with JSON once and for all. And yes, many of the examples shown are just close to magic. But when your models start to diverge from the simple one to one mapping between model and JSON, the magic seems to go away. Now you are back to implement everything yourself and this using a quite verbose API. The current version of Swift also lack APIs for building and parsing JSON on the fly (not going through model objects) which is common when e.g. building and parsing network requests. Hence, we believe the demand for third party JSON libraries will still be there for some time to come. + +## Usage + +### Introduction + +Let's start out with a simple example of how to extract data from some key value structured data such as JSON: + +```swift +let jar: Jar = ["name": "Lift", "version": 1.0] +let name: String = try jar["name"]^ +let version: Double = try jar["version"]^ +``` + +`Jar` is Lift's container of heterogenous values. In this example it holds a dictionary. The operator `^` (called the lift operator) is used to extract values out of the jar container. Because the jar typically holds values that are not known at compile time, extracting them might fail. This might happen if the value is missing, if the value is not of the expected type, or if some other validation is failing. This is why you will always see a `try` in the presence of the lift operator `^`. + +As mentioned, JSON does not always come in the form of a dictionary (key-values), but could also be simple primitive types or arrays of other JSON objects: + +```swift +let i: Int = try Jar(1)^ +let b: Bool = try Jar(true)^ +let a: [Int] = try Jar([1, 2, 3])^ +let jar: Jar = ["value": "lift"] +let s: String = try jar["value"]^ +``` + +The `^` operator is overloaded to allow conforming types to either be extracted as the type itself or as an optional version of it. You can also extract an array or optional array of conforming types: + +```swift +let i: Int = try jar^ +let i: Int? = try jar^ +let i: [Int] = try jar^ +let i: [Int]? = try jar^ +``` + +`Jar` implements subscripting for keys and indices and also allows them to be nested: + +```swift +let date: Date = try jar["payments"][3]["date"]^ +jar["payments"][2]["date"] = Date() +``` + + +### JSON serialization + +Lift adds convenience initializers to construct a `Jar` from JSON and back: + +```swift +let json = "{ \"val\": 3, \"vals\": [ 1, 2 ] }" +let jar = try Jar(json: json) +let jsonString = try String(json: jar, prettyPrinted: true) +let jsonData = try Data(json: jar, prettyPrinted: false) +``` + +You could also handle the serialization yourself and just pass an `Any` value: + +```swift +let json: Any = ... +let jar = try Jar(checked: json) // Will validate when constructed - slower +let jar = Jar(unchecked: json) // Will lazily validate at access - faster + +let any: Any = try jar.asAny() +``` + +### Generating JSON + +To help creating JSON, `Jar` implements several _expressible by literal_ protocols so you can write code like: + +```swift +func send(_ jar: Jar) { ... } + +send(true) +send(1) +send(3.14) +send("Hello") +send(["val": 5]) +send([1, 2, 3]) +``` + +When `Jar` can't be inferred by the complier, you can explicitly specify the type: + +```swift +let jar: Jar = ["val": 5] +let jar: Jar = [1, 2, 3] +``` + +When building nested hierarchies you have to cast nested non-primitive values[1](#f1): + +```swift +let jar: Jar = [5, ["val": [1, 2] as Jar] as Jar] // Use either `as` +let jar: Jar = [5, Jar(["val": Jar([1, 2])])] // or Jar(...) +``` + +And of course you can also build JSON from your custom types: + +```swift +let jar: Jar = ["payment": payment, "date": date] +``` + +### Modifying JSON + +Because `Jar` is a value type with value semantics, you can modify your `Jar` value when declared as `var`. + +```swift +var jar = Jar() +jar["payment"] = payment +jar["date"] = Date() +``` + +```swift +var jar = Jar() +jar.append(payment) +jar.append(Date()) +``` + +And if you need to modify your JSON before passing it on just make a copy: + +```swift +func receive(jar: Jar) { + var jar = jar + jar["timeReceived"] = Date() + // ... +} +``` + +A `Jar` is never bound to a specific type or value, hence it is always ok to change it: + +```swift +var jar: Jar = true // jar holds a boolean +jar = [1, 2] // holds an array +jar["val"] = 1 // holds a dictionary +jar = 4711 // holds an integer +jar = ["val2": 2] // holds a dictionary +jar = "Hello" // holds a string +``` + + +### Arrays + +Lift supports working with arrays of primitive types: + +```swift +var jar: Jar = [1, 2, 3] + +jar[1] = 4 +jar.append(5) + +let val: Int = try jar[2]^ +``` + +As well as arrays of your custom types: + +```swift +var jar: Jar = [Payment(...), Payment(...), ...] + +let payments: [Payment] = try jar^ +jar[2] = Payment(...) +``` + + +### Missing values and JSON null + +Sometimes the existence of a value is a requirement, sometimes it is optional. + +```swift +let i: Int = try jar["val"]^ // Will throw if val is missing or not an Int +let i: Int? = try jar["val"]^ // Will return nil if val is missing or null, else throw if not an Int +let i: Int = try jar["val"]^ ?? 4711 // Will throw if val is present and is not an Int +``` + +For your convenience Lift treats a value set to JSON null the same as a missing value. But if you need to check for the presence of the actual null value itself you can write: + +```swift +if let _: Null = try? jar["val"]^ { + //... +} +``` + +When building your JSON it is quite common that some values are optional: + +```swift +let optional: Int? = nil +var jar: Jar = ["val": 1] +jar["optional"] = optional // -> {"val": 1} +``` + +It is also possible to add your optional inline[1](#f1): + +```swift +var jar: Jar = ["val": 1, "optional": Jar(optional)] // -> {"val": 1} +``` + +If you actually want a JSON null value you can use the constant `null`: + +```swift +var jar: Jar = ["val": 1, "optional": optional ?? null] // -> {"val": 1, "optional": null} +``` + +### Heterogenous values + +Sometimes parts of your JSON could hold the union of different kinds of valid types. Then you could test between the different variations you support: + +```swift +let any: Any? = NSJSONSerialization... +let jar = try Jar(any) // any could be a dictionary, array or a primitive type + +if let int: Int = try? jar^ { + // ... +} else if let ints: [Int] = try? jar^ { + // ... +} else if let int: Int = try? jar["value"]^ { + // ... +} +``` + +JSON also supports arrays of mixed types: + +```swift +let jar = try Jar(any) // [ 1, [1, 2], { "val" : 3 } ] -- [Int, Array, Dictionary] + +let int: Int = try jar[0]^ +let array: [Int] = try jar[1]^ +let dict: Jar = try jar[2]^ +``` + +### Transformation of values + +You sometimes need to transform the values extracted from a `Jar` before using it. This might happen when you are working with types that cannot conform to `JarRepresentable`, such as when using tuples: + +```swift +typealias User = (name: String, age: Int) +let users: [User] = try jar.map { (jar: Jar) in try (jar["name"]^, jar["age"]^) } +``` + +Or when your type does not conform to `JarRepresentable`, as it might need some additional initialization data: + +```swift +let account: Account = ... + +let payments: [Payment] = try jar["payments"].map { jar in + Payment(jar: jar, account: account) +} +``` + +Even though you can use `map` for additional initialization data, often it is more convenient to add this data to the jar's context instead. Jar contexts will be described further down. + + +### Beyond JSON + +Setting and getting values of unknown types is not unique to JSON. Many Cocoa APIs use dictionaries and many of them are based on similar principles as JSON, such as p-lists. Lift provides protocols for extending these types to be able to access the power of Lift. E.g. Lift already extends UserDefaults: + +```swift +// extension UserDefaults: MutatingValueForKey { } + +let userDefaults = UserDefaults.standard + +let date: Date? = try userDefaults["lastLaunched"]^ +userDefaults["lastLaunched"] = Date() + +let payments: [Payments] = try userDefaults["payments"]^ ?? [] +``` + +### JarConvertible & JarRepresentable + +Out of the box, the Lift library supports JSON dictionaries, arrays and its primitive types: string, number, bool and null. But it's easy to extend your own types to work with the Lift library as well. + +To be able extract values out of a `Jar` using the lift operator `^`, you conform your type to the `JarConvertible` protocol: + +```swift +protocol JarConvertible { + init(jar: Jar) throws +} +``` + +And to be able to convert your type to a `Jar`, you conform your type to `JarRepresentable`: + +```swift +protocol JarRepresentable { + var jar: Jar { get } +} +``` + +It is common we want to implement both these protocols hence the convenience `JarElement` type alias: + +```swift +typealias JarElement = JarConvertible & JarRepresentable +``` + +The Lift library includes extensions for the most common primitive types such as `Int`, `Bool`, `String`, etc., by conforming them to `JarElement`. + + +### Handling custom types + +Your custom types are typically either simple types such as: + +```swift +struct Money { + let fractionized: Int +} + +extension Money: JarElement { + init(jar: Jar) throws { + fractionized = try jar^ + } + + var jar: Jar { return Jar(fractionized) } +} + +let jar: Jar = ["amount": Money(fractionized: 2000)] +let amount: Money = try jar["amount"]^ +``` + +Or perhaps more common, more complex and record like types such as: + +```swift +struct Payment { + let amount: Money + let date: Date +} + +extension Payment: JarElement { + init(jar: Jar) throws { + amount = try jar["amount"]^ + date = try jar["date"]^ + } + + var jar: Jar { + return ["amount": amount, "date": date] + } +} + +let jar: Jar = ["payment": Payment(...)] +let payment: Payment = try jar["payment"]^ +``` + +To make it easier to conform your custom enums with raw values, Lift comes with some default implementations. All you have to do is to conform the enum to `JarElement` to be able to use it with `Jar`s: + +```swift +enum MyEnum: String, JarElement { + case one, two, three +} + +let jar: Jar = ["enum": MyEnum.two] +let str: String = try jar["enum"]^ // -> "two" +let myEnum: MyEnum = try jar["enum"]^ // -> .two +``` + +If your type is a non-final class you can't retroactively conform it to `JarConvertible` as it requires you to implement a required init. Instead you have to use the `Liftable` protocol: + +```swift +extension MyClass: Liftable { + static func lift(from jar: Jar) throws -> MyClass { + // Implementation + } +} +``` + +### Model structure + +The Lift library does not enforce the structure of you custom types and also allows retroactive modeling. It is up to you how you decide to map between your types and JSON. For example you might have enums with associative values (in this example a recursive one): + +```swift +// [ { "type": "Product", "uuid": ”3b0bb980-2c…” }, +// { "type": "Folder", "name": "Coffee", "items": [ +// { "type": "Product", "uuid": ”3e493140-2c…” }, +// { "type": "Product", ”uuid": ”3e623780-2c…” }] }, +// ... ] + +indirect enum FlowLayout { + case product(uuid: UUID) + case folder(name: String, items: [FlowLayout]) +} +``` + +Because the JSON format has a weaker type-system than Swift, stricter validation becomes more important: + +```swift +extension FlowLayout: JarConvertible { + init(jar: Jar) throws { + switch try jar["type"]^ as String { + case "Product": + self = try .product(uuid: jar["uuid"]^) + case "Folder": + self = try .folder(name: jar["name"]^, items: jar["items"]^) + case let type: + throw jar.assertionFailure("Unknown layout type: \(type)") + } + } +} +``` + +Even for these more complex types, generation of JSON is still quite straightforward: + +```swift +extension FlowLayout: JarRepresentable { + var jar: Jar { + switch self { + case let .product(uuid): + return ["type": "Product", "uuid": uuid] + case let .folder(name, items): + return ["type": "Folder", "name": name, "items": Jar(items)] + } + } +} +``` + +### Handling errors + +Because JSON is typically nested, it is useful to extend errors with some positioning and context. Lift tries to keep track of the closest context and "key-path" into your data and will expose those in `LiftError`s: + +```swift +struct LiftError: Error { + let description: String + let key: String + let context: String +} +``` + +Because the context and key-path are really valuable during debugging, it's important to not loose those when throwing validation errors. Hence, Lift has added assert helper methods to `Jar` that you should use: + +```swift +init(jar: Jar) throws { + // ... + try jar.assert(i > 0, "Must greater than zero") + + guard validate(...) else { + throw jar.assertionFailure("Not a business nor a person") + } + + url = try jar.assertNotNil(URL(string: jar^), "Invalid URL") + // ... +} +``` + +### Jar context + +Sometimes your type's initializer needs access to more data than what is included in the JSON itself. E.g. perhaps your `Money` type needs a currency as well, but your JSON does not provide that or provides it far away from the actual amount value itself. This is where you can pass the currency in the jar's context instead: + +```swift +struct Money { + let fractionized: Int + let currency: Currency +} + +extension Money: JarElement { + init(jar: Jar) throws { + fractionized = try jar^ + currency = try jar.context.get() // will extract the currency + } + + var jar: Jar { return Jar(fractionized) } +} + +let amount: Money = try jar.union(context: currency)["amount"]^ +``` + +The jar's context is also useful for customizing the encoding and decoding of your types. E.g. `Date` will by default use the ISO8601 date format, but by providing another `DateFormatter` in the jar's context you could customize the date format: + +```swift +extension Date: JarConvertible, JarRepresentableWithContext { + init(jar: Jar) throws { + let formatter: DateFormatter = jar.context.get() ?? .iso8601 + self = try jar.assertNotNil(formatter.date(from: jar^), "Date failed to convert using formatter with dateFormat: \(formatter.dateFormat)") + } + + func asJar(using context: Jar.Context) -> Jar { + let formatter: DateFormatter = context.get() ?? .iso8601 + return Jar(formatter.string(from: self)) + } +} +``` + +As `JarRepresentable` does not provide any context, you will instead conform to `JarRepresentableWithContext` that passes the context in `asJar`: + +```swift +protocol JarRepresentableWithContext: JarRepresentable { + func asJar(using context: Jar.Context) -> Jar +} +``` + +The context could either be set externally or as part of some other type's encoding/decoding such as: + +```swift +struct Payment { + let amount: Money + let date: Date +} + +extension Payment: JarElement { + init(jar: Jar) throws { + let jar = jar.union(context: DateFormatter.custom) + amount = try jar["amount"]^ // a currency must be provided in the jar's context + date = try jar["date"]^ // date will format using DateFormatter.custom + } + + var jar: Jar { + let jar: Jar = ["amount": amount, "date": date] + return jar.union(context: DateFormatter.custom) + } +} + +let payment: Payment = try jar.union(context: currency)["payment"]^ +``` + +
+ +1 Currently arrays and optionals requires an explicit casting due to compiler limitations. When Swift adds support for conditional conformances this work-around should no longer be necessary. [↩](#a1) + + diff --git a/Tests/Info.plist b/Tests/Info.plist new file mode 100644 index 0000000..6c6c23c --- /dev/null +++ b/Tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Tests/LiftTests/IntegerTests.swift b/Tests/LiftTests/IntegerTests.swift new file mode 100644 index 0000000..a0ab752 --- /dev/null +++ b/Tests/LiftTests/IntegerTests.swift @@ -0,0 +1,31 @@ +// +// IntegerTests.swift +// Lift +// +// Created by Mattias Jähnke on 2017-05-10. +// Copyright © 2017 iZettle. All rights reserved. +// + +import XCTest +import Lift + +class IntegerTests: XCTestCase { + func testInteger() throws { + let maxInteger: Int = try Jar(Int.max)^ + XCTAssertEqual(maxInteger, Int.max) + } + + func testOverflowingInt16() throws { + let overflowInt: Int32 = Int32(Int16.max) + 1 + XCTAssertThrows(try Jar(overflowInt)^ as Int16) + XCTAssertEqual(try Jar(overflowInt)^ as Int32, overflowInt) + XCTAssertEqual(try Jar(overflowInt)^ as Int64, Int64(overflowInt)) + } + + func testOverflowingInteger32() throws { + let overflowInt: Int64 = Int64(Int32.max) + 1 + XCTAssertThrows(try Jar(overflowInt)^ as Int16) + XCTAssertThrows(try Jar(overflowInt)^ as Int32) + XCTAssertEqual(try Jar(overflowInt)^ as Int64, overflowInt) + } +} diff --git a/Tests/LiftTests/JarTests.swift b/Tests/LiftTests/JarTests.swift new file mode 100644 index 0000000..d5c979b --- /dev/null +++ b/Tests/LiftTests/JarTests.swift @@ -0,0 +1,808 @@ +// +// JarTests.swift +// JarTests +// +// Created by Måns Bernhardt on 2016-05-23. +// Copyright © 2016 iZettle. All rights reserved. +// + +import XCTest +import Lift +import Foundation + +infix operator ^ + +class JarTests: XCTestCase { + func testInteger() throws { + let original = 4711 + let value = Jar(original) + + let i1: Int = try value^ + XCTAssertEqual(i1, original) + + let i2: Int? = try value^ + XCTAssertEqual(i2, original) + + let i3 = try value^ as Int + XCTAssertEqual(i3, original) + + let i4: Int = try value.map { $0 * 2 } + XCTAssertEqual(i4, original*2) + + XCTAssertEqual(try value^ as Int, original) + XCTAssertEqual(try (value^ as Int) * 2, original*2) + + + var json: Jar = [ "val": original, "dict": [ "sub": 44 ] as Jar ] + + let i5: Int = try json["val"]^ + XCTAssertEqual(i5, original) + + json["val"] = 88 + let i52: Int = try json["val"]^ + XCTAssertEqual(i52, 88) + + let i6: Int = try json["dict"]["sub"]^ + XCTAssertEqual(i6, 44) + + json["dict"]["sub"] = 55 + let i7: Int = try json["dict"]["sub"]^ + XCTAssertEqual(i7, 55) + } + + func testDate() throws { + let original = try! fromIso8601("2016-05-23T10:35:52.046+02:00") + let value = Jar(original) + + let d1: Date = try value^ + XCTAssertEqual(d1, original) + let d2: Date = try value.map(fromIso8601) + XCTAssertEqual(d2, original) + + var json: Jar = ["date": original] + let d3: Date = try json["date"]^ + XCTAssertEqual(d3, original) + + let date = try! fromIso8601("2016-04-23T10:32:52.046+02:00") + json["date"] = date + let d4: Date = try json["date"]^ + XCTAssertEqual(d4, date) + + json["date"] = date.asIso8601 + let d5: Date = try json["date"]^ + XCTAssertEqual(d5, date) + } + + func testDecimalNumber() throws { + let d = NSDecimalNumber(string: "123456789.123456789") + let j = Jar(["amount": d]) + let json = try String(json: j, prettyPrinted: false) + XCTAssertEqual(json, "{\"amount\":\"123456789.123456789\"}") + + let d2: NSDecimalNumber = try j["amount"]^ + XCTAssertEqual(d, d2) + } + + func testURL() throws { + let jar = Jar("http://izettle.com") + let url = try jar.assertNotNil(URL(string: jar^), "Invalid URL") + + let _: URL = try jar^ + + XCTAssertThrows(try Jar("http://\\//izettle.com")^ as URL, isValidError: path("")) + + XCTAssertEqual(Jar(url).description, "http://izettle.com") + } + + struct Test: JarElement { + var val: Int = 47 + + init() {} + + init(jar: Jar) throws { + try val = jar["val"]^ + } + + var jar: Jar { + return ["val": val] + } + } + + func testStruct() throws { + var json: Jar = ["test": Test()] + let t1: Test = try json["test"]^ + XCTAssertEqual(t1.val, 47) + + json["test"]["val"] = 53 + + let t2: Test = try json["test"]^ + XCTAssertEqual(t2.val, 53) + + + let testsJson: Jar = [ Test(), Test() ] + let tests: Array = try testsJson.map { try Test(jar: $0) } + XCTAssertEqual(tests[0].val, 47) + } + + func testJar() throws { + let l1 = Jar(5) + + let i1: Int = try l1^ + XCTAssertEqual(i1, 5) + + let l2 = Jar(["value": 8]) + + let i2: Int = try l2["value"]^ + XCTAssertEqual(i2, 8) + + let l3 = Jar([7, 8, 9]) + + let i3: Int = try l3[2]^ + XCTAssertEqual(i3, 9) + + var json = Jar() + json["date"] = Date() + json["val"] = 5 + XCTAssertEqual(try json["val"]^ as Int, 5) + + var json2 = Jar() + json2.append(Date()) + json2.append(5) + XCTAssertEqual(try json2[1]^ as Int, 5) + + func log(_ json: Jar) { + var _json = json + _json["password"] = nil + print(_json) + } + + } + + func testArray() throws { + let l1 = Jar([5, 2, 3, 4]) + let a1: [Int] = try l1^ + XCTAssertEqual(a1[2], 3) + + let l2: Jar = [5, 2, 3, 4] + let a2: [Int] = try l2^ + XCTAssertEqual(a2[3], 4) + + var l3: Jar = [5, 2, 3, 4] + let a3: [Int] = try l3^ + XCTAssertEqual(a3[0], 5) + + l3[2] = 7 + let a33: [Int] = try l3^ + XCTAssertEqual(a33[2], 7) + + let json: Jar = ["key": Jar([ Jar(["val" : 1]), Jar(["val" : 2]) ])] + + let i1: Int = try json["key"][0]["val"]^ + XCTAssertEqual(i1, 1) + var json2 = json + json2["key"][0]["val"] = 48 + let i2: Int = try json2["key"][0]["val"]^ + XCTAssertEqual(i2, 48) + + let a4: Jar = ["1", "2"] + let i2s: [Int] = try a4.map { try Int($0).assertNotNil() } + XCTAssertEqual(i2s[1], 2) + + let i3s: [Int] = try a4.map { try Int($0).assertNotNil() } ?? [] + XCTAssertEqual(i3s[0], 1) + + let i4s: [Int] = try json["missing"].map { try Int($0).assertNotNil() } ?? [4, 5] + XCTAssertEqual(i4s[0], 4) + } + + func testDictionary() throws { + do { + let j1 = Jar(["1": 1, "2": 2, "3": 3]) + + let v1: [String: Int] = try j1^ + XCTAssertEqual(v1["1"], 1) + let v2: [String: Double]? = try j1^ + XCTAssertEqual(v2?["1"], 1.0) + + func sortTuple(lhs: (T, U), rhs: (T, U)) -> Bool where T: Comparable { + return lhs.0 < rhs.0 + } + let v3: [(String, String)] = try j1.map { $0.map { $0 }.sorted(by: sortTuple) } + XCTAssertTrue(v3.sorted(by: sortTuple)[1] == ("2", "2")) + + let v4: [(String, Bool)]? = try j1.map { $0.map { $0 }.sorted(by: sortTuple) } + XCTAssertTrue(v4![2] == ("3", true)) + + let v5: [Int: String] = try j1^ + XCTAssertEqual(v5[3], "3") + + } catch { + print(error) + } + } + + func testMapTuple() throws { + let jar: Jar = ["name": "Adam", "age": 25] + typealias NameAndAge = (name: String, age: Int) + let nameAndAge: NameAndAge = try jar.map { (jar: Jar) in try (jar["name"]^, jar["age"]^) } + XCTAssertEqual(nameAndAge.name, "Adam") + XCTAssertEqual(nameAndAge.age, 25) + } + + + func testUserDefaults() throws { + UserDefaults.standard["test"] = 4711 + let i: Int = try UserDefaults.standard["test"]^ + XCTAssertEqual(i, 4711) + UserDefaults.standard["test"] = "Hello" + let s: String = try UserDefaults.standard["test"]^ + XCTAssertEqual(s, "Hello") + + UserDefaults.standard["test"] = nil + UserDefaults.standard["test"] = Jar([1, 2, 4]) + UserDefaults.standard["test"] = [1, 2, 4] + + print(UserDefaults.standard.object(forKey: "test") as Any) + + UserDefaults.standard["test"] = Jar(["obj": Jar(["val": 4])]) + let i2: Int = try UserDefaults.standard["test"]["obj"]["val"]^ + XCTAssertEqual(i2, 4) + + +// let n = NSNotification(name: "Test", object: nil, userInfo: ["val": 45]) +// let ni: Int = try n["val"]^ +// XCTAssertEqual(ni, 45) + + } + + func testHeterogenous() throws { + let l1: Jar = try Jar(["val": 1])^ + let l2: Jar = try Jar(1)^ + let l3: Jar = try Jar([1, 2])^ + XCTAssertEqual(try l1["val"]^, 1) + XCTAssertEqual(try l2^, 1) + XCTAssertEqual(try l3^, [1, 2]) + + XCTAssertThrows { let _: Int? = try l2["val"]^ } + XCTAssertThrows { let _: Int? = try l2[3]^ } + XCTAssertThrows { let _: Int? = try l3[3]^ } + + let date = try! fromIso8601("2016-05-23T10:35:52.046+02:00") + let l5: Jar = [2, date, "String", NSNull()] + let v1: Int = try l5[0]^ + let v2: Date = try l5[1]^ + let v3: String = try l5[2]^ + let v4: NSNull = try l5[3]^ + let v5: Int? = try l5[3]^ + XCTAssertEqual(v1, 2) + XCTAssertEqual(v2, date) + XCTAssertEqual(v3, "String") + XCTAssertEqual(v4, NSNull()) + XCTAssertNil(v5) + + let json = Jar(5.5) + if let int: Int = try? json^ { + XCTAssertEqual(int, 5) + } else if let ints: [Int] = try? json^ { + XCTAssertEqual(ints[0], 4) + } else if let int: Int = try? json["value"]^ { + XCTAssertEqual(int, 4) + } + + print(try String(json: [5, ["val", [1, 2] as Jar] as Jar, Jar([2, 4])], prettyPrinted: false) as Any) + } + + enum MyEnum: String, JarElement { + case one, two, three + } + + func testEnum() throws { + let json: Jar = ["enum": MyEnum.two] + let str: String = try json["enum"]^ + XCTAssertEqual(str, "two") + let e: MyEnum = try json["enum"]^ + XCTAssertEqual(e, MyEnum.two) + } + + func testNull() throws { + var json: Jar = ["val": null] + + let str: String? = try json["val"]^ + XCTAssertNil(str) + + let null1: Null = try json["val"]^ + XCTAssertEqual(null1, null) + + json["val"] = nil + let null2: Null? = try json["val"]^ + XCTAssertNil(null2) + + json["val"] = null + let null3: Null? = try json["val"]^ + XCTAssertEqual(null3, null) + + let o1: Int? = 5 + let o2: Int? = nil + let json2: Jar = ["val1": Jar(o1), "val2": Jar(o2)] + let null4: Int = try json2["val1"]^ + XCTAssertEqual(null4, 5) + let null5: Int? = try json2["val2"]^ + XCTAssertNil(null5) + + do { + let json3: Jar = [Jar(o2), Jar(o1), Jar(o2)] + let null6: Int = try json3[0]^ + XCTAssertEqual(null6, 5) + XCTAssertThrows(try json3[1]^ as Int) + let ints: [Int] = try json3^ + XCTAssertEqual(ints.count, 1) + } + + do { + let json3: Jar = [Jar(o2), Jar(o1), Jar(o2), null] + let null6: Int = try json3[0]^ + XCTAssertEqual(null6, 5) + let null7: Int? = try json3[1]^ + XCTAssertNil(null7) + let null8: Null = try json3[1]^ + XCTAssertEqual(null8, null) + + XCTAssertThrows(try json3[1]^ as Int) + let ints: [Jar] = try json3^ + XCTAssertEqual(ints.count, 2) + } + + do { + let json3: Jar = [Jar(o2), Jar(o1), Jar(o2), Jar(null)] + let null6: Int = try json3[0]^ + XCTAssertEqual(null6, 5) + XCTAssertThrows(try json3[1]^ as Int) + let ints: [Jar] = try json3^ + XCTAssertEqual(ints.count, 2) + } + + let _: Null = try Jar(["val": null])["val"]^ + + if let _: Null = try? Jar(["val": 1])["val"]^ { + XCTAssert(false) + } else { + XCTAssert(true) + } + + if let _: Null = try? Jar(["val": null])["val"]^ { + XCTAssert(true) + } else { + XCTAssert(false) + } + } + + func testLiterals() throws { + var jsons = [Jar]() + func send(_ jar: Jar) { + jsons.append(jar) + } + +// send(nil) + send(true) + send(1) + send(3.14) + send("Hello") + send(["val", 5]) + send([1, 2, 3]) + + let json = Jar(jsons) + print(json) + } + + func testLiteralsWithAppend() throws { + var json: Jar = [] + json.append(null) + json.append(true) + json.append(1) + json.append(3.14) + json.append("Hello") + json.append(["val", 5]) + json.append([1, 2, 3]) + print(try String(json: json, prettyPrinted: false) as Any) + print(json) + } + + func testUnion() throws { + let jarA: Jar = ["val": 5, "val2": true] + let jarB: Jar = ["val": 6, "val3": "Hello"] + let union = jarA.union(jarB) + XCTAssertEqual(try union["val"]^, 6) + XCTAssertEqual(try union["val2"]^, true) + XCTAssertEqual(try union["val3"]^, "Hello") + + var union2 = jarB + union2.formUnion(jarA) + XCTAssertEqual(try union2["val"]^, 5) + XCTAssertEqual(try union2["val2"]^, true) + XCTAssertEqual(try union2["val3"]^, "Hello") + } + + func testIntegerBitSizes() throws { + let jar: Jar = 4711 + let i: Int = try jar^ + let i16: Int16 = try jar^ + let i32: Int32 = try jar^ + let i64: Int64 = try jar^ + _ = Jar(i) + _ = Jar(i16) + _ = Jar(i32) + _ = Jar(i64) + } + + func testErrorKeyPath() { + let jar: Jar = ["val": 5, "val2": Jar(["val3": 3]), "val4": Jar([1, 2, Jar([3, 4])]), "test": Test(), "tests": Jar([Test(), Test()])] + + XCTAssertThrows(try jar["val4"]^ as String, isValidError: path("val4")) + XCTAssertThrows(try jar["val4"]^ as Bool, isValidError: path("val4")) + XCTAssertThrows(try jar["val4"]^ as Null, isValidError: path("val4")) + XCTAssertThrows(try jar["val4"]^ as URL, isValidError: path("val4")) + + XCTAssertThrows(try jar["value"]^ as Date, isValidError: path("value")) + XCTAssertThrows(try jar["val"]^ as Date, isValidError: path("val")) + XCTAssertThrows(try jar["val"].map { $0 + 1 } as Date, isValidError: path("val")) + XCTAssertThrows(try jar["val2"]["value"]^ as Date, isValidError: path("val2.value")) + XCTAssertThrows(try jar["val2"]["val3"]^ as Date, isValidError: path("val2.val3")) + XCTAssertThrows(try jar["val4"][1]^ as Date, isValidError: path("val4[1]")) + XCTAssertThrows(try jar["val4"][4]^ as Date, isValidError: path("val4[4]")) + XCTAssertThrows(try jar["val4"][2][0]^ as Date, isValidError: path("val4[2][0]")) + XCTAssertThrows(try jar["val4"][2][3]^ as Date, isValidError: path("val4[2][3]")) + XCTAssertThrows(try jar["test"]["missing"]^ as Date, isValidError: path("test.missing")) + XCTAssertThrows(try jar["test"]["val"]^ as Date, isValidError: path("test.val")) + XCTAssertThrows(try jar["tests"][1]["missing"]^ as Date, isValidError: path("tests[1].missing")) + XCTAssertThrows(try jar["tests"][1]["val"]^ as Date, isValidError: path("tests[1].val")) + + XCTAssertThrows(try jar["val"]^ as Date?, isValidError: path("val")) + XCTAssertThrows(try jar["val2"]["val3"]^ as Date?, isValidError: path("val2.val3")) + XCTAssertThrows(try jar["val4"][1]^ as Date?, isValidError: path("val4[1]")) + XCTAssertThrows(try jar["val4"][4]^ as Date?, isValidError: path("val4[4]")) + XCTAssertThrows(try jar["val4"][2][0]^ as Date?, isValidError: path("val4[2][0]")) + XCTAssertThrows(try jar["val4"][2][3]^ as Date?, isValidError: path("val4[2][3]")) + + XCTAssertThrows(try jar["value"]^ as [Date], isValidError: path("value")) + XCTAssertThrows(try jar["val"]^ as [Date], isValidError: path("val")) + XCTAssertThrows(try jar["val2"]["value"]^ as [Date], isValidError: path("val2.value")) + XCTAssertThrows(try jar["val2"]["val3"]^ as [Date], isValidError: path("val2.val3")) + XCTAssertThrows(try jar["val4"][1]^ as [Date], isValidError: path("val4[1]")) + XCTAssertThrows(try jar["val4"][4]^ as [Date], isValidError: path("val4[4]")) + XCTAssertThrows(try jar["val4"][2][0]^ as [Date], isValidError: path("val4[2][0]")) + XCTAssertThrows(try jar["val4"][2][3]^ as [Date], isValidError: path("val4[2][3]")) + XCTAssertThrows(try jar["val4"][2]^ as [Date], isValidError: path("val4[2][0]")) + XCTAssertThrows(try jar["tests"][1]["missing"]^ as [Date], isValidError: path("tests[1].missing")) + XCTAssertThrows(try jar["tests"][1]["val"]^ as [Date], isValidError: path("tests[1].val")) + + + let arrayJar: Jar = [1, 2] + XCTAssertThrows(try arrayJar^ as [Date], isValidError: path("[0]")) + XCTAssertThrows(try arrayJar^ as [Date]?, isValidError: path("[0]")) + XCTAssertThrows(try arrayJar.map { $0 } as [Date], isValidError: path("[0]")) + XCTAssertThrows(try arrayJar.map { $0 } as [Date]?, isValidError: path("[0]")) + + + XCTAssertThrows(isValidError: path("val")) { + let _: Date = try jar["val"].map { (_: Int) -> Date in throw LiftError("Custom Error") } + return + } + + var jar1 = jar + jar1["tests"].append(["f": 1]) + XCTAssertThrows(isValidError: path("tests[2].val")) { + let _: [Test] = try jar1["tests"].map { try Test(jar: $0) } + return + } + + XCTAssertThrows(isValidError: path("tests[2].val")) { + let jars: [Jar] = try jar1["tests"]^ + let _: Int = try jars[2]["val"]^ + return + } + + XCTAssertThrows(isValidError: path("tests[2].val")) { + let _: [Test]? = try jar1["tests"].map { try Test(jar: $0) } + return + } + + XCTAssertThrows(isValidError: path("tests[2].val")) { + let jars: [Jar]? = try jar1["tests"]^ + let _: Int = try jars![2]["val"]^ + return + } + + XCTAssertThrows(isValidError: path("val")) { + let _: [String: Int] = try jar1["val"]^ + } + + XCTAssertThrows(isValidError: path("val")) { + let _: [String: Int]? = try jar1["val"]^ + } + + XCTAssertThrows(isValidError: path("val")) { + let _: String = try jar1["val"].map { $0[2]! } + } + + XCTAssertThrows(isValidError: path("val")) { + let _: Int? = try jar1["val"].map { $0[2]! } + } + + XCTAssertThrows(isValidError: path("tests[1]")) { + throw jar1["tests"][1].assertionFailure() + } + + let jar2: Jar = jar1["tests"] + XCTAssertThrows(isValidError: path("tests")) { + throw jar2.assertionFailure() + } + + XCTAssertThrows(isValidError: path("tests[1]")) { + throw jar2[1].assertionFailure() + } + } + + func testJSONDeserialization() throws { + let dictJar = try Jar(json: "{ \"val\": 3, \"vals\": [ 1, 2 ] }") + XCTAssertEqual(try dictJar["val"]^, 3) + XCTAssertEqual(try dictJar["vals"]^, [1, 2]) + + let arrayJar = try Jar(json: "[ 1, 2 ]") + XCTAssertEqual(try arrayJar^, [1, 2]) + XCTAssertEqual(try arrayJar[0]^, 1) + + let boolJar = try Jar(json: "true") + XCTAssertEqual(try boolJar^, true) + } + + func testJSONSserialization() throws { + XCTAssertEqual(Jar(true).description, "true") + XCTAssertEqual(Jar(4711).description, "4711") + XCTAssertEqual(Jar(47.11).description, "47.11") + XCTAssertEqual(Jar(null).description, "null") + XCTAssertEqual(Jar("Hello").description, "Hello") + XCTAssertEqual(try String(json: Jar([1, 2]), prettyPrinted: false), "[1,2]") + try XCTAssertEqual(String(json: Jar(["val": 2]), prettyPrinted: false), "{\"val\":2}") + + var jar: Jar = true + XCTAssertEqual(jar.description, "true") + jar = false + XCTAssertEqual(jar.description, "false") + jar = [1, 2] + try XCTAssertEqual(String(json: jar, prettyPrinted: false), "[1,2]") + jar["val"] = 1 + XCTAssertThrows(try String(json: jar, prettyPrinted: false)) // Can't set with key on array + jar = ["val2": 2] + try XCTAssertEqual(String(json: jar, prettyPrinted: false), "{\"val2\":2}") + jar = 4711 + XCTAssertEqual(jar.description, "4711") + jar = 47.25 + XCTAssertEqual(jar.description, "47.25") + jar = "Hello" + XCTAssertEqual(jar.description, "Hello") + + } + + func testChecked() throws { + try XCTAssertEqual(Jar(checked: 2).description, "2") + try XCTAssertEqual(String(json: Jar(checked: ["val": 2]), prettyPrinted: false), "{\"val\":2}") + XCTAssertThrows(try Jar(checked: ["val", JarTests()])) + } + + func testUnchecked() throws { + XCTAssertEqual(Jar(unchecked: 2).description, "2") + try XCTAssertEqual(String(json: Jar(unchecked: ["val": 2]), prettyPrinted: false), "{\"val\":2}") + XCTAssertThrows { let _: Int = try Jar(unchecked: ["val": JarTests()])["val"]["int"]^ } + XCTAssertThrows { let _: Int = try Jar(unchecked: ["val", JarTests()])["val"]["int"]^ } + } + + func testPayment() throws { + do { + let json = "[{\"amount\": 1000, \"date\": \"2016-05-23T10:35:52.0+02:00\"}, {\"amount\": 200, \"date\": \"2016-05-25T12:10:22.0+02:00\"}]" + + let jar = try Jar(json: json) + + var payments: [Payment] = try jar^ + payments[1].date = Date() + + let newJson = try String(json: Jar(payments), prettyPrinted: true) + print(newJson) + } catch { + print(error) + throw error + } + } + + func testUser() throws { + do { + let json = "[{\"name\": \"Adam\", \"age\": 25}, {\"name\": \"Eve\", \"age\": 20}]" + + let jar = try Jar(json: json) + + var users: [User] = try jar^ + users.append(User(name: "Junior", age: 2)) + + let newJson = try String(json: Jar(users), prettyPrinted: true) + print(newJson) + } catch { + print(error) + throw error + } + } + + func testAsDictionary() throws { + let jar: Jar = ["a": 1, "b": 1.1, "c": "1", "d": "1.1", "e": "str"] + let d = jar.dictionary! + XCTAssert(d["a"] is Int) + XCTAssert(d["b"] is Double) + XCTAssert(d["c"] is String) + XCTAssert(d["d"] is String) + XCTAssert(d["e"] is String) + } + + func testJarContext() throws { + var jar = Jar(5) + XCTAssertThrows(try jar^ as NeedContextType) + XCTAssertThrows(try jar.union(context: MyContext(8))^ as NeedContextType) + let _: NeedContextType = try jar.union(context: MyContext())^ + jar.context.formUnion(MyContext()) + let _: NeedContextType = try jar^ + + jar = Jar(NeedContextType()) + + XCTAssertThrows(try String(json: jar)) + print(try String(json: jar.union(context: MyContext()))) + } + + func testSetsJarContext() throws { + var jar = Jar(5) + let _: SetsContextType = try jar.union(context: MyContext())^ + let _: SetsContextType = try jar.union(context: MyContext(8))^ + + jar = Jar(SetsContextType()) + print(try String(json: jar)) + print(try String(json: jar.union(context: MyContext(8)))) + } + + func testJarArrayContext() throws { + let jar = Jar([SetsContextType(), SetsContextType()]) + print(try String(json: jar)) + } +} + +struct MyContext: JarContextValue { + static let `default` = 4 + let value: Int + init(_ val: Int = MyContext.default) { value = val } +} + +struct NeedContextType { + var value: Int = 4711 +} + +extension NeedContextType: JarRepresentableWithContext, JarConvertible { + init(jar: Jar) throws { + try jar.assert((try jar.context.get() as MyContext).value == MyContext.default) + value = try jar^ + } + + func asJar(using context: Jar.Context) -> Jar { + let myCtx: MyContext? = context.get() + guard myCtx != nil else { return Jar(error: LiftError("Missing context")) } + return Jar(value) + } +} + +struct SetsContextType { + var value = NeedContextType() +} + +extension SetsContextType: JarElement { + init(jar: Jar) throws { + let jar = jar.union(context: MyContext()) + value = try jar^ + } + + var jar: Jar { + let jar = Jar(value) + return jar.union(context: MyContext()) + } +} + + + +struct Money { + let fractionized: Int +} + +extension Money: JarElement { + init(jar: Jar) throws { + fractionized = try jar^ + } + + var jar: Jar { return Jar(fractionized) } +} + +struct Payment { + var amount: Money + var date: Date +} + +extension Payment: JarElement { + init(jar: Jar) throws { + amount = try jar["amount"]^ + date = try jar["date"]^ + } + + var jar: Jar { + return ["amount": amount, "date": date] + } +} + + +struct User { + let name: String + let age: Int +} + +extension User: JarElement { + init(jar: Jar) throws { + name = try jar["name"]^ + age = try jar["age"]^ + } + + var jar: Jar { + return ["name": name, "age": age] + } +} + + + +func XCTAssertThrows(_ message: String = "", file: StaticString = #file, line: UInt = #line, isValidError: ((Error) -> Bool)? = nil, expression: () throws -> T) { + do { + _ = try expression() + XCTFail("No error to catch! - \(message)", file: file, line: line) + } catch { + print("Error:", error) + if let isValidError = isValidError { + let isValid = isValidError(error) + if !isValid { + print("validation failed: ", error.localizedDescription) + } + XCTAssertTrue(isValid, "Invalid error: \(error.localizedDescription)") + } + } +} + +func XCTAssertThrows(_ expression: @autoclosure () throws -> T, _ message: String = "", file: StaticString = #file, line: UInt = #line, isValidError: ((Error) -> Bool)? = nil) { + XCTAssertThrows(message, file: file, line: line, isValidError: isValidError, expression: expression) +} + + +func path(_ path: String) -> (Error) -> Bool { + return { error in + guard case let e as LiftError = error else { return false } + return e.key == path + } +} + +extension Optional { + /// Will try to unwrap the `self` and throw a `LiftError` using `description` if unsuccessful + func assertNotNil(_ description: @autoclosure () -> String = "Expected value missing") throws -> Wrapped { + switch self { + case nil: + throw LiftError(description()) + case let val?: + return val + } + } +} + + +public let fromIso8601 = { val in try DateFormatter.iso8601.date(from: val).assertNotNil("Invalid ISO8601 date") } + +public extension Date { + var asIso8601: String { + return DateFormatter.iso8601.string(from: self) + } +} diff --git a/lift-logo.png b/lift-logo.png new file mode 100644 index 0000000..8fd4a4f Binary files /dev/null and b/lift-logo.png differ