diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9d81fd5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: node_js +dist: trusty +sudo: required +node_js: 6 +env: + - PATH=$HOME/purescript:$PATH +install: + - TAG=$(wget -q -O - https://github.com/purescript/purescript/releases/latest --server-response --max-redirect 0 2>&1 | sed -n -e 's/.*Location:.*tag\///p') + - wget -O $HOME/purescript.tar.gz https://github.com/purescript/purescript/releases/download/$TAG/linux64.tar.gz + - tar -xvf $HOME/purescript.tar.gz -C $HOME/ + - chmod a+x $HOME/purescript + - npm install -g bower + - npm install +script: + - bower install --production + - npm run -s build + - bower install + - npm -s test diff --git a/bower.json b/bower.json index b6a2e31..25175b1 100644 --- a/bower.json +++ b/bower.json @@ -19,11 +19,11 @@ "purescript-eff": "^2.0.0", "purescript-exceptions": "^2.0.0", "purescript-foreign": "^3.0.0", - "purescript-generics": "^3.0.0", + "purescript-generics-rep": "^4.0.0", "purescript-globals": "^2.0.0", "purescript-maps": "^2.0.0", "purescript-nullable": "^2.0.0", - "purescript-symbols": "^1.0.1" + "purescript-symbols": "^2.0.0" }, "devDependencies": { "purescript-assert": "^2.0.0" diff --git a/docs/Data/Foreign/Generic.md b/docs/Data/Foreign/Generic.md index aa295c6..3cd3b5e 100644 --- a/docs/Data/Foreign/Generic.md +++ b/docs/Data/Foreign/Generic.md @@ -1,18 +1,5 @@ ## Module Data.Foreign.Generic -#### `Options` - -``` purescript -type Options = { sumEncoding :: SumEncoding, unwrapNewtypes :: Boolean, unwrapSingleArgumentConstructors :: Boolean, maybeAsNull :: Boolean, tupleAsArray :: Boolean } -``` - -#### `SumEncoding` - -``` purescript -data SumEncoding - = TaggedObject { tagFieldName :: String, contentsFieldName :: String } -``` - #### `defaultOptions` ``` purescript @@ -22,7 +9,7 @@ defaultOptions :: Options #### `readGeneric` ``` purescript -readGeneric :: forall a. Generic a => Options -> Foreign -> F a +readGeneric :: forall a rep. (Generic a rep, GenericDecode rep) => Options -> Foreign -> F a ``` Read a value which has a `Generic` type. @@ -30,7 +17,7 @@ Read a value which has a `Generic` type. #### `toForeignGeneric` ``` purescript -toForeignGeneric :: forall a. Generic a => Options -> a -> Foreign +toForeignGeneric :: forall a rep. (Generic a rep, GenericEncode rep) => Options -> a -> Foreign ``` Generate a `Foreign` value compatible with the `readGeneric` function. @@ -38,7 +25,7 @@ Generate a `Foreign` value compatible with the `readGeneric` function. #### `readJSONGeneric` ``` purescript -readJSONGeneric :: forall a. Generic a => Options -> String -> F a +readJSONGeneric :: forall a rep. (Generic a rep, GenericDecode rep) => Options -> String -> F a ``` Read a value which has a `Generic` type from a JSON String @@ -46,7 +33,7 @@ Read a value which has a `Generic` type from a JSON String #### `toJSONGeneric` ``` purescript -toJSONGeneric :: forall a. Generic a => Options -> a -> String +toJSONGeneric :: forall a rep. (Generic a rep, GenericEncode rep) => Options -> a -> String ``` Write a value which has a `Generic` type as a JSON String diff --git a/docs/Data/Foreign/Generic/Classes.md b/docs/Data/Foreign/Generic/Classes.md new file mode 100644 index 0000000..d606d05 --- /dev/null +++ b/docs/Data/Foreign/Generic/Classes.md @@ -0,0 +1,102 @@ +## Module Data.Foreign.Generic.Classes + +#### `GenericDecode` + +``` purescript +class GenericDecode a where + decodeOpts :: Options -> Foreign -> F a +``` + +##### Instances +``` purescript +GenericDecode NoConstructors +(IsSymbol name, GenericDecodeArgs rep, GenericCountArgs rep) => GenericDecode (Constructor name rep) +(GenericDecode a, GenericDecode b) => GenericDecode (Sum a b) +``` + +#### `GenericEncode` + +``` purescript +class GenericEncode a where + encodeOpts :: Options -> a -> Foreign +``` + +##### Instances +``` purescript +GenericEncode NoConstructors +(IsSymbol name, GenericEncodeArgs rep) => GenericEncode (Constructor name rep) +(GenericEncode a, GenericEncode b) => GenericEncode (Sum a b) +``` + +#### `GenericDecodeArgs` + +``` purescript +class GenericDecodeArgs a where + decodeArgs :: Int -> List Foreign -> F { result :: a, rest :: List Foreign, next :: Int } +``` + +##### Instances +``` purescript +GenericDecodeArgs NoArguments +(IsForeign a) => GenericDecodeArgs (Argument a) +(GenericDecodeArgs a, GenericDecodeArgs b) => GenericDecodeArgs (Product a b) +(GenericDecodeFields fields) => GenericDecodeArgs (Rec fields) +``` + +#### `GenericEncodeArgs` + +``` purescript +class GenericEncodeArgs a where + encodeArgs :: a -> List Foreign +``` + +##### Instances +``` purescript +GenericEncodeArgs NoArguments +(AsForeign a) => GenericEncodeArgs (Argument a) +(GenericEncodeArgs a, GenericEncodeArgs b) => GenericEncodeArgs (Product a b) +(GenericEncodeFields fields) => GenericEncodeArgs (Rec fields) +``` + +#### `GenericDecodeFields` + +``` purescript +class GenericDecodeFields a where + decodeFields :: Foreign -> F a +``` + +##### Instances +``` purescript +(IsSymbol name, IsForeign a) => GenericDecodeFields (Field name a) +(GenericDecodeFields a, GenericDecodeFields b) => GenericDecodeFields (Product a b) +``` + +#### `GenericEncodeFields` + +``` purescript +class GenericEncodeFields a where + encodeFields :: a -> StrMap Foreign +``` + +##### Instances +``` purescript +(IsSymbol name, AsForeign a) => GenericEncodeFields (Field name a) +(GenericEncodeFields a, GenericEncodeFields b) => GenericEncodeFields (Product a b) +``` + +#### `GenericCountArgs` + +``` purescript +class GenericCountArgs a where + countArgs :: Proxy a -> Either a Int +``` + +##### Instances +``` purescript +GenericCountArgs NoArguments +GenericCountArgs (Argument a) +(GenericCountArgs a, GenericCountArgs b) => GenericCountArgs (Product a b) +GenericCountArgs (Rec fields) +``` + + diff --git a/docs/Data/Foreign/Generic/Types.md b/docs/Data/Foreign/Generic/Types.md new file mode 100644 index 0000000..5fba9f2 --- /dev/null +++ b/docs/Data/Foreign/Generic/Types.md @@ -0,0 +1,16 @@ +## Module Data.Foreign.Generic.Types + +#### `Options` + +``` purescript +type Options = { sumEncoding :: SumEncoding, unwrapSingleConstructors :: Boolean, unwrapSingleArguments :: Boolean } +``` + +#### `SumEncoding` + +``` purescript +data SumEncoding + = TaggedObject { tagFieldName :: String, contentsFieldName :: String } +``` + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..0e9f1ad --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "scripts": { + "clean": "rimraf output", + "build": "psa \"src/**/*.purs\" \"bower_components/purescript-*/src/**/*.purs\" --censor-lib --strict", + "test": "psc \"src/**/*.purs\" \"bower_components/purescript-*/src/**/*.purs\" \"test/**/*.purs\" && psc-bundle \"output/**/*.js\" --module Test.Main --main Test.Main | node" + }, + "devDependencies": { + "purescript-psa": "^0.3.8", + "rimraf": "^2.5.0" + } +} diff --git a/psc-package.json b/psc-package.json deleted file mode 100644 index 673d78d..0000000 --- a/psc-package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "foreign-generic", - "source": "https://github.com/purescript/package-sets.git", - "set": "psc-0.10.1", - "depends": [ - "arrays", - "assert", - "bifunctors", - "console", - "control", - "distributive", - "eff", - "either", - "exceptions", - "foldable-traversable", - "foreign", - "functions", - "generics", - "globals", - "identity", - "integers", - "invariant", - "lazy", - "lists", - "maps", - "math", - "maybe", - "monoid", - "newtype", - "nonempty", - "nullable", - "partial", - "prelude", - "proxy", - "st", - "strings", - "tailrec", - "transformers", - "tuples", - "unfoldable" - ] -} diff --git a/src/Data/Foreign/Generic.purs b/src/Data/Foreign/Generic.purs index aaf03e4..214395e 100644 --- a/src/Data/Foreign/Generic.purs +++ b/src/Data/Foreign/Generic.purs @@ -1,166 +1,35 @@ module Data.Foreign.Generic where import Prelude - -import Control.Monad.Eff.Exception.Unsafe (unsafeThrow) -import Data.Array (zipWith, zipWithA, sortBy) -import Data.Foldable (find) -import Data.Foreign (F, Foreign, ForeignError(..), fail, parseJSON, toForeign, - readArray, readString, isUndefined, isNull, readBoolean, - readChar, readInt, readNumber) -import Data.Foreign.Index (prop, (!)) -import Data.Function (on) -import Data.Generic (class Generic, GenericSignature(..), GenericSpine(..), toSpine, toSignature, fromSpine) -import Data.Maybe (Maybe(..)) -import Data.Nullable (toNullable) -import Data.StrMap as S -import Data.Traversable (for) -import Data.Tuple (Tuple(..)) +import Data.Foreign (F, Foreign, parseJSON) +import Data.Foreign.Generic.Classes (class GenericDecode, class GenericEncode, decodeOpts, encodeOpts) +import Data.Foreign.Generic.Types (Options, SumEncoding(..)) +import Data.Generic.Rep (class Generic, from, to) import Global.Unsafe (unsafeStringify) -import Type.Proxy (Proxy(..)) - -type Options = - { sumEncoding :: SumEncoding - , unwrapNewtypes :: Boolean - , unwrapSingleArgumentConstructors :: Boolean - , maybeAsNull :: Boolean - , tupleAsArray :: Boolean - } - -data SumEncoding - = TaggedObject - { tagFieldName :: String - , contentsFieldName :: String - } defaultOptions :: Options defaultOptions = - { sumEncoding: TaggedObject - { tagFieldName: "tag" - , contentsFieldName: "contents" - } - , unwrapNewtypes: false - , unwrapSingleArgumentConstructors: true - , maybeAsNull: true - , tupleAsArray: false + { sumEncoding: + TaggedObject + { tagFieldName: "tag" + , contentsFieldName: "contents" + } + , unwrapSingleConstructors: false + , unwrapSingleArguments: true } -- | Read a value which has a `Generic` type. -readGeneric :: forall a. Generic a => Options -> Foreign -> F a -readGeneric { sumEncoding - , unwrapNewtypes - , unwrapSingleArgumentConstructors - , maybeAsNull - , tupleAsArray - } = map fromSpineUnsafe <<< go (toSignature (Proxy :: Proxy a)) - where - fromSpineUnsafe :: GenericSpine -> a - fromSpineUnsafe sp = - case fromSpine sp of - Nothing -> unsafeThrow "Invalid spine for signature" - Just a -> a - - go :: GenericSignature -> Foreign -> F GenericSpine - go SigUnit _ = pure SUnit - go SigNumber f = map SNumber (readNumber f) - go SigInt f = map SInt (readInt f) - go SigChar f = map SChar (readChar f) - go SigString f = map SString (readString f) - go SigBoolean f = map SBoolean (readBoolean f) - go (SigArray el) f = do - arr <- readArray f - els <- for arr \f -> do - e <- go (el unit) f - pure (const e) - pure (SArray els) - go (SigRecord props) f = do - fs <- for props \prop -> do - pf <- f ! prop.recLabel - sp <- go (prop.recValue unit) pf - pure { recLabel: prop.recLabel, recValue: const sp } - pure (SRecord fs) - go (SigProd _ [{ sigConstructor: tag, sigValues: [sig] }]) f | unwrapNewtypes = do - sp <- go (sig unit) f - pure (SProd tag [\_ -> sp]) - go (SigProd "Data.Maybe.Maybe" [{ sigValues: [just] }, _]) f | maybeAsNull = do - if isNull f || isUndefined f - then pure (SProd "Data.Maybe.Nothing" []) - else do sp <- go (just unit) f - pure (SProd "Data.Maybe.Just" [\_ -> sp]) - go (SigProd "Data.Tuple.Tuple" [{ sigValues: [_1, _2] }]) f | tupleAsArray = do - arr <- readArray f - case arr of - [a, b] -> do - x <- go (_1 unit) a - y <- go (_2 unit) b - pure $ SProd "Data.Tuple.Tuple" [\_ -> x, \_ -> y] - _ -> fail (TypeMismatch "array of length 2" "array") - go (SigProd _ alts) f = - case sumEncoding of - TaggedObject { tagFieldName, contentsFieldName } -> do - tag <- prop tagFieldName f >>= readString - case find (\alt -> alt.sigConstructor == tag) alts of - Nothing -> fail (TypeMismatch ("one of " <> show (map _.sigConstructor alts)) tag) - Just { sigValues: [] } -> pure (SProd tag []) - Just { sigValues: [sig] } | unwrapSingleArgumentConstructors -> do - val <- prop contentsFieldName f - sp <- go (sig unit) val - pure (SProd tag [\_ -> sp]) - Just { sigValues } -> do - vals <- prop contentsFieldName f >>= readArray - sps <- zipWithA (\k -> go (k unit)) sigValues vals - pure (SProd tag (map const sps)) +readGeneric :: forall a rep. (Generic a rep, GenericDecode rep) => Options -> Foreign -> F a +readGeneric opts = map to <<< decodeOpts opts -- | Generate a `Foreign` value compatible with the `readGeneric` function. -toForeignGeneric :: forall a. (Generic a) => Options -> a -> Foreign -toForeignGeneric { sumEncoding - , unwrapNewtypes - , unwrapSingleArgumentConstructors - , maybeAsNull - , tupleAsArray - } = go (toSignature (Proxy :: Proxy a)) <<< toSpine - where - go :: GenericSignature -> GenericSpine -> Foreign - go _ (SNumber n) = toForeign n - go _ (SInt i) = toForeign i - go _ (SChar c) = toForeign c - go _ (SString s) = toForeign s - go _ (SBoolean b) = toForeign b - go (SigArray sig) (SArray arr) = toForeign (map (go (sig unit) <<< (_ $ unit)) arr) - go (SigRecord sigs) (SRecord sps) = toForeign (S.fromFoldable pairs) - where - pairs :: Array (Tuple String Foreign) - pairs = zipWith pair (sortBy (compare `on` _.recLabel) sigs) - (sortBy (compare `on` _.recLabel) sps) - - pair sig sp | sig.recLabel == sp.recLabel = Tuple sig.recLabel (go (sig.recValue unit) (sp.recValue unit)) - | otherwise = unsafeThrow "Record fields do not match signature" - go (SigProd "Data.Maybe.Maybe" _) (SProd "Data.Maybe.Nothing" []) | maybeAsNull = toForeign (toNullable Nothing) - go (SigProd "Data.Maybe.Maybe" [{ sigValues: [just] }, _]) (SProd "Data.Maybe.Just" [sp]) | maybeAsNull = go (just unit) (sp unit) - go (SigProd "Data.Tuple.Tuple" [{ sigValues: [_1, _2] }]) (SProd "Data.Tuple.Tuple" [a, b]) | tupleAsArray = do - toForeign [ go (_1 unit) (a unit), go (_2 unit) (b unit) ] - go (SigProd _ [{ sigConstructor: _, sigValues: [sig] }]) (SProd _ [sp]) | unwrapNewtypes = go (sig unit) (sp unit) - go (SigProd _ alts) (SProd tag sps) = - case sumEncoding of - TaggedObject { tagFieldName, contentsFieldName } -> - case find (\alt -> alt.sigConstructor == tag) alts of - Nothing -> unsafeThrow ("No signature for data constructor " <> tag) - Just { sigValues } -> - case zipWith (\sig sp -> go (sig unit) (sp unit)) sigValues sps of - [] -> toForeign (S.singleton tagFieldName (toForeign tag)) - [f] | unwrapSingleArgumentConstructors -> - toForeign (S.fromFoldable [ Tuple tagFieldName (toForeign tag) - , Tuple contentsFieldName f - ]) - fs -> toForeign (S.fromFoldable [ Tuple tagFieldName (toForeign tag) - , Tuple contentsFieldName (toForeign fs) - ]) - go _ _ = unsafeThrow "Invalid spine for signature" +toForeignGeneric :: forall a rep. (Generic a rep, GenericEncode rep) => Options -> a -> Foreign +toForeignGeneric opts = encodeOpts opts <<< from -- | Read a value which has a `Generic` type from a JSON String -readJSONGeneric :: forall a. (Generic a) => Options -> String -> F a +readJSONGeneric :: forall a rep. (Generic a rep, GenericDecode rep) => Options -> String -> F a readJSONGeneric opts = parseJSON >=> readGeneric opts -- | Write a value which has a `Generic` type as a JSON String -toJSONGeneric :: forall a. (Generic a) => Options -> a -> String +toJSONGeneric :: forall a rep. (Generic a rep, GenericEncode rep) => Options -> a -> String toJSONGeneric opts = toForeignGeneric opts >>> unsafeStringify diff --git a/src/Data/Foreign/Generic/Classes.purs b/src/Data/Foreign/Generic/Classes.purs new file mode 100644 index 0000000..8292f73 --- /dev/null +++ b/src/Data/Foreign/Generic/Classes.purs @@ -0,0 +1,211 @@ +module Data.Foreign.Generic.Classes where + +import Prelude +import Data.StrMap as S +import Control.Alt ((<|>)) +import Control.Monad.Except (mapExcept) +import Data.Bifunctor (lmap) +import Data.Either (Either(..)) +import Data.Foreign (F, Foreign, ForeignError(..), fail, readArray, readString, toForeign) +import Data.Foreign.Class (class AsForeign, class IsForeign, read, readProp, write) +import Data.Foreign.Generic.Types (Options, SumEncoding(..)) +import Data.Foreign.Index (prop) +import Data.Generic.Rep (Argument(Argument), Constructor(Constructor), Field(Field), NoArguments(NoArguments), NoConstructors, Product(Product), Rec(Rec), Sum(Inr, Inl)) +import Data.List (List(..), fromFoldable, null, singleton, toUnfoldable, (:)) +import Data.Maybe (Maybe(..), maybe) +import Data.Monoid (mempty) +import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol) +import Type.Proxy (Proxy(..)) + +class GenericDecode a where + decodeOpts :: Options -> Foreign -> F a + +class GenericEncode a where + encodeOpts :: Options -> a -> Foreign + +class GenericDecodeArgs a where + decodeArgs :: Int -> List Foreign -> F { result :: a + , rest :: List Foreign + , next :: Int + } + +class GenericEncodeArgs a where + encodeArgs :: a -> List Foreign + +class GenericDecodeFields a where + decodeFields :: Foreign -> F a + +class GenericEncodeFields a where + encodeFields :: a -> S.StrMap Foreign + +class GenericCountArgs a where + countArgs :: Proxy a -> Either a Int + +instance genericDecodeNoConstructors :: GenericDecode NoConstructors where + decodeOpts opts _ = fail (ForeignError "No constructors") + +instance genericEncodeNoConstructors :: GenericEncode NoConstructors where + encodeOpts opts a = encodeOpts opts a + +instance genericDecodeConstructor + :: (IsSymbol name, GenericDecodeArgs rep, GenericCountArgs rep) + => GenericDecode (Constructor name rep) where + decodeOpts opts f = + if opts.unwrapSingleConstructors + then Constructor <$> readArguments f + else case opts.sumEncoding of + TaggedObject { tagFieldName, contentsFieldName } -> do + tag <- mapExcept (lmap (map (ErrorAtProperty contentsFieldName))) do + tag <- prop tagFieldName f >>= readString + unless (tag == ctorName) $ + fail (ForeignError ("Expected " <> show ctorName <> " tag")) + pure tag + args <- mapExcept (lmap (map (ErrorAtProperty contentsFieldName))) + (prop contentsFieldName f >>= readArguments) + pure (Constructor args) + where + ctorName = reflectSymbol (SProxy :: SProxy name) + + numArgs = countArgs (Proxy :: Proxy rep) + + readArguments args = + case numArgs of + Left a -> pure a + Right 1 | opts.unwrapSingleArguments -> do + { result, rest } <- decodeArgs 0 (singleton args) + unless (null rest) $ + fail (ForeignError "Expected a single argument") + pure result + Right n -> do + vals <- readArray args + { result, rest } <- decodeArgs 0 (fromFoldable vals) + unless (null rest) $ + fail (ForeignError ("Expected " <> show n <> " constructor arguments")) + pure result + +instance genericEncodeConstructor + :: (IsSymbol name, GenericEncodeArgs rep) + => GenericEncode (Constructor name rep) where + encodeOpts opts (Constructor args) = + if opts.unwrapSingleConstructors + then maybe (toForeign {}) toForeign (encodeArgsArray args) + else case opts.sumEncoding of + TaggedObject { tagFieldName, contentsFieldName } -> + toForeign (S.singleton tagFieldName (toForeign ctorName) + `S.union` maybe S.empty (S.singleton contentsFieldName) (encodeArgsArray args)) + + where + ctorName = reflectSymbol (SProxy :: SProxy name) + + encodeArgsArray :: rep -> Maybe Foreign + encodeArgsArray = unwrapArguments <<< toUnfoldable <<< encodeArgs + + unwrapArguments :: Array Foreign -> Maybe Foreign + unwrapArguments [] = Nothing + unwrapArguments [x] | opts.unwrapSingleArguments = Just x + unwrapArguments xs = Just (toForeign xs) + +instance genericDecodeSum + :: (GenericDecode a, GenericDecode b) + => GenericDecode (Sum a b) where + decodeOpts opts f = Inl <$> decodeOpts opts' f <|> Inr <$> decodeOpts opts' f + where + -- Reuse the unwrapSingleConstructors flag, since we cannot have a single + -- constructor at this point anyway. + opts' = opts { unwrapSingleConstructors = false } + +instance genericEncodeSum + :: (GenericEncode a, GenericEncode b) + => GenericEncode (Sum a b) where + encodeOpts opts (Inl a) = encodeOpts (opts { unwrapSingleConstructors = false }) a + encodeOpts opts (Inr b) = encodeOpts (opts { unwrapSingleConstructors = false }) b + +instance genericDecodeArgsNoArguments :: GenericDecodeArgs NoArguments where + decodeArgs i Nil = pure { result: NoArguments, rest: Nil, next: i } + decodeArgs _ _ = fail (ForeignError "Too many constructor arguments") + +instance genericEncodeArgsNoArguments :: GenericEncodeArgs NoArguments where + encodeArgs _ = mempty + +instance genericDecodeArgsArgument + :: IsForeign a + => GenericDecodeArgs (Argument a) where + decodeArgs i (x : xs) = do + a <- mapExcept (lmap (map (ErrorAtIndex i))) (read x) + pure { result: Argument a, rest: xs, next: i + 1 } + decodeArgs _ _ = fail (ForeignError "Not enough constructor arguments") + +instance genericEncodeArgsArgument + :: AsForeign a + => GenericEncodeArgs (Argument a) where + encodeArgs (Argument a) = singleton (write a) + +instance genericDecodeArgsProduct + :: (GenericDecodeArgs a, GenericDecodeArgs b) + => GenericDecodeArgs (Product a b) where + decodeArgs i xs = do + { result: resA, rest: xs1, next: i1 } <- decodeArgs i xs + { result: resB, rest, next } <- decodeArgs i1 xs1 + pure { result: Product resA resB, rest, next } + +instance genericEncodeArgsProduct + :: (GenericEncodeArgs a, GenericEncodeArgs b) + => GenericEncodeArgs (Product a b) where + encodeArgs (Product a b) = encodeArgs a <> encodeArgs b + +instance genericDecodeArgsRec + :: GenericDecodeFields fields + => GenericDecodeArgs (Rec fields) where + decodeArgs i (x : xs) = do + fields <- mapExcept (lmap (map (ErrorAtIndex i))) (decodeFields x) + pure { result: Rec fields, rest: xs, next: i + 1 } + decodeArgs _ _ = fail (ForeignError "Not enough constructor arguments") + +instance genericEncodeArgsRec + :: GenericEncodeFields fields + => GenericEncodeArgs (Rec fields) where + encodeArgs (Rec fs) = singleton (toForeign (encodeFields fs)) + +instance genericDecodeFieldsField + :: (IsSymbol name, IsForeign a) + => GenericDecodeFields (Field name a) where + decodeFields x = do + let name = reflectSymbol (SProxy :: SProxy name) + -- If `name` field doesn't exist, then `y` will be `undefined`. + Field <$> readProp name x + +instance genericEncodeFieldsField + :: (IsSymbol name, AsForeign a) + => GenericEncodeFields (Field name a) where + encodeFields (Field a) = + let name = reflectSymbol (SProxy :: SProxy name) + in S.singleton name (write a) + +instance genericDecodeFieldsProduct + :: (GenericDecodeFields a, GenericDecodeFields b) + => GenericDecodeFields (Product a b) where + decodeFields x = Product <$> decodeFields x <*> decodeFields x + +instance genericEncodeFieldsProduct + :: (GenericEncodeFields a, GenericEncodeFields b) + => GenericEncodeFields (Product a b) where + encodeFields (Product a b) = encodeFields a `S.union` encodeFields b + +instance genericCountArgsNoArguments :: GenericCountArgs NoArguments where + countArgs _ = Left NoArguments + +instance genericCountArgsArgument :: GenericCountArgs (Argument a) where + countArgs _ = Right 1 + +instance genericCountArgsProduct + :: (GenericCountArgs a, GenericCountArgs b) + => GenericCountArgs (Product a b) where + countArgs _ = + case countArgs (Proxy :: Proxy a), countArgs (Proxy :: Proxy b) of + Left a , Left b -> Left (Product a b) + Left _ , Right n -> Right n + Right n, Left _ -> Right n + Right n, Right m -> Right (n + m) + +instance genericCountArgsRec :: GenericCountArgs (Rec fields) where + countArgs _ = Right 1 diff --git a/src/Data/Foreign/Generic/Types.purs b/src/Data/Foreign/Generic/Types.purs new file mode 100644 index 0000000..b4551c3 --- /dev/null +++ b/src/Data/Foreign/Generic/Types.purs @@ -0,0 +1,13 @@ +module Data.Foreign.Generic.Types where + +type Options = + { sumEncoding :: SumEncoding + , unwrapSingleConstructors :: Boolean + , unwrapSingleArguments :: Boolean + } + +data SumEncoding + = TaggedObject + { tagFieldName :: String + , contentsFieldName :: String + } diff --git a/test/Main.purs b/test/Main.purs index af97e11..4b1c6db 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -6,99 +6,44 @@ import Control.Monad.Eff.Console (CONSOLE, log) import Control.Monad.Except (runExcept) import Data.Bifunctor (bimap) import Data.Either (Either(..)) -import Data.Foreign (F) -import Data.Foreign.Generic (Options, defaultOptions, readJSONGeneric, toJSONGeneric) -import Data.Generic (class Generic, gEq, gShow) +import Data.Foreign.Class (class AsForeign, class IsForeign, readJSON, write) import Data.Tuple (Tuple(..)) +import Global.Unsafe (unsafeStringify) import Test.Assert (assert, assert', ASSERT) +import Test.Types (IntList(..), RecordTest(..), Tree(..), TupleArray(..)) --- | Balanced binary leaf trees -data Tree a = Leaf a | Branch (Tree (Tuple a a)) - -derive instance genericTree :: (Generic a) => Generic (Tree a) - -buildTree :: forall a. (a -> Tuple a a) -> Int -> a -> Tree a +buildTree :: forall a. (a -> TupleArray a a) -> Int -> a -> Tree a buildTree _ 0 a = Leaf a buildTree f n a = Branch $ buildTree (bimap f f) (n - 1) (f a) --- A balanced binary tree of depth 5 -tree :: Tree Int -tree = buildTree (\i -> Tuple (2 * i) (2 * i + 1)) 5 0 - -opts :: Options -opts = defaultOptions { unwrapNewtypes = true, tupleAsArray = true } - -readTree :: forall a. (Generic a) => String -> F (Tree a) -readTree = readJSONGeneric opts - -writeTree :: forall a. (Generic a) => Tree a -> String -writeTree = toJSONGeneric opts - -data WrappedArray a = WrappedArray (Array a) -derive instance genericWrappedArray :: (Generic a) => Generic (WrappedArray a) - -newtype WrappedArrayN a = WrappedArrayN (Array a) -derive instance genericWrappedArrayN :: (Generic a) => Generic (WrappedArrayN a) - -data TupleArray a b = TupleArray (Array (Tuple a b)) -derive instance genericTupleArray :: (Generic a, Generic b) => Generic (TupleArray a b) - -main :: forall eff. Eff (console :: CONSOLE, assert :: ASSERT | eff) Unit -main = do - testTree - test "hello, world" - test 'c' - test 1 - test 1.0 - test false - - test (Right "hi" :: Either String String) - test (Left "hi" :: Either String String) - test (Tuple "foo" 1) +-- A balanced binary tree of depth N +makeTree :: Int -> Tree Int +makeTree n = buildTree (\i -> TupleArray (Tuple (2 * i) (2 * i + 1))) n 0 - let arr = [Tuple "foo" 1, Tuple "bar" 2] - test arr - test (WrappedArray arr) - test (WrappedArrayN arr) - test (TupleArray arr) - -testTree - :: forall eff - . Eff ( console :: CONSOLE - , assert :: ASSERT - | eff - ) Unit -testTree = do - let json = writeTree tree - log json - case runExcept (readTree json) of - Right tree1 -> do - log (gShow tree1) - assert (gEq tree tree1) - Left err -> - throw (show err) +throw :: forall eff. String -> Eff (assert :: ASSERT | eff) Unit +throw = flip assert' false -test - :: forall a eff - . Generic a +testRoundTrip + :: ∀ a eff + . ( Eq a + , IsForeign a + , AsForeign a + ) => a -> Eff ( console :: CONSOLE , assert :: ASSERT | eff ) Unit -test thing = do - log "" - log ("testing: " <> gShow thing) - log "===" - log "" - let json = toJSONGeneric defaultOptions thing +testRoundTrip x = do + let json = unsafeStringify (write x) log json - case runExcept (readJSONGeneric defaultOptions json :: F a) of - Right thing1 -> do - log ("result: " <> gShow thing1) - assert (gEq thing thing1) - Left err -> - throw (show err) + case runExcept (readJSON json) of + Right y -> assert (x == y) + Left err -> throw (show err) -throw :: forall eff. String -> Eff (assert :: ASSERT | eff) Unit -throw = flip assert' false +main :: forall eff. Eff (console :: CONSOLE, assert :: ASSERT | eff) Unit +main = do + testRoundTrip (RecordTest { foo: 1, bar: "test", baz: 'a' }) + testRoundTrip (Cons 1 (Cons 2 (Cons 3 Nil))) + testRoundTrip (makeTree 0) + testRoundTrip (makeTree 5) diff --git a/test/Types.purs b/test/Types.purs new file mode 100644 index 0000000..71024e8 --- /dev/null +++ b/test/Types.purs @@ -0,0 +1,88 @@ +module Test.Types where + +import Prelude +import Data.Bifunctor (class Bifunctor) +import Data.Foreign (ForeignError(ForeignError), fail, readArray, toForeign) +import Data.Foreign.Class (class AsForeign, class IsForeign, read, write) +import Data.Foreign.Generic (defaultOptions, readGeneric, toForeignGeneric) +import Data.Generic.Rep (class Generic) +import Data.Generic.Rep.Eq (genericEq) +import Data.Generic.Rep.Show (genericShow) +import Data.Tuple (Tuple(..)) + +newtype TupleArray a b = TupleArray (Tuple a b) + +derive newtype instance bifunctorTupleArray :: Bifunctor TupleArray + +derive instance genericTupleArray :: Generic (TupleArray a b) _ + +instance showTupleArray :: (Show a, Show b) => Show (TupleArray a b) where + show x = genericShow x + +instance eqTupleArray :: (Eq a, Eq b) => Eq (TupleArray a b) where + eq x y = genericEq x y + +instance isForeignTupleArray :: (IsForeign a, IsForeign b) => IsForeign (TupleArray a b) where + read x = do + arr <- readArray x + case arr of + [y, z] -> TupleArray <$> (Tuple <$> read y <*> read z) + _ -> fail (ForeignError "Expected two array elements") + +instance asForeignTupleArray :: (AsForeign a, AsForeign b) => AsForeign (TupleArray a b) where + write (TupleArray (Tuple a b)) = toForeign [write a, write b] + +-- | An example record +newtype RecordTest = RecordTest + { foo :: Int + , bar :: String + , baz :: Char + } + +derive instance genericRecordTest :: Generic RecordTest _ + +instance showRecordTest :: Show RecordTest where + show x = genericShow x + +instance eqRecordTest :: Eq RecordTest where + eq x y = genericEq x y + +instance isForeignRecordTest :: IsForeign RecordTest where + read x = readGeneric (defaultOptions { unwrapSingleConstructors = true }) x + +instance asForeignRecordTest :: AsForeign RecordTest where + write x = toForeignGeneric (defaultOptions { unwrapSingleConstructors = true }) x + +-- | An example of an ADT with nullary constructors +data IntList = Nil | Cons Int IntList + +derive instance genericIntList :: Generic IntList _ + +instance showIntList :: Show IntList where + show x = genericShow x + +instance eqIntList :: Eq IntList where + eq x y = genericEq x y + +instance isForeignIntList :: IsForeign IntList where + read x = readGeneric (defaultOptions { unwrapSingleConstructors = true }) x + +instance asForeignIntList :: AsForeign IntList where + write x = toForeignGeneric (defaultOptions { unwrapSingleConstructors = true }) x + +-- | Balanced binary leaf trees +data Tree a = Leaf a | Branch (Tree (TupleArray a a)) + +derive instance genericTree :: Generic (Tree a) _ + +instance showTree :: Show a => Show (Tree a) where + show x = genericShow x + +instance eqTree :: Eq a => Eq (Tree a) where + eq x y = genericEq x y + +instance isForeignTree :: IsForeign a => IsForeign (Tree a) where + read x = readGeneric defaultOptions x + +instance asForeignTree :: AsForeign a => AsForeign (Tree a) where + write x = toForeignGeneric defaultOptions x