diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c69237a..0f3a6c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,30 +6,19 @@ on: pull_request: branches: [master] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - uses: purescript-contrib/setup-purescript@main - with: - purescript: "unstable" - - - uses: actions/setup-node@v2 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: - node-version: "14.x" - - - name: Install dependencies - run: | - npm install -g bower - npm install - bower install --production - - - name: Build source - run: npm run-script build - - - name: Run tests - run: | - bower install - npm run-script test --if-present + node-version: "24" + - run: npm install + - run: npm run build + - run: npm run build:example + - run: npm run example + - run: npm run format:check diff --git a/.gitignore b/.gitignore index 2c02232..afb2a45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ /.* !/.gitignore !/.github/ -package-lock.json +!/.tidyrc.json +/package-lock.json +/spago.lock /bower_components/ /node_modules/ /output/ diff --git a/.tidyrc.json b/.tidyrc.json new file mode 100644 index 0000000..9e39cf1 --- /dev/null +++ b/.tidyrc.json @@ -0,0 +1,10 @@ +{ + "importSort": "ide", + "importWrap": "source", + "indent": 2, + "operatorsFile": null, + "ribbon": 1, + "typeArrowPlacement": "first", + "unicode": "never", + "width": null +} diff --git a/CHANGELOG.md b/CHANGELOG.md index de3ecb5..5853660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [Unreleased](https://github.com/purescript/purescript-validation/compare/v6.0.1...HEAD) Breaking changes: @@ -12,9 +12,17 @@ Bugfixes: Other improvements: +## [v6.0.1](https://github.com/purescript/purescript-validation/releases/tag/v6.0.1) - 2026-10-17 + +Other improvements: + +- Migrate from Bower/Pulp to Spago, update CI and formatting, and restore the runnable example (#41 by @thomashoneyman). +- Use explicit case expressions for validation branches (#41 by @thomashoneyman). + ## [v6.0.0](https://github.com/purescript/purescript-validation/releases/tag/v6.0.0) - 2022-04-27 Breaking changes: + - Update project and deps to PureScript v0.15.0 (#37 by @JordanMartinez) - Drop deprecated `unV`; use `validation` instead (#38 by @JordanMartinez) @@ -27,6 +35,7 @@ Other improvements: ## [v5.0.0](https://github.com/purescript/purescript-validation/releases/tag/v5.0.0) - 2021-02-26 Breaking changes: + - Added support for PureScript 0.14 and dropped support for all previous versions (#28) New features: @@ -34,8 +43,9 @@ New features: Bugfixes: Other improvements: + - Added detailed `Semigroup` validation examples (#17) -- Added link to to Applicative Validation chapter in the PureScript book (#29) +- Added link to to Applicative Validation chapter in the PureScript book (#29) - Migrated CI to GitHub Actions and updated installation instructions to use Spago (#30) - Renamed `unV` to `validation` and deprecated `unV` (#33) - Added a changelog and pull request template (#34, #35) diff --git a/bower.json b/bower.json deleted file mode 100644 index e90a82a..0000000 --- a/bower.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "purescript-validation", - "homepage": "https://github.com/purescript/purescript-validation", - "license": "BSD-3-Clause", - "repository": { - "type": "git", - "url": "https://github.com/purescript/purescript-validation.git" - }, - "ignore": [ - "**/.*", - "bower_components", - "node_modules", - "output", - "test", - "bower.json", - "package.json" - ], - "dependencies": { - "purescript-bifunctors": "^6.0.0", - "purescript-control": "^6.0.0", - "purescript-either": "^6.0.0", - "purescript-foldable-traversable": "^6.0.0", - "purescript-newtype": "^5.0.0", - "purescript-prelude": "^6.0.0" - }, - "devDependencies": { - "purescript-console": "^6.0.0", - "purescript-ordered-collections": "^3.0.0", - "purescript-psci-support": "^6.0.0", - "purescript-strings": "^6.0.0" - } -} diff --git a/examples/spago.yaml b/examples/spago.yaml new file mode 100644 index 0000000..b7cb6dd --- /dev/null +++ b/examples/spago.yaml @@ -0,0 +1,17 @@ +package: + name: validation-examples + build: + strict: true + pedanticPackages: true + run: + main: Semigroup + dependencies: + - arrays: ">=7.0.0 <8.0.0" + - bifunctors: ">=6.0.0 <7.0.0" + - console: ">=6.0.0 <7.0.0" + - effect: ">=4.0.0 <5.0.0" + - newtype: ">=5.0.0 <6.0.0" + - ordered-collections: ">=3.0.0 <4.0.0" + - prelude: ">=6.0.0 <7.0.0" + - strings: ">=6.0.0 <7.0.0" + - validation: "*" diff --git a/examples/Semigroup.purs b/examples/src/Semigroup.purs similarity index 92% rename from examples/Semigroup.purs rename to examples/src/Semigroup.purs index b080eaa..cff6bc8 100644 --- a/examples/Semigroup.purs +++ b/examples/src/Semigroup.purs @@ -2,20 +2,20 @@ module Semigroup where import Prelude -import Effect (Effect) -import Effect.Console (logShow) import Data.Array.NonEmpty (NonEmptyArray) import Data.Array.NonEmpty as NonEmpty import Data.Bifunctor (bimap) +import Data.Eq.Generic (genericEq) import Data.Generic.Rep (class Generic) -import Data.Generic.Rep.Eq (genericEq) -import Data.Generic.Rep.Ord (genericCompare) -import Data.Generic.Rep.Show (genericShow) import Data.Map (Map) import Data.Map as Map import Data.Newtype (class Newtype, over2) +import Data.Ord.Generic (genericCompare) +import Data.Show.Generic (genericShow) import Data.String (length, null, toLower, toUpper) import Data.Validation.Semigroup (V, invalid) +import Effect (Effect) +import Effect.Console (logShow) -- | `UnvalidatedFormData` represents the raw data we might receive from a form -- | before any validation has been performed. @@ -46,7 +46,7 @@ type ValidatedFormData = } toString :: ValidatedFormData -> String -toString { username: (Username u)} = "{ username: '" <> u <> "', password: }" +toString { username: (Username u) } = "{ username: '" <> u <> "', password: }" -- | `ValidationError` represents the potential errors we might encounter during -- | the validation process. @@ -166,10 +166,10 @@ instance showFormValidationErrors :: Show FormValidationErrors where -- | Otherwise, it returns the input wrapped in the `Username` newtype to -- | distinguish it from a normal, unvalidated `String`. validateUsername :: String -> V FormValidationErrors Username -validateUsername input = +validateUsername input = bimap (FormValidationErrors <<< Map.singleton InvalidUsername) Username - $ validateNonEmpty input - *> validateMinimumLength 4 input + $ validateNonEmpty input + *> validateMinimumLength 4 input -- | This function validates that an input string conforms to our requirements -- | for a valid password. Namely, we require that the input be non-empty, at @@ -185,11 +185,11 @@ validateUsername input = -- | Otherwise, it returns the input wrapped in the `Password` newtype to -- | distinguish it from a normal, unvalidated `String`. validatePassword :: String -> V FormValidationErrors Password -validatePassword input = +validatePassword input = bimap (FormValidationErrors <<< Map.singleton InvalidPassword) Password - $ validateNonEmpty input - *> validateMinimumLength 6 input - *> validateMixedCase input + $ validateNonEmpty input + *> validateMinimumLength 6 input + *> validateMixedCase input -- | This function validates that an `UnvalidatedFormData` record contains both -- | a valid username and a valid password, per the requirements specified in @@ -201,34 +201,34 @@ validatePassword input = -- | -- | Otherwise, it returns the validated fields in the `ValidatedFormData` -- | record specified above. -validateForm - :: UnvalidatedFormData +validateForm + :: UnvalidatedFormData -> V FormValidationErrors ValidatedFormData -validateForm {username, password} = {username: _, password: _} +validateForm { username, password } = { username: _, password: _ } <$> validateUsername username <*> validatePassword password -- | This is a form that will fail validation, since both fields are empty -- | strings. emptyUsernameAndPassword :: UnvalidatedFormData -emptyUsernameAndPassword = {username: "", password: ""} +emptyUsernameAndPassword = { username: "", password: "" } -- | This is a form that will fail validation, since both fields are too short. shortUsernameAndPassword :: UnvalidatedFormData -shortUsernameAndPassword = {username: "foo", password: "bar"} +shortUsernameAndPassword = { username: "foo", password: "bar" } -- | This is a form that will fail validation, since the password lowercase. lowerCasePassword :: UnvalidatedFormData -lowerCasePassword = {username: "alice", password: "foobarbaz"} +lowerCasePassword = { username: "alice", password: "foobarbaz" } -- | This is a form that will fail validation, since the password uppercase. upperCasePassword :: UnvalidatedFormData -upperCasePassword = {username: "alice", password: "FOOBARBAZ"} +upperCasePassword = { username: "alice", password: "FOOBARBAZ" } -- | This is a form that will pass validation, as it conforms to all the -- | requirements outlined in the validation functions above. goodForm :: UnvalidatedFormData -goodForm = {username: "alice", password: "FooBarBaz"} +goodForm = { username: "alice", password: "FooBarBaz" } -- | Run through all of the example forms and print the validation results to -- | the console. @@ -253,4 +253,4 @@ main = do -- > pure ("{\"username\":\"alice\",\"password\":\"FooBarBaz\"}") where - printValidation = logShow <<< (map toString) + printValidation = logShow <<< (map toString) diff --git a/package.json b/package.json index e8e8435..c8cac5f 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,16 @@ { "private": true, "scripts": { - "clean": "rimraf output && rimraf .pulp-cache", - "build": "pulp build -- --censor-lib --strict", - "build:example": "pulp build --include examples" + "build": "spago build", + "build:example": "spago build -p validation-examples", + "example": "spago run -p validation-examples", + "format": "purs-tidy format-in-place src examples/src && oxfmt --write .", + "format:check": "purs-tidy check src examples/src && oxfmt --check ." }, "devDependencies": { - "pulp": "16.0.0-0", - "purescript-psa": "^0.8.2", - "rimraf": "^3.0.2" + "oxfmt": "^0.73.0", + "purescript": "^0.15.16", + "purs-tidy": "^0.11.1", + "spago": "^1.0.5" } } diff --git a/spago.yaml b/spago.yaml new file mode 100644 index 0000000..15a83d7 --- /dev/null +++ b/spago.yaml @@ -0,0 +1,20 @@ +package: + name: validation + description: Applicative validation + build: + strict: true + pedanticPackages: true + publish: + version: 6.0.1 + license: BSD-3-Clause + location: + githubOwner: purescript + githubRepo: purescript-validation + dependencies: + - bifunctors: ">=6.0.0 <7.0.0" + - control: ">=6.0.0 <7.0.0" + - either: ">=6.0.0 <7.0.0" + - foldable-traversable: ">=6.0.0 <7.0.0" + - newtype: ">=5.0.0 <6.0.0" + - prelude: ">=6.0.0 <7.0.0" +workspace: {} diff --git a/src/Data/Validation/Semigroup.purs b/src/Data/Validation/Semigroup.purs index 9e46aaa..a2cb149 100644 --- a/src/Data/Validation/Semigroup.purs +++ b/src/Data/Validation/Semigroup.purs @@ -20,9 +20,9 @@ import Data.Bifunctor (class Bifunctor) import Data.Either (Either(..)) import Data.Eq (class Eq1) import Data.Foldable (class Foldable) +import Data.Newtype (class Newtype) import Data.Ord (class Ord1) import Data.Traversable (class Traversable) -import Data.Newtype (class Newtype) -- | The `V` functor, used for applicative validation -- | @@ -46,8 +46,9 @@ derive instance newtypeV :: Newtype (V err result) _ -- | applied to the first function, if the validation succeeded the inner value -- | is applied to the second function. validation :: forall err result r. (err -> r) -> (result -> r) -> V err result -> r -validation f _ (V (Left err)) = f err -validation _ g (V (Right result)) = g result +validation f g (V value) = case value of + Left err -> f err + Right result -> g result -- | Fail with a validation error. invalid :: forall err result. err -> V err result @@ -55,8 +56,9 @@ invalid = V <<< Left -- | Test whether validation was successful or not. isValid :: forall err result. V err result -> Boolean -isValid (V (Right _)) = true -isValid _ = false +isValid = case _ of + V (Right _) -> true + _ -> false toEither :: forall err result. V err result -> Either err result toEither (V e) = e @@ -87,10 +89,11 @@ derive newtype instance functorV :: Functor (V err) derive newtype instance bifunctorV :: Bifunctor V instance applyV :: Semigroup err => Apply (V err) where - apply (V (Left err1)) (V (Left err2)) = V (Left (err1 <> err2)) - apply (V (Left err)) _ = V (Left err) - apply _ (V (Left err)) = V (Left err) - apply (V (Right f)) (V (Right x)) = V (Right (f x)) + apply left right = case left, right of + V (Left err1), V (Left err2) -> V (Left (err1 <> err2)) + V (Left err), _ -> V (Left err) + _, V (Left err) -> V (Left err) + V (Right f), V (Right x) -> V (Right (f x)) instance applicativeV :: Semigroup err => Applicative (V err) where pure = V <<< Right diff --git a/src/Data/Validation/Semiring.purs b/src/Data/Validation/Semiring.purs index 84c0b4e..366bbbf 100644 --- a/src/Data/Validation/Semiring.purs +++ b/src/Data/Validation/Semiring.purs @@ -19,9 +19,9 @@ import Data.Bifunctor (class Bifunctor) import Data.Either (Either(..)) import Data.Eq (class Eq1) import Data.Foldable (class Foldable) +import Data.Newtype (class Newtype) import Data.Ord (class Ord1) import Data.Traversable (class Traversable) -import Data.Newtype (class Newtype) -- | The `V` functor, used for alternative validation -- | @@ -47,8 +47,9 @@ derive instance newtypeV :: Newtype (V err result) _ -- | applied to the first function, if the validation succeeded the inner value -- | is applied to the second function. validation :: forall err result r. (err -> r) -> (result -> r) -> V err result -> r -validation f _ (V (Left err)) = f err -validation _ g (V (Right result)) = g result +validation f g (V value) = case value of + Left err -> f err + Right result -> g result -- | Fail with a validation error. invalid :: forall err result. err -> V err result @@ -56,8 +57,9 @@ invalid = V <<< Left -- | Test whether validation was successful or not. isValid :: forall err result. V err result -> Boolean -isValid (V (Right _)) = true -isValid _ = false +isValid = case _ of + V (Right _) -> true + _ -> false toEither :: forall err result. V err result -> Either err result toEither (V e) = e @@ -87,11 +89,12 @@ derive newtype instance functorV :: Functor (V err) derive newtype instance bifunctorV :: Bifunctor V -instance applyV :: Semiring err => Apply (V err) where - apply (V (Left err1)) (V (Left err2)) = V (Left (err1 * err2)) - apply (V (Left err)) _ = V (Left err) - apply _ (V (Left err)) = V (Left err) - apply (V (Right f)) (V (Right x)) = V (Right (f x)) +instance applyV :: Semiring err => Apply (V err) where + apply left right = case left, right of + V (Left err1), V (Left err2) -> V (Left (err1 * err2)) + V (Left err), _ -> V (Left err) + _, V (Left err) -> V (Left err) + V (Right f), V (Right x) -> V (Right (f x)) instance applicativeV :: Semiring err => Applicative (V err) where pure = V <<< Right @@ -103,9 +106,10 @@ instance monoidV :: (Semiring err, Monoid a) => Monoid (V err a) where mempty = pure mempty instance altV :: Semiring err => Alt (V err) where - alt (V (Left err1)) (V (Left err2)) = V (Left (err1 + err2)) - alt (V (Left _)) a = a - alt (V (Right a)) _ = V (Right a) + alt left right = case left, right of + V (Left err1), V (Left err2) -> V (Left (err1 + err2)) + V (Left _), a -> a + V (Right a), _ -> V (Right a) instance plusV :: Semiring err => Plus (V err) where empty = V (Left zero)