Validator (Space Code)
GitHub概要
space-code/validator
Validator is a framework written in Swift that provides functions that can be used to validate the contents of an input value.
スター14
ウォッチ1
フォーク0
作成日:2023年9月18日
言語:Swift
ライセンス:MIT License
トピックス
formswiftswiftuivalidation
スター履歴
データ取得日時: 2025/10/22 09:50
ライブラリ
Validator (Space Code)
概要
Validator (Space Code)は、Swift向けの軽量で柔軟な入力値検証フレームワークです。iOS、macOS、tvOS、watchOSアプリケーションでユーザー入力やデータの妥当性を確認するための包括的なバリデーション機能を提供します。Swift Package Indexに登録されており、Nikita Vasilevによって開発・保守されています。関数型プログラミングのアプローチを採用し、宣言的で読みやすいバリデーションルールの定義が可能です。
詳細
space-code/validatorは、現代のSwiftアプリケーション開発に特化した検証ライブラリとして設計されています。27コミット、3リリースを経て安定性を高め、アクティブなメンテナンスが継続されています。依存関係ゼロの軽量設計により、プロジェクトサイズを最小限に抑えながら強力なバリデーション機能を実現。Swift 5.5以上をサポートし、async/awaitパターンやResult型との統合により、モダンなSwift開発パラダイムにシームレスに対応します。
主な特徴
- 軽量設計: 外部依存関係なしでプロジェクトサイズを最小化
- マルチプラットフォーム対応: iOS、macOS、tvOS、watchOSをサポート
- 関数型アプローチ: 宣言的で組み合わせ可能なバリデーションルール
- 型安全性: Swiftの型システムを活用した安全なバリデーション
- Swift Package Manager対応: 簡単なプロジェクト統合
- 拡張性: カスタムバリデーターの柔軟な定義
メリット・デメリット
メリット
- Swift Package Managerによる簡単なインストールと管理
- 外部依存関係がなく軽量でセキュア
- すべてのAppleプラットフォームでの一貫した動作
- 関数型プログラミングパラダイムによる再利用可能なコード
- Swift標準ライブラリとの自然な統合
- カスタムバリデーションルールの簡単な実装
- メモリ効率が良く高速な実行
デメリット
- 小規模プロジェクトのため、大規模なコミュニティサポートが限定的
- 複雑なバリデーションシナリオでの包括的なドキュメント不足
- UIKit/SwiftUIとの専用統合ヘルパーが限定的
- 国際化(i18n)エラーメッセージの内蔵サポート不足
- 他の大手バリデーションライブラリと比較して機能セットが限定的
参考ページ
書き方の例
インストールと基本セットアップ
Swift Package Manager
// Package.swift
let package = Package(
name: "YourProject",
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.tvOS(.v13),
.watchOS(.v6)
],
dependencies: [
.package(url: "https://github.com/space-code/validator", from: "1.0.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["Validator"]
)
]
)
Xcodeプロジェクトでの追加
1. Xcode → File → Add Package Dependencies
2. URL: https://github.com/space-code/validator
3. Dependency Rule: Up to Next Major Version
4. Add Package
基本的なバリデーション
import Validator
// 基本的な文字列バリデーション
let emailValidator = Validator<String> { email in
guard !email.isEmpty else {
return .failure(ValidationError.empty)
}
guard email.contains("@") && email.contains(".") else {
return .failure(ValidationError.invalidFormat)
}
return .success(email)
}
// バリデーションの実行
let email = "[email protected]"
switch emailValidator.validate(email) {
case .success(let validEmail):
print("有効なメール: \(validEmail)")
case .failure(let error):
print("エラー: \(error)")
}
// 数値の範囲バリデーション
let ageValidator = Validator<Int> { age in
guard age >= 0 else {
return .failure(ValidationError.tooSmall)
}
guard age <= 150 else {
return .failure(ValidationError.tooLarge)
}
return .success(age)
}
// カスタムエラー型の定義
enum ValidationError: Error, LocalizedError {
case empty
case invalidFormat
case tooSmall
case tooLarge
case custom(String)
var errorDescription: String? {
switch self {
case .empty:
return "値が空です"
case .invalidFormat:
return "形式が正しくありません"
case .tooSmall:
return "値が小さすぎます"
case .tooLarge:
return "値が大きすぎます"
case .custom(let message):
return message
}
}
}
組み合わせバリデーション
// 複数のバリデーターを組み合わせ
struct UserValidator {
let nameValidator = Validator<String> { name in
guard !name.trimmingCharacters(in: .whitespaces).isEmpty else {
return .failure(ValidationError.empty)
}
guard name.count >= 2 else {
return .failure(ValidationError.custom("名前は2文字以上である必要があります"))
}
return .success(name)
}
let passwordValidator = Validator<String> { password in
guard password.count >= 8 else {
return .failure(ValidationError.custom("パスワードは8文字以上である必要があります"))
}
let hasUppercase = password.rangeOfCharacter(from: .uppercaseLetters) != nil
let hasLowercase = password.rangeOfCharacter(from: .lowercaseLetters) != nil
let hasDigits = password.rangeOfCharacter(from: .decimalDigits) != nil
guard hasUppercase && hasLowercase && hasDigits else {
return .failure(ValidationError.custom("パスワードは大文字、小文字、数字を含む必要があります"))
}
return .success(password)
}
func validateUser(name: String, email: String, password: String) -> Result<User, ValidationError> {
// 各フィールドを個別にバリデーション
guard case .success(let validName) = nameValidator.validate(name) else {
return .failure(.custom("名前が無効です"))
}
guard case .success(let validEmail) = emailValidator.validate(email) else {
return .failure(.custom("メールアドレスが無効です"))
}
guard case .success(let validPassword) = passwordValidator.validate(password) else {
return .failure(.custom("パスワードが無効です"))
}
return .success(User(name: validName, email: validEmail, password: validPassword))
}
}
struct User {
let name: String
let email: String
let password: String
}
SwiftUIとの統合
import SwiftUI
import Validator
struct RegistrationForm: View {
@State private var name = ""
@State private var email = ""
@State private var password = ""
@State private var nameError: String?
@State private var emailError: String?
@State private var passwordError: String?
private let userValidator = UserValidator()
var body: some View {
NavigationView {
Form {
Section(header: Text("ユーザー情報")) {
VStack(alignment: .leading) {
TextField("名前", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: name) { newValue in
validateName(newValue)
}
if let error = nameError {
Text(error)
.foregroundColor(.red)
.font(.caption)
}
}
VStack(alignment: .leading) {
TextField("メールアドレス", text: $email)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.emailAddress)
.autocapitalization(.none)
.onChange(of: email) { newValue in
validateEmail(newValue)
}
if let error = emailError {
Text(error)
.foregroundColor(.red)
.font(.caption)
}
}
VStack(alignment: .leading) {
SecureField("パスワード", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: password) { newValue in
validatePassword(newValue)
}
if let error = passwordError {
Text(error)
.foregroundColor(.red)
.font(.caption)
}
}
}
Section {
Button("登録") {
submitRegistration()
}
.disabled(!isFormValid)
}
}
.navigationTitle("ユーザー登録")
}
}
private var isFormValid: Bool {
nameError == nil && emailError == nil && passwordError == nil &&
!name.isEmpty && !email.isEmpty && !password.isEmpty
}
private func validateName(_ name: String) {
switch userValidator.nameValidator.validate(name) {
case .success:
nameError = nil
case .failure(let error):
nameError = error.localizedDescription
}
}
private func validateEmail(_ email: String) {
switch emailValidator.validate(email) {
case .success:
emailError = nil
case .failure(let error):
emailError = error.localizedDescription
}
}
private func validatePassword(_ password: String) {
switch userValidator.passwordValidator.validate(password) {
case .success:
passwordError = nil
case .failure(let error):
passwordError = error.localizedDescription
}
}
private func submitRegistration() {
switch userValidator.validateUser(name: name, email: email, password: password) {
case .success(let user):
print("ユーザー登録成功: \(user)")
// 登録処理を実行
case .failure(let error):
print("登録エラー: \(error.localizedDescription)")
}
}
}
非同期バリデーション
import Foundation
import Validator
// 非同期バリデーションの例(メールアドレスの重複チェック)
class AsyncEmailValidator {
func validateEmailUniqueness(_ email: String) async -> Result<String, ValidationError> {
// 基本的なフォーマットチェック
guard case .success(let validEmail) = emailValidator.validate(email) else {
return .failure(.invalidFormat)
}
// サーバーでの重複チェック(模擬)
do {
let isUnique = try await checkEmailUniqueness(validEmail)
return isUnique ? .success(validEmail) : .failure(.custom("このメールアドレスは既に使用されています"))
} catch {
return .failure(.custom("サーバーエラーが発生しました"))
}
}
private func checkEmailUniqueness(_ email: String) async throws -> Bool {
// 実際のAPIコールを模擬
try await Task.sleep(nanoseconds: 1_000_000_000) // 1秒待機
// 模擬的な重複チェック
let usedEmails = ["[email protected]", "[email protected]"]
return !usedEmails.contains(email)
}
}
// 使用例
Task {
let asyncValidator = AsyncEmailValidator()
let result = await asyncValidator.validateEmailUniqueness("[email protected]")
switch result {
case .success(let email):
print("有効で利用可能なメールアドレス: \(email)")
case .failure(let error):
print("エラー: \(error.localizedDescription)")
}
}
カスタムバリデーター
// 日本の郵便番号バリデーター
struct JapanesePostalCodeValidator {
static let validator = Validator<String> { postalCode in
let cleaned = postalCode.replacingOccurrences(of: "-", with: "")
guard cleaned.count == 7 else {
return .failure(ValidationError.custom("郵便番号は7桁である必要があります"))
}
guard cleaned.allSatisfy({ $0.isNumber }) else {
return .failure(ValidationError.custom("郵便番号は数字のみ使用可能です"))
}
return .success(postalCode)
}
}
// 日本の電話番号バリデーター
struct JapanesePhoneValidator {
static let validator = Validator<String> { phoneNumber in
let cleaned = phoneNumber.replacingOccurrences(of: "-", with: "")
// 一般的な日本の電話番号パターン
let patterns = [
"^0[1-9][0-9]{8}$", // 固定電話 (10桁)
"^0[789]0[0-9]{8}$", // 携帯電話 (11桁)
"^050[0-9]{8}$" // IP電話 (11桁)
]
let isValid = patterns.contains { pattern in
NSPredicate(format: "SELF MATCHES %@", pattern).evaluate(with: cleaned)
}
guard isValid else {
return .failure(ValidationError.custom("有効な日本の電話番号ではありません"))
}
return .success(phoneNumber)
}
}
// 複数のカスタムバリデーターを組み合わせ
struct ContactInfoValidator {
func validateContact(postalCode: String, phoneNumber: String) -> [String: ValidationError] {
var errors: [String: ValidationError] = [:]
if case .failure(let error) = JapanesePostalCodeValidator.validator.validate(postalCode) {
errors["postalCode"] = error
}
if case .failure(let error) = JapanesePhoneValidator.validator.validate(phoneNumber) {
errors["phoneNumber"] = error
}
return errors
}
}
テストとバリデーション
import XCTest
@testable import YourApp
class ValidatorTests: XCTestCase {
func testEmailValidation() {
// 有効なメールアドレス
let validEmails = [
"[email protected]",
"[email protected]",
"[email protected]"
]
for email in validEmails {
XCTAssertTrue(
emailValidator.validate(email).isSuccess,
"'\(email)' should be valid"
)
}
// 無効なメールアドレス
let invalidEmails = [
"",
"invalid",
"@example.com",
"test@",
"test.example.com"
]
for email in invalidEmails {
XCTAssertTrue(
emailValidator.validate(email).isFailure,
"'\(email)' should be invalid"
)
}
}
func testUserValidation() {
let validator = UserValidator()
// 有効なユーザーデータ
let result = validator.validateUser(
name: "田中太郎",
email: "[email protected]",
password: "Password123"
)
XCTAssertTrue(result.isSuccess, "Valid user data should pass validation")
// 無効なパスワード
let invalidResult = validator.validateUser(
name: "田中太郎",
email: "[email protected]",
password: "weak"
)
XCTAssertTrue(invalidResult.isFailure, "Weak password should fail validation")
}
func testJapanesePostalCode() {
let validator = JapanesePostalCodeValidator.validator
// 有効な郵便番号
XCTAssertTrue(validator.validate("123-4567").isSuccess)
XCTAssertTrue(validator.validate("1234567").isSuccess)
// 無効な郵便番号
XCTAssertTrue(validator.validate("12345").isFailure)
XCTAssertTrue(validator.validate("123-456a").isFailure)
XCTAssertTrue(validator.validate("123456789").isFailure)
}
}
// Result拡張でテストを簡単に
extension Result {
var isSuccess: Bool {
switch self {
case .success:
return true
case .failure:
return false
}
}
var isFailure: Bool {
return !isSuccess
}
}
パフォーマンス考慮事項
// パフォーマンス最適化のベストプラクティス
class OptimizedValidator {
// バリデーターの再利用
private static let emailValidator = createEmailValidator()
private static let phoneValidator = createPhoneValidator()
// 重い処理は一度だけ作成
private static func createEmailValidator() -> Validator<String> {
return Validator<String> { email in
// 複雑なバリデーションロジック
// ...
return .success(email)
}
}
// キャッシュを活用した重複チェック
private var validationCache: [String: Bool] = [:]
private let cacheQueue = DispatchQueue(label: "validation.cache", attributes: .concurrent)
func validateWithCache(_ input: String) -> Result<String, ValidationError> {
return cacheQueue.sync {
if let cached = validationCache[input] {
return cached ? .success(input) : .failure(.invalidFormat)
}
let result = Self.emailValidator.validate(input)
cacheQueue.async(flags: .barrier) {
self.validationCache[input] = result.isSuccess
}
return result
}
}
// 大量データの並列処理
func validateBatch(_ inputs: [String]) async -> [Result<String, ValidationError>] {
await withTaskGroup(of: (Int, Result<String, ValidationError>).self) { group in
for (index, input) in inputs.enumerated() {
group.addTask {
let result = Self.emailValidator.validate(input)
return (index, result)
}
}
var results = Array<Result<String, ValidationError>?>(repeating: nil, count: inputs.count)
for await (index, result) in group {
results[index] = result
}
return results.compactMap { $0 }
}
}
}