├── .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 |
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 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
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 |
--------------------------------------------------------------------------------