├── .envrc ├── .yamlfmt.yaml ├── .tidyrc.json ├── package.json ├── LICENSE ├── test ├── HowTo.MakeAFunctionWithRequiredAndOptionalValues.purs ├── HowTo.MakeAFunctionWithRequiredAndOptionalValuesFromARecord.purs ├── HowTo.MakeAFunctionWithOptionalValues.purs ├── HowTo.MakeAFunctionWithOptionalValuesFromARecord.purs ├── Test.Main.purs ├── HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptSimpleJSON.purs ├── HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptSimpleJSON.purs ├── HowTo.ProvideAnEasierAPIForDateTime.purs ├── HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptArgonaut.purs ├── HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptCodecArgonaut.purs ├── HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptArgonaut.purs ├── HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptCodecArgonaut.purs └── Test.Option.purs ├── bower.json ├── Makefile ├── .github └── workflows │ └── test.yaml ├── flake.lock ├── .gitignore ├── flake.nix └── README.md /.envrc: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | use flake 3 | -------------------------------------------------------------------------------- /.yamlfmt.yaml: -------------------------------------------------------------------------------- 1 | formatter: 2 | retain_line_breaks_single: true 3 | type: basic 4 | -------------------------------------------------------------------------------- /.tidyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "importSort": "source", 3 | "importWrap": "source", 4 | "indent": 2, 5 | "operatorsFile": null, 6 | "ribbon": 1, 7 | "typeArrowPlacement": "last", 8 | "unicode": "never", 9 | "width": null 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-option", 3 | "version": "1.0.0", 4 | "description": "Optional values", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "keywords": [], 10 | "author": "Hardy Jones ", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "bower": "^1.8.8", 14 | "purescript": "^0.14.0", 15 | "purescript-psa": "^0.8.2", 16 | "purs-tidy": "^0.11.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hardy Jones 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 | -------------------------------------------------------------------------------- /test/HowTo.MakeAFunctionWithRequiredAndOptionalValues.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.MakeAFunctionWithRequiredAndOptionalValues 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Maybe as Data.Maybe 9 | import Option as Option 10 | import Test.Spec as Test.Spec 11 | import Test.Spec.Assertions as Test.Spec.Assertions 12 | 13 | greeting :: 14 | Option.Record (name :: String) (title :: String) -> 15 | String 16 | greeting record' = "Hello, " <> title' <> record.name 17 | where 18 | record :: Record (name :: String, title :: Data.Maybe.Maybe String) 19 | record = Option.recordToRecord record' 20 | 21 | title' :: String 22 | title' = case record.title of 23 | Data.Maybe.Just title -> title <> " " 24 | Data.Maybe.Nothing -> "" 25 | 26 | spec :: Test.Spec.Spec Unit 27 | spec = 28 | Test.Spec.describe "HowTo.MakeAFunctionWithOptionalValues" do 29 | spec_greeting 30 | 31 | spec_greeting :: Test.Spec.Spec Unit 32 | spec_greeting = 33 | Test.Spec.describe "greeting" do 34 | Test.Spec.it "uses a name correctly" do 35 | Test.Spec.Assertions.shouldEqual 36 | (greeting (Option.recordFromRecord { name: "Pat" })) 37 | "Hello, Pat" 38 | Test.Spec.it "uses both a name and a title correctly" do 39 | Test.Spec.Assertions.shouldEqual 40 | (greeting (Option.recordFromRecord { name: "Pat", title: "Dr." })) 41 | "Hello, Dr. Pat" 42 | -------------------------------------------------------------------------------- /test/HowTo.MakeAFunctionWithRequiredAndOptionalValuesFromARecord.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.MakeAFunctionWithRequiredAndOptionalValuesFromARecord 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Maybe as Data.Maybe 9 | import Option as Option 10 | import Test.Spec as Test.Spec 11 | import Test.Spec.Assertions as Test.Spec.Assertions 12 | 13 | greeting :: 14 | forall record. 15 | Option.FromRecord record (name :: String) (title :: String) => 16 | Record record -> 17 | String 18 | greeting record'' = "Hello, " <> title' <> record.name 19 | where 20 | record :: Record (name :: String, title :: Data.Maybe.Maybe String) 21 | record = Option.recordToRecord record' 22 | 23 | record' :: Option.Record (name :: String) (title :: String) 24 | record' = Option.recordFromRecord record'' 25 | 26 | title' :: String 27 | title' = case record.title of 28 | Data.Maybe.Just title -> title <> " " 29 | Data.Maybe.Nothing -> "" 30 | 31 | spec :: Test.Spec.Spec Unit 32 | spec = 33 | Test.Spec.describe "HowTo.MakeAFunctionWithOptionalValuesFromARecord" do 34 | spec_greeting 35 | 36 | spec_greeting :: Test.Spec.Spec Unit 37 | spec_greeting = 38 | Test.Spec.describe "greeting" do 39 | Test.Spec.it "uses a name correctly" do 40 | Test.Spec.Assertions.shouldEqual 41 | (greeting { name: "Pat" }) 42 | "Hello, Pat" 43 | Test.Spec.it "uses both a name and a title correctly" do 44 | Test.Spec.Assertions.shouldEqual 45 | (greeting { name: "Pat", title: "Dr." }) 46 | "Hello, Dr. Pat" 47 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-option", 3 | "description": "Safe option handling", 4 | "authors": [ 5 | "Hardy Jones " 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "output" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/joneshf/purescript-option.git" 17 | }, 18 | "dependencies": { 19 | "purescript-argonaut-codecs": ">= 8.0.0 < 9.0.0", 20 | "purescript-argonaut-core": ">= 6.0.0 < 7.0.0", 21 | "purescript-codec": ">= 4.0.0 < 5.0.0", 22 | "purescript-codec-argonaut": ">= 8.0.0 < 9.0.0", 23 | "purescript-either": ">= 5.0.0 < 6.0.0", 24 | "purescript-foreign": ">= 6.0.0 < 7.0.0", 25 | "purescript-foreign-object": ">= 3.0.0 < 4.0.0", 26 | "purescript-lists": ">= 6.0.0 < 7.0.0", 27 | "purescript-maybe": ">= 5.0.0 < 6.0.0", 28 | "purescript-profunctor": ">= 5.0.0 < 6.0.0", 29 | "purescript-prelude": ">= 5.0.0 < 6.0.0", 30 | "purescript-record": ">= 3.0.0 < 4.0.0", 31 | "purescript-simple-json": ">= 8.0.0 < 9.0.0", 32 | "purescript-transformers": ">= 5.0.0 < 6.0.0", 33 | "purescript-tuples": ">= 6.0.0 < 7.0.0", 34 | "purescript-type-equality": ">= 4.0.0 < 5.0.0", 35 | "purescript-unsafe-coerce": ">= 5.0.0 < 6.0.0" 36 | }, 37 | "devDependencies": { 38 | "purescript-aff": ">= 6.0.0 < 7.0.0", 39 | "purescript-datetime": ">= 5.0.0 < 6.0.0", 40 | "purescript-foldable-traversable": ">= 5.0.0 < 6.0.0", 41 | "purescript-psci-support": ">= 5.0.0 < 6.0.0", 42 | "purescript-spec": ">= 5.0.0 < 6.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/HowTo.MakeAFunctionWithOptionalValues.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.MakeAFunctionWithOptionalValues 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Maybe as Data.Maybe 9 | import Option as Option 10 | import Test.Spec as Test.Spec 11 | import Test.Spec.Assertions as Test.Spec.Assertions 12 | import Type.Proxy as Type.Proxy 13 | 14 | greeting :: Option.Option (name :: String, title :: String) -> String 15 | greeting option = "Hello, " <> title' <> name' 16 | where 17 | name' :: String 18 | name' = case Option.get (Type.Proxy.Proxy :: _ "name") option of 19 | Data.Maybe.Just name -> name 20 | Data.Maybe.Nothing -> "World" 21 | 22 | title' :: String 23 | title' = case Option.get (Type.Proxy.Proxy :: _ "title") option of 24 | Data.Maybe.Just title -> title <> " " 25 | Data.Maybe.Nothing -> "" 26 | 27 | spec :: Test.Spec.Spec Unit 28 | spec = 29 | Test.Spec.describe "HowTo.MakeAFunctionWithOptionalValues" do 30 | spec_greeting 31 | 32 | spec_greeting :: Test.Spec.Spec Unit 33 | spec_greeting = 34 | Test.Spec.describe "greeting" do 35 | Test.Spec.it "defaults with no values" do 36 | Test.Spec.Assertions.shouldEqual 37 | (greeting (Option.fromRecord {})) 38 | "Hello, World" 39 | Test.Spec.it "uses a name correctly" do 40 | Test.Spec.Assertions.shouldEqual 41 | (greeting (Option.fromRecord { name: "Pat" })) 42 | "Hello, Pat" 43 | Test.Spec.it "uses a title correctly" do 44 | Test.Spec.Assertions.shouldEqual 45 | (greeting (Option.fromRecord { title: "wonderful" })) 46 | "Hello, wonderful World" 47 | Test.Spec.it "uses both a name and a title correctly" do 48 | Test.Spec.Assertions.shouldEqual 49 | (greeting (Option.fromRecord { name: "Pat", title: "Dr." })) 50 | "Hello, Dr. Pat" 51 | -------------------------------------------------------------------------------- /test/HowTo.MakeAFunctionWithOptionalValuesFromARecord.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.MakeAFunctionWithOptionalValuesFromARecord 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Maybe as Data.Maybe 9 | import Option as Option 10 | import Test.Spec as Test.Spec 11 | import Test.Spec.Assertions as Test.Spec.Assertions 12 | import Type.Proxy as Type.Proxy 13 | 14 | greeting :: 15 | forall record. 16 | Option.FromRecord record () (name :: String, title :: String) => 17 | Record record -> 18 | String 19 | greeting record = "Hello, " <> title' <> name' 20 | where 21 | name' :: String 22 | name' = case Option.get (Type.Proxy.Proxy :: _ "name") option of 23 | Data.Maybe.Just name -> name 24 | Data.Maybe.Nothing -> "World" 25 | 26 | option :: Option.Option (name :: String, title :: String) 27 | option = Option.fromRecord record 28 | 29 | title' :: String 30 | title' = case Option.get (Type.Proxy.Proxy :: _ "title") option of 31 | Data.Maybe.Just title -> title <> " " 32 | Data.Maybe.Nothing -> "" 33 | 34 | spec :: Test.Spec.Spec Unit 35 | spec = 36 | Test.Spec.describe "HowTo.MakeAFunctionWithOptionalValuesFromARecord" do 37 | spec_greeting 38 | 39 | spec_greeting :: Test.Spec.Spec Unit 40 | spec_greeting = 41 | Test.Spec.describe "greeting" do 42 | Test.Spec.it "defaults with no values" do 43 | Test.Spec.Assertions.shouldEqual 44 | (greeting {}) 45 | "Hello, World" 46 | Test.Spec.it "uses a name correctly" do 47 | Test.Spec.Assertions.shouldEqual 48 | (greeting { name: "Pat" }) 49 | "Hello, Pat" 50 | Test.Spec.it "uses a title correctly" do 51 | Test.Spec.Assertions.shouldEqual 52 | (greeting { title: "wonderful" }) 53 | "Hello, wonderful World" 54 | Test.Spec.it "uses both a name and a title correctly" do 55 | Test.Spec.Assertions.shouldEqual 56 | (greeting { name: "Pat", title: "Dr." }) 57 | "Hello, Dr. Pat" 58 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BOWER := npx bower 2 | BOWER_FLAGS ?= 3 | COMPILE_FLAGS ?= --censor-lib 4 | DEPENDENCIES := 'bower_components/purescript-*/src/**/*.purs' 5 | NIX := nix 6 | NODE := node 7 | NPM := npm 8 | OUTPUT := output 9 | PSA := npx psa 10 | PURS := npx purs 11 | REPL_FLAGS ?= 12 | SRC := src 13 | TEST := test 14 | 15 | SRCS := $(shell find $(SRC) -name '*.purs' -type f) 16 | TESTS := $(shell find $(TEST) -name '*.purs' -type f) 17 | SRC_OUTPUTS := $(patsubst $(SRC).%.purs,$(OUTPUT)/%/index.js,$(subst /,.,$(SRCS))) 18 | TEST_OUTPUTS := $(patsubst $(TEST).%.purs,$(OUTPUT)/%/index.js,$(subst /,.,$(TESTS))) 19 | 20 | define SRC_OUTPUT_RULE 21 | $(patsubst $(SRC).%.purs,$(OUTPUT)/%/index.js,$(subst /,.,$(1))): $(1) bower_components 22 | $(PSA) $(COMPILE_FLAGS) $(DEPENDENCIES) $(SRCS) 23 | endef 24 | 25 | define TEST_OUTPUT_RULE 26 | $(patsubst $(TEST).%.purs,$(OUTPUT)/%/index.js,$(subst /,.,$(1))): $(1) $(SRC_OUTPUTS) bower_components 27 | $(PSA) $(COMPILE_FLAGS) $(DEPENDENCIES) $(SRCS) $(TESTS) 28 | endef 29 | 30 | $(foreach source, $(SRCS), $(eval $(call SRC_OUTPUT_RULE, $(source)))) 31 | 32 | $(foreach test, $(TESTS), $(eval $(call TEST_OUTPUT_RULE, $(test)))) 33 | 34 | .DEFAULT_GOAL := build 35 | 36 | $(OUTPUT): 37 | mkdir -p $@ 38 | 39 | $(OUTPUT)/test.js: $(SRC_OUTPUTS) $(TEST_OUTPUTS) | $(OUTPUT) 40 | $(PURS) bundle \ 41 | --main Test.Main \ 42 | --module Test.Main \ 43 | --output $@ \ 44 | output/*/index.js \ 45 | output/*/foreign.js 46 | 47 | bower_components: bower.json node_modules 48 | $(BOWER) $(BOWER_FLAGS) install 49 | touch $@ 50 | 51 | .PHONY: build 52 | build: bower_components $(SRC_OUTPUTS) 53 | 54 | .PHONY: clean 55 | clean: 56 | rm -rf \ 57 | .psc-ide-port \ 58 | .psci_modules \ 59 | bower_components \ 60 | node_modules \ 61 | output 62 | 63 | .PHONY: format 64 | format: 65 | $(NIX) fmt 66 | 67 | .PHONE: lint 68 | lint: 69 | $(NIX) flake check 70 | 71 | node_modules: package.json 72 | $(NPM) install 73 | touch $@ 74 | 75 | .PHONY: repl 76 | repl: bower_components 77 | $(PURS) repl $(REPL_FLAGS) $(DEPENDENCIES) $(SRCS) 78 | 79 | .PHONY: test 80 | test: $(OUTPUT)/test.js bower_components $(SRC_OUTPUTS) $(TEST_OUTPUTS) lint 81 | $(NODE) $< 82 | 83 | .PHONY: variables 84 | variables: 85 | $(info $$(DEPENDENCIES) is [$(DEPENDENCIES)]) 86 | $(info $$(SRCS) is [$(SRCS)]) 87 | $(info $$(SRC_OUTPUTS) is [$(SRC_OUTPUTS)]) 88 | $(info $$(TESTS) is [$(TESTS)]) 89 | $(info $$(TEST_OUTPUTS) is [$(TEST_OUTPUTS)]) 90 | -------------------------------------------------------------------------------- /test/Test.Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main (main) where 2 | 3 | import Prelude 4 | import Effect as Effect 5 | import Effect.Aff as Effect.Aff 6 | import HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptArgonaut as HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptArgonaut 7 | import HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptCodecArgonaut as HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptCodecArgonaut 8 | import HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptSimpleJSON as HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptSimpleJSON 9 | import HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptArgonaut as HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptArgonaut 10 | import HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptCodecArgonaut as HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptCodecArgonaut 11 | import HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptSimpleJSON as HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptSimpleJSON 12 | import HowTo.MakeAFunctionWithOptionalValues as HowTo.MakeAFunctionWithOptionalValues 13 | import HowTo.MakeAFunctionWithOptionalValuesFromARecord as HowTo.MakeAFunctionWithOptionalValuesFromARecord 14 | import HowTo.MakeAFunctionWithRequiredAndOptionalValues as HowTo.MakeAFunctionWithRequiredAndOptionalValues 15 | import HowTo.MakeAFunctionWithRequiredAndOptionalValuesFromARecord as HowTo.MakeAFunctionWithRequiredAndOptionalValuesFromARecord 16 | import HowTo.ProvideAnEasierAPIForDateTime as HowTo.ProvideAnEasierAPIForDateTime 17 | import Test.Option as Test.Option 18 | import Test.Spec as Test.Spec 19 | import Test.Spec.Reporter.Console as Test.Spec.Reporter.Console 20 | import Test.Spec.Runner as Test.Spec.Runner 21 | 22 | data Proxy (symbol :: Symbol) = Proxy 23 | 24 | main :: Effect.Effect Unit 25 | main = Effect.Aff.launchAff_ (Test.Spec.Runner.runSpec reporters spec) 26 | 27 | reporters :: Array Test.Spec.Runner.Reporter 28 | reporters = 29 | [ Test.Spec.Reporter.Console.consoleReporter 30 | ] 31 | 32 | spec :: Test.Spec.Spec Unit 33 | spec = do 34 | Test.Option.spec 35 | HowTo.MakeAFunctionWithOptionalValues.spec 36 | HowTo.MakeAFunctionWithOptionalValuesFromARecord.spec 37 | HowTo.MakeAFunctionWithRequiredAndOptionalValues.spec 38 | HowTo.MakeAFunctionWithRequiredAndOptionalValuesFromARecord.spec 39 | HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptArgonaut.spec 40 | HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptCodecArgonaut.spec 41 | HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptSimpleJSON.spec 42 | HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptArgonaut.spec 43 | HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptCodecArgonaut.spec 44 | HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptSimpleJSON.spec 45 | HowTo.ProvideAnEasierAPIForDateTime.spec 46 | -------------------------------------------------------------------------------- /test/HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptSimpleJSON.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptSimpleJSON 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Either as Data.Either 9 | import Data.Semigroup.Foldable as Data.Semigroup.Foldable 10 | import Foreign as Foreign 11 | import Option as Option 12 | import Simple.JSON as Simple.JSON 13 | import Test.Spec as Test.Spec 14 | import Test.Spec.Assertions as Test.Spec.Assertions 15 | 16 | parse :: 17 | String -> 18 | Data.Either.Either String (Option.Record (name :: String) (title :: String)) 19 | parse string = case readJSON string of 20 | Data.Either.Left errors -> Data.Either.Left (Data.Semigroup.Foldable.intercalateMap " " Foreign.renderForeignError errors) 21 | Data.Either.Right record -> Data.Either.Right record 22 | 23 | readJSON :: 24 | String -> 25 | Simple.JSON.E (Option.Record (name :: String) (title :: String)) 26 | readJSON = Simple.JSON.readJSON 27 | 28 | writeJSON :: 29 | Option.Record (name :: String) (title :: String) -> 30 | String 31 | writeJSON = Simple.JSON.writeJSON 32 | 33 | spec :: Test.Spec.Spec Unit 34 | spec = 35 | Test.Spec.describe "HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptSimpleJSON" do 36 | spec_parse 37 | spec_writeJSON 38 | 39 | spec_parse :: Test.Spec.Spec Unit 40 | spec_parse = 41 | Test.Spec.describe "parse" do 42 | Test.Spec.it "fails if no fields are given" do 43 | Test.Spec.Assertions.shouldEqual 44 | (parse """{}""") 45 | (Data.Either.Left "Error at property \"name\": Type mismatch: expected String, found Undefined") 46 | Test.Spec.it "fails if only a title is given" do 47 | Test.Spec.Assertions.shouldEqual 48 | (parse """{ "title": "wonderful" }""") 49 | (Data.Either.Left "Error at property \"name\": Type mismatch: expected String, found Undefined") 50 | Test.Spec.it "requires a name" do 51 | Test.Spec.Assertions.shouldEqual 52 | (parse """{ "name": "Pat" }""") 53 | (Data.Either.Right (Option.recordFromRecord { name: "Pat" })) 54 | Test.Spec.it "requires a name and accepts a title" do 55 | Test.Spec.Assertions.shouldEqual 56 | (parse """{ "name": "Pat", "title": "Dr." }""") 57 | (Data.Either.Right (Option.recordFromRecord { name: "Pat", title: "Dr." })) 58 | Test.Spec.it "doesn't fail for null fields" do 59 | Test.Spec.Assertions.shouldEqual 60 | (parse """{ "name": "Pat", "title": null }""") 61 | (Data.Either.Right (Option.recordFromRecord { name: "Pat" })) 62 | 63 | spec_writeJSON :: Test.Spec.Spec Unit 64 | spec_writeJSON = 65 | Test.Spec.describe "writeJSON" do 66 | Test.Spec.it "inserts a name" do 67 | Test.Spec.Assertions.shouldEqual 68 | (writeJSON (Option.recordFromRecord { name: "Pat" })) 69 | "{\"name\":\"Pat\"}" 70 | Test.Spec.it "inserts both a name and a title if it exists" do 71 | Test.Spec.Assertions.shouldEqual 72 | (writeJSON (Option.recordFromRecord { name: "Pat", title: "Dr." })) 73 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 74 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | - push 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-24.04 9 | steps: 10 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 11 | - name: Install Nix 12 | uses: nixbuild/nix-quick-install-action@63ca48f939ee3b8d835f4126562537df0fee5b91 13 | - name: Cache Nix store 14 | uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a 15 | with: 16 | # The key includes the os/arch pair as artifacts in the Nix store are platform-specific. 17 | primary-key: nix-${{ format('{0}_{1}', runner.os, runner.arch) }}-${{ hashFiles('flake.lock') }}-${{ hashFiles('**/*.nix') }} 18 | restore-prefixes-first-match: | 19 | nix-${{ format('{0}_{1}', runner.os, runner.arch) }}-${{ hashFiles('flake.lock') }}- 20 | nix-${{ format('{0}_{1}', runner.os, runner.arch) }}- 21 | - name: Cache node_modules 22 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 23 | with: 24 | # The key includes the os/arch pair as artifacts in `node_modules` can be platform-specific. 25 | key: node_modules-${{ format('{0}_{1}', runner.os, runner.arch) }}-${{ hashFiles('flake.lock') }}-${{ hashFiles('**/*.nix') }}-${{ hashFiles('**/package-lock.json') }} 26 | path: "**/node_modules" 27 | restore-keys: | 28 | node_modules-${{ format('{0}_{1}', runner.os, runner.arch) }}-${{ hashFiles('flake.lock') }}-${{ hashFiles('**/*.nix') }}- 29 | node_modules-${{ format('{0}_{1}', runner.os, runner.arch) }}-${{ hashFiles('flake.lock') }}- 30 | node_modules-${{ format('{0}_{1}', runner.os, runner.arch) }}- 31 | - name: Cache bower_components 32 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 33 | with: 34 | key: bower_components-${{ hashFiles('flake.lock') }}-${{ hashFiles('**/*.nix') }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('bower.json') }} 35 | path: bower_components 36 | restore-keys: | 37 | bower_components-${{ hashFiles('flake.lock') }}-${{ hashFiles('**/*.nix') }}-${{ hashFiles('**/package-lock.json') }}- 38 | bower_components-${{ hashFiles('flake.lock') }}-${{ hashFiles('**/*.nix') }}- 39 | bower_components-${{ hashFiles('flake.lock') }}- 40 | bower_components- 41 | - name: Cache output 42 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 43 | with: 44 | key: output-${{ hashFiles('flake.lock') }}-${{ hashFiles('**/*.nix') }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('bower.json') }}-${{ hashFiles('**/*.js', '**/*.purs') }} 45 | path: output 46 | restore-keys: | 47 | output-${{ hashFiles('flake.lock') }}-${{ hashFiles('**/*.nix') }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('bower.json') }}- 48 | output-${{ hashFiles('flake.lock') }}-${{ hashFiles('**/*.nix') }}-${{ hashFiles('**/package-lock.json') }}- 49 | output-${{ hashFiles('flake.lock') }}-${{ hashFiles('**/*.nix') }}- 50 | output-${{ hashFiles('flake.lock') }}- 51 | output- 52 | - name: Test 53 | run: nix develop . --command make test 54 | -------------------------------------------------------------------------------- /test/HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptSimpleJSON.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptSimpleJSON 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Either as Data.Either 9 | import Option as Option 10 | import Simple.JSON as Simple.JSON 11 | import Test.Spec as Test.Spec 12 | import Test.Spec.Assertions as Test.Spec.Assertions 13 | 14 | readJSON :: 15 | String -> 16 | Simple.JSON.E (Option.Option (name :: String, title :: String)) 17 | readJSON = Simple.JSON.readJSON 18 | 19 | writeJSON :: 20 | Option.Option (name :: String, title :: String) -> 21 | String 22 | writeJSON = Simple.JSON.writeJSON 23 | 24 | spec :: Test.Spec.Spec Unit 25 | spec = 26 | Test.Spec.describe "HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptSimpleJSON" do 27 | spec_readJSON 28 | spec_writeJSON 29 | 30 | spec_readJSON :: Test.Spec.Spec Unit 31 | spec_readJSON = 32 | Test.Spec.describe "readJSON" do 33 | Test.Spec.it "doesn't require any fields" do 34 | Test.Spec.Assertions.shouldEqual 35 | (readJSON """{}""") 36 | (Data.Either.Right (Option.fromRecord {})) 37 | Test.Spec.it "accepts only a name" do 38 | Test.Spec.Assertions.shouldEqual 39 | (readJSON """{ "name": "Pat" }""") 40 | (Data.Either.Right (Option.fromRecord { name: "Pat" })) 41 | Test.Spec.it "accepts only a title" do 42 | Test.Spec.Assertions.shouldEqual 43 | (readJSON """{ "title": "wonderful" }""") 44 | (Data.Either.Right (Option.fromRecord { title: "wonderful" })) 45 | Test.Spec.it "accepts both a name and a title" do 46 | Test.Spec.Assertions.shouldEqual 47 | (readJSON """{ "name": "Pat", "title": "Dr." }""") 48 | (Data.Either.Right (Option.fromRecord { name: "Pat", title: "Dr." })) 49 | Test.Spec.it "doesn't fail a null name" do 50 | Test.Spec.Assertions.shouldEqual 51 | (readJSON """{ "name": null }""") 52 | (Data.Either.Right (Option.fromRecord {})) 53 | Test.Spec.it "doesn't fail for a null title" do 54 | Test.Spec.Assertions.shouldEqual 55 | (readJSON """{ "title": null }""") 56 | (Data.Either.Right (Option.fromRecord {})) 57 | Test.Spec.it "doesn't fail for a null name or title" do 58 | Test.Spec.Assertions.shouldEqual 59 | (readJSON """{ "name": null, "title": null }""") 60 | (Data.Either.Right (Option.fromRecord {})) 61 | 62 | spec_writeJSON :: Test.Spec.Spec Unit 63 | spec_writeJSON = 64 | Test.Spec.describe "writeJSON" do 65 | Test.Spec.it "inserts no fields if none exist" do 66 | Test.Spec.Assertions.shouldEqual 67 | (writeJSON (Option.fromRecord {})) 68 | "{}" 69 | Test.Spec.it "inserts a name if it exist" do 70 | Test.Spec.Assertions.shouldEqual 71 | (writeJSON (Option.fromRecord { name: "Pat" })) 72 | "{\"name\":\"Pat\"}" 73 | Test.Spec.it "inserts a title if it exist" do 74 | Test.Spec.Assertions.shouldEqual 75 | (writeJSON (Option.fromRecord { title: "wonderful" })) 76 | "{\"title\":\"wonderful\"}" 77 | Test.Spec.it "inserts both a name and a title if they exist" do 78 | Test.Spec.Assertions.shouldEqual 79 | (writeJSON (Option.fromRecord { name: "Pat", title: "Dr." })) 80 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 81 | -------------------------------------------------------------------------------- /test/HowTo.ProvideAnEasierAPIForDateTime.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.ProvideAnEasierAPIForDateTime 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Date as Data.Date 9 | import Data.Date.Component as Data.Date.Component 10 | import Data.DateTime as Data.DateTime 11 | import Data.Enum as Data.Enum 12 | import Data.Maybe as Data.Maybe 13 | import Data.Symbol as Data.Symbol 14 | import Data.Time as Data.Time 15 | import Data.Time.Component as Data.Time.Component 16 | import Option as Option 17 | import Prim.Row as Prim.Row 18 | import Test.Spec as Test.Spec 19 | import Test.Spec.Assertions as Test.Spec.Assertions 20 | import Type.Proxy as Type.Proxy 21 | 22 | type Option = 23 | ( day :: Int 24 | , hour :: Int 25 | , millisecond :: Int 26 | , minute :: Int 27 | , month :: Data.Date.Component.Month 28 | , second :: Int 29 | , year :: Int 30 | ) 31 | 32 | dateTime :: 33 | forall record. 34 | Option.FromRecord record () Option => 35 | Record record -> 36 | Data.DateTime.DateTime 37 | dateTime record = Data.DateTime.DateTime date time 38 | where 39 | date :: Data.Date.Date 40 | date = Data.Date.canonicalDate year month day 41 | where 42 | day :: Data.Date.Component.Day 43 | day = get (Type.Proxy.Proxy :: _ "day") 44 | 45 | month :: Data.Date.Component.Month 46 | month = Option.getWithDefault bottom (Type.Proxy.Proxy :: _ "month") options 47 | 48 | year :: Data.Date.Component.Year 49 | year = get (Type.Proxy.Proxy :: _ "year") 50 | 51 | get :: 52 | forall label proxy record' value. 53 | Data.Enum.BoundedEnum value => 54 | Data.Symbol.IsSymbol label => 55 | Prim.Row.Cons label Int record' Option => 56 | proxy label -> 57 | value 58 | get proxy = case Option.get proxy options of 59 | Data.Maybe.Just x -> Data.Enum.toEnumWithDefaults bottom top x 60 | Data.Maybe.Nothing -> bottom 61 | 62 | options :: Option.Option Option 63 | options = Option.fromRecord record 64 | 65 | time :: Data.Time.Time 66 | time = Data.Time.Time hour minute second millisecond 67 | where 68 | hour :: Data.Time.Component.Hour 69 | hour = get (Type.Proxy.Proxy :: _ "hour") 70 | 71 | minute :: Data.Time.Component.Minute 72 | minute = get (Type.Proxy.Proxy :: _ "minute") 73 | 74 | millisecond :: Data.Time.Component.Millisecond 75 | millisecond = get (Type.Proxy.Proxy :: _ "millisecond") 76 | 77 | second :: Data.Time.Component.Second 78 | second = get (Type.Proxy.Proxy :: _ "second") 79 | 80 | spec :: Test.Spec.Spec Unit 81 | spec = 82 | Test.Spec.describe "HowTo.ProvideAnEasierAPIForDateTime" do 83 | spec_dateTime 84 | 85 | spec_dateTime :: Test.Spec.Spec Unit 86 | spec_dateTime = 87 | Test.Spec.describe "dateTime" do 88 | let 89 | dateTime' :: Data.DateTime.DateTime 90 | dateTime' = dateTime { minute: 30, month: Data.Date.Component.April, year: 2019 } 91 | Test.Spec.it "sets the year" do 92 | Test.Spec.Assertions.shouldEqual 93 | (Data.Enum.fromEnum (Data.Date.year (Data.DateTime.date dateTime'))) 94 | 2019 95 | Test.Spec.it "sets the month" do 96 | Test.Spec.Assertions.shouldEqual 97 | (Data.Date.month (Data.DateTime.date dateTime')) 98 | Data.Date.Component.April 99 | Test.Spec.it "sets the minute" do 100 | Test.Spec.Assertions.shouldEqual 101 | (Data.Enum.fromEnum (Data.Time.minute (Data.DateTime.time dateTime'))) 102 | 30 103 | -------------------------------------------------------------------------------- /test/HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptArgonaut.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptArgonaut 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Argonaut.Core as Data.Argonaut.Core 9 | import Data.Argonaut.Decode.Class as Data.Argonaut.Decode.Class 10 | import Data.Argonaut.Decode.Error as Data.Argonaut.Decode.Error 11 | import Data.Argonaut.Encode.Class as Data.Argonaut.Encode.Class 12 | import Data.Argonaut.Parser as Data.Argonaut.Parser 13 | import Data.Either as Data.Either 14 | import Option as Option 15 | import Test.Spec as Test.Spec 16 | import Test.Spec.Assertions as Test.Spec.Assertions 17 | 18 | decode :: 19 | Data.Argonaut.Core.Json -> 20 | Data.Either.Either Data.Argonaut.Decode.Error.JsonDecodeError (Option.Record (name :: String) (title :: String)) 21 | decode = Data.Argonaut.Decode.Class.decodeJson 22 | 23 | encode :: 24 | Option.Record (name :: String) (title :: String) -> 25 | Data.Argonaut.Core.Json 26 | encode = Data.Argonaut.Encode.Class.encodeJson 27 | 28 | parse :: 29 | String -> 30 | Data.Either.Either String (Option.Record (name :: String) (title :: String)) 31 | parse string = case Data.Argonaut.Parser.jsonParser string of 32 | Data.Either.Left error -> Data.Either.Left error 33 | Data.Either.Right json -> case decode json of 34 | Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error) 35 | Data.Either.Right record -> Data.Either.Right record 36 | 37 | spec :: Test.Spec.Spec Unit 38 | spec = 39 | Test.Spec.describe "HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptArgonaut" do 40 | spec_parse 41 | spec_encode 42 | 43 | spec_encode :: Test.Spec.Spec Unit 44 | spec_encode = 45 | Test.Spec.describe "encode" do 46 | Test.Spec.it "inserts a name" do 47 | Test.Spec.Assertions.shouldEqual 48 | (Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat" }))) 49 | "{\"name\":\"Pat\"}" 50 | Test.Spec.it "inserts both a name and a title if it exists" do 51 | Test.Spec.Assertions.shouldEqual 52 | (Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat", title: "Dr." }))) 53 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 54 | 55 | spec_parse :: Test.Spec.Spec Unit 56 | spec_parse = 57 | Test.Spec.describe "parse" do 58 | Test.Spec.it "fails if no fields are given" do 59 | Test.Spec.Assertions.shouldEqual 60 | (parse """{}""") 61 | (Data.Either.Left "An error occurred while decoding a JSON value:\n At object key 'name':\n No value was found.") 62 | Test.Spec.it "fails if only a title is given" do 63 | Test.Spec.Assertions.shouldEqual 64 | (parse """{ "title": "wonderful" }""") 65 | (Data.Either.Left "An error occurred while decoding a JSON value:\n At object key 'name':\n No value was found.") 66 | Test.Spec.it "requires a name" do 67 | Test.Spec.Assertions.shouldEqual 68 | (parse """{ "name": "Pat" }""") 69 | (Data.Either.Right (Option.recordFromRecord { name: "Pat" })) 70 | Test.Spec.it "requires a name and accepts a title" do 71 | Test.Spec.Assertions.shouldEqual 72 | (parse """{ "name": "Pat", "title": "Dr." }""") 73 | (Data.Either.Right (Option.recordFromRecord { name: "Pat", title: "Dr." })) 74 | Test.Spec.it "doesn't fail for null fields" do 75 | Test.Spec.Assertions.shouldEqual 76 | (parse """{ "name": "Pat", "title": null }""") 77 | (Data.Either.Right (Option.recordFromRecord { name: "Pat" })) 78 | -------------------------------------------------------------------------------- /test/HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptCodecArgonaut.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptCodecArgonaut 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Argonaut.Core as Data.Argonaut.Core 9 | import Data.Argonaut.Parser as Data.Argonaut.Parser 10 | import Data.Codec.Argonaut as Data.Codec.Argonaut 11 | import Data.Either as Data.Either 12 | import Option as Option 13 | import Test.Spec as Test.Spec 14 | import Test.Spec.Assertions as Test.Spec.Assertions 15 | 16 | decode :: 17 | Data.Argonaut.Core.Json -> 18 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Option.Record (name :: String) (title :: String)) 19 | decode = Data.Codec.Argonaut.decode jsonCodec 20 | 21 | encode :: 22 | Option.Record (name :: String) (title :: String) -> 23 | Data.Argonaut.Core.Json 24 | encode = Data.Codec.Argonaut.encode jsonCodec 25 | 26 | jsonCodec :: Data.Codec.Argonaut.JsonCodec (Option.Record (name :: String) (title :: String)) 27 | jsonCodec = 28 | Option.jsonCodecRecord 29 | "Greeting" 30 | { name: Data.Codec.Argonaut.string 31 | , title: Data.Codec.Argonaut.string 32 | } 33 | 34 | parse :: 35 | String -> 36 | Data.Either.Either String (Option.Record (name :: String) (title :: String)) 37 | parse string = do 38 | json <- Data.Argonaut.Parser.jsonParser string 39 | case decode json of 40 | Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err) 41 | Data.Either.Right option -> Data.Either.Right option 42 | 43 | spec :: Test.Spec.Spec Unit 44 | spec = 45 | Test.Spec.describe "HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptCodecArgonaut" do 46 | spec_encode 47 | spec_parse 48 | 49 | spec_encode :: Test.Spec.Spec Unit 50 | spec_encode = 51 | Test.Spec.describe "encode" do 52 | Test.Spec.it "inserts a name" do 53 | Test.Spec.Assertions.shouldEqual 54 | (Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat" }))) 55 | "{\"name\":\"Pat\"}" 56 | Test.Spec.it "inserts both a name and a title if it exists" do 57 | Test.Spec.Assertions.shouldEqual 58 | (Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat", title: "Dr." }))) 59 | "{\"name\":\"Pat\",\"title\":\"Dr.\"}" 60 | 61 | spec_parse :: Test.Spec.Spec Unit 62 | spec_parse = 63 | Test.Spec.describe "parse" do 64 | Test.Spec.it "fails if no fields are given" do 65 | Test.Spec.Assertions.shouldEqual 66 | (parse """{}""") 67 | (Data.Either.Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key name:\n No value was found.") 68 | Test.Spec.it "fails if only a title is given" do 69 | Test.Spec.Assertions.shouldEqual 70 | (parse """{ "title": "wonderful" }""") 71 | (Data.Either.Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key name:\n No value was found.") 72 | Test.Spec.it "requires a name" do 73 | Test.Spec.Assertions.shouldEqual 74 | (parse """{ "name": "Pat" }""") 75 | (Data.Either.Right (Option.recordFromRecord { name: "Pat" })) 76 | Test.Spec.it "requires a name and accepts a title" do 77 | Test.Spec.Assertions.shouldEqual 78 | (parse """{ "name": "Pat", "title": "Dr." }""") 79 | (Data.Either.Right (Option.recordFromRecord { name: "Pat", title: "Dr." })) 80 | Test.Spec.it "doesn't fail for null fields" do 81 | Test.Spec.Assertions.shouldEqual 82 | (parse """{ "name": "Pat", "title": null }""") 83 | (Data.Either.Right (Option.recordFromRecord { name: "Pat" })) 84 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1696426674, 7 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-parts": { 20 | "inputs": { 21 | "nixpkgs-lib": [ 22 | "nixpkgs" 23 | ] 24 | }, 25 | "locked": { 26 | "lastModified": 1751413152, 27 | "narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=", 28 | "owner": "hercules-ci", 29 | "repo": "flake-parts", 30 | "rev": "77826244401ea9de6e3bac47c2db46005e1f30b5", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "hercules-ci", 35 | "ref": "main", 36 | "repo": "flake-parts", 37 | "type": "github" 38 | } 39 | }, 40 | "git-hooks_nix": { 41 | "inputs": { 42 | "flake-compat": "flake-compat", 43 | "gitignore": "gitignore", 44 | "nixpkgs": [ 45 | "nixpkgs" 46 | ] 47 | }, 48 | "locked": { 49 | "lastModified": 1750779888, 50 | "narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=", 51 | "owner": "cachix", 52 | "repo": "git-hooks.nix", 53 | "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d", 54 | "type": "github" 55 | }, 56 | "original": { 57 | "owner": "cachix", 58 | "ref": "master", 59 | "repo": "git-hooks.nix", 60 | "type": "github" 61 | } 62 | }, 63 | "gitignore": { 64 | "inputs": { 65 | "nixpkgs": [ 66 | "git-hooks_nix", 67 | "nixpkgs" 68 | ] 69 | }, 70 | "locked": { 71 | "lastModified": 1709087332, 72 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 73 | "owner": "hercules-ci", 74 | "repo": "gitignore.nix", 75 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 76 | "type": "github" 77 | }, 78 | "original": { 79 | "owner": "hercules-ci", 80 | "repo": "gitignore.nix", 81 | "type": "github" 82 | } 83 | }, 84 | "nixpkgs": { 85 | "locked": { 86 | "lastModified": 1751792365, 87 | "narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=", 88 | "owner": "NixOS", 89 | "repo": "nixpkgs", 90 | "rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb", 91 | "type": "github" 92 | }, 93 | "original": { 94 | "owner": "NixOS", 95 | "ref": "nixos-unstable", 96 | "repo": "nixpkgs", 97 | "type": "github" 98 | } 99 | }, 100 | "root": { 101 | "inputs": { 102 | "flake-parts": "flake-parts", 103 | "git-hooks_nix": "git-hooks_nix", 104 | "nixpkgs": "nixpkgs", 105 | "treefmt-nix": "treefmt-nix" 106 | } 107 | }, 108 | "treefmt-nix": { 109 | "inputs": { 110 | "nixpkgs": [ 111 | "nixpkgs" 112 | ] 113 | }, 114 | "locked": { 115 | "lastModified": 1750931469, 116 | "narHash": "sha256-0IEdQB1nS+uViQw4k3VGUXntjkDp7aAlqcxdewb/hAc=", 117 | "owner": "numtide", 118 | "repo": "treefmt-nix", 119 | "rev": "ac8e6f32e11e9c7f153823abc3ab007f2a65d3e1", 120 | "type": "github" 121 | }, 122 | "original": { 123 | "owner": "numtide", 124 | "ref": "main", 125 | "repo": "treefmt-nix", 126 | "type": "github" 127 | } 128 | } 129 | }, 130 | "root": "root", 131 | "version": 7 132 | } 133 | -------------------------------------------------------------------------------- /test/HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptArgonaut.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptArgonaut 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Argonaut.Core as Data.Argonaut.Core 9 | import Data.Argonaut.Decode.Class as Data.Argonaut.Decode.Class 10 | import Data.Argonaut.Decode.Error as Data.Argonaut.Decode.Error 11 | import Data.Argonaut.Encode.Class as Data.Argonaut.Encode.Class 12 | import Data.Argonaut.Parser as Data.Argonaut.Parser 13 | import Data.Either as Data.Either 14 | import Option as Option 15 | import Test.Spec as Test.Spec 16 | import Test.Spec.Assertions as Test.Spec.Assertions 17 | 18 | decode :: 19 | Data.Argonaut.Core.Json -> 20 | Data.Either.Either Data.Argonaut.Decode.Error.JsonDecodeError (Option.Option (name :: String, title :: String)) 21 | decode = Data.Argonaut.Decode.Class.decodeJson 22 | 23 | encode :: 24 | Option.Option (name :: String, title :: String) -> 25 | Data.Argonaut.Core.Json 26 | encode = Data.Argonaut.Encode.Class.encodeJson 27 | 28 | parse :: 29 | String -> 30 | Data.Either.Either String (Option.Option (name :: String, title :: String)) 31 | parse string = case Data.Argonaut.Parser.jsonParser string of 32 | Data.Either.Left error -> Data.Either.Left error 33 | Data.Either.Right json -> case decode json of 34 | Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error) 35 | Data.Either.Right option -> Data.Either.Right option 36 | 37 | spec :: Test.Spec.Spec Unit 38 | spec = 39 | Test.Spec.describe "HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptArgonaut" do 40 | spec_encode 41 | spec_parse 42 | 43 | spec_encode :: Test.Spec.Spec Unit 44 | spec_encode = 45 | Test.Spec.describe "encode" do 46 | Test.Spec.it "inserts no fields if none exist" do 47 | Test.Spec.Assertions.shouldEqual 48 | (Data.Argonaut.Core.stringify (encode (Option.fromRecord {}))) 49 | "{}" 50 | Test.Spec.it "inserts a name if it exist" do 51 | Test.Spec.Assertions.shouldEqual 52 | (Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat" }))) 53 | "{\"name\":\"Pat\"}" 54 | Test.Spec.it "inserts a title if it exist" do 55 | Test.Spec.Assertions.shouldEqual 56 | (Data.Argonaut.Core.stringify (encode (Option.fromRecord { title: "wonderful" }))) 57 | "{\"title\":\"wonderful\"}" 58 | Test.Spec.it "inserts both a name and a title if they exist" do 59 | Test.Spec.Assertions.shouldEqual 60 | (Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat", title: "Dr." }))) 61 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 62 | 63 | spec_parse :: Test.Spec.Spec Unit 64 | spec_parse = 65 | Test.Spec.describe "parse" do 66 | Test.Spec.it "doesn't require any fields" do 67 | Test.Spec.Assertions.shouldEqual 68 | (parse """{}""") 69 | (Data.Either.Right (Option.fromRecord {})) 70 | Test.Spec.it "accepts only a name" do 71 | Test.Spec.Assertions.shouldEqual 72 | (parse """{ "name": "Pat" }""") 73 | (Data.Either.Right (Option.fromRecord { name: "Pat" })) 74 | Test.Spec.it "accepts only a title" do 75 | Test.Spec.Assertions.shouldEqual 76 | (parse """{ "title": "wonderful" }""") 77 | (Data.Either.Right (Option.fromRecord { title: "wonderful" })) 78 | Test.Spec.it "accepts both a name and a title" do 79 | Test.Spec.Assertions.shouldEqual 80 | (parse """{ "name": "Pat", "title": "Dr." }""") 81 | (Data.Either.Right (Option.fromRecord { name: "Pat", title: "Dr." })) 82 | Test.Spec.it "doesn't fail a null name" do 83 | Test.Spec.Assertions.shouldEqual 84 | (parse """{ "name": null }""") 85 | (Data.Either.Right (Option.fromRecord {})) 86 | Test.Spec.it "doesn't fail for a null title" do 87 | Test.Spec.Assertions.shouldEqual 88 | (parse """{ "title": null }""") 89 | (Data.Either.Right (Option.fromRecord {})) 90 | Test.Spec.it "doesn't fail for a null name or title" do 91 | Test.Spec.Assertions.shouldEqual 92 | (parse """{ "name": null, "title": null }""") 93 | (Data.Either.Right (Option.fromRecord {})) 94 | -------------------------------------------------------------------------------- /test/HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptCodecArgonaut.purs: -------------------------------------------------------------------------------- 1 | -- | If anything changes here, 2 | -- | make sure the README is updated accordingly. 3 | module HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptCodecArgonaut 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | import Data.Argonaut.Core as Data.Argonaut.Core 9 | import Data.Argonaut.Parser as Data.Argonaut.Parser 10 | import Data.Codec.Argonaut as Data.Codec.Argonaut 11 | import Data.Either as Data.Either 12 | import Option as Option 13 | import Test.Spec as Test.Spec 14 | import Test.Spec.Assertions as Test.Spec.Assertions 15 | 16 | decode :: 17 | Data.Argonaut.Core.Json -> 18 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Option.Option (name :: String, title :: String)) 19 | decode = Data.Codec.Argonaut.decode jsonCodec 20 | 21 | encode :: 22 | Option.Option (name :: String, title :: String) -> 23 | Data.Argonaut.Core.Json 24 | encode = Data.Codec.Argonaut.encode jsonCodec 25 | 26 | jsonCodec :: Data.Codec.Argonaut.JsonCodec (Option.Option (name :: String, title :: String)) 27 | jsonCodec = 28 | Option.jsonCodec 29 | "Greeting" 30 | { name: Data.Codec.Argonaut.string 31 | , title: Data.Codec.Argonaut.string 32 | } 33 | 34 | parse :: 35 | String -> 36 | Data.Either.Either String (Option.Option (name :: String, title :: String)) 37 | parse string = do 38 | json <- Data.Argonaut.Parser.jsonParser string 39 | case decode json of 40 | Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err) 41 | Data.Either.Right option -> Data.Either.Right option 42 | 43 | spec :: Test.Spec.Spec Unit 44 | spec = 45 | Test.Spec.describe "HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptCodecArgonaut" do 46 | spec_encode 47 | spec_parse 48 | 49 | spec_encode :: Test.Spec.Spec Unit 50 | spec_encode = 51 | Test.Spec.describe "encode" do 52 | Test.Spec.it "inserts no fields if none exist" do 53 | Test.Spec.Assertions.shouldEqual 54 | (Data.Argonaut.Core.stringify (encode (Option.fromRecord {}))) 55 | "{}" 56 | Test.Spec.it "inserts a name if it exist" do 57 | Test.Spec.Assertions.shouldEqual 58 | (Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat" }))) 59 | "{\"name\":\"Pat\"}" 60 | Test.Spec.it "inserts a title if it exist" do 61 | Test.Spec.Assertions.shouldEqual 62 | (Data.Argonaut.Core.stringify (encode (Option.fromRecord { title: "wonderful" }))) 63 | "{\"title\":\"wonderful\"}" 64 | Test.Spec.it "inserts both a name and a title if they exist" do 65 | Test.Spec.Assertions.shouldEqual 66 | (Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat", title: "Dr." }))) 67 | "{\"name\":\"Pat\",\"title\":\"Dr.\"}" 68 | 69 | spec_parse :: Test.Spec.Spec Unit 70 | spec_parse = 71 | Test.Spec.describe "parse" do 72 | Test.Spec.it "doesn't require any fields" do 73 | Test.Spec.Assertions.shouldEqual 74 | (parse """{}""") 75 | (Data.Either.Right (Option.fromRecord {})) 76 | Test.Spec.it "accepts only a name" do 77 | Test.Spec.Assertions.shouldEqual 78 | (parse """{ "name": "Pat" }""") 79 | (Data.Either.Right (Option.fromRecord { name: "Pat" })) 80 | Test.Spec.it "accepts only a title" do 81 | Test.Spec.Assertions.shouldEqual 82 | (parse """{ "title": "wonderful" }""") 83 | (Data.Either.Right (Option.fromRecord { title: "wonderful" })) 84 | Test.Spec.it "accepts both a name and a title" do 85 | Test.Spec.Assertions.shouldEqual 86 | (parse """{ "name": "Pat", "title": "Dr." }""") 87 | (Data.Either.Right (Option.fromRecord { name: "Pat", title: "Dr." })) 88 | Test.Spec.it "doesn't fail a null name" do 89 | Test.Spec.Assertions.shouldEqual 90 | (parse """{ "name": null }""") 91 | (Data.Either.Right (Option.fromRecord {})) 92 | Test.Spec.it "doesn't fail for a null title" do 93 | Test.Spec.Assertions.shouldEqual 94 | (parse """{ "title": null }""") 95 | (Data.Either.Right (Option.fromRecord {})) 96 | Test.Spec.it "doesn't fail for a null name or title" do 97 | Test.Spec.Assertions.shouldEqual 98 | (parse """{ "name": null, "title": null }""") 99 | (Data.Either.Right (Option.fromRecord {})) 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Project-specific ### 2 | 3 | .direnv 4 | .jj 5 | .psc-ide-port 6 | 7 | ################################################################################ 8 | # DO NOT CHANGE ANYTHING BELOW THIS SECTION BY HAND. 9 | # If you need something not generated by the gitignore.io API, 10 | # add it to the `Project-specific` section above. 11 | ################################################################################ 12 | 13 | # Created by https://www.gitignore.io/api/vim,node,macos,bower,linux,emacs,windows,purescript,sublimetext,visualstudiocode 14 | # Edit at https://www.gitignore.io/?templates=vim,node,macos,bower,linux,emacs,windows,purescript,sublimetext,visualstudiocode 15 | 16 | ### Bower ### 17 | bower_components 18 | .bower-cache 19 | .bower-registry 20 | .bower-tmp 21 | 22 | ### Emacs ### 23 | # -*- mode: gitignore; -*- 24 | *~ 25 | \#*\# 26 | /.emacs.desktop 27 | /.emacs.desktop.lock 28 | *.elc 29 | auto-save-list 30 | tramp 31 | .\#* 32 | 33 | # Org-mode 34 | .org-id-locations 35 | *_archive 36 | 37 | # flymake-mode 38 | *_flymake.* 39 | 40 | # eshell files 41 | /eshell/history 42 | /eshell/lastdir 43 | 44 | # elpa packages 45 | /elpa/ 46 | 47 | # reftex files 48 | *.rel 49 | 50 | # AUCTeX auto folder 51 | /auto/ 52 | 53 | # cask packages 54 | .cask/ 55 | dist/ 56 | 57 | # Flycheck 58 | flycheck_*.el 59 | 60 | # server auth directory 61 | /server/ 62 | 63 | # projectiles files 64 | .projectile 65 | 66 | # directory configuration 67 | .dir-locals.el 68 | 69 | # network security 70 | /network-security.data 71 | 72 | 73 | ### Linux ### 74 | 75 | # temporary files which can be created if a process still has a handle open of a deleted file 76 | .fuse_hidden* 77 | 78 | # KDE directory preferences 79 | .directory 80 | 81 | # Linux trash folder which might appear on any partition or disk 82 | .Trash-* 83 | 84 | # .nfs files are created when an open file is removed but is still being accessed 85 | .nfs* 86 | 87 | ### macOS ### 88 | # General 89 | .DS_Store 90 | .AppleDouble 91 | .LSOverride 92 | 93 | # Icon must end with two \r 94 | Icon 95 | 96 | # Thumbnails 97 | ._* 98 | 99 | # Files that might appear in the root of a volume 100 | .DocumentRevisions-V100 101 | .fseventsd 102 | .Spotlight-V100 103 | .TemporaryItems 104 | .Trashes 105 | .VolumeIcon.icns 106 | .com.apple.timemachine.donotpresent 107 | 108 | # Directories potentially created on remote AFP share 109 | .AppleDB 110 | .AppleDesktop 111 | Network Trash Folder 112 | Temporary Items 113 | .apdisk 114 | 115 | ### Node ### 116 | # Logs 117 | logs 118 | *.log 119 | npm-debug.log* 120 | yarn-debug.log* 121 | yarn-error.log* 122 | 123 | # Runtime data 124 | pids 125 | *.pid 126 | *.seed 127 | *.pid.lock 128 | 129 | # Directory for instrumented libs generated by jscoverage/JSCover 130 | lib-cov 131 | 132 | # Coverage directory used by tools like istanbul 133 | coverage 134 | 135 | # nyc test coverage 136 | .nyc_output 137 | 138 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 139 | .grunt 140 | 141 | # Bower dependency directory (https://bower.io/) 142 | 143 | # node-waf configuration 144 | .lock-wscript 145 | 146 | # Compiled binary addons (https://nodejs.org/api/addons.html) 147 | build/Release 148 | 149 | # Dependency directories 150 | node_modules/ 151 | jspm_packages/ 152 | 153 | # TypeScript v1 declaration files 154 | typings/ 155 | 156 | # Optional npm cache directory 157 | .npm 158 | 159 | # Optional eslint cache 160 | .eslintcache 161 | 162 | # Optional REPL history 163 | .node_repl_history 164 | 165 | # Output of 'npm pack' 166 | *.tgz 167 | 168 | # Yarn Integrity file 169 | .yarn-integrity 170 | 171 | # dotenv environment variables file 172 | .env 173 | .env.test 174 | 175 | # parcel-bundler cache (https://parceljs.org/) 176 | .cache 177 | 178 | # next.js build output 179 | .next 180 | 181 | # nuxt.js build output 182 | .nuxt 183 | 184 | # vuepress build output 185 | .vuepress/dist 186 | 187 | # Serverless directories 188 | .serverless/ 189 | 190 | # FuseBox cache 191 | .fusebox/ 192 | 193 | # DynamoDB Local files 194 | .dynamodb/ 195 | 196 | ### PureScript ### 197 | # Dependencies 198 | .psci_modules 199 | node_modules 200 | 201 | # Generated files 202 | .psci 203 | output 204 | 205 | ### SublimeText ### 206 | # Cache files for Sublime Text 207 | *.tmlanguage.cache 208 | *.tmPreferences.cache 209 | *.stTheme.cache 210 | 211 | # Workspace files are user-specific 212 | *.sublime-workspace 213 | 214 | # Project files should be checked into the repository, unless a significant 215 | # proportion of contributors will probably not be using Sublime Text 216 | # *.sublime-project 217 | 218 | # SFTP configuration file 219 | sftp-config.json 220 | 221 | # Package control specific files 222 | Package Control.last-run 223 | Package Control.ca-list 224 | Package Control.ca-bundle 225 | Package Control.system-ca-bundle 226 | Package Control.cache/ 227 | Package Control.ca-certs/ 228 | Package Control.merged-ca-bundle 229 | Package Control.user-ca-bundle 230 | oscrypto-ca-bundle.crt 231 | bh_unicode_properties.cache 232 | 233 | # Sublime-github package stores a github token in this file 234 | # https://packagecontrol.io/packages/sublime-github 235 | GitHub.sublime-settings 236 | 237 | ### Vim ### 238 | # Swap 239 | [._]*.s[a-v][a-z] 240 | [._]*.sw[a-p] 241 | [._]s[a-rt-v][a-z] 242 | [._]ss[a-gi-z] 243 | [._]sw[a-p] 244 | 245 | # Session 246 | Session.vim 247 | 248 | # Temporary 249 | .netrwhist 250 | # Auto-generated tag files 251 | tags 252 | # Persistent undo 253 | [._]*.un~ 254 | 255 | ### VisualStudioCode ### 256 | .vscode/* 257 | !.vscode/settings.json 258 | !.vscode/tasks.json 259 | !.vscode/launch.json 260 | !.vscode/extensions.json 261 | 262 | ### VisualStudioCode Patch ### 263 | # Ignore all local history of files 264 | .history 265 | 266 | ### Windows ### 267 | # Windows thumbnail cache files 268 | Thumbs.db 269 | ehthumbs.db 270 | ehthumbs_vista.db 271 | 272 | # Dump file 273 | *.stackdump 274 | 275 | # Folder config file 276 | [Dd]esktop.ini 277 | 278 | # Recycle Bin used on file shares 279 | $RECYCLE.BIN/ 280 | 281 | # Windows Installer files 282 | *.cab 283 | *.msi 284 | *.msix 285 | *.msm 286 | *.msp 287 | 288 | # Windows shortcuts 289 | *.lnk 290 | 291 | # End of https://www.gitignore.io/api/vim,node,macos,bower,linux,emacs,windows,purescript,sublimetext,visualstudiocode 292 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A data type for optional values"; 3 | 4 | inputs = { 5 | flake-parts = { 6 | inputs = { 7 | nixpkgs-lib = { 8 | follows = "nixpkgs"; 9 | }; 10 | }; 11 | 12 | owner = "hercules-ci"; 13 | 14 | ref = "main"; 15 | 16 | repo = "flake-parts"; 17 | 18 | type = "github"; 19 | }; 20 | 21 | git-hooks_nix = { 22 | inputs = { 23 | nixpkgs = { 24 | follows = "nixpkgs"; 25 | }; 26 | }; 27 | 28 | owner = "cachix"; 29 | 30 | ref = "master"; 31 | 32 | repo = "git-hooks.nix"; 33 | 34 | type = "github"; 35 | }; 36 | 37 | nixpkgs = { 38 | owner = "NixOS"; 39 | 40 | ref = "nixos-unstable"; 41 | 42 | repo = "nixpkgs"; 43 | 44 | type = "github"; 45 | }; 46 | 47 | treefmt-nix = { 48 | inputs = { 49 | nixpkgs = { 50 | follows = "nixpkgs"; 51 | }; 52 | }; 53 | 54 | owner = "numtide"; 55 | 56 | ref = "main"; 57 | 58 | repo = "treefmt-nix"; 59 | 60 | type = "github"; 61 | }; 62 | }; 63 | 64 | outputs = 65 | inputs: 66 | inputs.flake-parts.lib.mkFlake { inherit inputs; } { 67 | imports = [ 68 | # The `git-hooks_nix` module will play nicely with the `treefmt-nix` module. 69 | # It will take the configuration we supply, 70 | # and use it appropriately instead of competing with it. 71 | inputs.git-hooks_nix.flakeModule 72 | # The `treefmt-nix` module will set the `formatter` to `treefmt`. 73 | # This lets `nix fmt` use our configuration for `treefmt`. 74 | inputs.treefmt-nix.flakeModule 75 | ]; 76 | 77 | perSystem = 78 | { config, pkgs, ... }: 79 | { 80 | devShells = { 81 | default = pkgs.mkShellNoCC { 82 | inputsFrom = [ 83 | # Include the shell from `git-hooks_nix`. 84 | # This makes all `pre-commit` binaries (include `pre-commit`) available in the shell. 85 | config.pre-commit.devShell 86 | # Include the shell from `treefmt`. 87 | # This makes all `treefmt` binaries (including `treefmt`) available in the shell. 88 | config.treefmt.build.devShell 89 | ]; 90 | 91 | nativeBuildInputs = 92 | [ 93 | pkgs.coreutils 94 | pkgs.findutils 95 | pkgs.gnumake 96 | pkgs.nodejs_22 97 | ] 98 | # The version of PureScript we use doesn't have pre-built binaries for ARM architectures. 99 | # The `npm` package will fall back to building from source, 100 | # but we need somre more dependencies in order to do that. 101 | # We should be able to remove this once we update to at least version [0.15.9](https://github.com/purescript/purescript/releases/tag/v0.15.9) of PureScript, 102 | # as this was the first version to start producing pre-built ARM binaries: https://github.com/purescript/purescript/pull/4455. 103 | ++ pkgs.lib.lists.optionals pkgs.stdenv.hostPlatform.isAarch [ 104 | pkgs.llvmPackages_12.libcxxClang 105 | pkgs.llvmPackages_12.libllvm 106 | pkgs.stack 107 | ]; 108 | }; 109 | }; 110 | 111 | pre-commit = { 112 | settings = { 113 | default_stages = [ 114 | "pre-commit" 115 | "pre-push" 116 | ]; 117 | 118 | hooks = { 119 | # Check `.github/workflows` code. 120 | actionlint = { 121 | enable = true; 122 | }; 123 | 124 | # Check Nix code for anything that is unused. 125 | deadnix = { 126 | enable = true; 127 | }; 128 | 129 | # Check that we're not accidentally committing AWS credentials. 130 | detect-aws-credentials = { 131 | enable = true; 132 | }; 133 | 134 | # Check that we're not accidentally committing private keys 135 | detect-private-keys = { 136 | enable = true; 137 | }; 138 | 139 | # Git submodules are nothing but a can of worms that fails in some non-obvious way each time they're used. 140 | forbid-new-submodules = { 141 | enable = true; 142 | }; 143 | 144 | # Check markdown files for consistency. 145 | markdownlint = { 146 | enable = true; 147 | 148 | settings = { 149 | configuration = { 150 | MD013 = { 151 | # We'd like to use something like `wrap:inner-sentence`: 152 | # https://cirosantilli.com/markdown-style-guide/#option-wrap-inner-sentence, 153 | # or something related to SemBr: https://sembr.org/. 154 | # But that's stymied in an issue: https://github.com/DavidAnson/markdownlint/issues/298. 155 | # 156 | # We set the line length to something large enough to not get hit by it regularly. 157 | line_length = 1000; 158 | }; 159 | }; 160 | }; 161 | }; 162 | 163 | # While `nil` is a language server, 164 | # it also has come static analysis we want to check. 165 | nil = { 166 | enable = true; 167 | }; 168 | 169 | # Check for any common mistakes in Shell code we write. 170 | shellcheck = { 171 | enable = true; 172 | }; 173 | 174 | # Check that we're not making any simple typos in prose. 175 | # This checks not just plain text files (like Markdown), 176 | # but also comments in source code like this comment you're reading right now. 177 | typos = { 178 | enable = true; 179 | }; 180 | }; 181 | }; 182 | }; 183 | 184 | treefmt = { 185 | programs = { 186 | # Format JSON code consistently. 187 | jsonfmt = { 188 | enable = true; 189 | }; 190 | 191 | # Format Markdown code consistently. 192 | mdformat = { 193 | enable = true; 194 | }; 195 | 196 | # Format Nix code consistently. 197 | nixfmt = { 198 | enable = true; 199 | }; 200 | 201 | # Format PureScript code consistently. 202 | purs-tidy = { 203 | enable = true; 204 | 205 | includes = [ "*.purs" ]; 206 | }; 207 | 208 | # Format Shell code consistently. 209 | shfmt = { 210 | enable = true; 211 | }; 212 | 213 | # Format YAML code consistently. 214 | yamlfmt = { 215 | enable = true; 216 | }; 217 | }; 218 | 219 | settings.global.excludes = [ 220 | ".direnv/*" 221 | ".jj/*" 222 | "bower_components/*" 223 | "node_modules/*" 224 | "output/*" 225 | ]; 226 | }; 227 | }; 228 | 229 | systems = [ 230 | "aarch64-darwin" 231 | "aarch64-linux" 232 | "x86_64-darwin" 233 | "x86_64-linux" 234 | ]; 235 | }; 236 | } 237 | -------------------------------------------------------------------------------- /test/Test.Option.purs: -------------------------------------------------------------------------------- 1 | module Test.Option 2 | ( spec 3 | ) where 4 | 5 | import Prelude 6 | import Data.Maybe as Data.Maybe 7 | import Option as Option 8 | import Test.Spec as Test.Spec 9 | import Test.Spec.Assertions as Test.Spec.Assertions 10 | 11 | data Proxy (symbol :: Symbol) = Proxy 12 | 13 | spec :: Test.Spec.Spec Unit 14 | spec = 15 | Test.Spec.describe "Test.Option" do 16 | spec_alter 17 | spec_fromRecord 18 | spec_get' 19 | spec_modify' 20 | spec_recordFromRecord 21 | spec_recordRename 22 | spec_recordToRecord 23 | spec_recordSet 24 | spec_rename 25 | spec_set 26 | spec_set' 27 | 28 | spec_alter :: Test.Spec.Spec Unit 29 | spec_alter = 30 | Test.Spec.describe "alter" do 31 | Test.Spec.it "does nothing if values are not set" do 32 | let 33 | someOption :: Option.Option (foo :: Boolean, bar :: Int, qux :: String) 34 | someOption = Option.empty 35 | 36 | bar :: 37 | Data.Maybe.Maybe Int -> 38 | Data.Maybe.Maybe String 39 | bar value' = case value' of 40 | Data.Maybe.Just value -> if value > 0 then Data.Maybe.Just "positive" else Data.Maybe.Nothing 41 | Data.Maybe.Nothing -> Data.Maybe.Nothing 42 | Option.alter { bar } someOption `Test.Spec.Assertions.shouldEqual` Option.fromRecord {} 43 | Test.Spec.it "manipulates all fields it can" do 44 | let 45 | someOption :: Option.Option (foo :: Boolean, bar :: Int, qux :: String) 46 | someOption = Option.fromRecord { bar: 31, qux: "hi" } 47 | 48 | bar :: 49 | Data.Maybe.Maybe Int -> 50 | Data.Maybe.Maybe String 51 | bar value' = case value' of 52 | Data.Maybe.Just value -> if value > 0 then Data.Maybe.Just "positive" else Data.Maybe.Nothing 53 | Data.Maybe.Nothing -> Data.Maybe.Nothing 54 | 55 | qux :: 56 | Data.Maybe.Maybe String -> 57 | Data.Maybe.Maybe String 58 | qux _ = Data.Maybe.Nothing 59 | Option.alter { bar, qux } someOption `Test.Spec.Assertions.shouldEqual` Option.fromRecord { bar: "positive" } 60 | 61 | spec_fromRecord :: Test.Spec.Spec Unit 62 | spec_fromRecord = 63 | Test.Spec.describe "fromRecord" do 64 | Test.Spec.it "only sets the given values" do 65 | let 66 | option :: 67 | Option.Option 68 | ( age :: Int 69 | , name :: String 70 | ) 71 | option = 72 | Option.fromRecord 73 | { name: "Pat" 74 | } 75 | Option.toRecord option `Test.Spec.Assertions.shouldEqual` { age: Data.Maybe.Nothing, name: Data.Maybe.Just "Pat" } 76 | Test.Spec.it "allows `Data.Maybe.Just _`" do 77 | let 78 | option :: 79 | Option.Option 80 | ( age :: Int 81 | , name :: String 82 | ) 83 | option = 84 | Option.fromRecord 85 | { name: Data.Maybe.Just "Pat" 86 | } 87 | Option.toRecord option `Test.Spec.Assertions.shouldEqual` { age: Data.Maybe.Nothing, name: Data.Maybe.Just "Pat" } 88 | Test.Spec.it "allows `Data.Maybe.Nothing`" do 89 | let 90 | option :: 91 | Option.Option 92 | ( age :: Int 93 | , name :: String 94 | ) 95 | option = 96 | Option.fromRecord 97 | { name: Data.Maybe.Nothing 98 | } 99 | Option.toRecord option `Test.Spec.Assertions.shouldEqual` { age: Data.Maybe.Nothing, name: Data.Maybe.Nothing } 100 | Test.Spec.it "allows `Data.Maybe.Just _` and `Data.Maybe.Nothing`" do 101 | let 102 | option :: 103 | Option.Option 104 | ( age :: Int 105 | , name :: String 106 | ) 107 | option = 108 | Option.fromRecord 109 | { age: Data.Maybe.Just 31 110 | , name: Data.Maybe.Nothing 111 | } 112 | Option.toRecord option `Test.Spec.Assertions.shouldEqual` { age: Data.Maybe.Just 31, name: Data.Maybe.Nothing } 113 | Test.Spec.it "allows nested `Data.Maybe.Maybe _`s" do 114 | let 115 | option :: 116 | Option.Option 117 | ( active :: Data.Maybe.Maybe Boolean 118 | , age :: Data.Maybe.Maybe (Data.Maybe.Maybe Int) 119 | , name :: String 120 | ) 121 | option = 122 | Option.fromRecord 123 | { active: Data.Maybe.Just (Data.Maybe.Just true) 124 | , age: Data.Maybe.Just (Data.Maybe.Just (Data.Maybe.Just 31)) 125 | , name: Data.Maybe.Just "Pat" 126 | } 127 | Option.toRecord option `Test.Spec.Assertions.shouldEqual` { active: Data.Maybe.Just (Data.Maybe.Just true), age: Data.Maybe.Just (Data.Maybe.Just (Data.Maybe.Just 31)), name: Data.Maybe.Just "Pat" } 128 | 129 | spec_get' :: Test.Spec.Spec Unit 130 | spec_get' = 131 | Test.Spec.describe "get'" do 132 | Test.Spec.it "gets all fields it can" do 133 | let 134 | someOption :: Option.Option (foo :: Boolean, bar :: Int, qux :: String) 135 | someOption = Option.empty 136 | 137 | bar :: 138 | Data.Maybe.Maybe Int -> 139 | String 140 | bar value' = case value' of 141 | Data.Maybe.Just value -> if value > 0 then "positive" else "non-positive" 142 | Data.Maybe.Nothing -> "not set" 143 | Option.get' { foo: false, bar, qux: Data.Maybe.Nothing } someOption `Test.Spec.Assertions.shouldEqual` { foo: false, bar: "not set", qux: Data.Maybe.Nothing } 144 | 145 | spec_modify' :: Test.Spec.Spec Unit 146 | spec_modify' = 147 | Test.Spec.describe "modify'" do 148 | Test.Spec.it "does nothing if values are not set" do 149 | let 150 | someOption :: Option.Option (foo :: Boolean, bar :: Int, qux :: String) 151 | someOption = Option.empty 152 | 153 | bar :: 154 | Int -> 155 | String 156 | bar value = if value > 0 then "positive" else "non-positive" 157 | Option.modify' { bar } someOption `Test.Spec.Assertions.shouldEqual` Option.fromRecord {} 158 | Test.Spec.it "manipulates all fields it can" do 159 | let 160 | someOption :: Option.Option (foo :: Boolean, bar :: Int, qux :: String) 161 | someOption = Option.fromRecord { bar: 31 } 162 | 163 | bar :: 164 | Int -> 165 | String 166 | bar value = if value > 0 then "positive" else "non-positive" 167 | Option.modify' { bar } someOption `Test.Spec.Assertions.shouldEqual` Option.fromRecord { bar: "positive" } 168 | 169 | spec_recordFromRecord :: Test.Spec.Spec Unit 170 | spec_recordFromRecord = 171 | Test.Spec.describe "recordFromRecord" do 172 | Test.Spec.it "requires correct fields" do 173 | let 174 | record :: 175 | Option.Record 176 | ( name :: String 177 | ) 178 | ( greeting :: String 179 | , title :: String 180 | ) 181 | record = 182 | Option.recordFromRecord 183 | { name: "Pat" 184 | } 185 | Option.required record `Test.Spec.Assertions.shouldEqual` { name: "Pat" } 186 | Option.optional record `Test.Spec.Assertions.shouldEqual` Option.empty 187 | 188 | spec_recordRename :: Test.Spec.Spec Unit 189 | spec_recordRename = 190 | Test.Spec.describe "recordRename" do 191 | Test.Spec.it "renames a value when it doesn't exist" do 192 | let 193 | someRecord :: Option.Record (foo :: Boolean) (bar :: Int) 194 | someRecord = Option.recordFromRecord { foo: false } 195 | 196 | anotherRecord :: Option.Record (foo :: Boolean) (bar2 :: Int) 197 | anotherRecord = Option.recordRename { bar: Proxy :: Proxy "bar2" } someRecord 198 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { bar2: Data.Maybe.Nothing, foo: false } 199 | Test.Spec.it "can rename both required and optional values" do 200 | let 201 | someRecord :: Option.Record (foo :: Boolean) (bar :: Int) 202 | someRecord = Option.recordFromRecord { foo: false } 203 | 204 | anotherRecord :: Option.Record (foo1 :: Boolean) (bar2 :: Int) 205 | anotherRecord = Option.recordRename { foo: Proxy :: Proxy "foo1", bar: Proxy :: Proxy "bar2" } someRecord 206 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { bar2: Data.Maybe.Nothing, foo1: false } 207 | Test.Spec.it "can rename any required and optional values" do 208 | let 209 | someRecord :: Option.Record (foo :: Boolean, bar :: Int, baz :: String) (qux :: Boolean, cor :: Int, gar :: String) 210 | someRecord = Option.recordFromRecord { foo: false, bar: 31, baz: "hi" } 211 | 212 | anotherRecord :: Option.Record (foo :: Boolean, bar :: Int, baz3 :: String) (qux4 :: Boolean, cor :: Int, gar :: String) 213 | anotherRecord = Option.recordRename { baz: Proxy :: Proxy "baz3", qux: Proxy :: Proxy "qux4" } someRecord 214 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { bar: 31, baz3: "hi", cor: Data.Maybe.Nothing, foo: false, gar: Data.Maybe.Nothing, qux4: Data.Maybe.Nothing } 215 | Test.Spec.it "can rename any required and optional values with any interleaving of names" do 216 | let 217 | someRecord :: Option.Record (a :: Boolean, c :: Boolean, e :: Boolean) (b :: Boolean, d :: Boolean, f :: Boolean) 218 | someRecord = Option.recordFromRecord { a: false, c: false, e: false } 219 | 220 | anotherRecord :: Option.Record (a :: Boolean, c2 :: Boolean, e :: Boolean) (b4 :: Boolean, d5 :: Boolean, f :: Boolean) 221 | anotherRecord = Option.recordRename { b: Proxy :: Proxy "b4", c: Proxy :: Proxy "c2", d: Proxy :: Proxy "d5" } someRecord 222 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { a: false, b4: Data.Maybe.Nothing, c2: false, d5: Data.Maybe.Nothing, e: false, f: Data.Maybe.Nothing } 223 | 224 | spec_recordSet :: Test.Spec.Spec Unit 225 | spec_recordSet = 226 | Test.Spec.describe "recordSet" do 227 | Test.Spec.it "sets a value when it doesn't exist" do 228 | let 229 | someRecord :: Option.Record (foo :: Boolean) (bar :: Int) 230 | someRecord = Option.recordFromRecord { foo: false } 231 | 232 | anotherRecord :: Option.Record (foo :: Boolean) (bar :: Int) 233 | anotherRecord = Option.recordSet { bar: 31 } someRecord 234 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { bar: Data.Maybe.Just 31, foo: false } 235 | Test.Spec.it "can change the type" do 236 | let 237 | someRecord :: Option.Record (foo :: Boolean) (bar :: Boolean) 238 | someRecord = Option.recordFromRecord { foo: false } 239 | 240 | anotherRecord :: Option.Record (foo :: Boolean) (bar :: Int) 241 | anotherRecord = Option.recordSet { bar: 31 } someRecord 242 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { bar: Data.Maybe.Just 31, foo: false } 243 | Test.Spec.it "can use a `Data.Maybe.Maybe _`" do 244 | let 245 | someRecord :: Option.Record (foo :: Boolean) (bar :: Int) 246 | someRecord = Option.recordFromRecord { foo: false } 247 | 248 | anotherRecord :: Option.Record (foo :: Boolean) (bar :: Int) 249 | anotherRecord = Option.recordSet { bar: Data.Maybe.Just 31 } someRecord 250 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { bar: Data.Maybe.Just 31, foo: false } 251 | Test.Spec.it "`Data.Maybe.Nothing` removes the previous value" do 252 | let 253 | someRecord :: Option.Record (foo :: Boolean) (bar :: Int) 254 | someRecord = Option.recordFromRecord { bar: 31, foo: false } 255 | 256 | anotherRecord :: Option.Record (foo :: Boolean) (bar :: Int) 257 | anotherRecord = Option.recordSet { bar: Data.Maybe.Nothing } someRecord 258 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { bar: Data.Maybe.Nothing, foo: false } 259 | Test.Spec.it "can set both required and optional values" do 260 | let 261 | someRecord :: Option.Record (foo :: Boolean) (bar :: Int) 262 | someRecord = Option.recordFromRecord { foo: false } 263 | 264 | anotherRecord :: Option.Record (foo :: Boolean) (bar :: Int) 265 | anotherRecord = Option.recordSet { bar: 31 } someRecord 266 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { bar: Data.Maybe.Just 31, foo: false } 267 | Test.Spec.it "can set any required and optional values" do 268 | let 269 | someRecord :: Option.Record (foo :: Boolean, bar :: Int, baz :: String) (qux :: Boolean, cor :: Int, gar :: String) 270 | someRecord = Option.recordFromRecord { foo: false, bar: 31, baz: "hi" } 271 | 272 | anotherRecord :: Option.Record (foo :: Boolean, bar :: Int, baz :: String) (qux :: Boolean, cor :: Int, gar :: String) 273 | anotherRecord = Option.recordSet { baz: "hello", qux: true } someRecord 274 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { bar: 31, baz: "hello", cor: Data.Maybe.Nothing, foo: false, gar: Data.Maybe.Nothing, qux: Data.Maybe.Just true } 275 | Test.Spec.it "can set any required and optional values with any interleaving of names" do 276 | let 277 | someRecord :: Option.Record (a :: Boolean, c :: Boolean, e :: Boolean) (b :: Boolean, d :: Boolean, f :: Boolean) 278 | someRecord = Option.recordFromRecord { a: false, c: false, e: false } 279 | 280 | anotherRecord :: Option.Record (a :: Boolean, c :: Boolean, e :: Boolean) (b :: Boolean, d :: Boolean, f :: Boolean) 281 | anotherRecord = Option.recordSet { b: true, c: true, d: true } someRecord 282 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { a: false, b: Data.Maybe.Just true, c: true, d: Data.Maybe.Just true, e: false, f: Data.Maybe.Nothing } 283 | Test.Spec.it "can change the type of both required and optional values" do 284 | let 285 | someRecord :: Option.Record (foo :: Boolean) (bar :: Boolean) 286 | someRecord = Option.recordFromRecord { foo: false } 287 | 288 | anotherRecord :: Option.Record (foo :: Int) (bar :: Int) 289 | anotherRecord = Option.recordSet { bar: 31, foo: 43 } someRecord 290 | Option.recordToRecord anotherRecord `Test.Spec.Assertions.shouldEqual` { bar: Data.Maybe.Just 31, foo: 43 } 291 | 292 | spec_recordToRecord :: Test.Spec.Spec Unit 293 | spec_recordToRecord = 294 | Test.Spec.describe "recordToRecord" do 295 | Test.Spec.it "requires correct fields" do 296 | let 297 | record :: 298 | Option.Record 299 | ( name :: String 300 | ) 301 | ( greeting :: String 302 | , title :: String 303 | ) 304 | record = 305 | Option.recordFromRecord 306 | { name: "Pat" 307 | , title: "Dr." 308 | } 309 | Option.recordToRecord record `Test.Spec.Assertions.shouldEqual` { greeting: Data.Maybe.Nothing, name: "Pat", title: Data.Maybe.Just "Dr." } 310 | 311 | spec_rename :: Test.Spec.Spec Unit 312 | spec_rename = 313 | Test.Spec.describe "rename" do 314 | Test.Spec.it "renames a value when it doesn't exist" do 315 | let 316 | someOption :: Option.Option (foo :: Boolean, bar :: Int) 317 | someOption = Option.empty 318 | 319 | anotherOption :: Option.Option (foo :: Boolean, bar2 :: Int) 320 | anotherOption = Option.rename { bar: Proxy :: Proxy "bar2" } someOption 321 | Option.get (Proxy :: _ "bar2") anotherOption `Test.Spec.Assertions.shouldEqual` Data.Maybe.Nothing 322 | Test.Spec.it "can rename any field" do 323 | let 324 | someOption :: Option.Option (foo :: Boolean, bar :: Int, qux :: String) 325 | someOption = Option.empty 326 | 327 | anotherOption :: Option.Option (foo :: Boolean, bar :: Int, qux3 :: String) 328 | anotherOption = Option.rename { qux: Proxy :: Proxy "qux3" } someOption 329 | Option.get (Proxy :: _ "qux3") anotherOption `Test.Spec.Assertions.shouldEqual` Data.Maybe.Nothing 330 | 331 | spec_set :: Test.Spec.Spec Unit 332 | spec_set = 333 | Test.Spec.describe "set" do 334 | Test.Spec.it "sets a value when it doesn't exist" do 335 | let 336 | someOption :: Option.Option (foo :: Boolean, bar :: Int) 337 | someOption = Option.empty 338 | 339 | anotherOption :: Option.Option (foo :: Boolean, bar :: Int) 340 | anotherOption = Option.set (Proxy :: _ "bar") 31 someOption 341 | Option.get (Proxy :: _ "bar") anotherOption `Test.Spec.Assertions.shouldEqual` Data.Maybe.Just 31 342 | 343 | spec_set' :: Test.Spec.Spec Unit 344 | spec_set' = 345 | Test.Spec.describe "set'" do 346 | Test.Spec.it "sets a value when it doesn't exist" do 347 | let 348 | someOption :: Option.Option (foo :: Boolean, bar :: Int) 349 | someOption = Option.empty 350 | 351 | anotherOption :: Option.Option (foo :: Boolean, bar :: Int) 352 | anotherOption = Option.set' { bar: 31 } someOption 353 | Option.get (Proxy :: _ "bar") anotherOption `Test.Spec.Assertions.shouldEqual` Data.Maybe.Just 31 354 | Test.Spec.it "can work for any field" do 355 | let 356 | someOption :: Option.Option (foo :: Boolean, bar :: Int, qux :: String) 357 | someOption = Option.empty 358 | 359 | anotherOption :: Option.Option (foo :: Boolean, bar :: Int, qux :: String) 360 | anotherOption = Option.set' { qux: "hi" } someOption 361 | Option.get (Proxy :: _ "qux") anotherOption `Test.Spec.Assertions.shouldEqual` Data.Maybe.Just "hi" 362 | Test.Spec.it "can change the type" do 363 | let 364 | someOption :: Option.Option (foo :: Boolean, bar :: Boolean) 365 | someOption = Option.empty 366 | 367 | anotherOption :: Option.Option (foo :: Boolean, bar :: Int) 368 | anotherOption = Option.set' { bar: 31 } someOption 369 | Option.get (Proxy :: _ "bar") anotherOption `Test.Spec.Assertions.shouldEqual` Data.Maybe.Just 31 370 | Test.Spec.it "can use a `Data.Maybe.Maybe _`" do 371 | let 372 | someOption :: Option.Option (foo :: Boolean, bar :: Int) 373 | someOption = Option.empty 374 | 375 | anotherOption :: Option.Option (foo :: Boolean, bar :: Int) 376 | anotherOption = Option.set' { bar: Data.Maybe.Just 31 } someOption 377 | Option.get (Proxy :: _ "bar") anotherOption `Test.Spec.Assertions.shouldEqual` Data.Maybe.Just 31 378 | Test.Spec.it "`Data.Maybe.Nothing` removes the previous value" do 379 | let 380 | someOption :: Option.Option (foo :: Boolean, bar :: Int) 381 | someOption = Option.fromRecord { bar: 31 } 382 | 383 | anotherOption :: Option.Option (foo :: Boolean, bar :: Int) 384 | anotherOption = Option.set' { bar: Data.Maybe.Nothing } someOption 385 | Option.get (Proxy :: _ "bar") anotherOption `Test.Spec.Assertions.shouldEqual` Data.Maybe.Nothing 386 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-option 2 | 3 | A data type for optional values. 4 | 5 | ## Table of Contents 6 | 7 | - [Explanation: Motivation for `Option _`](#explanation-motivation-for-option-_) 8 | - [How To: Make a function with optional values](#how-to-make-a-function-with-optional-values) 9 | - [How To: Make a function with optional values from a record](#how-to-make-a-function-with-optional-values-from-a-record) 10 | - [How To: Make a function with required and optional values](#how-to-make-a-function-with-required-and-optional-values) 11 | - [How To: Make a function with required and optional values from a record](#how-to-make-a-function-with-required-and-optional-values-from-a-record) 12 | - [How To: Decode and Encode JSON with optional values in `purescript-argonaut`](#how-to-decode-and-encode-json-with-optional-values-in-purescript-argonaut) 13 | - [How To: Decode and Encode JSON with optional values in `purescript-codec-argonaut`](#how-to-decode-and-encode-json-with-optional-values-in-purescript-codec-argonaut) 14 | - [How To: Decode and Encode JSON with optional values in `purescript-simple-json`](#how-to-decode-and-encode-json-with-optional-values-in-purescript-simple-json) 15 | - [How To: Decode and Encode JSON with required and optional values in `purescript-argonaut`](#how-to-decode-and-encode-json-with-required-and-optional-values-in-purescript-argonaut) 16 | - [How To: Decode and Encode JSON with required and optional values in `purescript-codec-argonaut`](#how-to-decode-and-encode-json-with-required-and-optional-values-in-purescript-codec-argonaut) 17 | - [How To: Decode and Encode JSON with required and optional values in `purescript-simple-json`](#how-to-decode-and-encode-json-with-required-and-optional-values-in-purescript-simple-json) 18 | - [How To: Provide an easier API for `DateTime`](#how-to-provide-an-easier-api-for-datetime) 19 | - [Reference: `FromRecord _ _ _`](#reference-fromrecord-_-_-_) 20 | 21 | ## Explanation: Motivation for `Option _` 22 | 23 | There are a few different data types that encapsulate ideas in programming. 24 | 25 | Records capture the idea of a collection of key/value pairs where every key and value exist. 26 | E.g. `Record (foo :: Boolean, bar :: Int)` means that both `foo` and `bar` exist and with values all of the time. 27 | 28 | Variants capture the idea of a collection of key/value pairs where exactly one of the key/value pairs exist. 29 | E.g. `Data.Variant.Variant (foo :: Boolean, bar :: Int)` means that either only `foo` exists with a value or only `bar` exists with a value, but not both at the same time. 30 | 31 | Options capture the idea of a collection of key/value pairs where any key and value may or may not exist. 32 | E.g. `Option.Option (foo :: Boolean, bar :: Int)` means that either only `foo` exists with a value, only `bar` exists with a value, both `foo` and `bar` exist with values, or neither `foo` nor `bar` exist. 33 | 34 | The distinction between these data types means that we can describe problems more accurately. 35 | Options are typically what you find in dynamic languages or in weakly-typed static languages. 36 | Their use cases range from making APIs more flexible to interfacing with serialization formats to providing better ergonomics around data types. 37 | 38 | These data types are all specific to the PureScript language. 39 | Different data types exist in other languages that combine some of these ideas. 40 | In many languages records are a combination of both PureScript-style records and PureScript-style options. 41 | E.g. `Option.Record (foo :: Boolean) (bar :: Int)` means that `foo` exists with a value all of the time, and either `bar` exists with a value or `bar` doesn't exist with a value. 42 | 43 | Other languages might signify optional fields with a question mark. 44 | E.g. In TypeScript, the previous example would be `{ foo: boolean; bar?: number }` 45 | 46 | This is different from a required field with an optional value. 47 | In PureScript, we might signify that by using: `Record (foo :: Boolean, bar :: Data.Maybe.Maybe Int)`. 48 | In TypeScript, we might signify that by using: `{ foo: boolean; bar: number | null }` 49 | 50 | ## How To: Make a function with optional values 51 | 52 | Let's say we want to make a `greeting` function where people can pass in an `Option ( name :: String, title :: String )` to override the default behavior. 53 | I.e. we want something like: `greeting :: Option.Option ( name :: String, title :: String ) -> String`. 54 | The implementation should be fairly straight forward: 55 | 56 | ```PureScript 57 | greeting :: Option.Option ( name :: String, title :: String ) -> String 58 | greeting option = "Hello, " <> title' <> name' 59 | where 60 | name' :: String 61 | name' = case Option.get (Type.Proxy.Proxy :: _ "name") option of 62 | Data.Maybe.Just name -> name 63 | Data.Maybe.Nothing -> "World" 64 | 65 | title' :: String 66 | title' = case Option.get (Type.Proxy.Proxy :: _ "title") option of 67 | Data.Maybe.Just title -> title <> " " 68 | Data.Maybe.Nothing -> "" 69 | ``` 70 | 71 | We look up each key in the given `Option _`, and decide what to do with it. 72 | In the case of the `"title"`, we append a space so the output is still legible. 73 | With the `greeting` function, we can pass in an option and alter the behavior: 74 | 75 | ```PureScript 76 | > greeting (Option.fromRecord {}) 77 | "Hello, World" 78 | 79 | > greeting (Option.fromRecord { title: "wonderful" }) 80 | "Hello, wonderful World" 81 | 82 | > greeting (Option.fromRecord { name: "Pat" }) 83 | "Hello, Pat" 84 | 85 | > greeting (Option.fromRecord { name: "Pat", title: "Dr." }) 86 | "Hello, Dr. Pat" 87 | ``` 88 | 89 | We've allowed people to override the behavior of the function with optional values! 90 | 91 | It might be instructive to compare how we might write a similar function using a `Record _` instead of `Option _`: 92 | 93 | ```PureScript 94 | greeting' :: 95 | Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) -> 96 | String 97 | greeting' option = "Hello, " <> title' <> name' 98 | where 99 | name' :: String 100 | name' = case option.name of 101 | Data.Maybe.Just name -> name 102 | Data.Maybe.Nothing -> "World" 103 | 104 | title' :: String 105 | title' = case option.title of 106 | Data.Maybe.Just title -> title <> " " 107 | Data.Maybe.Nothing -> "" 108 | ``` 109 | 110 | To implement `greeting'`, nothing really changed. 111 | We used the built-in dot operator to fetch the keys out of the record, but we could have just as easily used `Record.get` (which would have highlighted the similarlities even more). 112 | 113 | To use `greeting'`, we force the users of `greeting'` to do always give us a value in the record: 114 | 115 | ```PureScript 116 | > User.greeting' { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing } 117 | "Hello, World" 118 | 119 | > User.greeting' { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" } 120 | "Hello, wonderful World" 121 | 122 | > User.greeting' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing } 123 | "Hello, Pat" 124 | 125 | > User.greeting' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." } 126 | "Hello, Dr. Pat" 127 | ``` 128 | 129 | ## How To: Make a function with optional values from a record 130 | 131 | Let's say we want to solve a similar problem as before, but we don't want to force people to create the `Option _` themselves. 132 | We want to allow people to pass in a record that may be missing some fields. 133 | To write this version of `greeting`, we'll need to use the `FromRecord` typeclass (see the section on `FromRecord` for more information). 134 | I.e. we want something like: `greeting :: forall record. Option.FromRecord record ( name :: String, title :: String ) => Record record -> String`. 135 | 136 | The implementation moves the work of constructing the `Option _`, but should also be straight forward: 137 | 138 | ```PureScript 139 | greeting :: 140 | forall record. 141 | Option.FromRecord record () ( name :: String, title :: String ) => 142 | Record record -> 143 | String 144 | greeting record = "Hello, " <> title' <> name' 145 | where 146 | name' :: String 147 | name' = case Option.get (Type.Proxy.Proxy :: _ "name") option of 148 | Data.Maybe.Just name -> name 149 | Data.Maybe.Nothing -> "World" 150 | 151 | option :: Option.Option ( name :: String, title :: String ) 152 | option = Option.fromRecord record 153 | 154 | title' :: String 155 | title' = case Option.get (Type.Proxy.Proxy :: _ "title") option of 156 | Data.Maybe.Just title -> title <> " " 157 | Data.Maybe.Nothing -> "" 158 | ``` 159 | 160 | We can use this similar to how we used the previous implementation of `greeting`. 161 | Instead of needing to construct an `Option _` and pass it in, we give the `Record _` directly: 162 | 163 | ```PureScript 164 | > greeting {} 165 | "Hello, World" 166 | 167 | > greeting { title: "wonderful" } 168 | "Hello, wonderful World" 169 | 170 | > greeting { name: "Pat" } 171 | "Hello, Pat" 172 | 173 | > greeting { name: "Pat", title: "Dr." } 174 | "Hello, Dr. Pat" 175 | ``` 176 | 177 | We've allowed people to override the behavior of the function with optional values using a record! 178 | 179 | ## How To: Make a function with required and optional values 180 | 181 | Let's say we want to make a `greeting` function where people can pass in an `Option.Record ( name :: String ) ( title :: String )`. 182 | The `"name"` is required, but a `"title"` can be given to override the default behavior. 183 | I.e. we want something like: `greeting :: Option.Record ( name :: String ) ( title :: String ) -> String`. 184 | The implementation should be fairly straight forward: 185 | 186 | ```PureScript 187 | import Prelude 188 | import Data.Maybe as Data.Maybe 189 | import Option as Option 190 | 191 | greeting :: 192 | Option.Record ( name :: String ) ( title :: String ) -> 193 | String 194 | greeting record' = "Hello, " <> title' <> record.name 195 | where 196 | record :: Record ( name :: String, title :: Data.Maybe.Maybe String ) 197 | record = Option.recordToRecord record' 198 | 199 | title' :: String 200 | title' = case record.title of 201 | Data.Maybe.Just title -> title <> " " 202 | Data.Maybe.Nothing -> "" 203 | ``` 204 | 205 | We look up the `"title"` in the given `Option.Record _ _`, and decide what to do with it. 206 | In the case of the `"title"`, we append a space so the output is still legible. 207 | With the `greeting` function, we can pass in an option and alter the behavior: 208 | 209 | ```PureScript 210 | > greeting (Option.recordFromRecord { name: "Pat" }) 211 | "Hello, Pat" 212 | 213 | > greeting (Option.recordFromRecord { name: "Pat", title: "Dr." }) 214 | "Hello, Dr. Pat" 215 | ``` 216 | 217 | We've allowed people to override the behavior of the function with optional values! 218 | 219 | It might be instructive to compare how we might write a similar function using a `Record _` instead of `Option _`: 220 | 221 | ```PureScript 222 | greeting' :: 223 | Record ( name :: String, title :: Data.Maybe.Maybe String ) -> 224 | String 225 | greeting' record = "Hello, " <> title' <> record.name 226 | where 227 | title' :: String 228 | title' = case record.title of 229 | Data.Maybe.Just title -> title <> " " 230 | Data.Maybe.Nothing -> "" 231 | ``` 232 | 233 | To implement `greeting'`, nothing really changed. 234 | We don't have to convert down to a language-level Record because the argument is already a language-level Record. 235 | That's the only difference as far as implementing `greeting'`. 236 | 237 | To use `greeting'`, we force the users of `greeting'` to do always give us a value in the record: 238 | 239 | ```PureScript 240 | > User.greeting' { name: "Pat", title: Data.Maybe.Nothing } 241 | "Hello, Pat" 242 | 243 | > User.greeting' { name: "Pat", title: Data.Maybe.Just "Dr." } 244 | "Hello, Dr. Pat" 245 | ``` 246 | 247 | ## How To: Make a function with required and optional values from a record 248 | 249 | Let's say we want to solve a similar problem as before, but we don't want to force people to create the `Option.Record _ _` themselves. 250 | We want to allow people to pass in a record that may be missing some fields. 251 | To write this version of `greeting`, we'll need to use the `FromRecord` typeclass (see the section on `FromRecord` for more information). 252 | I.e. we want something like: `greeting :: forall record. Option.FromRecord record ( name :: String ) ( title :: String ) => Record record -> String`. 253 | 254 | The implementation moves the work of constructing the `Option.Record _ _`, but should also be straight forward: 255 | 256 | ```PureScript 257 | import Prelude 258 | import Data.Maybe as Data.Maybe 259 | import Option as Option 260 | 261 | greeting :: 262 | forall record. 263 | Option.FromRecord record ( name :: String ) ( title :: String ) => 264 | Record record -> 265 | String 266 | greeting record'' = "Hello, " <> title' <> record.name 267 | where 268 | record :: Record ( name :: String, title :: Data.Maybe.Maybe String ) 269 | record = Option.recordToRecord record' 270 | 271 | record' :: Option.Record ( name :: String ) ( title :: String ) 272 | record' = Option.recordFromRecord record'' 273 | 274 | title' :: String 275 | title' = case record.title of 276 | Data.Maybe.Just title -> title <> " " 277 | Data.Maybe.Nothing -> "" 278 | ``` 279 | 280 | We can use this similar to how we used the previous implementation of `greeting`. 281 | Instead of needing to construct an `Option.Record _ _` and pass it in, we give the `Record _` directly: 282 | 283 | ```PureScript 284 | > greeting { name: "Pat" } 285 | "Hello, Pat" 286 | 287 | > greeting { name: "Pat", title: "Dr." } 288 | "Hello, Dr. Pat" 289 | ``` 290 | 291 | We've allowed people to override the behavior of the function with optional values using a record! 292 | 293 | ## How To: Decode and Encode JSON with optional values in `purescript-argonaut` 294 | 295 | A common pattern with JSON objects is that keys do not always have to be present. 296 | Some APIs make the distinction between a JSON object like `{ "name": "Pat" }` and one like `{ "name": "Pat", "title": null }`. 297 | In the first case, it might recognize that the `"title"` key does not exist, and behave in a different way from the `"title"` key having a value of `null`. 298 | In the second case, it might notice that the `"title"` key exists and work with the value assuming it's good to go; the `null` might eventually cause a failure later. 299 | 300 | In many cases, what we want is to not generate any fields that do not exist. 301 | Using `purescript-argonaut`, `Option _` can help with that idea: 302 | 303 | ```PureScript 304 | decode :: 305 | Data.Argonaut.Core.Json -> 306 | Data.Either.Either String (Option.Option ( name :: String, title :: String )) 307 | decode = Data.Argonaut.Decode.Class.decodeJson 308 | 309 | encode :: 310 | Option.Option ( name :: String, title :: String ) -> 311 | Data.Argonaut.Core.Json 312 | encode = Data.Argonaut.Encode.Class.encodeJson 313 | 314 | parse :: 315 | String -> 316 | Data.Either.Either String (Option.Option (name :: String, title :: String)) 317 | parse string = case Data.Argonaut.Parser.jsonParser string of 318 | Data.Either.Left error -> Data.Either.Left error 319 | Data.Either.Right json -> case decode json of 320 | Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error) 321 | Data.Either.Right option -> Data.Either.Right option 322 | ``` 323 | 324 | We can give that a spin with some different JSON values: 325 | 326 | ```PureScript 327 | > parse """{}""" 328 | (Right (Option.fromRecord {})) 329 | 330 | > parse """{"title": "wonderful"}""" 331 | (Right (Option.fromRecord { title: "wonderful" })) 332 | 333 | > parse """{"name": "Pat"}""" 334 | (Right (Option.fromRecord { name: "Pat" })) 335 | 336 | > parse """{"name": "Pat", "title": "Dr."}""" 337 | (Right (Option.fromRecord { name: "Pat", title: "Dr." })) 338 | 339 | > parse """{ "name": null }""" 340 | Right (Option.fromRecord {}) 341 | 342 | > parse """{ "title": null }""" 343 | Right (Option.fromRecord {}) 344 | 345 | > parse """{ "name": null, "title": null }""" 346 | Right (Option.fromRecord {}) 347 | ``` 348 | 349 | We can also produce some different JSON values: 350 | 351 | ```PureScript 352 | > Data.Argonaut.Core.stringify (encode (Option.fromRecord {})) 353 | "{}" 354 | 355 | > Data.Argonaut.Core.stringify (encode (Option.fromRecord { title: "wonderful" })) 356 | "{\"title\":\"wonderful\"}" 357 | 358 | > Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat" })) 359 | "{\"name\":\"Pat\"}" 360 | 361 | > Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat", title: "Dr." })) 362 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 363 | ``` 364 | 365 | Notice that we don't end up with a `"title"` field in the JSON output unless we have a `title` field in our record. 366 | 367 | It might be instructive to compare how we might write a similar functions using a `Record _` instead of `Option _`: 368 | With `purescript-argonaut`, the instances for decoding and encoding on records expect the field to always exist no matter its value. 369 | If we attempt to go directly to `Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )`: 370 | 371 | ```PureScript 372 | decode' :: 373 | Data.Argonaut.Core.Json -> 374 | Data.Either.Either String (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 375 | decode' = Data.Argonaut.Decode.Class.decodeJson 376 | 377 | encode' :: 378 | Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) -> 379 | Data.Argonaut.Core.Json 380 | encode' = Data.Argonaut.Encode.Class.encodeJson 381 | 382 | parse' :: 383 | String -> 384 | Data.Either.Either String (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 385 | parse' string = case Data.Argonaut.Parser.jsonParser string of 386 | Data.Either.Left error -> Data.Either.Left error 387 | Data.Either.Right json -> case decode json of 388 | Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error) 389 | Data.Either.Right record -> Data.Either.Right record 390 | ``` 391 | 392 | We won't get the behavior we expect: 393 | 394 | ```PureScript 395 | > parse' """{}""" 396 | (Left "An error occurred while decoding a JSON value:\n At object key 'title':\n No value was found.") 397 | 398 | > parse' """{"title": "wonderful"}""" 399 | (Left "An error occurred while decoding a JSON value:\n At object key 'name':\n No value was found.") 400 | 401 | > parse' """{"name": "Pat"}""" 402 | (Left "An error occurred while decoding a JSON value:\n At object key 'title':\n No value was found.") 403 | 404 | > parse' """{"name": "Pat", "title": "Dr."}""" 405 | (Right { name: (Just "Pat"), title: (Just "Dr.") }) 406 | 407 | > Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing }) 408 | "{\"title\":null,\"name\":null}" 409 | 410 | > Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" }) 411 | "{\"title\":\"wonderful\",\"name\":null}" 412 | 413 | > Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing }) 414 | "{\"title\":null,\"name\":\"Pat\"}" 415 | 416 | > Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." }) 417 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 418 | ``` 419 | 420 | Unless both fields exist, we cannot decode the JSON object. 421 | Similarly, no matter what the values are, we always encode them into a JSON object. 422 | 423 | In order to emulate the behavior of an optional field, we have to name the record, and write our own instances: 424 | 425 | ```PureScript 426 | newtype Greeting 427 | = Greeting 428 | ( Record 429 | ( name :: Data.Maybe.Maybe String 430 | , title :: Data.Maybe.Maybe String 431 | ) 432 | ) 433 | 434 | derive instance genericGreeting :: Data.Generic.Rep.Generic Greeting _ 435 | 436 | instance showGreeting :: Show Greeting where 437 | show = Data.Generic.Rep.Show.genericShow 438 | 439 | instance decodeJsonGreeting :: Data.Argonaut.Decode.Class.DecodeJson Greeting where 440 | decodeJson json = do 441 | object <- Data.Argonaut.Decode.Class.decodeJson json 442 | name <- Data.Argonaut.Decode.Combinators.getFieldOptional object "name" 443 | title <- Data.Argonaut.Decode.Combinators.getFieldOptional object "title" 444 | pure (Greeting { name, title }) 445 | 446 | instance encodeJsonGreeting :: Data.Argonaut.Encode.Class.EncodeJson Greeting where 447 | encodeJson (Greeting { name, title }) = 448 | Data.Argonaut.Encode.Combinators.extendOptional 449 | (Data.Argonaut.Encode.Combinators.assocOptional "name" name) 450 | ( Data.Argonaut.Encode.Combinators.extendOptional 451 | (Data.Argonaut.Encode.Combinators.assocOptional "title" title) 452 | (Data.Argonaut.Core.jsonEmptyObject) 453 | ) 454 | 455 | parse'' :: 456 | String -> 457 | Data.Either.Either String Greeting 458 | parse'' string = case Data.Argonaut.Parser.jsonParser string of 459 | Data.Either.Left error -> Data.Either.Left error 460 | Data.Either.Right json -> case decode json of 461 | Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error) 462 | Data.Either.Right greeting -> Data.Either.Right greeting 463 | ``` 464 | 465 | If we try decoding and encoding now, we get something closer to what we wanted: 466 | 467 | ```PureScript 468 | > parse'' """{}""" 469 | (Right (Greeting { name: Nothing, title: Nothing })) 470 | 471 | > parse'' """{"title": "wonderful"}""" 472 | (Right (Greeting { name: Nothing, title: (Just "wonderful") })) 473 | 474 | > parse'' """{"name": "Pat"}""" 475 | (Right (Greeting { name: (Just "Pat"), title: Nothing })) 476 | 477 | > parse'' """{"name": "Pat", "title": "Dr."}""" 478 | (Right (Greeting { name: (Just "Pat"), title: (Just "Dr.") })) 479 | 480 | > Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing })) 481 | "{}" 482 | 483 | > Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" })) 484 | "{\"title\":\"wonderful\"}" 485 | 486 | > Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing })) 487 | "{\"name\":\"Pat\"}" 488 | 489 | > Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." })) 490 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 491 | ``` 492 | 493 | ## How To: Decode and Encode JSON with optional values in `purescript-codec-argonaut` 494 | 495 | A common pattern with JSON objects is that keys do not always have to be present. 496 | Some APIs make the distinction between a JSON object like `{ "name": "Pat" }` and one like `{ "name": "Pat", "title": null }`. 497 | In the first case, it might recognize that the `"title"` key does not exist, and behave in a different way from the `"title"` key having a value of `null`. 498 | In the second case, it might notice that the `"title"` key exists and work with the value assuming it's good to go; the `null` might eventually cause a failure later. 499 | 500 | In many cases, what we want is to not generate any fields that do not exist. 501 | Using `purescript-codec-argonaut`, `Option _` can help with that idea: 502 | 503 | ```PureScript 504 | jsonCodec :: Data.Codec.Argonaut.JsonCodec (Option.Option ( name :: String, title :: String )) 505 | jsonCodec = 506 | Option.jsonCodec 507 | { name: Data.Codec.Argonaut.string 508 | , title: Data.Codec.Argonaut.string 509 | } 510 | ``` 511 | 512 | We can add a couple of helpers to make decoding/encoding easier in the REPL: 513 | 514 | ```PureScript 515 | decode :: 516 | Data.Argonaut.Core.Json -> 517 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Option.Option ( name :: String, title :: String )) 518 | decode = Data.Codec.Argonaut.decode jsonCodec 519 | 520 | encode :: 521 | Option.Option ( name :: String, title :: String ) -> 522 | Data.Argonaut.Core.Json 523 | encode = Data.Codec.Argonaut.encode jsonCodec 524 | 525 | parse :: 526 | String -> 527 | Data.Either.Either String (Option.Option ( name :: String, title :: String )) 528 | parse string = do 529 | json <- Data.Argonaut.Parser.jsonParser string 530 | case decode json of 531 | Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err) 532 | Data.Either.Right option -> Data.Either.Right option 533 | ``` 534 | 535 | We can give that a spin with some different JSON values: 536 | 537 | ```PureScript 538 | > parse """{}""" 539 | (Right (Option.fromRecord {})) 540 | 541 | > parse """{"title": "wonderful"}""" 542 | (Right (Option.fromRecord { title: "wonderful" })) 543 | 544 | > parse """{"name": "Pat"}""" 545 | (Right (Option.fromRecord { name: "Pat" })) 546 | 547 | > parse """{"name": "Pat", "title": "Dr."}""" 548 | (Right (Option.fromRecord { name: "Pat", title: "Dr." })) 549 | 550 | > parse """{ "name": null }""" 551 | Right (Option.fromRecord {}) 552 | 553 | > parse """{ "title": null }""" 554 | Right (Option.fromRecord {}) 555 | 556 | > parse """{ "name": null, "title": null }""" 557 | Right (Option.fromRecord {}) 558 | ``` 559 | 560 | We can also produce some different JSON values: 561 | 562 | ```PureScript 563 | > Data.Argonaut.Core.stringify (encode (Option.fromRecord {})) 564 | "{}" 565 | 566 | > Data.Argonaut.Core.stringify (encode (Option.fromRecord { title: "wonderful" })) 567 | "{\"title\":\"wonderful\"}" 568 | 569 | > Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat" })) 570 | "{\"name\":\"Pat\"}" 571 | 572 | > Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat", title: "Dr." })) 573 | "{\"name\":\"Pat\",\"title\":\"Dr.\"}" 574 | ``` 575 | 576 | Notice that we don't end up with a `"title"` field in the JSON output unless we have a `title` field in our record. 577 | 578 | It might be instructive to compare how we might write a similar functions using a `Record _` instead of `Option _`: 579 | With `purescript-codec-argonaut`, there are a couple of codecs that ship with the package for records: `Data.Codec.Argonaut.recordProp` and `Data.Codec.Argonaut.Record.record`. 580 | Each of those codecs expect the field to always exist no matter its value. 581 | The difference between those codecs is not very relevant except to say that the latter requires less characters to use. 582 | There are also a couple of codecs that ship with the package for `Data.Maybe.Maybe _`: `Data.Codec.Argonaut.Common.maybe` and `Data.Codec.Argonaut.Compat.maybe`. 583 | The former decodes/encodes with tagged values, the latter with `null`s. 584 | If we attempt to go directly to `Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )` and `Data.Codec.Argonaut.Common.maybe`: 585 | 586 | ```PureScript 587 | decode' :: 588 | Data.Argonaut.Core.Json -> 589 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 590 | decode' = Data.Codec.Argonaut.decode jsonCodec' 591 | 592 | encode' :: 593 | Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) -> 594 | Data.Argonaut.Core.Json 595 | encode' = Data.Codec.Argonaut.encode jsonCodec' 596 | 597 | jsonCodec' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 598 | jsonCodec' = 599 | Data.Codec.Argonaut.Record.object 600 | "Greeting" 601 | { name: Data.Codec.Argonaut.Common.maybe Data.Codec.Argonaut.string 602 | , title: Data.Codec.Argonaut.Common.maybe Data.Codec.Argonaut.string 603 | } 604 | 605 | parse' :: 606 | String -> 607 | Data.Either.Either String (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 608 | parse' string = do 609 | json <- Data.Argonaut.Parser.jsonParser string 610 | case decode' json of 611 | Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err) 612 | Data.Either.Right option -> Data.Either.Right option 613 | ``` 614 | 615 | We won't get the behavior we expect: 616 | 617 | ```PureScript 618 | > parse' """{}""" 619 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n No value was found.") 620 | 621 | > parse' """{"title": "wonderful"}""" 622 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n Under 'Maybe':\n Expected value of type 'Object'.") 623 | 624 | > parse' """{"name": "Pat"}""" 625 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n No value was found.") 626 | 627 | > parse' """{"name": "Pat", "title": "Dr."}""" 628 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n Under 'Maybe':\n Expected value of type 'Object'.") 629 | 630 | > parse' """{"name": {"tag": "Just", "value": "Pat"}, "title": {"tag": "Just", "value": "Dr."}}""" 631 | (Right { name: (Just "Pat"), title: (Just "Dr.") }) 632 | 633 | > Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing }) 634 | "{\"name\":{\"tag\":\"Nothing\"},\"title\":{\"tag\":\"Nothing\"}}" 635 | 636 | > Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" }) 637 | "{\"name\":{\"tag\":\"Nothing\"},\"title\":{\"tag\":\"Just\",\"value\":\"wonderful\"}}" 638 | 639 | > Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing }) 640 | "{\"name\":{\"tag\":\"Just\",\"value\":\"Pat\"},\"title\":{\"tag\":\"Nothing\"}}" 641 | 642 | > Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." }) 643 | "{\"name\":{\"tag\":\"Just\",\"value\":\"Pat\"},\"title\":{\"tag\":\"Just\",\"value\":\"Dr.\"}}" 644 | ``` 645 | 646 | Unless both fields exist, we cannot decode the JSON object. 647 | Not only is every field required, they're serialized as tagged values. 648 | Similarly, no matter what the values are, we always encode them into a JSON object. 649 | 650 | If we try with `Data.Codec.Argonaut.Compat.maybe`: 651 | 652 | ```PureScript 653 | decode'' :: 654 | Data.Argonaut.Core.Json -> 655 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 656 | decode'' = Data.Codec.Argonaut.decode jsonCodec'' 657 | 658 | encode'' :: 659 | Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) -> 660 | Data.Argonaut.Core.Json 661 | encode'' = Data.Codec.Argonaut.encode jsonCodec'' 662 | 663 | jsonCodec'' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 664 | jsonCodec'' = 665 | Data.Codec.Argonaut.Record.object 666 | "Greeting" 667 | { name: Data.Codec.Argonaut.Compat.maybe Data.Codec.Argonaut.string 668 | , title: Data.Codec.Argonaut.Compat.maybe Data.Codec.Argonaut.string 669 | } 670 | 671 | parse'' :: 672 | String -> 673 | Data.Either.Either String (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 674 | parse'' string = do 675 | json <- Data.Argonaut.Parser.jsonParser string 676 | case decode'' json of 677 | Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err) 678 | Data.Either.Right option -> Data.Either.Right option 679 | ``` 680 | 681 | We also don't get the behavior we expect: 682 | 683 | ```PureScript 684 | > parse'' """{}""" 685 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n No value was found.") 686 | 687 | > parse'' """{"title": "wonderful"}""" 688 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key name:\n No value was found.") 689 | 690 | > parse'' """{"name": "Pat"}""" 691 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n No value was found.") 692 | 693 | > parse'' """{"name": "Pat", "title": "Dr."}""" 694 | (Right { name: (Just "Pat"), title: (Just "Dr.") }) 695 | 696 | > Data.Argonaut.Core.stringify (encode'' { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing }) 697 | "{\"name\":null,\"title\":null}" 698 | 699 | > Data.Argonaut.Core.stringify (encode'' { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" }) 700 | "{\"name\":null,\"title\":\"wonderful\"}" 701 | 702 | > Data.Argonaut.Core.stringify (encode'' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing }) 703 | "{\"name\":\"Pat\",\"title\":null}" 704 | 705 | > Data.Argonaut.Core.stringify (encode'' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." }) 706 | "{\"name\":\"Pat\",\"title\":\"Dr.\"}" 707 | ``` 708 | 709 | Unless both fields exist, we cannot decode the JSON object. 710 | Similarly, no matter what the values are, we always encode them into a JSON object. 711 | 712 | In order to emulate the behavior of an optional field, we have to use a different codec: 713 | 714 | ```PureScript 715 | optionalField :: 716 | forall label record record' value. 717 | Data.Symbol.IsSymbol label => 718 | Prim.Row.Cons label (Data.Maybe.Maybe value) record' record => 719 | Prim.Row.Lacks label record' => 720 | Type.Proxy.Proxy label -> 721 | Data.Codec.Argonaut.JsonCodec value -> 722 | Data.Codec.Argonaut.JPropCodec (Record record') -> 723 | Data.Codec.Argonaut.JPropCodec (Record record) 724 | optionalField label codecValue codecRecord = 725 | Data.Codec.GCodec 726 | (Control.Monad.Reader.Trans.ReaderT decodeField) 727 | (Data.Profunctor.Star.Star encodeField) 728 | where 729 | decodeField :: 730 | Foreign.Object.Object Data.Argonaut.Core.Json -> 731 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record record) 732 | decodeField object' = do 733 | record <- Data.Codec.Argonaut.decode codecRecord object' 734 | case Foreign.Object.lookup key object' of 735 | Data.Maybe.Just json -> case Data.Codec.Argonaut.decode codecValue json of 736 | Data.Either.Left error -> Data.Either.Left (Data.Codec.Argonaut.AtKey key error) 737 | Data.Either.Right value -> Data.Either.Right (Record.insert label (Data.Maybe.Just value) record) 738 | Data.Maybe.Nothing -> Data.Either.Right (Record.insert label Data.Maybe.Nothing record) 739 | 740 | encodeField :: 741 | Record record -> 742 | Control.Monad.Writer.Writer (Data.List.List (Data.Tuple.Tuple String Data.Argonaut.Core.Json)) (Record record) 743 | encodeField record = do 744 | case Record.get label record of 745 | Data.Maybe.Just value -> 746 | Control.Monad.Writer.Class.tell 747 | ( Data.List.Cons 748 | (Data.Tuple.Tuple key (Data.Codec.Argonaut.encode codecValue value)) 749 | Data.List.Nil 750 | ) 751 | Data.Maybe.Nothing -> pure unit 752 | Control.Monad.Writer.Class.tell 753 | (Data.Codec.Argonaut.encode codecRecord (Record.delete label record)) 754 | pure record 755 | 756 | key :: String 757 | key = Data.Symbol.reflectSymbol label 758 | ``` 759 | 760 | With this codec defined, we can implement a codec for the record with optional fields: 761 | 762 | ```PureScript 763 | decode''' :: 764 | Data.Argonaut.Core.Json -> 765 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 766 | decode''' = Data.Codec.Argonaut.decode jsonCodec''' 767 | 768 | encode''' :: 769 | Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) -> 770 | Data.Argonaut.Core.Json 771 | encode''' = Data.Codec.Argonaut.encode jsonCodec''' 772 | 773 | jsonCodec''' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 774 | jsonCodec''' = 775 | Data.Codec.Argonaut.object 776 | "Greeting" 777 | ( optionalField (Type.Proxy.Proxy :: _ "name") Data.Codec.Argonaut.string 778 | $ optionalField (Type.Proxy.Proxy :: _ "title") Data.Codec.Argonaut.string 779 | $ Data.Codec.Argonaut.record 780 | ) 781 | 782 | parse''' :: 783 | String -> 784 | Data.Either.Either String (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 785 | parse''' string = do 786 | json <- Data.Argonaut.Parser.jsonParser string 787 | case decode''' json of 788 | Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err) 789 | Data.Either.Right option -> Data.Either.Right option 790 | ``` 791 | 792 | If we try decoding and encoding now, we get something closer to what we wanted: 793 | 794 | ```PureScript 795 | > parse''' """{}""" 796 | (Right { name: Nothing, title: Nothing }) 797 | 798 | > parse''' """{"title": "wonderful"}""" 799 | (Right { name: Nothing, title: (Just "wonderful") }) 800 | 801 | > parse''' """{"name": "Pat"}""" 802 | (Right { name: (Just "Pat"), title: Nothing }) 803 | 804 | > parse''' """{"name": "Pat", "title": "wonderful"}""" 805 | (Right { name: (Just "Pat"), title: (Just "wonderful") }) 806 | 807 | > Data.Argonaut.Core.stringify (encode''' { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing }) 808 | "{}" 809 | 810 | > Data.Argonaut.Core.stringify (encode''' { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" }) 811 | "{\"title\":\"wonderful\"}" 812 | 813 | > Data.Argonaut.Core.stringify (encode''' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing }) 814 | "{\"name\":\"Pat\"}" 815 | 816 | > Data.Argonaut.Core.stringify (encode''' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." }) 817 | "{\"name\":\"Pat\",\"title\":\"Dr.\"}" 818 | ``` 819 | 820 | ## How To: Decode and Encode JSON with optional values in `purescript-simple-json` 821 | 822 | A common pattern with JSON objects is that keys do not always have to be present. 823 | Some APIs make the distinction between a JSON object like `{ "name": "Pat" }` and one like `{ "name": "Pat", "title": null }`. 824 | In the first case, it might recognize that the `"title"` key does not exist, and behave in a different way from the `"title"` key having a value of `null`. 825 | In the second case, it might notice that the `"title"` key exists and work with the value assuming it's good to go; the `null` might eventually cause a failure later. 826 | 827 | In many cases, what we want is to not generate any fields that do not exist. 828 | Using `purescript-simple-json`, `Option _` can help with that idea: 829 | 830 | ```PureScript 831 | readJSON :: 832 | String -> 833 | Simple.JSON.E (Option.Option ( name :: String, title :: String )) 834 | readJSON = Simple.JSON.readJSON 835 | 836 | writeJSON :: 837 | Option.Option ( name :: String, title :: String ) -> 838 | String 839 | writeJSON = Simple.JSON.writeJSON 840 | ``` 841 | 842 | We can give that a spin with some different JSON values: 843 | 844 | ```PureScript 845 | > readJSON """{}""" 846 | (Right (Option.fromRecord {})) 847 | 848 | > readJSON """{"title": "wonderful"}""" 849 | (Right (Option.fromRecord { title: "wonderful" })) 850 | 851 | > readJSON """{"name": "Pat"}""" 852 | (Right (Option.fromRecord { name: "Pat" })) 853 | 854 | > readJSON """{"name": "Pat", "title": "Dr."}""" 855 | (Right (Option.fromRecord { name: "Pat", title: "Dr." })) 856 | 857 | > readJSON """{ "name": null }""" 858 | Right (Option.fromRecord {}) 859 | 860 | > readJSON """{ "title": null }""" 861 | Right (Option.fromRecord {}) 862 | 863 | > readJSON """{ "name": null, "title": null }""" 864 | Right (Option.fromRecord {}) 865 | ``` 866 | 867 | We can also produce some different JSON values: 868 | 869 | ```PureScript 870 | > writeJSON (Option.fromRecord {}) 871 | "{}" 872 | 873 | > writeJSON (Option.fromRecord {title: "wonderful"}) 874 | "{\"title\":\"wonderful\"}" 875 | 876 | > writeJSON (Option.fromRecord {name: "Pat"}) 877 | "{\"name\":\"Pat\"}" 878 | 879 | > writeJSON (Option.fromRecord {name: "Pat", title: "Dr."}) 880 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 881 | ``` 882 | 883 | Notice that we don't end up with a `"title"` field in the JSON output unless we have a `title` field in our record. 884 | 885 | It might be instructive to compare how we might write a similar functions using a `Record _` instead of `Option _`: 886 | With `purescript-simple-json`, the instances for decoding and encoding on records handle `Data.Maybe.Maybe _` values like they are optional. 887 | If we attempt to go directly to `Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )`: 888 | 889 | ```PureScript 890 | readJSON' :: 891 | String -> 892 | Simple.JSON.E (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String )) 893 | readJSON' = Simple.JSON.readJSON 894 | 895 | writeJSON' :: 896 | Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) -> 897 | String 898 | writeJSON' = Simple.JSON.writeJSON 899 | ``` 900 | 901 | We get the behavior we expect: 902 | 903 | ```PureScript 904 | > readJSON' """{}""" 905 | (Right { name: Nothing, title: Nothing }) 906 | 907 | > readJSON' """{"title": "wonderful"}""" 908 | (Right { name: Nothing, title: (Just "wonderful") }) 909 | 910 | > readJSON' """{"name": "Pat"}""" 911 | (Right { name: (Just "Pat"), title: Nothing }) 912 | 913 | > readJSON' """{"name": "Pat", "title": "Dr."}""" 914 | (Right { name: (Just "Pat"), title: (Just "Dr.") }) 915 | > decode' =<< Data.Argonaut.Parser.jsonParser """{}""" 916 | (Left "JSON was missing expected field: title") 917 | 918 | > writeJSON' {name: Data.Maybe.Nothing, title: Data.Maybe.Nothing} 919 | "{}" 920 | 921 | > writeJSON' {name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful"} 922 | "{\"title\":\"wonderful\"}" 923 | 924 | > writeJSON' {name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing} 925 | "{\"name\":\"Pat\"}" 926 | 927 | > writeJSON' {name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr."} 928 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 929 | ``` 930 | 931 | ## How To: Decode and Encode JSON with required and optional values in `purescript-argonaut` 932 | 933 | Another common pattern with JSON objects is that some keys always have to be present while others do not. 934 | Some APIs make the distinction between a JSON object like `{ "name": "Pat" }` and one like `{ "name": "Pat", "title": null }`. 935 | In the first case, it might recognize that the `"title"` key does not exist, and behave in a different way from the `"title"` key having a value of `null`. 936 | In the second case, it might notice that the `"title"` key exists and work with the value assuming it's good to go; the `null` might eventually cause a failure later. 937 | 938 | In many cases, what we want is to not generate any fields that do not exist. 939 | Using `purescript-argonaut`, `Option.Record _ _` can help with that idea: 940 | 941 | ```PureScript 942 | import Prelude 943 | import Data.Argonaut.Core as Data.Argonaut.Core 944 | import Data.Argonaut.Decode.Class as Data.Argonaut.Decode.Class 945 | import Data.Argonaut.Decode.Error as Data.Argonaut.Decode.Error 946 | import Data.Argonaut.Encode.Class as Data.Argonaut.Encode.Class 947 | import Data.Argonaut.Parser as Data.Argonaut.Parser 948 | import Data.Either as Data.Either 949 | import Option as Option 950 | 951 | decode :: 952 | Data.Argonaut.Core.Json -> 953 | Data.Either.Either String (Option.Record ( name :: String ) ( title :: String )) 954 | decode = Data.Argonaut.Decode.Class.decodeJson 955 | 956 | encode :: 957 | Option.Record ( name :: String ) ( title :: String ) -> 958 | Data.Argonaut.Core.Json 959 | encode = Data.Argonaut.Encode.Class.encodeJson 960 | 961 | parse :: 962 | String -> 963 | Data.Either.Either String (Option.Record ( name :: String ) ( title :: String )) 964 | parse string = case Data.Argonaut.Parser.jsonParser string of 965 | Data.Either.Left error -> Data.Either.Left error 966 | Data.Either.Right json -> case decode json of 967 | Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error) 968 | Data.Either.Right record -> Data.Either.Right record 969 | ``` 970 | 971 | We can give that a spin with some different JSON values: 972 | 973 | ```PureScript 974 | > parse """{}""" 975 | (Left "An error occurred while decoding a JSON value:\n At object key 'name':\n No value was found.") 976 | 977 | > parse """{"title": "wonderful"}""" 978 | (Left "An error occurred while decoding a JSON value:\n At object key 'name':\n No value was found.") 979 | 980 | > parse """{"name": "Pat"}""" 981 | (Right (Option.recordFromRecord { name: "Pat" })) 982 | 983 | > parse """{"name": "Pat", "title": "Dr."}""" 984 | (Right (Option.recordFromRecord { name: "Pat", title: "Dr." })) 985 | 986 | > parse """{ "name": "Pat", "title": null }""" 987 | Right (Option.recordFromRecord { name: "Pat" }) 988 | ``` 989 | 990 | We can also produce some different JSON values: 991 | 992 | ```PureScript 993 | > Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat" })) 994 | "{\"name\":\"Pat\"}" 995 | 996 | > Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat", title: "Dr." })) 997 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 998 | ``` 999 | 1000 | Notice that we don't end up with a `"title"` field in the JSON output unless we have a `title` field in our record. 1001 | 1002 | It might be instructive to compare how we might write a similar functions using a `Record _` instead of `Option.Record _ _`: 1003 | With `purescript-argonaut`, the instances for decoding and encoding on records expect the field to always exist no matter its value. 1004 | If we attempt to go directly to `Record ( name :: String, title :: Data.Maybe.Maybe String )`: 1005 | 1006 | ```PureScript 1007 | import Data.Argonaut.Core as Data.Argonaut.Core 1008 | import Data.Argonaut.Decode.Class as Data.Argonaut.Decode.Class 1009 | import Data.Argonaut.Decode.Error as Data.Argonaut.Decode.Error 1010 | import Data.Argonaut.Encode.Class as Data.Argonaut.Encode.Class 1011 | import Data.Argonaut.Parser as Data.Argonaut.Parser 1012 | import Data.Either as Data.Either 1013 | import Data.Maybe as Data.Maybe 1014 | 1015 | decode' :: 1016 | Data.Argonaut.Core.Json -> 1017 | Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1018 | decode' = Data.Argonaut.Decode.Class.decodeJson 1019 | 1020 | encode' :: 1021 | Record ( name :: String, title :: Data.Maybe.Maybe String ) -> 1022 | Data.Argonaut.Core.Json 1023 | encode' = Data.Argonaut.Encode.Class.encodeJson 1024 | 1025 | parse' :: 1026 | String -> 1027 | Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1028 | parse' string = case Data.Argonaut.Parser.jsonParser string of 1029 | Data.Either.Left error -> Data.Either.Left error 1030 | Data.Either.Right json -> case decode json of 1031 | Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error) 1032 | Data.Either.Right record -> Data.Either.Right record 1033 | ``` 1034 | 1035 | We won't get the behavior we expect: 1036 | 1037 | ```PureScript 1038 | > parse' """{}""" 1039 | (Left "An error occurred while decoding a JSON value:\n At object key 'title':\n No value was found.") 1040 | 1041 | > parse' """{"title": "wonderful"}""" 1042 | (Left "An error occurred while decoding a JSON value:\n At object key 'name':\n No value was found.") 1043 | 1044 | > parse' """{"name": "Pat"}""" 1045 | (Left "An error occurred while decoding a JSON value:\n At object key 'title':\n No value was found.") 1046 | 1047 | > parse' """{"name": "Pat", "title": "Dr."}""" 1048 | (Right { name: "Pat", title: (Just "Dr.") }) 1049 | 1050 | > Data.Argonaut.Core.stringify (encode' { name: "Pat", title: Data.Maybe.Nothing }) 1051 | "{\"title\":null,\"name\":\"Pat\"}" 1052 | 1053 | > Data.Argonaut.Core.stringify (encode' { name: "Pat", title: Data.Maybe.Just "Dr." }) 1054 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 1055 | ``` 1056 | 1057 | Unless both fields exist, we cannot decode the JSON object. 1058 | Similarly, no matter what the values are, we always encode them into a JSON object. 1059 | 1060 | In order to emulate the behavior of an optional field, we have to name the record, and write our own instances: 1061 | 1062 | ```PureScript 1063 | import Prelude 1064 | import Data.Argonaut.Core as Data.Argonaut.Core 1065 | import Data.Argonaut.Decode.Class as Data.Argonaut.Decode.Class 1066 | import Data.Argonaut.Decode.Combinators as Data.Argonaut.Decode.Combinators 1067 | import Data.Argonaut.Decode.Error as Data.Argonaut.Decode.Error 1068 | import Data.Argonaut.Encode.Class as Data.Argonaut.Encode.Class 1069 | import Data.Argonaut.Encode.Combinators as Data.Argonaut.Encode.Combinators 1070 | import Data.Argonaut.Parser as Data.Argonaut.Parser 1071 | import Data.Either as Data.Either 1072 | import Data.Generic.Rep as Data.Generic.Rep 1073 | import Data.Generic.Rep.Show as Data.Generic.Rep.Show 1074 | import Data.Maybe as Data.Maybe 1075 | 1076 | newtype Greeting 1077 | = Greeting 1078 | ( Record 1079 | ( name :: String 1080 | , title :: Data.Maybe.Maybe String 1081 | ) 1082 | ) 1083 | 1084 | derive instance genericGreeting :: Data.Generic.Rep.Generic Greeting _ 1085 | 1086 | instance showGreeting :: Show Greeting where 1087 | show = Data.Generic.Rep.Show.genericShow 1088 | 1089 | instance decodeJsonGreeting :: Data.Argonaut.Decode.Class.DecodeJson Greeting where 1090 | decodeJson json = do 1091 | object <- Data.Argonaut.Decode.Class.decodeJson json 1092 | name <- Data.Argonaut.Decode.Combinators.getField object "name" 1093 | title <- Data.Argonaut.Decode.Combinators.getFieldOptional object "title" 1094 | pure (Greeting { name, title }) 1095 | 1096 | instance encodeJsonGreeting :: Data.Argonaut.Encode.Class.EncodeJson Greeting where 1097 | encodeJson (Greeting { name, title }) = 1098 | Data.Argonaut.Encode.Combinators.extend 1099 | (Data.Argonaut.Encode.Combinators.assoc "name" name) 1100 | ( Data.Argonaut.Encode.Combinators.extendOptional 1101 | (Data.Argonaut.Encode.Combinators.assocOptional "title" title) 1102 | (Data.Argonaut.Core.jsonEmptyObject) 1103 | ) 1104 | 1105 | parse'' :: 1106 | String -> 1107 | Data.Either.Either String Greeting 1108 | parse'' string = case Data.Argonaut.Parser.jsonParser string of 1109 | Data.Either.Left error -> Data.Either.Left error 1110 | Data.Either.Right json -> case decode json of 1111 | Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error) 1112 | Data.Either.Right greeting -> Data.Either.Right greeting 1113 | ``` 1114 | 1115 | If we try decoding and encoding now, we get something closer to what we wanted: 1116 | 1117 | ```PureScript 1118 | > parse'' """{}""" 1119 | (Left "An error occurred while decoding a JSON value:\n At object key 'name':\n No value was found.") 1120 | 1121 | > parse'' """{"title": "wonderful"}""" 1122 | (Left "An error occurred while decoding a JSON value:\n At object key 'name':\n No value was found.") 1123 | 1124 | > parse'' """{"name": "Pat"}""" 1125 | (Right (Greeting { name: "Pat", title: Nothing })) 1126 | 1127 | > parse'' """{"name": "Pat", "title": "Dr."}""" 1128 | (Right (Greeting { name: "Pat", title: (Just "Dr.") })) 1129 | 1130 | > Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: "Pat", title: Data.Maybe.Nothing })) 1131 | "{\"name\":\"Pat\"}" 1132 | 1133 | > Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: "Pat", title: Data.Maybe.Just "Dr." })) 1134 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 1135 | ``` 1136 | 1137 | ## How To: Decode and Encode JSON with required and optional values in `purescript-codec-argonaut` 1138 | 1139 | Another common pattern with JSON objects is that some keys always have to be present while others do not. 1140 | Some APIs make the distinction between a JSON object like `{ "name": "Pat" }` and one like `{ "name": "Pat", "title": null }`. 1141 | In the first case, it might recognize that the `"title"` key does not exist, and behave in a different way from the `"title"` key having a value of `null`. 1142 | In the second case, it might notice that the `"title"` key exists and work with the value assuming it's good to go; the `null` might eventually cause a failure later. 1143 | 1144 | In many cases, what we want is to not generate any fields that do not exist. 1145 | Using `purescript-codec-argonaut`, `Option.Record _ _` can help with that idea: 1146 | 1147 | ```PureScript 1148 | import Data.Codec.Argonaut as Data.Codec.Argonaut 1149 | import Option as Option 1150 | 1151 | jsonCodec :: Data.Codec.Argonaut.JsonCodec (Option.Record ( name :: String ) ( title :: String )) 1152 | jsonCodec = 1153 | Option.jsonCodecRecord 1154 | "Greeting" 1155 | { name: Data.Codec.Argonaut.string 1156 | , title: Data.Codec.Argonaut.string 1157 | } 1158 | ``` 1159 | 1160 | We can add a couple of helpers to make decoding/encoding easier in the REPL: 1161 | 1162 | ```PureScript 1163 | import Prelude 1164 | import Data.Argonaut.Core as Data.Argonaut.Core 1165 | import Data.Argonaut.Parser as Data.Argonaut.Parser 1166 | import Data.Codec.Argonaut as Data.Codec.Argonaut 1167 | import Data.Either as Data.Either 1168 | import Option as Option 1169 | 1170 | decode :: 1171 | Data.Argonaut.Core.Json -> 1172 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Option.Record ( name :: String ) ( title :: String )) 1173 | decode = Data.Codec.Argonaut.decode jsonCodec 1174 | 1175 | encode :: 1176 | Option.Record ( name :: String ) ( title :: String ) -> 1177 | Data.Argonaut.Core.Json 1178 | encode = Data.Codec.Argonaut.encode jsonCodec 1179 | 1180 | parse :: 1181 | String -> 1182 | Data.Either.Either String (Option.Record ( name :: String ) ( title :: String )) 1183 | parse string = do 1184 | json <- Data.Argonaut.Parser.jsonParser string 1185 | case decode json of 1186 | Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err) 1187 | Data.Either.Right record -> Data.Either.Right record 1188 | ``` 1189 | 1190 | We can give that a spin with some different JSON values: 1191 | 1192 | ```PureScript 1193 | > parse """{}""" 1194 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key name:\n No value was found.") 1195 | 1196 | > parse """{"title": "wonderful"}""" 1197 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key name:\n No value was found.") 1198 | 1199 | > parse """{"name": "Pat"}""" 1200 | (Right (Option.recordFromRecord { name: "Pat" })) 1201 | 1202 | > parse """{"name": "Pat", "title": "Dr."}""" 1203 | (Right (Option.recordFromRecord { name: "Pat", title: "Dr." })) 1204 | 1205 | > parse """{ "name": "Pat", "title": null }""" 1206 | Right (Option.recordFromRecord { name: "Pat" }) 1207 | ``` 1208 | 1209 | Notice that we have to supply a `"name"` field in the JSON input otherwise it will not parse. 1210 | 1211 | We can also produce some different JSON values: 1212 | 1213 | ```PureScript 1214 | > Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat" })) 1215 | "{\"name\":\"Pat\"}" 1216 | 1217 | > Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat", title: "Dr." })) 1218 | "{\"name\":\"Pat\",\"title\":\"Dr.\"}" 1219 | ``` 1220 | 1221 | Notice that we don't end up with a `"title"` field in the JSON output unless we have a `title` field in our record. 1222 | 1223 | It might be instructive to compare how we might write a similar functions using a `Record _` instead of `Option.Record _`: 1224 | With `purescript-codec-argonaut`, there are a couple of codecs that ship with the package for records: `Data.Codec.Argonaut.recordProp` and `Data.Codec.Argonaut.Record.record`. 1225 | Each of those codecs expect the field to always exist no matter its value. 1226 | The difference between those codecs is not very relevant except to say that the latter requires less characters to use. 1227 | There are also a couple of codecs that ship with the package for `Data.Maybe.Maybe _`: `Data.Codec.Argonaut.Common.maybe` and `Data.Codec.Argonaut.Compat.maybe`. 1228 | The former decodes/encodes with tagged values, the latter with `null`s. 1229 | If we attempt to go directly to `Record ( name :: String, title :: Data.Maybe.Maybe String )` and `Data.Codec.Argonaut.Common.maybe`: 1230 | 1231 | ```PureScript 1232 | import Prelude 1233 | import Data.Argonaut.Core as Data.Argonaut.Core 1234 | import Data.Argonaut.Parser as Data.Argonaut.Parser 1235 | import Data.Codec.Argonaut as Data.Codec.Argonaut 1236 | import Data.Codec.Argonaut.Common as Data.Codec.Argonaut.Common 1237 | import Data.Codec.Argonaut.Compat as Data.Codec.Argonaut.Compat 1238 | import Data.Codec.Argonaut.Record as Data.Codec.Argonaut.Record 1239 | import Data.Either as Data.Either 1240 | import Data.Maybe as Data.Maybe 1241 | 1242 | decode' :: 1243 | Data.Argonaut.Core.Json -> 1244 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1245 | decode' = Data.Codec.Argonaut.decode jsonCodec' 1246 | 1247 | encode' :: 1248 | Record ( name :: String, title :: Data.Maybe.Maybe String ) -> 1249 | Data.Argonaut.Core.Json 1250 | encode' = Data.Codec.Argonaut.encode jsonCodec' 1251 | 1252 | jsonCodec' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1253 | jsonCodec' = 1254 | Data.Codec.Argonaut.Record.object 1255 | "Greeting" 1256 | { name: Data.Codec.Argonaut.string 1257 | , title: Data.Codec.Argonaut.Common.maybe Data.Codec.Argonaut.string 1258 | } 1259 | 1260 | parse' :: 1261 | String -> 1262 | Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1263 | parse' string = do 1264 | json <- Data.Argonaut.Parser.jsonParser string 1265 | case decode' json of 1266 | Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err) 1267 | Data.Either.Right option -> Data.Either.Right option 1268 | ``` 1269 | 1270 | We won't get the behavior we expect: 1271 | 1272 | ```PureScript 1273 | > parse' """{}""" 1274 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n No value was found.") 1275 | 1276 | > parse' """{"title": "wonderful"}""" 1277 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n Under 'Maybe':\n Expected value of type 'Object'.") 1278 | 1279 | > parse' """{"name": "Pat"}""" 1280 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n No value was found.") 1281 | 1282 | > parse' """{"name": "Pat", "title": "Dr."}""" 1283 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n Under 'Maybe':\n Expected value of type 'Object'.") 1284 | 1285 | > parse' """{"name": "Pat", "title": {"tag": "Just", "value": "Dr."}}""" 1286 | (Right { name: "Pat", title: (Just "Dr.") }) 1287 | 1288 | > Data.Argonaut.Core.stringify (encode' { name: "Pat", title: Data.Maybe.Nothing }) 1289 | "{\"name\":\"Pat\",\"title\":{\"tag\":\"Nothing\"}}" 1290 | 1291 | > Data.Argonaut.Core.stringify (encode' { name: "Pat", title: Data.Maybe.Just "Dr." }) 1292 | "{\"name\":\"Pat\",\"title\":{\"tag\":\"Just\",\"value\":\"Dr.\"}}" 1293 | ``` 1294 | 1295 | Unless both fields exist, we cannot decode the JSON object. 1296 | Not only is every field required, they're serialized as tagged values. 1297 | Similarly, no matter what the optional values are, we always encode them into a JSON object. 1298 | 1299 | If we try with `Data.Codec.Argonaut.Compat.maybe`: 1300 | 1301 | ```PureScript 1302 | decode'' :: 1303 | Data.Argonaut.Core.Json -> 1304 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1305 | decode'' = Data.Codec.Argonaut.decode jsonCodec'' 1306 | 1307 | encode'' :: 1308 | Record ( name :: String, title :: Data.Maybe.Maybe String ) -> 1309 | Data.Argonaut.Core.Json 1310 | encode'' = Data.Codec.Argonaut.encode jsonCodec'' 1311 | 1312 | jsonCodec'' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1313 | jsonCodec'' = 1314 | Data.Codec.Argonaut.Record.object 1315 | "Greeting" 1316 | { name: Data.Codec.Argonaut.string 1317 | , title: Data.Codec.Argonaut.Compat.maybe Data.Codec.Argonaut.string 1318 | } 1319 | 1320 | parse'' :: 1321 | String -> 1322 | Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1323 | parse'' string = do 1324 | json <- Data.Argonaut.Parser.jsonParser string 1325 | case decode'' json of 1326 | Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err) 1327 | Data.Either.Right option -> Data.Either.Right option 1328 | ``` 1329 | 1330 | We also don't get the behavior we expect: 1331 | 1332 | ```PureScript 1333 | > parse'' """{}""" 1334 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n No value was found.") 1335 | 1336 | > parse'' """{"title": "wonderful"}""" 1337 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key name:\n No value was found.") 1338 | 1339 | > parse'' """{"name": "Pat"}""" 1340 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key title:\n No value was found.") 1341 | 1342 | > parse'' """{"name": "Pat", "title": "Dr."}""" 1343 | (Right { name: "Pat", title: (Just "Dr.") }) 1344 | 1345 | > Data.Argonaut.Core.stringify (encode'' { name: "Pat", title: Data.Maybe.Nothing }) 1346 | "{\"name\":\"Pat\",\"title\":null}" 1347 | 1348 | > Data.Argonaut.Core.stringify (encode'' { name: "Pat", title: Data.Maybe.Just "Dr." }) 1349 | "{\"name\":\"Pat\",\"title\":\"Dr.\"}" 1350 | ``` 1351 | 1352 | Unless both fields exist, we cannot decode the JSON object. 1353 | Similarly, no matter what the optional values are, we always encode them into a JSON object. 1354 | 1355 | In order to emulate the behavior of an optional field, we have to use a different codec: 1356 | 1357 | ```PureScript 1358 | import Prelude 1359 | import Control.Monad.Reader.Trans as Control.Monad.Reader.Trans 1360 | import Control.Monad.Writer as Control.Monad.Writer 1361 | import Control.Monad.Writer.Class as Control.Monad.Writer.Class 1362 | import Data.Codec as Data.Codec 1363 | import Data.List as Data.List 1364 | import Data.Profunctor.Star as Data.Profunctor.Star 1365 | import Data.Tuple as Data.Tuple 1366 | import Foreign.Object as Foreign.Object 1367 | import Prim.Row as Prim.Row 1368 | import Record as Record 1369 | import Type.Proxy as Type.Proxy 1370 | 1371 | optionalField :: 1372 | forall label record record' value. 1373 | Data.Symbol.IsSymbol label => 1374 | Prim.Row.Cons label (Data.Maybe.Maybe value) record' record => 1375 | Prim.Row.Lacks label record' => 1376 | Type.Proxy.Proxy label -> 1377 | Data.Codec.Argonaut.JsonCodec value -> 1378 | Data.Codec.Argonaut.JPropCodec (Record record') -> 1379 | Data.Codec.Argonaut.JPropCodec (Record record) 1380 | optionalField label codecValue codecRecord = 1381 | Data.Codec.GCodec 1382 | (Control.Monad.Reader.Trans.ReaderT decodeField) 1383 | (Data.Profunctor.Star.Star encodeField) 1384 | where 1385 | decodeField :: 1386 | Foreign.Object.Object Data.Argonaut.Core.Json -> 1387 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record record) 1388 | decodeField object' = do 1389 | record <- Data.Codec.Argonaut.decode codecRecord object' 1390 | case Foreign.Object.lookup key object' of 1391 | Data.Maybe.Just json -> case Data.Codec.Argonaut.decode codecValue json of 1392 | Data.Either.Left error -> Data.Either.Left (Data.Codec.Argonaut.AtKey key error) 1393 | Data.Either.Right value -> Data.Either.Right (Record.insert label (Data.Maybe.Just value) record) 1394 | Data.Maybe.Nothing -> Data.Either.Right (Record.insert label Data.Maybe.Nothing record) 1395 | 1396 | encodeField :: 1397 | Record record -> 1398 | Control.Monad.Writer.Writer (Data.List.List (Data.Tuple.Tuple String Data.Argonaut.Core.Json)) (Record record) 1399 | encodeField record = do 1400 | case Record.get label record of 1401 | Data.Maybe.Just value -> 1402 | Control.Monad.Writer.Class.tell 1403 | ( Data.List.Cons 1404 | (Data.Tuple.Tuple key (Data.Codec.Argonaut.encode codecValue value)) 1405 | Data.List.Nil 1406 | ) 1407 | Data.Maybe.Nothing -> pure unit 1408 | Control.Monad.Writer.Class.tell 1409 | (Data.Codec.Argonaut.encode codecRecord (Record.delete label record)) 1410 | pure record 1411 | 1412 | key :: String 1413 | key = Data.Symbol.reflectSymbol label 1414 | ``` 1415 | 1416 | With this codec defined, we can implement a codec for the record with required and optional fields: 1417 | 1418 | ```PureScript 1419 | decode''' :: 1420 | Data.Argonaut.Core.Json -> 1421 | Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1422 | decode''' = Data.Codec.Argonaut.decode jsonCodec''' 1423 | 1424 | encode''' :: 1425 | Record ( name :: String, title :: Data.Maybe.Maybe String ) -> 1426 | Data.Argonaut.Core.Json 1427 | encode''' = Data.Codec.Argonaut.encode jsonCodec''' 1428 | 1429 | jsonCodec''' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1430 | jsonCodec''' = 1431 | Data.Codec.Argonaut.object 1432 | "Greeting" 1433 | ( Data.Codec.Argonaut.recordProp (Type.Proxy.Proxy :: _ "name") Data.Codec.Argonaut.string 1434 | $ optionalField (Type.Proxy.Proxy :: _ "title") Data.Codec.Argonaut.string 1435 | $ Data.Codec.Argonaut.record 1436 | ) 1437 | 1438 | parse''' :: 1439 | String -> 1440 | Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1441 | parse''' string = do 1442 | json <- Data.Argonaut.Parser.jsonParser string 1443 | case decode''' json of 1444 | Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err) 1445 | Data.Either.Right option -> Data.Either.Right option 1446 | ``` 1447 | 1448 | If we try decoding and encoding now, we get something closer to what we wanted: 1449 | 1450 | ```PureScript 1451 | > parse''' """{}""" 1452 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key name:\n No value was found.") 1453 | 1454 | > parse''' """{"title": "wonderful"}""" 1455 | (Left "An error occurred while decoding a JSON value:\n Under 'Greeting':\n At object key name:\n No value was found.") 1456 | 1457 | > parse''' """{"name": "Pat"}""" 1458 | (Right { name: "Pat", title: Nothing }) 1459 | 1460 | > parse''' """{"name": "Pat", "title": "wonderful"}""" 1461 | (Right { name: "Pat", title: (Just "wonderful") }) 1462 | 1463 | > Data.Argonaut.Core.stringify (encode''' { name: "Pat", title: Data.Maybe.Nothing }) 1464 | "{\"name\":\"Pat\"}" 1465 | 1466 | > Data.Argonaut.Core.stringify (encode''' { name: "Pat", title: Data.Maybe.Just "Dr." }) 1467 | "{\"name\":\"Pat\",\"title\":\"Dr.\"}" 1468 | ``` 1469 | 1470 | ## How To: Decode and Encode JSON with required and optional values in `purescript-simple-json` 1471 | 1472 | Another common pattern with JSON objects is that some keys always have to be present while others do not. 1473 | Some APIs make the distinction between a JSON object like `{ "name": "Pat" }` and one like `{ "name": "Pat", "title": null }`. 1474 | In the first case, it might recognize that the `"title"` key does not exist, and behave in a different way from the `"title"` key having a value of `null`. 1475 | In the second case, it might notice that the `"title"` key exists and work with the value assuming it's good to go; the `null` might eventually cause a failure later. 1476 | 1477 | In many cases, what we want is to not generate any fields that do not exist. 1478 | Using `purescript-simple-json`, `Option.Record _ _` can help with that idea: 1479 | 1480 | ```PureScript 1481 | import Prelude 1482 | import Data.Either as Data.Either 1483 | import Data.Semigroup.Foldable as Data.Semigroup.Foldable 1484 | import Foreign as Foreign 1485 | import Option as Option 1486 | import Simple.JSON as Simple.JSON 1487 | 1488 | parse :: 1489 | String -> 1490 | Data.Either.Either String (Option.Record ( name :: String ) ( title :: String )) 1491 | parse string = case readJSON string of 1492 | Data.Either.Left errors -> Data.Either.Left (Data.Semigroup.Foldable.intercalateMap " " Foreign.renderForeignError errors) 1493 | Data.Either.Right record -> Data.Either.Right record 1494 | 1495 | readJSON :: 1496 | String -> 1497 | Simple.JSON.E (Option.Record ( name :: String ) ( title :: String )) 1498 | readJSON = Simple.JSON.readJSON 1499 | 1500 | writeJSON :: 1501 | Option.Record ( name :: String ) ( title :: String ) -> 1502 | String 1503 | writeJSON = Simple.JSON.writeJSON 1504 | ``` 1505 | 1506 | We can give that a spin with some different JSON values: 1507 | 1508 | ```PureScript 1509 | > parse """{}""" 1510 | (Left "Error at property \"name\": Type mismatch: expected String, found Undefined") 1511 | 1512 | > parse """{"title": "wonderful"}""" 1513 | (Left "Error at property \"name\": Type mismatch: expected String, found Undefined") 1514 | 1515 | > parse """{"name": "Pat"}""" 1516 | (Right (Option.recordFromRecord { name: "Pat" })) 1517 | 1518 | > parse """{"name": "Pat", "title": "Dr."}""" 1519 | (Right (Option.recordFromRecord { name: "Pat", title: "Dr." })) 1520 | 1521 | > parse """{ "name": "Pat", "title": null }""" 1522 | Right (Option.recordFromRecord { name: "Pat" }) 1523 | ``` 1524 | 1525 | We can also produce some different JSON values: 1526 | 1527 | ```PureScript 1528 | > writeJSON (Option.recordFromRecord {name: "Pat"}) 1529 | "{\"name\":\"Pat\"}" 1530 | 1531 | > writeJSON (Option.recordFromRecord {name: "Pat", title: "Dr."}) 1532 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 1533 | ``` 1534 | 1535 | Notice that we don't end up with a `"title"` field in the JSON output unless we have a `title` field in our record. 1536 | 1537 | It might be instructive to compare how we might write a similar functions using a `Record _` instead of `Option.Record _ _`: 1538 | With `purescript-simple-json`, the instances for decoding and encoding on records handle `Data.Maybe.Maybe _` values like they are optional. 1539 | If we attempt to go directly to `Record ( name :: String, title :: Data.Maybe.Maybe String )`: 1540 | 1541 | ```PureScript 1542 | import Prelude 1543 | import Data.Either as Data.Either 1544 | import Data.Maybe as Data.Maybe 1545 | import Data.Semigroup.Foldable as Data.Semigroup.Foldable 1546 | import Foreign as Foreign 1547 | import Simple.JSON as Simple.JSON 1548 | 1549 | parse' :: 1550 | String -> 1551 | Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1552 | parse' string = case readJSON string of 1553 | Data.Either.Left errors -> Data.Either.Left (Data.Semigroup.Foldable.intercalateMap " " Foreign.renderForeignError errors) 1554 | Data.Either.Right record -> Data.Either.Right record 1555 | 1556 | readJSON' :: 1557 | String -> 1558 | Simple.JSON.E (Record ( name :: String, title :: Data.Maybe.Maybe String )) 1559 | readJSON' = Simple.JSON.readJSON 1560 | 1561 | writeJSON' :: 1562 | Record ( name :: String, title :: Data.Maybe.Maybe String ) -> 1563 | String 1564 | writeJSON' = Simple.JSON.writeJSON 1565 | ``` 1566 | 1567 | We get the behavior we expect: 1568 | 1569 | ```PureScript 1570 | > parse' """{}""" 1571 | (Left "Error at property \"name\": Type mismatch: expected String, found Undefined") 1572 | 1573 | > parse' """{"title": "wonderful"}""" 1574 | (Left "Error at property \"name\": Type mismatch: expected String, found Undefined") 1575 | 1576 | > parse' """{"name": "Pat"}""" 1577 | (Right { name: "Pat", title: Nothing }) 1578 | 1579 | > parse' """{"name": "Pat", "title": "Dr."}""" 1580 | (Right { name: "Pat", title: (Just "Dr.") }) 1581 | 1582 | > writeJSON' {name: "Pat", title: Data.Maybe.Nothing} 1583 | "{\"name\":\"Pat\"}" 1584 | 1585 | > writeJSON' {name: "Pat", title: Data.Maybe.Just "Dr."} 1586 | "{\"title\":\"Dr.\",\"name\":\"Pat\"}" 1587 | ``` 1588 | 1589 | ## How To: Provide an easier API for `DateTime` 1590 | 1591 | The API for `Data.DateTime` is pretty nice because it means we cannot construct invalid dates. 1592 | What's not so nice about it is that it pushes all of the correctness onto us. 1593 | It might be a little easier to use if the API would allow optional values to be passed in and default to something sensible. 1594 | For instance, constructing a `Data.DateTime.DateTime` can be done by passing in both a `Data.Date.Date` and a `Data.Time.Time`: 1595 | The issue is, how do we construct a `Data.Date.Date` or `Data.Time.Time`. 1596 | 1597 | One way to get construct these values is to use the `Data.Enum.Enum` instance for both of them: 1598 | 1599 | ```PureScript 1600 | > Data.DateTime.DateTime bottom bottom 1601 | (DateTime (Date (Year -271820) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0))) 1602 | ``` 1603 | 1604 | This gives us a value, but some of its parts might not be what we really want; e.g. the year is `-271820`. 1605 | 1606 | If we wanted to alter the year, we have to use `Data.Enum.toEnum` to construct a different year, then `Data.DateTime.modifyDate`, and `Data.Date.canonicalDate` to thread the year through: 1607 | 1608 | ```PureScript 1609 | > Data.DateTime.modifyDate (\date -> Data.Date.canonicalDate (Data.Maybe.fromMaybe bottom (Data.Enum.toEnum 2019)) (Data.Date.month date) (Data.Date.day date)) (Data.DateTime.DateTime bottom bottom) 1610 | (DateTime (Date (Year 2019) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0))) 1611 | ``` 1612 | 1613 | Or create it with the correct year from the get-go: 1614 | 1615 | ```PureScript 1616 | > Data.DateTime.DateTime (Data.Date.canonicalDate (Data.Maybe.fromMaybe bottom (Data.Enum.toEnum 2019)) bottom bottom) bottom 1617 | (DateTime (Date (Year 2019) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0))) 1618 | ``` 1619 | 1620 | That's a non-trivial amount of work in order to alter the year. 1621 | We can clean it up with a named function that takes in an `Int` for the year and does all the boilerplate (using `Data.Enum.toEnumWithDefaults` to handle bounds a bit better): 1622 | 1623 | ```PureScript 1624 | dateTimeFromYear :: Int -> Data.DateTime.DateTime 1625 | dateTimeFromYear year = 1626 | Data.DateTime.DateTime 1627 | ( Data.Date.canonicalDate 1628 | (Data.Enum.toEnumWithDefaults bottom top year) 1629 | bottom 1630 | bottom 1631 | ) 1632 | bottom 1633 | ``` 1634 | 1635 | This works decently for the year alone. 1636 | 1637 | ```PureScript 1638 | > dateTimeFromYear 2019 1639 | (DateTime (Date (Year 2019) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0))) 1640 | ``` 1641 | 1642 | Once we decide we are okay with the year, but want to alter the day instead, or that we want to alter both at the same time it becomes just as hard as if we hadn't done anything. 1643 | We either need to implement something similar for each part or change the arguments to `Data.Maybe.Maybe Int`s. 1644 | 1645 | An alternative is to use an option for each part of the `Data.DateTime.DateTime`: 1646 | 1647 | ```PureScript 1648 | type Option 1649 | = ( day :: Int 1650 | , hour :: Int 1651 | , millisecond :: Int 1652 | , minute :: Int 1653 | , month :: Data.Date.Component.Month 1654 | , second :: Int 1655 | , year :: Int 1656 | ) 1657 | ``` 1658 | 1659 | Then we can build a `Data.DateTime.DateTime` from whatever happens to be passed in: 1660 | 1661 | ```PureScript 1662 | dateTime :: 1663 | forall record. 1664 | Option.FromRecord record Option => 1665 | Record record -> 1666 | Data.DateTime.DateTime 1667 | dateTime record = Data.DateTime.DateTime date time 1668 | where 1669 | date :: Data.Date.Date 1670 | date = Data.Date.canonicalDate year month day 1671 | where 1672 | day :: Data.Date.Component.Day 1673 | day = get (Type.Proxy.Proxy :: _ "day") 1674 | 1675 | month :: Data.Date.Component.Month 1676 | month = Option.getWithDefault bottom (Type.Proxy.Proxy :: _ "month") options 1677 | 1678 | year :: Data.Date.Component.Year 1679 | year = get (Type.Proxy.Proxy :: _ "year") 1680 | 1681 | get :: 1682 | forall label proxy record' value. 1683 | Data.Enum.BoundedEnum value => 1684 | Data.Symbol.IsSymbol label => 1685 | Prim.Row.Cons label Int record' Option => 1686 | proxy label -> 1687 | value 1688 | get proxy = case Option.get proxy options of 1689 | Data.Maybe.Just x -> Data.Enum.toEnumWithDefaults bottom top x 1690 | Data.Maybe.Nothing -> bottom 1691 | 1692 | options :: Option.Option Option 1693 | options = Option.fromRecord record 1694 | 1695 | time :: Data.Time.Time 1696 | time = Data.Time.Time hour minute second millisecond 1697 | where 1698 | hour :: Data.Time.Component.Hour 1699 | hour = get (Type.Proxy.Proxy :: _ "hour") 1700 | 1701 | minute :: Data.Time.Component.Minute 1702 | minute = get (Type.Proxy.Proxy :: _ "minute") 1703 | 1704 | millisecond :: Data.Time.Component.Millisecond 1705 | millisecond = get (Type.Proxy.Proxy :: _ "millisecond") 1706 | 1707 | second :: Data.Time.Component.Second 1708 | second = get (Type.Proxy.Proxy :: _ "second") 1709 | ``` 1710 | 1711 | Now, we can construct a `Data.DateTime.DateTime` fairly easily: 1712 | 1713 | ```PureScript 1714 | > dateTime {} 1715 | (DateTime (Date (Year -271820) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0))) 1716 | ``` 1717 | 1718 | We can alter the year: 1719 | 1720 | ```PureScript 1721 | > dateTime {year: 2019} 1722 | (DateTime (Date (Year 2019) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0))) 1723 | ``` 1724 | 1725 | And, we can alter any of the components: 1726 | 1727 | ```PureScript 1728 | > dateTime {minute: 30, month: Data.Date.Component.April, year: 2019} 1729 | (DateTime (Date (Year 2019) April (Day 1)) (Time (Hour 0) (Minute 30) (Second 0) (Millisecond 0))) 1730 | ``` 1731 | 1732 | ## Reference: `FromRecord _ _ _` 1733 | 1734 | A typeclass for converting a `Record _` into an `Option _`. 1735 | 1736 | An instance `FromRecord record required optional` states that we can make a `Record required` and an `Option optional` from a `Record record` where every required field is in the record and the rest of the present fields in the record is present in the option. 1737 | E.g. `FromRecord () () ( name :: String )` says that the `Record ()` has no fields and the `Option ( name :: String )` will have no value; 1738 | `FromRecord ( name :: String ) () ( name :: String )` says that the `Record ()` has no fields and the `Option ( name :: String )` will have the given `name` value; 1739 | `FromRecord ( name :: String ) ( name :: String ) ()` says that the `Record ( name :: String )` has the given `name` value and the `Option ()` will have no value; 1740 | `FromRecord () ( name :: String) ()` is a type error since the `name` field is required but the given record lacks the field. 1741 | 1742 | Since there is syntax for creating records, but no syntax for creating options, this typeclass can be useful for providing an easier to use interface to options. 1743 | 1744 | E.g. Someone can say: 1745 | 1746 | ```PureScript 1747 | Option.fromRecord' { foo: true, bar: 31 } 1748 | ``` 1749 | 1750 | Instead of having to say: 1751 | 1752 | ```PureScript 1753 | Option.insert 1754 | (Type.Proxy.Proxy :: _ "foo") 1755 | true 1756 | ( Option.insert 1757 | (Type.Proxy.Proxy :: _ "bar") 1758 | 31 1759 | Option.empty 1760 | ) 1761 | ``` 1762 | 1763 | Not only does it save a bunch of typing, it also mitigates the need for a direct dependency on `SProxy _`. 1764 | --------------------------------------------------------------------------------