From a03888dc97ce6046c1aaf8826ea0f4740d35e9e9 Mon Sep 17 00:00:00 2001 From: Dominick Gendill Date: Sat, 20 May 2017 15:32:55 -0600 Subject: [PATCH] Documentation. Removed Foreign Code. No longer support YAML 1.1. Extra quote in YAML output fixed. --- .gitignore | 1 + README.md | 220 ++++++++++----------- bower.json | 7 +- generated-docs/Data/YAML/Foreign/Decode.md | 19 ++ generated-docs/Data/YAML/Foreign/Encode.md | 66 +++++++ src/Data/YAML/Foreign/Decode.purs | 5 +- src/Data/YAML/Foreign/Encode.js | 27 ++- src/Data/YAML/Foreign/Encode.purs | 9 +- test/Instances.purs | 55 +++--- test/Main.purs | 68 ++++--- 10 files changed, 294 insertions(+), 183 deletions(-) create mode 100644 generated-docs/Data/YAML/Foreign/Decode.md create mode 100644 generated-docs/Data/YAML/Foreign/Encode.md diff --git a/.gitignore b/.gitignore index 87be35f..e6bbe53 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /.psci* /src/.webpack.js .pulp-cache/ +.psc-ide-port diff --git a/README.md b/README.md index 2ced9b3..f2666b9 100644 --- a/README.md +++ b/README.md @@ -1,129 +1,127 @@ # purescript-yaml +## Install + +This repo depends on js-yaml, so you'll need to install js-yaml in your +project. It is not compatible with YAML 1.1. + +``` +npm install js-yaml@^3.4.6 +``` + +Then, if you're using bower, add the this repo as a project dependency, e.g. + + +``` +bower install git://github.com/dgendill/purescript-yaml#1.0.0 +``` + +Or you can manually add the github repo as a project dependency, e.g. + +``` +"dependencies": { + "purescript-yaml" : git://github.com/dgendill/purescript-yaml#tagOrBranch +} +``` + +## YAML to Data Type Usage + +Assuming we have the following Point data type and yaml... + + ```purescript data Point = Point Int Int -data Mobility - = Fix - | Flex - -data GeoObject = GeoObject - { name :: String - , scale :: Number - , points :: Array Point - , mobility :: Mobility - , coverage :: Number - } -``` - -## Decode YAML - -Write functions to read your data from foreign values. - -```purescript -readPoint :: Foreign -> F Point -readPoint value = do - x <- readInt =<< readProp "X" value - y <- readInt =<< readProp "Y" value - pure $ Point x y - -readMobility :: Foreign -> F Mobility -readMobility value = do - mob <- readString value - case mob of - "Fix" -> pure Fix - "Flex" -> pure Flex - _ -> fail $ JSONError "Mobility must be either Flex or Fix" - -readGeoObject :: Foreign -> F GeoObject -readGeoObject value = do - name <- readString =<< readProp "Name" value - scale <- readNumber =<< readProp "Scale" value - points <- traverse readPoint =<< readArray =<< readProp "Points" value - mobility <- readMobility =<< readProp "Mobility" value - coverage <- readNumber =<< readProp "Coverage" value - pure $ GeoObject { name, scale, points, mobility, coverage } - -``` - -Read the YAML into your data structures. - -```purescript -yamlInput :: String -yamlInput = """ -- Name: House - Scale: 9.5 - Points: - - X: 10 - Y: 10 - - X: 20 - Y: 10 - - X: 5 - Y: 5 - Mobility: Fix - Coverage: 10 -- Name: Tree - Scale: 1 - Points: - - X: 1 - Y: 1 - - X: 2 - Y: 2 - - X: 0 - Y: 0 - Mobility: Fix - Coverage: 10 +yamlPoint :: String +yamlPoint = """ +X: 1 +Y: 1 """ - -let decoded = - (parseYAML yamlInput) >>= - readArray >>= - traverse readGeoObject ``` -## Encode YAML +We can read a `Point` from the yaml by convertion the YAML into JSON +and then using [purescript-argonaut](https://github.com/purescript-contrib/purescript-argonaut)'s encoding functionality to get the +type we need (specifically, [purescript-argonaut-codecs](https://github.com/purescript-contrib/purescript-argonaut-codecs) +functionality). + +``` +getPoint :: Either String Point +getPoint = case runExcept $ parseYAMLToJson yamlPoint of + Left err -> Left "Could not parse yaml" + Right json -> decodeJson json + +instance pointJson :: DecodeJson Point where + decodeJson s = do + obj <- maybe (Left "Point is not an object.") Right (toObject s) + x <- getField obj "X" + y <- getField obj "Y" + pure $ Point x y +``` + + +## Data Type to YAML Usage + +YAML is represented with the following data type. + +``` +data YValue + = YObject (M.Map String YValue) + | YArray (Array YValue) + | YString String + | YNumber Number + | YInt Int + | YBoolean Boolean + | YNull +``` + +To convert data into YValue, create instances of the ToYAML class for your +data types. ```purescript +class ToYAML a where + toYAML :: a -> YValue +``` + +For example to take a `Point` to `YValue` + +```purescript +import Data.YAML.Foreign.Encode (object, entry, class ToYAML) + instance pointToYAML :: ToYAML Point where toYAML (Point x y) = object - [ "X" := x - , "Y" := y - ] - -instance mobilityToYAML :: ToYAML Mobility where - toYAML Fix = toYAML "Fix" - toYAML Flex = toYAML "Flex" - -instance archiObjectToYAML :: ToYAML GeoObject where - toYAML (GeoObject o) = - object - [ "Name" := o.name - , "Scale" := o.scale - , "Points" := o.points - , "Mobility" := o.mobility - , "Coverage" := o.coverage + [ "X" `entry` x + , "Y" `entry` y ] ``` +You can find helper functions for converting basic types into `YValue` +in the Data.YAML.Foreign.Encode module. + +Finally, if you want to convert YValue into a String, you can use the +`printYAML` function from Data.YAML.Foreign.Encode. + + ```purescript -data :: Array GeoObject -data = - [ GeoObject - { coverage: 10.0 - , mobility: Fix - , name: "House" - , points: [ Point 10 10, Point 20 10, Point 5 5 ] - , scale: 9.5 - } - , GeoObject - { coverage: 10.0 - , mobility: Fix - , name: "Tree" - , points: [ Point 1 1, Point 2 2, Point 0 0 ] - , scale: 1.0 - } - ] - -encoded = printYAML data +printYAML :: forall a. (ToYAML a) => a -> String +``` + +## Summary + +Using the previous code and the type classes we defined earlier, we can go +full circle from a YAML string to a PureScript Data Type and back to a YAML string. + +``` +fullCircle :: String -> Either String String +fullCircle yamlString = (readPoint yamlString) >>= pure <<< printYAML +``` + +## Contributing + +Check out the repo and make changes. You can run/add tests by running pulp test. + +``` +bower install +npm install +pulp test ``` diff --git a/bower.json b/bower.json index ff58e0b..bf53343 100644 --- a/bower.json +++ b/bower.json @@ -8,7 +8,9 @@ "**/.*", "node_modules", "bower_components", - "output" + "output", + "test", + "docs" ], "dependencies": { "js-yaml": "^3.4.6", @@ -20,6 +22,7 @@ }, "devDependencies": { "purescript-console": "^3.0.0", - "purescript-spec": "^1.0.0" + "purescript-spec": "^1.0.0", + "purescript-argonaut-codecs": "^3.0.1" } } diff --git a/generated-docs/Data/YAML/Foreign/Decode.md b/generated-docs/Data/YAML/Foreign/Decode.md new file mode 100644 index 0000000..4186e02 --- /dev/null +++ b/generated-docs/Data/YAML/Foreign/Decode.md @@ -0,0 +1,19 @@ +## Module Data.YAML.Foreign.Decode + +#### `parseYAMLToJson` + +``` purescript +parseYAMLToJson :: String -> F Json +``` + +Attempt to parse a YAML string, returning the result as Json + +#### `readYAMLGeneric` + +``` purescript +readYAMLGeneric :: forall a rep. Generic a rep => GenericDecode rep => Options -> String -> F a +``` + +Automatically generate a YAML parser for your data from a generic instance. + + diff --git a/generated-docs/Data/YAML/Foreign/Encode.md b/generated-docs/Data/YAML/Foreign/Encode.md new file mode 100644 index 0000000..18b6bf6 --- /dev/null +++ b/generated-docs/Data/YAML/Foreign/Encode.md @@ -0,0 +1,66 @@ +## Module Data.YAML.Foreign.Encode + +#### `YValue` + +``` purescript +data YValue +``` + +##### Instances +``` purescript +Show YValue +Eq YValue +``` + +#### `ToYAML` + +``` purescript +class ToYAML a where + toYAML :: a -> YValue +``` + +##### Instances +``` purescript +(ToYAML a) => ToYAML (StrMap a) +(ToYAML a) => ToYAML (Map String a) +ToYAML Boolean +ToYAML Int +ToYAML Number +ToYAML String +(ToYAML a) => ToYAML (Array a) +(ToYAML a) => ToYAML (Maybe a) +``` + +#### `entry` + +``` purescript +entry :: forall a. ToYAML a => String -> a -> Pair +``` + +Helper function to create a key-value tuple for a YAML object. + +`name = "Name" := "This is the name"` + +#### `(:=)` + +``` purescript +infixl 4 entry as := +``` + +#### `object` + +``` purescript +object :: Array Pair -> YValue +``` + +Helper function to create a YAML object. + +`obj = object [ "Name" := "This is the name", "Size" := 1.5 ]` + +#### `printYAML` + +``` purescript +printYAML :: forall a. ToYAML a => a -> String +``` + + diff --git a/src/Data/YAML/Foreign/Decode.purs b/src/Data/YAML/Foreign/Decode.purs index bbe6fa3..74be7e8 100644 --- a/src/Data/YAML/Foreign/Decode.purs +++ b/src/Data/YAML/Foreign/Decode.purs @@ -1,4 +1,7 @@ -module Data.YAML.Foreign.Decode (parseYAML, readYAMLGeneric, parseYAMLToJson) where +module Data.YAML.Foreign.Decode ( + readYAMLGeneric, + parseYAMLToJson + ) where import Data.Foreign (F, Foreign, ForeignError(..), fail) import Data.Foreign.Generic (genericDecode) diff --git a/src/Data/YAML/Foreign/Encode.js b/src/Data/YAML/Foreign/Encode.js index ff09d26..ff87d11 100644 --- a/src/Data/YAML/Foreign/Encode.js +++ b/src/Data/YAML/Foreign/Encode.js @@ -1,23 +1,18 @@ "use strict"; -// module Data.YAML.Foreign.Encode - var yaml = require('js-yaml'); exports.jsNull = null; -exports.objToHash = - function(valueToYAMLImpl, fst, snd, obj) - { - var hash = {}; - for(var i = 0; i < obj.length; i++) { - hash[fst(obj[i])] = valueToYAMLImpl(snd(obj[i])); - } - return hash; - }; +exports.objToHash = function(valueToYAMLImpl, fst, snd, obj) { + var hash = {}; + for(var i = 0; i < obj.length; i++) { + hash[fst(obj[i])] = valueToYAMLImpl(snd(obj[i])); + } + return hash; +}; -exports.toYAMLImpl = - function(a) - { - return yaml.safeDump(a); - } +exports.toYAMLImpl = function(a) { + // noCompatMode does not support YAML 1.1 + return yaml.safeDump(a, {noCompatMode : true}); +} diff --git a/src/Data/YAML/Foreign/Encode.purs b/src/Data/YAML/Foreign/Encode.purs index 05c652f..b00744c 100644 --- a/src/Data/YAML/Foreign/Encode.purs +++ b/src/Data/YAML/Foreign/Encode.purs @@ -1,4 +1,11 @@ -module Data.YAML.Foreign.Encode where +module Data.YAML.Foreign.Encode ( + YValue, + class ToYAML, + toYAML, + entry, (:=), + object, + printYAML + ) where import Data.Map as M import Data.Map (Map) diff --git a/test/Instances.purs b/test/Instances.purs index e40725b..e686498 100644 --- a/test/Instances.purs +++ b/test/Instances.purs @@ -1,11 +1,13 @@ module Test.Instances where -import Prelude (class Eq, class Show, bind, pure, ($), (=<<)) -import Data.Traversable (traverse) -import Data.Foreign (readArray, readNumber, readString, readInt, F, Foreign, ForeignError(..), fail) -import Data.Foreign.Index (readProp) -import Data.Generic (class Generic, gShow, gEq) import Data.YAML.Foreign.Encode +import Data.Argonaut.Core (toObject, toString) +import Data.Argonaut.Decode (getField) +import Data.Argonaut.Decode.Class (class DecodeJson) +import Data.Either (Either(..)) +import Data.Generic (class Generic, gShow, gEq) +import Data.Maybe (maybe) +import Prelude (class Eq, class Show, bind, pure, ($)) data Point = Point Int Int @@ -33,28 +35,31 @@ derive instance genericMobility :: Generic Mobility instance showMobility :: Show Mobility where show = gShow instance eqMobility :: Eq Mobility where eq = gEq -readGeoObject :: Foreign -> F GeoObject -readGeoObject value = do - name <- readString =<< readProp "Name" value - scale <- readNumber =<< readProp "Scale" value - points <- traverse readPoint =<< readArray =<< readProp "Points" value - mobility <- readMobility =<< readProp "Mobility" value - coverage <- readNumber =<< readProp "Coverage" value - pure $ GeoObject { name, scale, points, mobility, coverage } +instance geoJson :: DecodeJson GeoObject where + decodeJson s = do + obj <- maybe (Left "GeoObject is not an object.") Right (toObject s) + name <- getField obj "Name" + scale <- getField obj "Scale" + points <- getField obj "Points" + mobility <- getField obj "Mobility" + coverage <- getField obj "Coverage" + pure $ GeoObject { name, scale, points, mobility, coverage } -readPoint :: Foreign -> F Point -readPoint value = do - x <- readInt =<< readProp "X" value - y <- readInt =<< readProp "Y" value - pure $ Point x y +instance mobilityJson :: DecodeJson Mobility where + decodeJson s = do + mob <- maybe (Left "Mobility is not a string.") Right (toString s) + case mob of + "Fix" -> pure Fix + "Flex" -> pure Flex + _ -> Left "Mobility must be either Flex or Fix" + +instance pointJson :: DecodeJson Point where + decodeJson s = do + obj <- maybe (Left "Point is not an object.") Right (toObject s) + x <- getField obj "X" + y <- getField obj "Y" + pure $ Point x y -readMobility :: Foreign -> F Mobility -readMobility value = do - mob <- readString value - case mob of - "Fix" -> pure Fix - "Flex" -> pure Flex - _ -> fail $ JSONError "Mobility must be either Flex or Fix" instance pointToYAML :: ToYAML Point where toYAML (Point x y) = diff --git a/test/Main.purs b/test/Main.purs index 2edc588..9bf3c0d 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -1,23 +1,21 @@ module Test.Main where +import Data.Map as Map +import Data.StrMap as StrMap import Control.Monad.Eff (Eff) import Control.Monad.Except (runExcept) +import Data.Argonaut.Decode (class DecodeJson, decodeJson) import Data.Either (Either(..)) -import Data.Foreign (F, readArray) -import Data.YAML.Foreign.Decode (parseYAML) +import Data.Map (Map) +import Data.StrMap (StrMap) +import Data.YAML.Foreign.Decode (parseYAMLToJson) import Data.YAML.Foreign.Encode (printYAML) -import Data.Traversable (traverse) -import Prelude (Unit, bind, ($), void, discard, (>>=)) -import Test.Instances (readGeoObject, readMobility, readPoint, GeoObject(..), Mobility(..), Point(..)) +import Prelude (Unit, discard, pure, ($), (<<<), (>>=)) +import Test.Instances (GeoObject(..), Mobility(..), Point(..)) import Test.Spec (describe, it) import Test.Spec.Assertions (shouldEqual) import Test.Spec.Reporter.Console (consoleReporter) import Test.Spec.Runner (RunnerEffects, run) -import Control.Monad.Eff.Console (log, CONSOLE) -import Data.Map as Map -import Data.Map (Map) -import Data.StrMap as StrMap -import Data.StrMap (StrMap) yamlInput :: String yamlInput = """ @@ -50,22 +48,22 @@ yamlOutput :: String yamlOutput = """- Mobility: Fix Points: - X: 10 - 'Y': 10 + Y: 10 - X: 20 - 'Y': 10 + Y: 10 - X: 5 - 'Y': 5 + Y: 5 Coverage: 10 Name: House Scale: 9.5 - Mobility: Fix Points: - X: 1 - 'Y': 1 + Y: 1 - X: 2 - 'Y': 2 + Y: 2 - X: 0 - 'Y': 0 + Y: 0 Coverage: 10 Name: Tree Scale: 1 @@ -77,27 +75,37 @@ yamlMapOutput = """key: - Mobility: Fix Points: - X: 10 - 'Y': 10 + Y: 10 - X: 20 - 'Y': 10 + Y: 10 - X: 5 - 'Y': 5 + Y: 5 Coverage: 10 Name: House Scale: 9.5 - Mobility: Fix Points: - X: 1 - 'Y': 1 + Y: 1 - X: 2 - 'Y': 2 + Y: 2 - X: 0 - 'Y': 0 + Y: 0 Coverage: 10 Name: Tree Scale: 1 """ +pointYaml :: String +pointYaml = """X: 1 +Y: 1 +""" + +yamlToData :: forall a. (DecodeJson a) => String -> Either String a +yamlToData s = case runExcept $ parseYAMLToJson s of + Left err -> Left "Could not parse yaml" + Right json -> decodeJson json + testStrMap :: StrMap (Array GeoObject) testStrMap = StrMap.singleton "key" parsedData @@ -123,16 +131,19 @@ parsedData = } ] +readPoint :: String -> Either String Point +readPoint = yamlToData + +fullCircle :: String -> Either String String +fullCircle yamlString = (readPoint yamlString) >>= pure <<< printYAML + main :: Eff (RunnerEffects ()) Unit main = run [consoleReporter] do describe "purescript-yaml" do describe "decode" do it "Decodes YAML" do - let decoded = - (parseYAML yamlInput) >>= - readArray >>= - traverse readGeoObject - (runExcept decoded) `shouldEqual` (Right parsedData) + let decoded = yamlToData yamlInput + decoded `shouldEqual` (Right parsedData) describe "encode" do it "Encodes YAML" $ do let encoded = printYAML parsedData @@ -143,3 +154,6 @@ main = run [consoleReporter] do let encodedMap = printYAML testMap encodedMap `shouldEqual` yamlMapOutput + + let s = fullCircle pointYaml + s `shouldEqual` (Right pointYaml)