{"id":2555,"url":"https://github.com/nsagora/peppermint","last_synced_at":"2025-10-21T10:51:43.433Z","repository":{"id":49291013,"uuid":"65064363","full_name":"nsagora/peppermint","owner":"nsagora","description":"Declarative data validation framework, written in Swift","archived":false,"fork":false,"pushed_at":"2024-01-11T10:24:09.000Z","size":3344,"stargazers_count":46,"open_issues_count":2,"forks_count":15,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-05-29T04:52:09.436Z","etag":null,"topics":["ios","macos","swift","validation","validation-engine","validation-kit"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nsagora.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-08-06T04:09:32.000Z","updated_at":"2024-06-19T11:13:54.836Z","dependencies_parsed_at":"2022-09-21T19:11:09.837Z","dependency_job_id":"d325d984-e883-42a8-b134-9f4717df2051","html_url":"https://github.com/nsagora/peppermint","commit_stats":{"total_commits":387,"total_committers":9,"mean_commits":43.0,"dds":0.3565891472868217,"last_synced_commit":"9e3db637fbf8041c23a1b7f71e4ef16ac54f833f"},"previous_names":["nsagora/validation-toolkit"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsagora%2Fpeppermint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsagora%2Fpeppermint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsagora%2Fpeppermint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsagora%2Fpeppermint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nsagora","download_url":"https://codeload.github.com/nsagora/peppermint/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228210077,"owners_count":17885627,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ios","macos","swift","validation","validation-engine","validation-kit"],"created_at":"2024-01-05T20:16:16.762Z","updated_at":"2025-10-21T10:51:38.391Z","avatar_url":"https://github.com/nsagora.png","language":"Swift","readme":"# Peppermint [![badge-version]][url-peppermint]\n\n[![badge-build-macos]][url-peppermint]\n[![badge-build-linux]][url-peppermint]\n[![badge-docs]][url-peppermint-docs]\n[![badge-codecov]][url-codecov]\n[![badge-license]][url-license]\n[![badge-twitter]][url-twitter]\n\n1. [Introduction](#introduction)\n2. [Requirements](#requirements)\n3. [Installation](#installation)\n   - [Swift Package Manager](#swift-package-manager)\n4. [Usage Examples](#usage-examples)\n   - [Predicates](#predicates)\n   - [Constraints](#constraints)\n   - [Predicate Constraint](#predicate-constraint)\n   - [Compound Constraint](#compound-constraint)\n5. [Contribute](#contribute)\n6. [Meta](#meta)\n\n## Introduction\n\n```swift\nlet constraint = TypeConstraint\u003cAccount, Account.Error\u003e {\n    KeyPathConstraint(\\.username) {\n        BlockConstraint {\n            $0.count \u003e= 5\n        } errorBuilder: {\n            .username\n        }\n    }\n    KeyPathConstraint(\\.password) {\n        GroupConstraint(.all) {\n            PredicateConstraint {\n                .characterSet(.lowercaseLetters, mode: .inclusive)\n            } errorBuilder: {\n                .password(.missingLowercase)\n            }\n            PredicateConstraint{\n                .characterSet(.uppercaseLetters, mode: .inclusive)\n            } errorBuilder: {\n                .password(.missingUppercase)\n            }\n            PredicateConstraint {\n                .characterSet(.decimalDigits, mode: .inclusive)\n            } errorBuilder: {\n                .password(.missingDigits)\n            }\n            PredicateConstraint {\n                .characterSet(CharacterSet(charactersIn: \"!?@#$%^\u0026*()|\\\\/\u003c\u003e,.~`_+-=\"), mode: .inclusive)\n            } errorBuilder: {\n                .password(.missingSpecialChars)\n            }\n            PredicateConstraint {\n                .length(min: 8)\n            }  errorBuilder: {\n                .password(.tooShort)\n            }\n        }\n    }\n    BlockConstraint {\n        $0.password == $0.passwordConfirmation\n    } errorBuilder: {\n        .password(.confirmationMismatch)\n    }\n    KeyPathConstraint(\\.email) {\n        PredicateConstraint(.email, error: .email)\n    }\n    KeyPathConstraint(\\.age) {\n        PredicateConstraint(.range(min: 14), error: .underAge)\n    }\n    KeyPathConstraint(\\.website) {\n        PredicateConstraint(.url, error: .website)\n            .optional()\n    }\n}\n\nlet result = constraint.evaluate(with: account)\nswitch result {\ncase .success:\n    handleSuccess()\ncase .failure(let summary):\n    handleErrors(summary.errors)\n}\n\n```\n\n`Peppermint` is a declarative and lightweight data validation framework.\n\nAt the core of it, there are 2 principles:\n\n- Empower composition.\n- Embrace standard library.\n\nEvery project is unique in it's own challenges and it's great when we can focus on solving them instead of spending our time on boilerplate tasks.\n\nWith this idea in mind, the framework follows the Protocol Oriented Programming paradigm and was designed from a small set of protocols and structures that can easily be composed to fit your project needs. Thus, you can think of `Peppermint` as an adjustable wrench more than a Swiss knife.\n\nSince validation can take place at many levels, `Peppermint` is available on iOS, macOS, tvOS, watchOS and native Swift projects, such as server-side apps.\n\n## Requirements\n\n- Swift 4.2+\n- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+\n- Xcode 8.1+\n\n## Installation\n\n`Peppermint` is available only through Swift Package Manager.\n\n### Swift Package Manager\n\nYou can add `Peppermint` to your project [in Xcode][url-swift-package-manager] by going to `File \u003e Swift Packages \u003e Add Package Dependency`.\n\nOr, if you want to use it as a dependency to your own package, you can add it to your `Package.swift` file:\n\n```swift\nimport PackageDescription\n\nlet package = Package(\n    name: \"YOUR_PROJECT_NAME\",\n    targets: [],\n    dependencies: [\n        .Package(url: \"https://github.com/nsagora/peppermint\", majorVersion: 1),\n    ]\n)\n```\n\n## Usage example\n\nFor a comprehensive list of examples try out the `Examples.playground`:\n\n1. Download the repository locally on your machine\n2. Open the project in Xcode\n3. Select the `Examples` playground from the Project navigator\n\nThe `Peppermint` framework is compact and offers you the foundation you need to build data validation around your project needs. In addition, it includes a set of common validation predicates and constraints that most projects can benefit off.\n\n### Predicates\n\nThe `Predicate` represents the core `protocol` and has the role to `evaluate` if an input matches on a given validation condition.\n\nAt the core of `Peppermint` there are the following two predicates, which allows you to compose predicates specific to the project needs:\n\n\u003cdetails\u003e\n\u003csummary\u003eBlockPredicate\u003c/summary\u003e\n\n```swift\nlet predicate = BlockPredicate\u003cString\u003e { $0.characters.count \u003e 2 }\npredicate.evaluate(with: \"a\") // returns false\npredicate.evaluate(with: \"abc\") // returns true\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eRegexPredicate\u003c/summary\u003e\n\n```swift\nlet predicate = RegexPredicate(expression: \"^[a-z]$\")\npredicate.evaluate(with: \"a\") // returns true\npredicate.evaluate(with: \"5\") // returns false\npredicate.evaluate(with: \"ab\") // returns false\n```\n\n\u003c/details\u003e\n\nIn addition, the framework offers a set of common validation predicates that your project can benefit of:\n\n\u003cdetails\u003e\n\u003csummary\u003eEmailPredicate\u003c/summary\u003e\n\n```swift\nlet predicate = EmailPredicate()\npredicate.evaluate(with: \"hello@\") // returns false\npredicate.evaluate(with: \"hello@nsagora.com\") // returns true\npredicate.evaluate(with: \"héllo@nsagora.com\") // returns true\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eURLPredicate\u003c/summary\u003e\n\n```swift\nlet predicate = URLPredicate()\npredicate.evaluate(with: \"http://www.url.com\") // returns true\npredicate.evaluate(with: \"http:\\\\www.url.com\") // returns false\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eRangePredicate\u003c/summary\u003e\n\n```swift\nlet predicate = let range = RangePredicate(10...20)\npredicate.evaluate(with: 15) // returns true\npredicate.evaluate(with: 21) // returns false\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eLengthPredicate\u003c/summary\u003e\n\n```swift\nlet predicate = LengthPredicate\u003cString\u003e(min: 5)\npredicate.evaluate(with: \"abcde\")   // returns true\npredicate.evaluate(with: \"abcd\")    // returns false\n```\n\n\u003c/details\u003e\n\nOn top of that, developers can build more advanced or complex predicates by extending the `Predicate` protocol, and/ or by composing or decorating the existing predicates:\n\n\u003cdetails\u003e\n\u003csummary\u003eCustom Predicate\u003c/summary\u003e\n\n```swift\npublic struct CustomPredicate: Predicate {\n\n    public typealias InputType = String\n\n    private let custom: String\n\n    public init(custom: String) {\n        self.custom = custom\n    }\n\n    public func evaluate(with input: String) -\u003e Bool {\n        return input == custom\n    }\n}\n\nlet predicate = CustomPredicate(custom: \"alphabet\")\npredicate.evaluate(with: \"alp\") // returns false\npredicate.evaluate(with: \"alpha\") // returns false\npredicate.evaluate(with: \"alphabet\") // returns true\n```\n\n\u003c/details\u003e\n\n### Constraints\n\n#### Predicate Constraint\n\nA `PredicateConstraint` represents a data type that links a `Predicate` to an `Error`, in order to provide useful feedback for the end users.\n\n\u003cdetails\u003e\n\u003csummary\u003ePredicateConstraint\u003c/summary\u003e\n\n```swift\nlet constraint = PredicateConstraint\u003cString, MyError\u003e(.email, error: .invalid)\n\nlet result = constraint.evaluate(with: \"hello@nsagora.com\")\nswitch result {\ncase .valid:\n    print(\"Hi there 👋!\")\ncase .invalid(let summary):\n    print(\"Oh, I was expecting a valid email address!\")\n}  // prints \"Hi there 👋!\"\n```\n\n```swift\nenum MyError: Error {\n    case invalid\n}\n```\n\n\u003c/details\u003e\n\n#### Block Constraint\n\nA `BlockConstraint` represents a data type that links a custom validation closure to an `Error` that describes why the evaluation has failed. It's a shortcut of a `PredicateConstraint` that is initialised with a `BlockPredicate`.\n\n\u003cdetails\u003e\n\u003csummary\u003eBlockConstraint\u003c/summary\u003e\n\n```swift\nlet constraint = BlockConstraint\u003cInt, MyError\u003e {\n    $0 % 2 == 0\n} errorBuilder: {\n    .magicNumber\n}\n\nconstraint.evaluate(with: 3)\n```\n\n```swift\nenum Failure: MyError {\n    case magicNumber\n}\n```\n\n\u003c/details\u003e\n\n#### Group Constraint\n\nA `GroupConstraint` represents a composition of constraints that allows the evaluation to be made on:\n\n- all constraints\n- or any of the constraints\n\nTo provide context, a `GroupConstraint` allows us to constraint a piece of data as being required and also as being a valid email.\n\n\u003cdetails\u003e\n\u003csummary\u003eGroupConstraint\u003c/summary\n\nAn example of a registration form, whereby users are prompted to enter a strong _password_. This process typically entails some form of validation, but the logic itself is often unstructured and spread out through a view controller.\n\n`Peppermint` seeks instead to consolidate, standardise, and make explicit the logic that is being used to validate user input. To this end, the below example demonstrates construction of a full `GroupConstraint` object that can be used to enforce requirements on the user's password data:\n\n```swift\nvar passwordConstraint = GroupConstraint\u003cString, Form.Password\u003e(.all) {\n    PredicateConstraint {\n        .characterSet(.lowercaseLetters, mode: .loose)\n    } errorBuilder: {\n        .missingLowercase\n    }\n    PredicateConstraint{\n        .characterSet(.uppercaseLetters, mode: .loose)\n    } errorBuilder: {\n        .missingUppercase\n    }\n    PredicateConstraint {\n        .characterSet(.decimalDigits, mode: .loose)\n    } errorBuilder: {\n        .missingDigits\n    }\n    PredicateConstraint {\n        .characterSet(CharacterSet(charactersIn: \"!?@#$%^\u0026*()|\\\\/\u003c\u003e,.~`_+-=\"), mode: .loose)\n    } errorBuilder: {\n        .missingSpecialChars\n    }\n    PredicateConstraint {\n        .length(min: 8)\n    }  errorBuilder: {\n        .minLength(8)\n    }\n}\n\nlet password = \"3nGuard!\"\nlet result = passwordConstraint.evaluate(with: password)\n\nswitch result {\ncase .success:\n    print(\"Wow, that's a 💪 password!\")\ncase .failure(let summary):\n    print(summary.errors.map({$0.localizedDescription}))\n} // prints \"Wow, that's a 💪 password!\"\n```\n\nFrom above, we see that once we've constructed the `passwordConstraint`, we're simply calling `evaluate(with:)` to get our evaluation `Result`. This contains a `Summary` that can be handled as we please.\n\n\u003c/details\u003e\n\n## Contribute\n\nWe would love you for the contribution to **Peppermint**, check the [`LICENSE`][url-license-file] file for more info.\n\n## Meta\n\nThis project is developed and maintained by the members of [iOS NSAgora][url-twitter], the community of iOS Developers of Iași, Romania.\n\nDistributed under the [MIT][url-license] license. See [`LICENSE`][url-license-file] for more information.\n\n[https://github.com/nsagora/peppermint]\n\n[url-peppermint]: https://github.com/nsagora/peppermint\n[url-peppermint-docs]: https://nsagora.github.io/peppermint/\n[url-swift-package-manager]: https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app\n[url-license]: http://choosealicense.com/licenses/mit/\n[url-license-file]: https://github.com/nsagora/peppermint/blob/master/LICENSE\n[url-twitter]: https://x.com/nsagora\n[url-codecov]: https://codecov.io/gh/nsagora/peppermint\n[badge-license]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat\n[badge-twitter]: https://img.shields.io/badge/𝕏-%40nsgaora-blue.svg?style=flat\n[badge-build-macos]: https://github.com/nsagora/peppermint/actions/workflows/build-macos.yml/badge.svg\n[badge-build-linux]: https://github.com/nsagora/peppermint/actions/workflows/build-linux.yml/badge.svg\n[badge-codecov]: https://codecov.io/gh/nsagora/peppermint/branch/develop/graph/badge.svg\n[badge-version]: https://img.shields.io/badge/version-1.2.0-blue.svg?style=flat\n[badge-docs]: https://github.com/nsagora/peppermint/actions/workflows/docs.yml/badge.svg\n","funding_links":[],"categories":["UI"],"sub_categories":["Form \u0026 Settings"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnsagora%2Fpeppermint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnsagora%2Fpeppermint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnsagora%2Fpeppermint/lists"}