├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── workflows │ ├── build_and_test.yml │ ├── build_example_project.yml │ └── deploy_documentation.yml ├── .gitignore ├── Assets └── logo.png ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Example │ ├── App.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ └── ContentView.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── SwiftUI │ └── View+validated.swift ├── Validatable │ └── Validatable.swift ├── Validated.swift └── Validation │ ├── Validation+BinaryInteger.swift │ ├── Validation+Collection.swift │ ├── Validation+Comparable.swift │ ├── Validation+ComparisonOperators.swift │ ├── Validation+Constant.swift │ ├── Validation+Equatable.swift │ ├── Validation+KeyPath.swift │ ├── Validation+LogicalOperators.swift │ ├── Validation+Sequence.swift │ ├── Validation+String.swift │ └── Validation.swift └── Tests ├── ValidationCollectionTests.swift ├── ValidationComparableTests.swift ├── ValidationEquatableTests.swift ├── ValidationSequenceTests.swift └── ValidationStringTests.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: SvenTiigi 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | labels: ["bug"] 4 | assignees: 5 | - SvenTiigi 6 | body: 7 | - type: textarea 8 | id: bug-description 9 | attributes: 10 | label: What happened? 11 | description: Please describe the bug. 12 | placeholder: Description of the bug. 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: steps-to-reproduce 17 | attributes: 18 | label: What are the steps to reproduce? 19 | description: Please describe the steps to reproduce the bug. 20 | placeholder: | 21 | Step 1: ... 22 | Step 2: ... 23 | Step 3: ... 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: expected-behavior 28 | attributes: 29 | label: What is the expected behavior? 30 | description: Please describe the behavior you expect of ValidatedPropertyKit. 31 | placeholder: I expect that ValidatedPropertyKit would... 32 | validations: 33 | required: true 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | labels: ["feature"] 4 | assignees: 5 | - SvenTiigi 6 | body: 7 | - type: textarea 8 | id: problem 9 | attributes: 10 | label: Is your feature request related to a problem? 11 | description: A clear and concise description of what the problem is. 12 | placeholder: Yes, the problem is that... 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: solution 17 | attributes: 18 | label: What solution would you like? 19 | description: A clear and concise description of what you want to happen. 20 | placeholder: I would like that... 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: alternatives 25 | attributes: 26 | label: What alternatives have you considered? 27 | description: A clear and concise description of any alternative solutions or features you've considered. 28 | placeholder: I have considered to... 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: context 33 | attributes: 34 | label: Any additional context? 35 | description: Add any other context or screenshots about the feature request here. 36 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - 'Sources/**' 8 | - 'Tests/**' 9 | - '!Sources/Documentation.docc/**' 10 | pull_request: 11 | paths: 12 | - 'Sources/**' 13 | - 'Tests/**' 14 | - '!Sources/Documentation.docc/**' 15 | 16 | jobs: 17 | iOS: 18 | name: Build and test on iOS 19 | runs-on: macOS-12 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Build 23 | run: xcodebuild build-for-testing -scheme ValidatedPropertyKit -destination 'platform=iOS Simulator,name=iPhone 14' 24 | - name: Test 25 | run: xcodebuild test-without-building -scheme ValidatedPropertyKit -destination 'platform=iOS Simulator,name=iPhone 14' 26 | macOS: 27 | name: Build and test on macOS 28 | runs-on: macos-latest 29 | steps: 30 | - uses: actions/checkout@v3 31 | - name: Build 32 | run: swift build -v 33 | - name: Test 34 | run: swift test -v 35 | watchOS: 36 | name: Build and test on watchOS 37 | runs-on: macOS-12 38 | steps: 39 | - uses: actions/checkout@v3 40 | - name: Build 41 | run: xcodebuild build-for-testing -scheme ValidatedPropertyKit -destination 'platform=watchOS Simulator,name=Apple Watch Series 8 (45mm)' 42 | - name: Test 43 | run: xcodebuild test-without-building -scheme ValidatedPropertyKit -destination 'platform=watchOS Simulator,name=Apple Watch Series 8 (45mm)' 44 | tvOS: 45 | name: Build and test on tvOS 46 | runs-on: macOS-12 47 | steps: 48 | - uses: actions/checkout@v3 49 | - name: Build 50 | run: xcodebuild build-for-testing -scheme ValidatedPropertyKit -destination 'platform=tvOS Simulator,name=Apple TV' 51 | - name: Test 52 | run: xcodebuild test-without-building -scheme ValidatedPropertyKit -destination 'platform=tvOS Simulator,name=Apple TV' 53 | -------------------------------------------------------------------------------- /.github/workflows/build_example_project.yml: -------------------------------------------------------------------------------- 1 | name: Build Example Project 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - 'Example/**' 8 | - 'Sources/**' 9 | pull_request: 10 | paths: 11 | - 'Example/**' 12 | - 'Sources/**' 13 | 14 | jobs: 15 | build: 16 | name: Build example project 17 | runs-on: macOS-12 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | - name: Build 22 | run: xcodebuild build -project Example/Example.xcodeproj -scheme Example -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' 23 | -------------------------------------------------------------------------------- /.github/workflows/deploy_documentation.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | build: 20 | runs-on: macos-12 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | - name: Setup Pages 25 | uses: actions/configure-pages@v2 26 | - name: Build Documentation 27 | run: | 28 | xcodebuild docbuild\ 29 | -scheme ValidatedPropertyKit\ 30 | -destination 'generic/platform=iOS'\ 31 | -derivedDataPath ../DerivedData 32 | - name: Process Archive 33 | run: | 34 | mkdir _site 35 | $(xcrun --find docc) process-archive \ 36 | transform-for-static-hosting ../DerivedData/Build/Products/Debug-iphoneos/ValidatedPropertyKit.doccarchive \ 37 | --output-path _site \ 38 | --hosting-base-path ValidatedPropertyKit 39 | - name: Create Custom index.html 40 | run: | 41 | rm _site/index.html 42 | cat > _site/index.html <<- EOM 43 | 44 | 45 | 46 | 47 | 48 | 49 |

Please follow this link.

50 | 51 | 52 | EOM 53 | - name: Upload Artifact 54 | uses: actions/upload-pages-artifact@v1 55 | 56 | deploy: 57 | environment: 58 | name: github-pages 59 | url: ${{ steps.deployment.outputs.page_url }} 60 | runs-on: ubuntu-latest 61 | needs: build 62 | steps: 63 | - name: Deploy to GitHub Pages 64 | id: deployment 65 | uses: actions/deploy-pages@v1 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc -------------------------------------------------------------------------------- /Assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SvenTiigi/ValidatedPropertyKit/632978d0fe4e4962b3f88f0d53adc61fe1019072/Assets/logo.png -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3D5D898A281D511A00DD9301 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D5D8989281D511A00DD9301 /* Assets.xcassets */; }; 11 | 3D7BB5D827A46B8C009D4145 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7BB5D727A46B8C009D4145 /* App.swift */; }; 12 | 3D7BB5DA27A46B8C009D4145 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7BB5D927A46B8C009D4145 /* ContentView.swift */; }; 13 | 3D7BB5E827A46BA4009D4145 /* ValidatedPropertyKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3D7BB5E727A46BA4009D4145 /* ValidatedPropertyKit */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 3D5D8989281D511A00DD9301 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 18 | 3D7BB5D427A46B8C009D4145 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 3D7BB5D727A46B8C009D4145 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 20 | 3D7BB5D927A46B8C009D4145 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 21 | 3D7BB5E527A46B9F009D4145 /* ValidatedPropertyKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ValidatedPropertyKit; path = ..; sourceTree = ""; }; 22 | /* End PBXFileReference section */ 23 | 24 | /* Begin PBXFrameworksBuildPhase section */ 25 | 3D7BB5D127A46B8C009D4145 /* Frameworks */ = { 26 | isa = PBXFrameworksBuildPhase; 27 | buildActionMask = 2147483647; 28 | files = ( 29 | 3D7BB5E827A46BA4009D4145 /* ValidatedPropertyKit in Frameworks */, 30 | ); 31 | runOnlyForDeploymentPostprocessing = 0; 32 | }; 33 | /* End PBXFrameworksBuildPhase section */ 34 | 35 | /* Begin PBXGroup section */ 36 | 3D7BB5CB27A46B8C009D4145 = { 37 | isa = PBXGroup; 38 | children = ( 39 | 3D7BB5D627A46B8C009D4145 /* Example */, 40 | 3D7BB5D527A46B8C009D4145 /* Products */, 41 | 3D7BB5E627A46BA4009D4145 /* Frameworks */, 42 | 3D7BB5E527A46B9F009D4145 /* ValidatedPropertyKit */, 43 | ); 44 | sourceTree = ""; 45 | }; 46 | 3D7BB5D527A46B8C009D4145 /* Products */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | 3D7BB5D427A46B8C009D4145 /* Example.app */, 50 | ); 51 | name = Products; 52 | sourceTree = ""; 53 | }; 54 | 3D7BB5D627A46B8C009D4145 /* Example */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 3D7BB5D727A46B8C009D4145 /* App.swift */, 58 | 3D7BB5D927A46B8C009D4145 /* ContentView.swift */, 59 | 3D5D8989281D511A00DD9301 /* Assets.xcassets */, 60 | ); 61 | path = Example; 62 | sourceTree = ""; 63 | }; 64 | 3D7BB5E627A46BA4009D4145 /* Frameworks */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | ); 68 | name = Frameworks; 69 | sourceTree = ""; 70 | }; 71 | /* End PBXGroup section */ 72 | 73 | /* Begin PBXNativeTarget section */ 74 | 3D7BB5D327A46B8C009D4145 /* Example */ = { 75 | isa = PBXNativeTarget; 76 | buildConfigurationList = 3D7BB5E227A46B8D009D4145 /* Build configuration list for PBXNativeTarget "Example" */; 77 | buildPhases = ( 78 | 3D7BB5D027A46B8C009D4145 /* Sources */, 79 | 3D7BB5D127A46B8C009D4145 /* Frameworks */, 80 | 3D7BB5D227A46B8C009D4145 /* Resources */, 81 | ); 82 | buildRules = ( 83 | ); 84 | dependencies = ( 85 | ); 86 | name = Example; 87 | packageProductDependencies = ( 88 | 3D7BB5E727A46BA4009D4145 /* ValidatedPropertyKit */, 89 | ); 90 | productName = Example; 91 | productReference = 3D7BB5D427A46B8C009D4145 /* Example.app */; 92 | productType = "com.apple.product-type.application"; 93 | }; 94 | /* End PBXNativeTarget section */ 95 | 96 | /* Begin PBXProject section */ 97 | 3D7BB5CC27A46B8C009D4145 /* Project object */ = { 98 | isa = PBXProject; 99 | attributes = { 100 | BuildIndependentTargetsInParallel = 1; 101 | LastSwiftUpdateCheck = 1320; 102 | LastUpgradeCheck = 1320; 103 | TargetAttributes = { 104 | 3D7BB5D327A46B8C009D4145 = { 105 | CreatedOnToolsVersion = 13.2.1; 106 | }; 107 | }; 108 | }; 109 | buildConfigurationList = 3D7BB5CF27A46B8C009D4145 /* Build configuration list for PBXProject "Example" */; 110 | compatibilityVersion = "Xcode 13.0"; 111 | developmentRegion = en; 112 | hasScannedForEncodings = 0; 113 | knownRegions = ( 114 | en, 115 | Base, 116 | ); 117 | mainGroup = 3D7BB5CB27A46B8C009D4145; 118 | productRefGroup = 3D7BB5D527A46B8C009D4145 /* Products */; 119 | projectDirPath = ""; 120 | projectRoot = ""; 121 | targets = ( 122 | 3D7BB5D327A46B8C009D4145 /* Example */, 123 | ); 124 | }; 125 | /* End PBXProject section */ 126 | 127 | /* Begin PBXResourcesBuildPhase section */ 128 | 3D7BB5D227A46B8C009D4145 /* Resources */ = { 129 | isa = PBXResourcesBuildPhase; 130 | buildActionMask = 2147483647; 131 | files = ( 132 | 3D5D898A281D511A00DD9301 /* Assets.xcassets in Resources */, 133 | ); 134 | runOnlyForDeploymentPostprocessing = 0; 135 | }; 136 | /* End PBXResourcesBuildPhase section */ 137 | 138 | /* Begin PBXSourcesBuildPhase section */ 139 | 3D7BB5D027A46B8C009D4145 /* Sources */ = { 140 | isa = PBXSourcesBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | 3D7BB5DA27A46B8C009D4145 /* ContentView.swift in Sources */, 144 | 3D7BB5D827A46B8C009D4145 /* App.swift in Sources */, 145 | ); 146 | runOnlyForDeploymentPostprocessing = 0; 147 | }; 148 | /* End PBXSourcesBuildPhase section */ 149 | 150 | /* Begin XCBuildConfiguration section */ 151 | 3D7BB5E027A46B8D009D4145 /* Debug */ = { 152 | isa = XCBuildConfiguration; 153 | buildSettings = { 154 | ALWAYS_SEARCH_USER_PATHS = NO; 155 | CLANG_ANALYZER_NONNULL = YES; 156 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 157 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 158 | CLANG_CXX_LIBRARY = "libc++"; 159 | CLANG_ENABLE_MODULES = YES; 160 | CLANG_ENABLE_OBJC_ARC = YES; 161 | CLANG_ENABLE_OBJC_WEAK = YES; 162 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 163 | CLANG_WARN_BOOL_CONVERSION = YES; 164 | CLANG_WARN_COMMA = YES; 165 | CLANG_WARN_CONSTANT_CONVERSION = YES; 166 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 167 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 168 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 169 | CLANG_WARN_EMPTY_BODY = YES; 170 | CLANG_WARN_ENUM_CONVERSION = YES; 171 | CLANG_WARN_INFINITE_RECURSION = YES; 172 | CLANG_WARN_INT_CONVERSION = YES; 173 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 174 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 175 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 176 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 177 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 178 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 179 | CLANG_WARN_STRICT_PROTOTYPES = YES; 180 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 181 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 182 | CLANG_WARN_UNREACHABLE_CODE = YES; 183 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 184 | COPY_PHASE_STRIP = NO; 185 | DEBUG_INFORMATION_FORMAT = dwarf; 186 | ENABLE_STRICT_OBJC_MSGSEND = YES; 187 | ENABLE_TESTABILITY = YES; 188 | GCC_C_LANGUAGE_STANDARD = gnu11; 189 | GCC_DYNAMIC_NO_PIC = NO; 190 | GCC_NO_COMMON_BLOCKS = YES; 191 | GCC_OPTIMIZATION_LEVEL = 0; 192 | GCC_PREPROCESSOR_DEFINITIONS = ( 193 | "DEBUG=1", 194 | "$(inherited)", 195 | ); 196 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 197 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 198 | GCC_WARN_UNDECLARED_SELECTOR = YES; 199 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 200 | GCC_WARN_UNUSED_FUNCTION = YES; 201 | GCC_WARN_UNUSED_VARIABLE = YES; 202 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 203 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 204 | MTL_FAST_MATH = YES; 205 | ONLY_ACTIVE_ARCH = YES; 206 | SDKROOT = iphoneos; 207 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 208 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 209 | }; 210 | name = Debug; 211 | }; 212 | 3D7BB5E127A46B8D009D4145 /* Release */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | ALWAYS_SEARCH_USER_PATHS = NO; 216 | CLANG_ANALYZER_NONNULL = YES; 217 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 218 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 219 | CLANG_CXX_LIBRARY = "libc++"; 220 | CLANG_ENABLE_MODULES = YES; 221 | CLANG_ENABLE_OBJC_ARC = YES; 222 | CLANG_ENABLE_OBJC_WEAK = YES; 223 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 224 | CLANG_WARN_BOOL_CONVERSION = YES; 225 | CLANG_WARN_COMMA = YES; 226 | CLANG_WARN_CONSTANT_CONVERSION = YES; 227 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 228 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 229 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 230 | CLANG_WARN_EMPTY_BODY = YES; 231 | CLANG_WARN_ENUM_CONVERSION = YES; 232 | CLANG_WARN_INFINITE_RECURSION = YES; 233 | CLANG_WARN_INT_CONVERSION = YES; 234 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 235 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 236 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 237 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 238 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 239 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 240 | CLANG_WARN_STRICT_PROTOTYPES = YES; 241 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 242 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 243 | CLANG_WARN_UNREACHABLE_CODE = YES; 244 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 245 | COPY_PHASE_STRIP = NO; 246 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 247 | ENABLE_NS_ASSERTIONS = NO; 248 | ENABLE_STRICT_OBJC_MSGSEND = YES; 249 | GCC_C_LANGUAGE_STANDARD = gnu11; 250 | GCC_NO_COMMON_BLOCKS = YES; 251 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 252 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 253 | GCC_WARN_UNDECLARED_SELECTOR = YES; 254 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 255 | GCC_WARN_UNUSED_FUNCTION = YES; 256 | GCC_WARN_UNUSED_VARIABLE = YES; 257 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 258 | MTL_ENABLE_DEBUG_INFO = NO; 259 | MTL_FAST_MATH = YES; 260 | SDKROOT = iphoneos; 261 | SWIFT_COMPILATION_MODE = wholemodule; 262 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 263 | VALIDATE_PRODUCT = YES; 264 | }; 265 | name = Release; 266 | }; 267 | 3D7BB5E327A46B8D009D4145 /* Debug */ = { 268 | isa = XCBuildConfiguration; 269 | buildSettings = { 270 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 271 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 272 | CODE_SIGN_STYLE = Automatic; 273 | CURRENT_PROJECT_VERSION = 1; 274 | DEVELOPMENT_ASSET_PATHS = ""; 275 | ENABLE_PREVIEWS = YES; 276 | GENERATE_INFOPLIST_FILE = YES; 277 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 278 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 279 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 280 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 281 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 282 | LD_RUNPATH_SEARCH_PATHS = ( 283 | "$(inherited)", 284 | "@executable_path/Frameworks", 285 | ); 286 | MARKETING_VERSION = 1.0; 287 | PRODUCT_BUNDLE_IDENTIFIER = "de.tiigi.ValidatedPropertyKit-Example"; 288 | PRODUCT_NAME = "$(TARGET_NAME)"; 289 | SWIFT_EMIT_LOC_STRINGS = YES; 290 | SWIFT_VERSION = 5.0; 291 | TARGETED_DEVICE_FAMILY = "1,2"; 292 | }; 293 | name = Debug; 294 | }; 295 | 3D7BB5E427A46B8D009D4145 /* Release */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 300 | CODE_SIGN_STYLE = Automatic; 301 | CURRENT_PROJECT_VERSION = 1; 302 | DEVELOPMENT_ASSET_PATHS = ""; 303 | ENABLE_PREVIEWS = YES; 304 | GENERATE_INFOPLIST_FILE = YES; 305 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 306 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 307 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 308 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 309 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 310 | LD_RUNPATH_SEARCH_PATHS = ( 311 | "$(inherited)", 312 | "@executable_path/Frameworks", 313 | ); 314 | MARKETING_VERSION = 1.0; 315 | PRODUCT_BUNDLE_IDENTIFIER = "de.tiigi.ValidatedPropertyKit-Example"; 316 | PRODUCT_NAME = "$(TARGET_NAME)"; 317 | SWIFT_EMIT_LOC_STRINGS = YES; 318 | SWIFT_VERSION = 5.0; 319 | TARGETED_DEVICE_FAMILY = "1,2"; 320 | }; 321 | name = Release; 322 | }; 323 | /* End XCBuildConfiguration section */ 324 | 325 | /* Begin XCConfigurationList section */ 326 | 3D7BB5CF27A46B8C009D4145 /* Build configuration list for PBXProject "Example" */ = { 327 | isa = XCConfigurationList; 328 | buildConfigurations = ( 329 | 3D7BB5E027A46B8D009D4145 /* Debug */, 330 | 3D7BB5E127A46B8D009D4145 /* Release */, 331 | ); 332 | defaultConfigurationIsVisible = 0; 333 | defaultConfigurationName = Release; 334 | }; 335 | 3D7BB5E227A46B8D009D4145 /* Build configuration list for PBXNativeTarget "Example" */ = { 336 | isa = XCConfigurationList; 337 | buildConfigurations = ( 338 | 3D7BB5E327A46B8D009D4145 /* Debug */, 339 | 3D7BB5E427A46B8D009D4145 /* Release */, 340 | ); 341 | defaultConfigurationIsVisible = 0; 342 | defaultConfigurationName = Release; 343 | }; 344 | /* End XCConfigurationList section */ 345 | 346 | /* Begin XCSwiftPackageProductDependency section */ 347 | 3D7BB5E727A46BA4009D4145 /* ValidatedPropertyKit */ = { 348 | isa = XCSwiftPackageProductDependency; 349 | productName = ValidatedPropertyKit; 350 | }; 351 | /* End XCSwiftPackageProductDependency section */ 352 | }; 353 | rootObject = 3D7BB5CC27A46B8C009D4145 /* Project object */; 354 | } 355 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example/App.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct App {} 5 | 6 | extension App: SwiftUI.App { 7 | 8 | var body: some Scene { 9 | WindowGroup { 10 | ContentView() 11 | } 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import ValidatedPropertyKit 3 | 4 | struct ContentView { 5 | 6 | @Validated(!.isEmpty) 7 | var username = String() 8 | 9 | } 10 | 11 | extension ContentView: View { 12 | 13 | var body: some View { 14 | NavigationView { 15 | List { 16 | Section( 17 | header: Text(verbatim: "Username"), 18 | footer: Group { 19 | if self._username.isInvalidAfterChanges { 20 | Text( 21 | verbatim: "Username is not valid" 22 | ) 23 | .foregroundColor(.red) 24 | } 25 | } 26 | ) { 27 | TextField( 28 | "John Doe", 29 | text: self.$username 30 | ) 31 | } 32 | Section( 33 | footer: Button( 34 | action: { 35 | print("Login") 36 | } 37 | ) { 38 | Text(verbatim: "Login") 39 | } 40 | .buttonStyle(.borderedProminent) 41 | .validated(self._username) 42 | ) { 43 | 44 | } 45 | } 46 | .navigationTitle("ValidatedPropertyKit") 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Sven Tiigi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ValidatedPropertyKit", 7 | platforms: [ 8 | .iOS(.v13), 9 | .tvOS(.v13), 10 | .watchOS(.v6), 11 | .macOS(.v10_15) 12 | ], 13 | products: [ 14 | .library( 15 | name: "ValidatedPropertyKit", 16 | targets: [ 17 | "ValidatedPropertyKit" 18 | ] 19 | ) 20 | ], 21 | targets: [ 22 | .target( 23 | name: "ValidatedPropertyKit", 24 | path: "Sources" 25 | ), 26 | .testTarget( 27 | name: "ValidatedPropertyKitTests", 28 | dependencies: [ 29 | "ValidatedPropertyKit" 30 | ], 31 | path: "Tests" 32 | ) 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | ValidatedPropertyKit Logo 5 |

6 | 7 |

8 | ValidatedPropertyKit 9 |

10 | 11 |

12 | A Swift Package to easily validate your properties using Property Wrappers 👮 13 |

14 | 15 |

16 | 17 | Swift Version 18 | 19 | 20 | Platforms 21 | 22 |
23 | 24 | Build and Test Status 25 | 26 | 27 | Documentation 28 | 29 | 30 | Twitter 31 | 32 |

33 | 34 | ```swift 35 | import SwiftUI 36 | import ValidatedPropertyKit 37 | 38 | struct LoginView: View { 39 | 40 | @Validated(!.isEmpty && .isEmail) 41 | var mailAddress = String() 42 | 43 | @Validated(.range(8...)) 44 | var password = String() 45 | 46 | var body: some View { 47 | List { 48 | TextField( 49 | "E-Mail", 50 | text: self.$mailAddress 51 | ) 52 | if self._mailAddress.isInvalidAfterChanges { 53 | Text(verbatim: "Please enter a valid E-Mail address.") 54 | } 55 | TextField( 56 | "Password", 57 | text: self.$password 58 | ) 59 | if self._password.isInvalidAfterChanges { 60 | Text(verbatim: "Please enter a valid password.") 61 | } 62 | Button { 63 | print("Login", self.mailAddress, self.password) 64 | } label: { 65 | Text(verbatim: "Submit") 66 | } 67 | .validated( 68 | self._mailAddress, 69 | self._password 70 | ) 71 | } 72 | } 73 | 74 | } 75 | ``` 76 | 77 | ## Features 78 | 79 | - [x] Easily validate your properties 👮 80 | - [x] Predefined validations 🚦 81 | - [x] Logical Operators to combine validations 🔗 82 | - [x] Customization and configuration to your needs 💪 83 | 84 | ## Installation 85 | 86 | ### Swift Package Manager 87 | 88 | To integrate using Apple's [Swift Package Manager](https://swift.org/package-manager/), add the following as a dependency to your `Package.swift`: 89 | 90 | ```swift 91 | dependencies: [ 92 | .package(url: "https://github.com/SvenTiigi/ValidatedPropertyKit.git", from: "0.0.6") 93 | ] 94 | ``` 95 | 96 | Or navigate to your Xcode project then select `Swift Packages`, click the “+” icon and search for `ValidatedPropertyKit`. 97 | 98 | ### Manually 99 | 100 | If you prefer not to use any of the aforementioned dependency managers, you can integrate ValidatedPropertyKit into your project manually. Simply drag the `Sources` Folder into your Xcode project. 101 | 102 | ## Validated 👮‍♂️ 103 | 104 | The `@Validated` attribute allows you to specify a validation alongside to the declaration of your property. 105 | 106 | > **Note**: @Validated supports SwiftUI View updates and will basically work the same way as @State does. 107 | 108 | ```swift 109 | @Validated(!.isEmpty) 110 | var username = String() 111 | 112 | @Validated(.hasPrefix("https")) 113 | var avatarURL: String? 114 | ``` 115 | 116 | If `@Validated` is applied on an optional type e.g. `String?` you can specify whether the validation should fail or succeed when the value is `nil`. 117 | 118 | ```swift 119 | @Validated( 120 | .isURL && .hasPrefix("https"), 121 | isNilValid: true 122 | ) 123 | var avatarURL: String? 124 | ``` 125 | 126 | > By default the argument `nilValidation` is set to `.constant(false)` 127 | 128 | In addition the `SwiftUI.View` extension `validated()` allows you to disable or enable a certain `SwiftUI.View` based on your `@Validated` properties. The `validated()` function will disable the `SwiftUI.View` if at least one of the passed in `@Validated` properties evaluates to `false`. 129 | 130 | ```swift 131 | @Validated(!.isEmpty && .contains("@")) 132 | var mailAddress = String() 133 | 134 | @Validated(.range(8...)) 135 | var password = String() 136 | 137 | Button( 138 | action: {}, 139 | label: { Text("Submit") } 140 | ) 141 | .validated(self._mailAddress && self._password) 142 | ``` 143 | 144 | > By using the underscore notation you are passing the `@Validated` property wrapper to the `validated()` function 145 | 146 | ## Validation 🚦 147 | 148 | Each `@Validated` attribute will be initialized with a `Validation` which can be initialized with a simple closure that must return a `Bool` value. 149 | 150 | ```swift 151 | @Validated(.init { value in 152 | value.isEmpty 153 | }) 154 | var username = String() 155 | ``` 156 | 157 | Therefore, ValidatedPropertyKit comes along with many built-in convenience functions for various types and protocols. 158 | 159 | ```swift 160 | @Validated(!.contains("Android", options: .caseInsensitive)) 161 | var favoriteOperatingSystem = String() 162 | 163 | @Validated(.equals(42)) 164 | var magicNumber = Int() 165 | 166 | @Validated(.keyPath(\.isEnabled, .equals(true))) 167 | var object = MyCustomObject() 168 | ``` 169 | 170 | > Head over the [Predefined Validations](https://github.com/SvenTiigi/ValidatedPropertyKit#predefined-validations) section to learn more 171 | 172 | Additionally, you can extend the `Validation` via conditional conformance to easily declare your own Validations. 173 | 174 | ```swift 175 | extension Validation where Value == Int { 176 | 177 | /// Will validate if the Integer is the meaning of life 178 | static var isMeaningOfLife: Self { 179 | .init { value in 180 | value == 42 181 | } 182 | } 183 | 184 | } 185 | ``` 186 | 187 | And apply them to your validated property. 188 | 189 | ```swift 190 | @Validated(.isMeaningOfLife) 191 | var number = Int() 192 | ``` 193 | 194 | ## isValid ✅ 195 | 196 | You can access the `isValid` state at anytime by using the underscore notation to directly access the `@Validated` property wrapper. 197 | 198 | ```swift 199 | @Validated(!.isEmpty) 200 | var username = String() 201 | 202 | username = "Mr.Robot" 203 | print(_username.isValid) // true 204 | 205 | username = "" 206 | print(_username.isValid) // false 207 | ``` 208 | 209 | ## Validation Operators 🔗 210 | 211 | Validation Operators allowing you to combine multiple Validations like you would do with Bool values. 212 | 213 | ```swift 214 | // Logical AND 215 | @Validated(.hasPrefix("https") && .hasSuffix("png")) 216 | var avatarURL = String() 217 | 218 | // Logical OR 219 | @Validated(.hasPrefix("Mr.") || .hasPrefix("Mrs.")) 220 | var name = String() 221 | 222 | // Logical NOT 223 | @Validated(!.contains("Android", options: .caseInsensitive)) 224 | var favoriteOperatingSystem = String() 225 | ``` 226 | 227 | ## Predefined Validations 228 | 229 | The `ValidatedPropertyKit` comes with many predefined common validations which you can make use of in order to specify a `Validation` for your validated property. 230 | 231 | **KeyPath** 232 | 233 | The `keyPath` validation will allow you to specify a validation for a given `KeyPath` of the attributed property. 234 | 235 | ```swift 236 | @Validated(.keyPath(\.isEnabled, .equals(true))) 237 | var object = MyCustomObject() 238 | ``` 239 | 240 | **Strings** 241 | 242 | A String property can be validated in many ways like `contains`, `hasPrefix` and even `RegularExpressions`. 243 | 244 | ```swift 245 | @Validated(.isEmail) 246 | var string = String() 247 | 248 | @Validated(.contains("Mr.Robot")) 249 | var string = String() 250 | 251 | @Validated(.hasPrefix("Mr.")) 252 | var string = String() 253 | 254 | @Validated(.hasSuffix("OS")) 255 | var string = String() 256 | 257 | @Validated(.regularExpression("[0-9]+$")) 258 | var string = String() 259 | ``` 260 | 261 | **Equatable** 262 | 263 | A `Equatable` type can be validated against a specified value. 264 | 265 | ```swift 266 | @Validated(.equals(42)) 267 | var number = Int() 268 | ``` 269 | 270 | **Sequence** 271 | 272 | A property of type `Sequence` can be validated via the `contains` or `startsWith` validation. 273 | 274 | ```swift 275 | @Validated(.contains("Mr.Robot", "Elliot")) 276 | var sequence = [String]() 277 | 278 | @Validated(.startsWith("First Entry")) 279 | var sequence = [String]() 280 | ``` 281 | 282 | **Collection** 283 | 284 | Every `Collection` type offers the `isEmpty` validation and the `range` validation where you can easily declare the valid capacity. 285 | 286 | ```swift 287 | @Validated(!.isEmpty) 288 | var collection = [String]() 289 | 290 | @Validated(.range(1...10)) 291 | var collection = [String]() 292 | ``` 293 | 294 | **Comparable** 295 | 296 | A `Comparable` type can be validated with all common comparable operators. 297 | 298 | ```swift 299 | @Validated(.less(50)) 300 | var comparable = Int() 301 | 302 | @Validated(.lessOrEqual(50)) 303 | var comparable = Int() 304 | 305 | @Validated(.greater(50)) 306 | var comparable = Int() 307 | 308 | @Validated(.greaterOrEqual(50)) 309 | var comparable = Int() 310 | ``` 311 | 312 | ## License 313 | 314 | ``` 315 | ValidatedPropertyKit 316 | Copyright (c) 2022 Sven Tiigi sven.tiigi@gmail.com 317 | 318 | Permission is hereby granted, free of charge, to any person obtaining a copy 319 | of this software and associated documentation files (the "Software"), to deal 320 | in the Software without restriction, including without limitation the rights 321 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 322 | copies of the Software, and to permit persons to whom the Software is 323 | furnished to do so, subject to the following conditions: 324 | 325 | The above copyright notice and this permission notice shall be included in 326 | all copies or substantial portions of the Software. 327 | 328 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 329 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 330 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 331 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 332 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 333 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 334 | THE SOFTWARE. 335 | ``` 336 | -------------------------------------------------------------------------------- /Sources/SwiftUI/View+validated.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // MARK: - View+validated 4 | 5 | public extension View { 6 | 7 | /// Adds a condition that controls whether users can interact with this view 8 | /// if all given `Validatable` instances are valid 9 | /// - Parameter validatable: A varadic sequence of Validatable instances 10 | func validated( 11 | _ validatables: Validatable... 12 | ) -> some View { 13 | self.disabled( 14 | validatables.contains { !$0.isValid } 15 | ) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Validatable/Validatable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validatable 4 | 5 | /// A Validatable type 6 | public protocol Validatable { 7 | 8 | /// A Bool value specifying if is valid or not 9 | var isValid: Bool { get } 10 | 11 | } 12 | 13 | // MARK: - DefaultValidatable 14 | 15 | /// A Default Validatable 16 | private struct DefaultValidatable: Validatable { 17 | 18 | /// A Bool value specifying if is valid or not 19 | let isValid: Bool 20 | 21 | } 22 | 23 | // MARK: - Validatable+Not 24 | 25 | public extension Validatable { 26 | 27 | /// Performs a logical `NOT` (`!`) operation on a Validatable type 28 | /// - Parameter validatable: The Validatable object to negate 29 | static prefix func ! ( 30 | validatable: Self 31 | ) -> Validatable { 32 | DefaultValidatable( 33 | isValid: !validatable.isValid 34 | ) 35 | } 36 | 37 | } 38 | 39 | // MARK: - Validatable+And 40 | 41 | public extension Validatable { 42 | 43 | /// Performs a logical `AND` (`&&`) operation on two Validatable types 44 | /// - Parameters: 45 | /// - lhs: The left-hand side of the operation 46 | /// - rhs: The right-hand side of the operation 47 | static func && ( 48 | lhs: Self, 49 | rhs: @autoclosure @escaping () -> Self 50 | ) -> Validatable { 51 | DefaultValidatable( 52 | isValid: lhs.isValid && rhs().isValid 53 | ) 54 | } 55 | 56 | } 57 | 58 | // MARK: - Validatable+Or 59 | 60 | public extension Validatable { 61 | 62 | /// Performs a logical `OR` (`||`) operation on two Validatable types 63 | /// - Parameters: 64 | /// - lhs: The left-hand side of the operation 65 | /// - rhs: The right-hand side of the operation 66 | static func || ( 67 | lhs: Self, 68 | rhs: @autoclosure @escaping () -> Self 69 | ) -> Validatable { 70 | DefaultValidatable( 71 | isValid: lhs.isValid || rhs().isValid 72 | ) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Validated.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // MARK: - Validated 4 | 5 | /// A Validated PropertyWrapper 6 | @propertyWrapper 7 | public struct Validated: Validatable, DynamicProperty { 8 | 9 | // MARK: Properties 10 | 11 | /// The Validation 12 | public var validation: Validation { 13 | didSet { 14 | // Re-Validate 15 | self.isValid = self.validation.validate(self.value) 16 | } 17 | } 18 | 19 | /// The Value 20 | @State 21 | private var value: Value 22 | 23 | /// Bool value if the value has been modified 24 | @State 25 | public private(set) var hasChanges = false 26 | 27 | /// Bool value if validated value is valid 28 | @State 29 | public private(set) var isValid: Bool 30 | 31 | // MARK: PropertyWrapper-Properties 32 | 33 | /// The underlying value referenced by the Validated variable 34 | public var wrappedValue: Value { 35 | get { 36 | self.projectedValue.wrappedValue 37 | } 38 | nonmutating set { 39 | self.projectedValue.wrappedValue = newValue 40 | } 41 | } 42 | 43 | /// A binding to the Validated value 44 | public var projectedValue: Binding { 45 | .init( 46 | get: { 47 | self.value 48 | }, 49 | set: { newValue in 50 | self.value = newValue 51 | if !self.hasChanges { 52 | self.hasChanges.toggle() 53 | } 54 | self.isValid = self.validation.validate(newValue) 55 | } 56 | ) 57 | } 58 | 59 | // MARK: Initializer 60 | 61 | /// Creates a new instance of `Validated` 62 | /// - Parameters: 63 | /// - wrappedValue: The wrapped `Value` 64 | /// - validation: The `Validation` 65 | public init( 66 | wrappedValue: Value, 67 | _ validation: Validation 68 | ) { 69 | self.validation = validation 70 | self._value = .init( 71 | initialValue: wrappedValue 72 | ) 73 | self._isValid = .init( 74 | initialValue: validation 75 | .validate(wrappedValue) 76 | ) 77 | } 78 | 79 | /// Creates a new instance of `Validated` 80 | /// - Parameters: 81 | /// - wrappedValue: The `WrappedValue`. Default value `nil` 82 | /// - validation: The `Validation` 83 | /// - isNilValid: A closure that returns a Boolean value if `nil` should be treated as valid or not. Default value `false` 84 | public init( 85 | wrappedValue: WrappedValue? = nil, 86 | _ validation: Validation, 87 | isNilValid: @autoclosure @escaping () -> Bool = false 88 | ) where WrappedValue? == Value { 89 | self.init( 90 | wrappedValue: wrappedValue, 91 | .init( 92 | validation, 93 | isNilValid: isNilValid() 94 | ) 95 | ) 96 | } 97 | 98 | } 99 | 100 | // MARK: - Validated+validatedValue 101 | 102 | public extension Validated { 103 | 104 | /// The value if is valid otherwise returns `nil` 105 | var validatedValue: Value? { 106 | self.isValid ? self.value : nil 107 | } 108 | 109 | } 110 | 111 | // MARK: - Validated+isInvalid 112 | 113 | public extension Validated { 114 | 115 | /// A Boolean value if the value is invalid 116 | var isInvalid: Bool { 117 | !self.isValid 118 | } 119 | 120 | } 121 | 122 | // MARK: - Validated+isInvalidAfterChanges 123 | 124 | public extension Validated { 125 | 126 | /// A Boolean value if the value is invalid and has been previously modified 127 | var isInvalidAfterChanges: Bool { 128 | self.hasChanges && !self.isValid 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /Sources/Validation/Validation+BinaryInteger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation+BinaryInteger 4 | 5 | public extension Validation where Value: BinaryInteger { 6 | 7 | /// Validation that validates if thie value is a multiple of the given value 8 | /// - Parameter other: The other Value 9 | static func isMultiple( 10 | of other: @autoclosure @escaping () -> Value 11 | ) -> Self { 12 | .init { value in 13 | value.isMultiple(of: other()) 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Validation/Validation+Collection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation+Collection 4 | 5 | public extension Validation where Value: Collection { 6 | 7 | /// The isEmpty Validation 8 | static var isEmpty: Self { 9 | .init { value in 10 | value.isEmpty 11 | } 12 | } 13 | 14 | /// Validation with RangeExpression 15 | /// - Parameter range: The RangeExpression 16 | static func range( 17 | _ range: @autoclosure @escaping () -> R 18 | ) -> Self where R.Bound == Int { 19 | .init { value in 20 | range().contains(value.count) 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Validation/Validation+Comparable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation+Comparable 4 | 5 | public extension Validation where Value: Comparable { 6 | 7 | /// Validation with less `<` than comparable value 8 | /// - Parameter comparableValue: The Comparable value 9 | static func less( 10 | _ comparableValue: @autoclosure @escaping () -> Value 11 | ) -> Self { 12 | .init { value in 13 | value < comparableValue() 14 | } 15 | } 16 | 17 | /// Validation with less or equal `<=` than comparable value 18 | /// - Parameter comparableValue: The Comparable value 19 | static func lessOrEqual( 20 | _ comparableValue: @autoclosure @escaping () -> Value 21 | ) -> Self { 22 | .init { value in 23 | value <= comparableValue() 24 | } 25 | } 26 | 27 | /// Validation with greater `>` than comparable value 28 | /// - Parameter comparableValue: The Comparable value 29 | static func greater( 30 | _ comparableValue: @autoclosure @escaping () -> Value 31 | ) -> Self { 32 | .init { value in 33 | value > comparableValue() 34 | } 35 | } 36 | 37 | /// Validation with greater or equal `>=` than comparable value 38 | /// - Parameter comparableValue: The Comparable value 39 | static func greaterOrEqual( 40 | _ comparableValue: @autoclosure @escaping () -> Value 41 | ) -> Self { 42 | .init { value in 43 | value >= comparableValue() 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Validation/Validation+ComparisonOperators.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation+Equal 4 | 5 | public extension Validation { 6 | 7 | /// Returns a Validation where two given Validation results will be compared to equality 8 | /// - Parameters: 9 | /// - lhs: The left-hand side of the operation 10 | /// - rhs: The right-hand side of the operation 11 | static func == ( 12 | lhs: Self, 13 | rhs: Self 14 | ) -> Self { 15 | .init { value in 16 | lhs.validate(value) == rhs.validate(value) 17 | } 18 | } 19 | 20 | } 21 | 22 | // MARK: - Validation+Unequal 23 | 24 | public extension Validation { 25 | 26 | /// Returns a Validation where two given Validation results will be compared to unequality 27 | /// - Parameters: 28 | /// - lhs: The left-hand side of the operation 29 | /// - rhs: The right-hand side of the operation 30 | static func != ( 31 | lhs: Self, 32 | rhs: Self 33 | ) -> Self { 34 | .init { value in 35 | lhs.validate(value) != rhs.validate(value) 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Validation/Validation+Constant.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation+Constant 4 | 5 | public extension Validation { 6 | 7 | /// Constant Validation which always evalutes to a given Bool value 8 | /// - Parameter isValid: The isValid Bool value 9 | static func constant( 10 | _ isValid: @autoclosure @escaping () -> Bool 11 | ) -> Self { 12 | .init { _ in isValid() } 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Validation/Validation+Equatable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation+Equatable 4 | 5 | public extension Validation where Value: Equatable { 6 | 7 | /// Returns a Validation indicating whether two values are equal. 8 | /// - Parameter equatableValue: The Equatable value 9 | static func equals( 10 | _ equatableValue: @autoclosure @escaping () -> Value 11 | ) -> Self { 12 | .init { value in 13 | value == equatableValue() 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Validation/Validation+KeyPath.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation+KeyPath 4 | 5 | public extension Validation { 6 | 7 | /// Validation via KeyPath 8 | /// - Parameters: 9 | /// - keyPath: A key path from a specific root type to a specific resulting value type 10 | /// - validation: The Validation for the specific resulting value type 11 | static func keyPath( 12 | _ keyPath: @autoclosure @escaping () -> KeyPath, 13 | _ validation: @autoclosure @escaping () -> Validation 14 | ) -> Self { 15 | .init { value in 16 | validation().validate(value[keyPath: keyPath()]) 17 | } 18 | } 19 | 20 | /// Validation that checks if a given Bool value KeyPath evaluates to `true` 21 | /// - Parameter keyPath: The Bool value KeyPath 22 | static func keyPath( 23 | _ keyPath: @autoclosure @escaping () -> KeyPath 24 | ) -> Self { 25 | .init { value in 26 | value[keyPath: keyPath()] 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Validation/Validation+LogicalOperators.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation+Not 4 | 5 | public extension Validation { 6 | 7 | /// Performs a logical `NOT` (`!`) operation on a Validation 8 | /// - Parameter validation: The Validation value to negate 9 | static prefix func ! ( 10 | validation: Self 11 | ) -> Self { 12 | .init { value in 13 | !validation.validate(value) 14 | } 15 | } 16 | 17 | } 18 | 19 | // MARK: - Validation+And 20 | 21 | public extension Validation { 22 | 23 | /// Performs a logical `AND` (`&&`) operation on two Validations 24 | /// - Parameters: 25 | /// - lhs: The left-hand side of the operation 26 | /// - rhs: The right-hand side of the operation 27 | static func && ( 28 | lhs: Self, 29 | rhs: @autoclosure @escaping () -> Self 30 | ) -> Self { 31 | .init { value in 32 | lhs.validate(value) && rhs().validate(value) 33 | } 34 | } 35 | 36 | } 37 | 38 | // MARK: - Validation+Or 39 | 40 | public extension Validation { 41 | 42 | /// Performs a logical `OR` (`||`) operation on two Validations 43 | /// - Parameters: 44 | /// - lhs: The left-hand side of the operation 45 | /// - rhs: The right-hand side of the operation 46 | static func || ( 47 | lhs: Self, 48 | rhs: @autoclosure @escaping () -> Self 49 | ) -> Self { 50 | .init { value in 51 | lhs.validate(value) || rhs().validate(value) 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Validation/Validation+Sequence.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation+Sequence 4 | 5 | public extension Validation where Value: Sequence, Value.Element: Equatable { 6 | 7 | /// Validation with contains elements 8 | /// - Parameter elements: The Elements that should be contained 9 | static func contains( 10 | _ elements: Value.Element... 11 | ) -> Self { 12 | .init { value in 13 | elements.map(value.contains).contains(true) 14 | } 15 | } 16 | 17 | /// Returns a Validation indicating whether the initial elements 18 | /// of the sequence are the same as the elements in another sequence 19 | /// - Parameter elements: The Elements to compare to 20 | static func startsWith( 21 | _ elements: Value.Element... 22 | ) -> Self { 23 | .init { value in 24 | value.starts(with: elements) 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Validation/Validation+String.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation+StringProtocol 4 | 5 | public extension Validation where Value: StringProtocol { 6 | 7 | /// Validation with contains String 8 | /// - Parameters: 9 | /// - string: The String that should be contained 10 | /// - options: The String ComparisonOptions. Default value `.init` 11 | static func contains( 12 | _ string: @autoclosure @escaping () -> S, 13 | options: @autoclosure @escaping () -> NSString.CompareOptions = .init() 14 | ) -> Self { 15 | .init { value in 16 | value.range(of: string(), options: options()) != nil 17 | } 18 | } 19 | 20 | /// Validation with has prefix 21 | /// - Parameter prefix: The prefix 22 | static func hasPrefix( 23 | _ prefix: @autoclosure @escaping () -> S 24 | ) -> Self { 25 | .init { value in 26 | value.hasPrefix(prefix()) 27 | } 28 | } 29 | 30 | /// Validation with has suffix 31 | /// - Parameter suffix: The suffix 32 | static func hasSuffix( 33 | _ suffix: @autoclosure @escaping () -> S 34 | ) -> Self { 35 | .init { value in 36 | value.hasSuffix(suffix()) 37 | } 38 | } 39 | 40 | } 41 | 42 | // MARK: - Validation+String 43 | 44 | public extension Validation where Value == String { 45 | 46 | /// Validation if the String is a subset of a given CharacterSet 47 | /// - Parameter characterSet: The CharacterSet 48 | static func isSubset( 49 | of characterSet: @autoclosure @escaping () -> CharacterSet 50 | ) -> Self { 51 | .init { value in 52 | CharacterSet( 53 | charactersIn: value 54 | ) 55 | .isSubset( 56 | of: characterSet() 57 | ) 58 | } 59 | } 60 | 61 | /// Validation if the String is numeric 62 | /// - Parameter locale: The Locale. Default value `.current` 63 | static func isNumeric( 64 | locale: @autoclosure @escaping () -> Locale = .current 65 | ) -> Self { 66 | .init { value in 67 | let scanner = Scanner(string: value) 68 | scanner.locale = locale() 69 | return scanner.scanDecimal() != nil && scanner.isAtEnd 70 | } 71 | } 72 | 73 | /// Validation if the String matches with a given NSTextCheckingResult CheckingType 74 | /// - Parameters: 75 | /// - textCheckingResult: The NSTextCheckingResult CheckingType 76 | /// - validate: An optional closure to validate the NSTextCheckingResult. Default value `nil` 77 | static func matches( 78 | _ textCheckingResult: @autoclosure @escaping () -> NSTextCheckingResult.CheckingType, 79 | validate: ((NSTextCheckingResult) -> Bool)? = nil 80 | ) -> Self { 81 | .init { value in 82 | // Initialize NSDataDetector with link checking type 83 | let detector = try? NSDataDetector( 84 | types: textCheckingResult().rawValue 85 | ) 86 | // Initialize Range from value 87 | let range = NSRange( 88 | value.startIndex.. NSRegularExpression, 125 | matchingOptions: @autoclosure @escaping () -> NSRegularExpression.MatchingOptions = .init() 126 | ) -> Self { 127 | .init { value in 128 | regularExpression().firstMatch( 129 | in: value, 130 | options: matchingOptions(), 131 | range: .init(value.startIndex..., in: value) 132 | ) != nil 133 | } 134 | } 135 | 136 | /// Validation with RegularExpression Pattern 137 | /// - Parameters: 138 | /// - pattern: The RegularExpression Pattern 139 | /// - onInvalidPatternValidation: The Validation that should be used when the pattern is invalid. Default value `.constant(false)` 140 | /// - matchingOptions: The NSRegularExpression.MatchingOptions. Default value `.init` 141 | static func regularExpression( 142 | _ pattern: @autoclosure @escaping () -> String, 143 | onInvalidPatternValidation: @autoclosure @escaping () -> Validation = .constant(false), 144 | matchingOptions: @autoclosure @escaping () -> NSRegularExpression.MatchingOptions = .init() 145 | ) -> Self { 146 | let regularExpression: NSRegularExpression 147 | do { 148 | regularExpression = try .init(pattern: pattern()) 149 | } catch { 150 | return .init { _ in 151 | onInvalidPatternValidation().validate(()) 152 | } 153 | } 154 | return self.regularExpression( 155 | regularExpression, 156 | matchingOptions: matchingOptions() 157 | ) 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /Sources/Validation/Validation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Validation 4 | 5 | /// A Validation 6 | public struct Validation { 7 | 8 | // MARK: Typealias 9 | 10 | /// The validation predicate typealias representing a `(Value) -> Bool` closure 11 | public typealias Predicate = (Value) -> Bool 12 | 13 | // MARK: Properties 14 | 15 | /// The Predicate 16 | private let predicate: Predicate 17 | 18 | // MARK: Initializer 19 | 20 | /// Creates a new instance of `Validation` 21 | /// - Parameter predicate: A closure that takes a value and returns a Boolean value if the passed value is valid 22 | public init( 23 | predicate: @escaping Predicate 24 | ) { 25 | self.predicate = predicate 26 | } 27 | 28 | /// Creates a new instance of `Validation` 29 | /// - Parameters: 30 | /// - validation: The WrappedValue Validation 31 | /// - isNilValid: A closure that returns a Boolean value if `nil` should be treated as valid or not. Default value `false` 32 | public init( 33 | _ validation: Validation, 34 | isNilValid: @autoclosure @escaping () -> Bool = false 35 | ) where WrappedValue? == Value { 36 | self.init { value in 37 | value.flatMap(validation.validate) ?? isNilValid() 38 | } 39 | } 40 | 41 | } 42 | 43 | // MARK: - Validate 44 | 45 | public extension Validation { 46 | 47 | /// Validates a value and returns a Boolean value wether the value is valid or invalid 48 | /// - Parameter value: The value that should be validated 49 | /// - Returns: A Boolen value wether the value is valid or invalid 50 | func validate( 51 | _ value: Value 52 | ) -> Bool { 53 | self.predicate(value) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Tests/ValidationCollectionTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ValidatedPropertyKit 2 | import XCTest 3 | 4 | class ValidationCollectionTests: XCTestCase { 5 | 6 | func testNonEmpty() { 7 | let validStrings = ["1"] 8 | let invalidStrings: [String] = .init() 9 | let validation = !Validation<[String]>.isEmpty 10 | XCTAssert(validation.validate(validStrings)) 11 | XCTAssertFalse(validation.validate(invalidStrings)) 12 | } 13 | 14 | func testRange() { 15 | struct FakeRangeExpression: RangeExpression { 16 | var containsResult: Bool 17 | func relative(to collection: C) -> Range where C.Index == Int { 18 | return 0..<1 19 | } 20 | func contains(_ element: Int) -> Bool { 21 | return self.containsResult 22 | } 23 | } 24 | let fakeRangeExpressionTrue = FakeRangeExpression(containsResult: true) 25 | let validatedTrue = Validation<[String]>.range(fakeRangeExpressionTrue) 26 | XCTAssert(validatedTrue.validate(.init())) 27 | let fakeRangeExpressionFalse = FakeRangeExpression(containsResult: false) 28 | let validatedFalse = Validation<[String]>.range(fakeRangeExpressionFalse) 29 | XCTAssertFalse(validatedFalse.validate(.init())) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Tests/ValidationComparableTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ValidatedPropertyKit 2 | import XCTest 3 | 4 | class ValidationComparableTests: XCTestCase { 5 | 6 | func testLess() { 7 | let input1 = Int.random(in: 0...500) 8 | let input2 = Int.random(in: 0...500) 9 | let validation = Validation.less(input2) 10 | XCTAssertEqual(input1 < input2, validation.validate(input1)) 11 | } 12 | 13 | func testLessOrEqual() { 14 | let input1 = Int.random(in: 0...500) 15 | let input2 = Int.random(in: 0...500) 16 | let validation = Validation.lessOrEqual(input2) 17 | XCTAssertEqual(input1 <= input2, validation.validate(input1)) 18 | } 19 | 20 | func testGreater() { 21 | let input1 = Int.random(in: 0...500) 22 | let input2 = Int.random(in: 0...500) 23 | let validation = Validation.greater(input2) 24 | XCTAssertEqual(input1 > input2, validation.validate(input1)) 25 | } 26 | 27 | func testGreaterOrEqual() { 28 | let input1 = Int.random(in: 0...500) 29 | let input2 = Int.random(in: 0...500) 30 | let validation = Validation.greaterOrEqual(input2) 31 | XCTAssertEqual(input1 >= input2, validation.validate(input1)) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Tests/ValidationEquatableTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ValidatedPropertyKit 2 | import XCTest 3 | 4 | class ValidationEquatableTests: XCTestCase { 5 | 6 | func testEqual() { 7 | let validation = Validation.equals("1") 8 | XCTAssert(validation.validate("1")) 9 | XCTAssertFalse(validation.validate("0")) 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Tests/ValidationSequenceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ValidatedPropertyKit 2 | import XCTest 3 | 4 | class ValidationSequenceTests: XCTestCase { 5 | 6 | func testContains() { 7 | let validStrings = ["1", "2", "3"] 8 | let invalidStrings = ["2", "3", "4"] 9 | let validation = Validation<[String]>.contains("1") 10 | XCTAssert(validation.validate(validStrings)) 11 | XCTAssertFalse(validation.validate(invalidStrings)) 12 | } 13 | 14 | func testStartsWith() { 15 | let validStrings = ["1", "2", "3"] 16 | let invalidStrings = ["2", "3", "4"] 17 | let validation = Validation<[String]>.startsWith("1", "2") 18 | XCTAssert(validation.validate(validStrings)) 19 | XCTAssertFalse(validation.validate(invalidStrings)) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Tests/ValidationStringTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ValidatedPropertyKit 2 | import XCTest 3 | 4 | class ValidationStringTests: XCTestCase { 5 | 6 | func testIsEmail() { 7 | XCTAssert( 8 | Validation.isEmail.validate("john@doe.com") 9 | ) 10 | XCTAssertFalse( 11 | Validation.isEmail.validate(UUID().uuidString) 12 | ) 13 | } 14 | 15 | func testContains() { 16 | let string = UUID().uuidString 17 | let substring1 = String(string.prefix(.random(in: 0...contains(substring1, options: .caseInsensitive) 20 | XCTAssert(validation1.validate(string)) 21 | let validation2 = Validation.contains(substring2) 22 | XCTAssertFalse(validation2.validate(string)) 23 | } 24 | 25 | func testHasPrefix() { 26 | let string = UUID().uuidString 27 | let substring1 = String(string.prefix(.random(in: 0...hasPrefix(substring1) 30 | XCTAssert(validation1.validate(string)) 31 | let validation2 = Validation.hasPrefix(substring2) 32 | XCTAssertFalse(validation2.validate(string)) 33 | } 34 | 35 | func testHasSuffx() { 36 | let string = UUID().uuidString 37 | let substring1 = String(string.suffix(.random(in: 0...hasSuffix(substring1) 40 | XCTAssert(validation1.validate(string)) 41 | let validation2 = Validation.hasSuffix(substring2) 42 | XCTAssertFalse(validation2.validate(string)) 43 | } 44 | 45 | func testRegularExpressionPattern() { 46 | let validRegularExpressionPattern = "[0-9]+$" 47 | let validString = "123456789" 48 | let invalidString = "ABCDEFGHIJ" 49 | let invalidRegularExpressionPattern = "" 50 | let validatation = Validation.regularExpression(validRegularExpressionPattern) 51 | XCTAssert(validatation.validate(validString)) 52 | XCTAssertFalse(validatation.validate(invalidString)) 53 | let validation2 = Validation.regularExpression(invalidRegularExpressionPattern) 54 | XCTAssertFalse(validation2.validate(validString)) 55 | XCTAssertFalse(validation2.validate(invalidString)) 56 | } 57 | 58 | } 59 | --------------------------------------------------------------------------------