|
1 | 1 | module Data.Foreign.Generic where |
2 | | - |
| 2 | + |
3 | 3 | import Prelude |
4 | 4 |
|
5 | 5 | import Data.Maybe |
6 | | -import Data.Maybe.Unsafe (fromJust) |
7 | | -import Data.Array (zipWithA) |
| 6 | +import Data.Array (zipWith, zipWithA, sortBy) |
| 7 | +import Data.Tuple |
8 | 8 | import Data.Either |
9 | 9 | import Data.Foreign |
10 | 10 | import Data.Foreign.Class |
11 | 11 | import Data.Foreign.Index |
| 12 | +import Data.Function (on) |
| 13 | +import Data.Nullable (toNullable) |
12 | 14 | import Data.Generic |
13 | 15 | import Data.Foldable (find) |
14 | 16 | import Data.Traversable (for) |
| 17 | +import Data.List as L |
| 18 | +import Data.StrMap as S |
15 | 19 |
|
16 | | -import Control.Alt ((<|>)) |
17 | | -import Control.Plus (empty) |
18 | 20 | import Control.Bind ((>=>)) |
19 | | -import Control.Monad (when) |
20 | | - |
| 21 | +import Control.Monad.Eff.Exception.Unsafe (unsafeThrow) |
| 22 | + |
| 23 | +import Global.Unsafe (unsafeStringify) |
| 24 | + |
| 25 | +import Type.Proxy (Proxy(..)) |
| 26 | + |
| 27 | +type Options = |
| 28 | + { sumEncoding :: SumEncoding |
| 29 | + , unwrapNewtypes :: Boolean |
| 30 | + , unwrapSingleArgumentConstructors :: Boolean |
| 31 | + , maybeAsNull :: Boolean |
| 32 | + } |
| 33 | + |
| 34 | +data SumEncoding |
| 35 | + = TaggedObject |
| 36 | + { tagFieldName :: String |
| 37 | + , contentsFieldName :: String |
| 38 | + } |
| 39 | + |
| 40 | +defaultOptions :: Options |
| 41 | +defaultOptions = |
| 42 | + { sumEncoding: TaggedObject |
| 43 | + { tagFieldName: "tag" |
| 44 | + , contentsFieldName: "contents" |
| 45 | + } |
| 46 | + , unwrapNewtypes: false |
| 47 | + , unwrapSingleArgumentConstructors: true |
| 48 | + , maybeAsNull: true |
| 49 | + } |
| 50 | + |
21 | 51 | -- | Read a value which has a `Generic` type. |
22 | | -readGeneric :: forall a. (Generic a) => Foreign -> F a |
23 | | -readGeneric = map (fromJust <<< fromSpine) <<< go (toSignature (anyProxy :: Proxy a)) |
| 52 | +readGeneric :: forall a. (Generic a) => Options -> Foreign -> F a |
| 53 | +readGeneric { sumEncoding, unwrapNewtypes, unwrapSingleArgumentConstructors, maybeAsNull } = |
| 54 | + map fromSpineUnsafe <<< go (toSignature (Proxy :: Proxy a)) |
24 | 55 | where |
| 56 | + fromSpineUnsafe :: GenericSpine -> a |
| 57 | + fromSpineUnsafe sp = |
| 58 | + case fromSpine sp of |
| 59 | + Nothing -> unsafeThrow "Invalid spine for signature" |
| 60 | + Just a -> a |
| 61 | + |
25 | 62 | go :: GenericSignature -> Foreign -> F GenericSpine |
26 | 63 | go SigNumber f = map SNumber (readNumber f) |
27 | 64 | go SigInt f = map SInt (readInt f) |
| 65 | + go SigChar f = map SChar (readChar f) |
28 | 66 | go SigString f = map SString (readString f) |
29 | 67 | go SigBoolean f = map SBoolean (readBoolean f) |
30 | | - go (SigArray el) f = do arr <- readArray f |
31 | | - els <- for arr \f -> do |
32 | | - e <- go (el unit) f |
33 | | - return (const e) |
34 | | - return (SArray els) |
35 | | - go (SigRecord props) f = do fs <- for props \prop -> do pf <- f ! prop.recLabel |
36 | | - sp <- go (prop.recValue unit) pf |
37 | | - return { recLabel: prop.recLabel, recValue: const sp } |
38 | | - return (SRecord fs) |
39 | | - go (SigProd alts) f = do |
40 | | - tag <- prop "tag" f >>= readString |
41 | | - case find (\alt -> alt.sigConstructor == tag) alts of |
42 | | - Nothing -> Left (TypeMismatch "" tag) |
43 | | - Just { sigValues: sigValues } -> do |
44 | | - vals <- prop "values" f >>= readArray |
45 | | - sps <- zipWithA (\k -> go (k unit)) sigValues vals |
46 | | - return (SProd tag (map const sps)) |
47 | | - |
| 68 | + go (SigArray el) f = do |
| 69 | + arr <- readArray f |
| 70 | + els <- for arr \f -> do |
| 71 | + e <- go (el unit) f |
| 72 | + return (const e) |
| 73 | + return (SArray els) |
| 74 | + go (SigRecord props) f = do |
| 75 | + fs <- for props \prop -> do |
| 76 | + pf <- f ! prop.recLabel |
| 77 | + sp <- go (prop.recValue unit) pf |
| 78 | + return { recLabel: prop.recLabel, recValue: const sp } |
| 79 | + return (SRecord fs) |
| 80 | + go (SigProd _ [{ sigConstructor: tag, sigValues: [sig] }]) f | unwrapNewtypes = do |
| 81 | + sp <- go (sig unit) f |
| 82 | + return (SProd tag [\_ -> sp]) |
| 83 | + go (SigProd "Data.Maybe.Maybe" [{ sigValues: [just] }, _]) f | maybeAsNull = do |
| 84 | + if isNull f || isUndefined f |
| 85 | + then return (SProd "Data.Maybe.Nothing" []) |
| 86 | + else do sp <- go (just unit) f |
| 87 | + return (SProd "Data.Maybe.Just" [\_ -> sp]) |
| 88 | + go (SigProd _ alts) f = |
| 89 | + case sumEncoding of |
| 90 | + TaggedObject { tagFieldName, contentsFieldName } -> do |
| 91 | + tag <- prop tagFieldName f >>= readString |
| 92 | + case find (\alt -> alt.sigConstructor == tag) alts of |
| 93 | + Nothing -> Left (TypeMismatch ("one of " <> show (map _.sigConstructor alts)) tag) |
| 94 | + Just { sigValues: [] } -> return (SProd tag []) |
| 95 | + Just { sigValues: [sig] } | unwrapSingleArgumentConstructors -> do |
| 96 | + val <- prop contentsFieldName f |
| 97 | + sp <- go (sig unit) val |
| 98 | + return (SProd tag [\_ -> sp]) |
| 99 | + Just { sigValues } -> do |
| 100 | + vals <- prop contentsFieldName f >>= readArray |
| 101 | + sps <- zipWithA (\k -> go (k unit)) sigValues vals |
| 102 | + return (SProd tag (map const sps)) |
| 103 | + |
| 104 | +-- | Generate a `Foreign` value compatible with the `readGeneric` function. |
| 105 | +toForeignGeneric :: forall a. (Generic a) => Options -> a -> Foreign |
| 106 | +toForeignGeneric { sumEncoding, unwrapNewtypes, unwrapSingleArgumentConstructors, maybeAsNull } = go (toSignature (Proxy :: Proxy a)) <<< toSpine |
| 107 | + where |
| 108 | + go :: GenericSignature -> GenericSpine -> Foreign |
| 109 | + go _ (SNumber n) = toForeign n |
| 110 | + go _ (SInt i) = toForeign i |
| 111 | + go _ (SChar c) = toForeign c |
| 112 | + go _ (SString s) = toForeign s |
| 113 | + go _ (SBoolean b) = toForeign b |
| 114 | + go (SigArray sig) (SArray arr) = toForeign (map (go (sig unit) <<< ($ unit)) arr) |
| 115 | + go (SigRecord sigs) (SRecord sps) = toForeign (S.fromList (L.toList pairs)) |
| 116 | + where |
| 117 | + pairs :: Array (Tuple String Foreign) |
| 118 | + pairs = zipWith pair (sortBy (compare `on` _.recLabel) sigs) |
| 119 | + (sortBy (compare `on` _.recLabel) sps) |
| 120 | + |
| 121 | + pair sig sp | sig.recLabel == sp.recLabel = Tuple sig.recLabel (go (sig.recValue unit) (sp.recValue unit)) |
| 122 | + | otherwise = unsafeThrow "Record fields do not match signature" |
| 123 | + go (SigProd "Data.Maybe.Maybe" _) (SProd "Data.Maybe.Nothing" []) | maybeAsNull = toForeign (toNullable Nothing) |
| 124 | + go (SigProd "Data.Maybe.Maybe" [{ sigValues: [just] }, _]) (SProd "Data.Maybe.Just" [sp]) | maybeAsNull = go (just unit) (sp unit) |
| 125 | + go (SigProd _ [{ sigConstructor: _, sigValues: [sig] }]) (SProd _ [sp]) | unwrapNewtypes = go (sig unit) (sp unit) |
| 126 | + go (SigProd _ alts) (SProd tag sps) = |
| 127 | + case sumEncoding of |
| 128 | + TaggedObject { tagFieldName, contentsFieldName } -> |
| 129 | + case find (\alt -> alt.sigConstructor == tag) alts of |
| 130 | + Nothing -> unsafeThrow ("No signature for data constructor " <> tag) |
| 131 | + Just { sigValues } -> |
| 132 | + case zipWith (\sig sp -> go (sig unit) (sp unit)) sigValues sps of |
| 133 | + [] -> toForeign (S.fromList (L.singleton (Tuple tagFieldName (toForeign tag)))) |
| 134 | + [f] | unwrapSingleArgumentConstructors -> |
| 135 | + toForeign (S.fromList (L.toList [ Tuple tagFieldName (toForeign tag) |
| 136 | + , Tuple contentsFieldName f |
| 137 | + ])) |
| 138 | + fs -> toForeign (S.fromList (L.toList [ Tuple tagFieldName (toForeign tag) |
| 139 | + , Tuple contentsFieldName (toForeign fs) |
| 140 | + ])) |
| 141 | + go _ _ = unsafeThrow "Invalid spine for signature" |
| 142 | + |
48 | 143 | -- | Read a value which has a `Generic` type from a JSON String |
49 | | -readJSONGeneric :: forall a. (Generic a) => String -> F a |
50 | | -readJSONGeneric = parseJSON >=> readGeneric |
| 144 | +readJSONGeneric :: forall a. (Generic a) => Options -> String -> F a |
| 145 | +readJSONGeneric opts = parseJSON >=> readGeneric opts |
| 146 | + |
| 147 | +-- | Write a value which has a `Generic` type as a JSON String |
| 148 | +toJSONGeneric :: forall a. (Generic a) => Options -> a -> String |
| 149 | +toJSONGeneric opts = toForeignGeneric opts >>> unsafeStringify |
0 commit comments