packages feed

aeson-generics-typescript (empty) → 0.0.0.1

raw patch · 9 files changed

+1196/−0 lines, 9 filesdep +QuickCheckdep +aesondep +base

Dependencies added: QuickCheck, aeson, base, bytestring, containers, directory, filepath, hspec, process, random, split, string-interpolate, text, time

Files

+ LICENSE view
@@ -0,0 +1,11 @@+Copyright 2023 Platonic.Systems++Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,48 @@+# Aeson Generics TypeScript++This project generates TypeScript type definitions using GHC.Generics, that match Aeson instances generated the same way.++```haskell+data Foo = ...+  deriving stock (Generic)+  deriving anyclass (ToJSON, FromJSON, TypeScriptDefinition)+```++Is all it takes to have your TypeScript type definition for your data type. Now you can obtain the TypeScript definition as a string like so.++```haskell+fooTSDef :: String+fooTSDef = getPrintedDefinition $ Proxy @Foo+```++## Example++You can see many examples in the tests. One provided here for documentation purposes:++```haskell+data Sum a b = Foyst a+             | Loser b+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++-- getPrintedDefinition must have a concrete type. If you want to retain+-- generic type variables in TypeScript, use `TSGenericVar` to make the+-- type concrete.+sumTSDef :: String+sumTSDef = getPrintedDefinition $ Proxy @(Sum (TSGenericVar "a") (TSGenericVar "b"))+```++will generate++```TypeScript+// Defined in Data.Aeson.Generics.TypeScript.Types of main+type Sum<A,B> = Foyst<A> | Loser<B>;+interface Foyst<A> {+  readonly tag: "Foyst";+  readonly contents: A;+}+interface Loser<B> {+  readonly tag: "Loser";+  readonly contents: B;+}+```
+ aeson-generics-typescript.cabal view
@@ -0,0 +1,106 @@+cabal-version:      3.0+name:               aeson-generics-typescript+version:            0.0.0.1+synopsis:+  Generates TypeScript definitions that match Generic Aeson encodings++description:+  This project uses GHC.Generics to generate TypeScript type definitions that match the Generic Aseon encoding for the same type.+  Included here are tests that round trip by compling the TypeScript with tsc, and checking that generated values type check.++category:           Web+author:             Platonic.Systems+maintainer:         info@Platonic.Systems+copyright:          2023+license:            BSD-3-Clause+license-file:       LICENSE+build-type:         Simple+extra-source-files: README.md++source-repository head+  type: git+  location: https://gitlab.com/platonic/aeson-generics-typescript.git++common shared+  default-language:   GHC2021+  default-extensions:+    AllowAmbiguousTypes+    ApplicativeDo+    BlockArguments+    CPP+    DataKinds+    DefaultSignatures+    DeriveAnyClass+    DerivingStrategies+    DerivingVia+    DuplicateRecordFields+    ExtendedDefaultRules+    FunctionalDependencies+    GADTs+    ImpredicativeTypes+    LambdaCase+    LinearTypes+    MultiWayIf+    OverloadedLabels+    OverloadedStrings+    PartialTypeSignatures+    PatternSynonyms+    QuantifiedConstraints+    RecordWildCards+    RoleAnnotations+    TypeFamilies+    TypeFamilyDependencies+    UndecidableInstances+    ViewPatterns++library+  import:          shared+  hs-source-dirs:  src+  exposed-modules: Data.Aeson.Generics.TypeScript+  hs-source-dirs:  src+  ghc-options:+    -Weverything -Wno-all-missed-specialisations -Wno-implicit-prelude+    -Wno-missing-local-signatures -Wno-missing-import-lists+    -Wno-missing-export-lists -Wno-missing-safe-haskell-mode+    -Wno-prepositive-qualified-module -Wno-safe -Wno-unsafe++  build-depends:+    , base+    , containers+    , string-interpolate+    , text+    , time++executable tests+  import:         shared+  main-is:        Main.hs+  hs-source-dirs: test src+  ghc-options:+    -Wall -Wcompat -fwarn-redundant-constraints+    -fwarn-incomplete-uni-patterns -fwarn-tabs+    -fwarn-incomplete-record-updates -fwarn-identities -threaded+    -fno-warn-missing-home-modules -rtsopts -freduction-depth=1000+    -with-rtsopts=-N +RTS -H2G -A32M -RTS++  build-depends:+    , aeson               >=2.1.2  && <2.2+    , base                >=4.17.2 && <4.18+    , bytestring          >=0.11.5 && <0.12+    , containers          >=0.6.7  && <0.7+    , directory           >=1.3.7  && <1.4+    , filepath            >=1.4.2  && <1.5+    , hspec               >=2.11.7 && <2.12+    , process             >=1.6.17 && <1.7+    , QuickCheck          >=2.14.3 && <2.15+    , random              >=1.2.1  && <1.3+    , split               >=0.2.4  && <0.3+    , string-interpolate  >=0.3.2  && <0.4+    , text                >=2.0.2  && <2.1+    , time                >=1.12.2 && <1.13++  other-modules:+    Data.Aeson.Generics.TypeScript+    Data.Aeson.Generics.TypeScript.AesonSpec+    Data.Aeson.Generics.TypeScript.ASTSpec+    Data.Aeson.Generics.TypeScript.PrintSpec+    Data.Aeson.Generics.TypeScript.Types
+ src/Data/Aeson/Generics/TypeScript.hs view
@@ -0,0 +1,401 @@+{-# LANGUAGE IncoherentInstances #-}+{-# LANGUAGE QuasiQuotes         #-}+module Data.Aeson.Generics.TypeScript+  ( -- * Primary generation functions+    getPrintedDefinition+  , printTS+    -- * Type Classes+  , FieldTypeName (..)+  , TypeScriptDefinition (..)+    -- * TypeScript AST data types+  , FieldSpec (..)+  , FieldType (..)+  , IsNewtype (..)+  , TSField (..)+  , TSGenericVar+  , TSInterface (..)+  , TSType (..)+    -- * Convenience builders+  , concretely+  , genericly+  ) where++import           Data.Char (toUpper)+import           Data.Containers.ListUtils (nubOrd)+import           Data.Data (Proxy (..))+import           Data.Kind (Constraint, Type)+import           Data.List (intercalate)+import           Data.List.NonEmpty (NonEmpty, toList)+import           Data.Map (Map)+import           Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe)+import           Data.String.Interpolate (i)+import qualified Data.Text as T+import           Data.Time.Clock (UTCTime)+import           GHC.Generics+  ( C1+  , D1+  , Generic (Rep)+  , Meta (MetaCons, MetaData, MetaSel)+  , Rec0+  , S1+  , U1+  , type (:*:)+  , type (:+:)+  )+import           GHC.TypeLits+  ( ErrorMessage (ShowType, Text, (:$$:), (:<>:))+  , KnownSymbol+  , Symbol+  , TypeError+  , symbolVal+  )++-- | Type level rep of a named generic type variable+type TSGenericVar :: Symbol -> Type+data TSGenericVar s++-- | Determine if this is a newtype and will not be wrapped+type IsNewtype :: Type+data IsNewtype+  = Newtype+  | Oldtype+  deriving stock (Eq, Ord, Show)++-- | The top level TypeScript type declaration+type TSType :: Type+data TSType = TSType+  { tst_constructor :: !String+  , tst_doc         :: !String+  , tst_interfaces  :: !(NonEmpty TSInterface)+  , tst_newtype     :: !IsNewtype+  }+  deriving stock (Eq, Ord, Show)++-- | A term constructor in Haskell, most likely an interface in TypeScript+type TSInterface :: Type+data TSInterface = TSInterface+  { tsi_constructor :: !String+  , tsi_typeName    :: !(Maybe String)+  , tsi_members     :: ![TSField]+  }+  deriving stock (Eq, Ord, Show)++-- | Fields can be concrete types, or generic type variables+type FieldType :: Type+data FieldType+  = GenericField+  | ConcreteField+  deriving stock (Bounded, Enum, Eq, Ord, Show)+instance Semigroup FieldType where+  GenericField <> _ = GenericField+  _ <> GenericField = GenericField+  _ <> _            = ConcreteField++-- | A field within a term constructor+type TSField :: Type+data TSField = TSField+  { fieldName :: !(Maybe String)+  , fieldType :: !FieldSpec+  }+  deriving stock (Eq, Ord, Show)++-- | Helper for printing fields+type FieldSpec :: Type+data FieldSpec = FieldSpec+  { fs_type      :: !FieldType+  , fs_wrapped   :: !String+  , fs_unwrapped :: !String+  }+  deriving stock (Eq, Ord, Show)++-- | Construct a FieldSpec assuming standard use and a concrete type variable+concretely :: String -> FieldSpec+concretely x = FieldSpec ConcreteField x x++-- | Construct a FieldSpec assuming standard use and a generic type variable+genericly :: String -> FieldSpec+genericly x = FieldSpec GenericField x x++-- | Typeclass to determine the FieldSpec from a payload's type+type FieldTypeName :: a -> Constraint+class FieldTypeName a where+  fieldTypeName :: Proxy a -> FieldSpec++-- | Lists are Arrays according to Aeson+instance FieldTypeName a => FieldTypeName [a] where+  fieldTypeName _ = let+      FieldSpec t wrapped unwrapped = fieldTypeName $ Proxy @a+    in FieldSpec t ("Array<" <> wrapped <> ">") unwrapped++-- | Handle wrapped payload+instance FieldTypeName a => FieldTypeName (Rec0 a) where+  fieldTypeName _ = fieldTypeName $ Proxy @a++-- | This needs to overlap so it doesn't get treated as an Array+instance {-# OVERLAPS #-} FieldTypeName String where+  fieldTypeName _ = concretely "string"++instance FieldTypeName UTCTime where+  fieldTypeName _ = concretely "string"++instance FieldTypeName T.Text where+  fieldTypeName _ = concretely "string"++instance (FieldTypeName a, FieldTypeName b) =>  FieldTypeName (Either a b) where+  fieldTypeName _ =+    let a = fieldTypeName (Proxy @a)+        b = fieldTypeName (Proxy @b)+        eType l r = [i|{ Left: #{l} } | { Right: #{r} }|]+    in FieldSpec (fs_type a <> fs_type b) (fs_wrapped a `eType` fs_wrapped b) (fs_unwrapped a `eType` fs_unwrapped b)++instance (FieldTypeName a, FieldTypeName b) => FieldTypeName (Map a b) where+  fieldTypeName _ = FieldSpec (fs_type a <> fs_type b) asMap $ fs_unwrapped a <> "," <> fs_unwrapped b+    where+      a = fieldTypeName $ Proxy @a+      b = fieldTypeName $ Proxy @b+      wrappedB = fs_wrapped b+      asMap = [i|{ [key: string]: #{wrappedB} }|]++instance FieldTypeName Int where fieldTypeName _ = concretely "number"+instance FieldTypeName Integer where fieldTypeName _ = concretely "number"+instance FieldTypeName Float where fieldTypeName _ = concretely "number"+instance FieldTypeName Bool where fieldTypeName _ = concretely "boolean"+instance FieldTypeName a => FieldTypeName (Maybe a) where+  fieldTypeName _ = inner { fs_wrapped = fs_wrapped inner <> " | null" }+    where inner = fieldTypeName (Proxy @a)++instance FieldTypeName () where+  fieldTypeName _ = concretely "[]"++instance {-# OVERLAPPABLE #-} TypeScriptDefinition a => FieldTypeName a where+  fieldTypeName _ = let x = gen @a in ly x $ tst_constructor x where+    ly TSType {..} = if any ((== GenericField) . fs_type . fieldType) . mconcat . toList $ tsi_members <$> tst_interfaces+                     then genericly else concretely++instance KnownSymbol s => FieldTypeName (TSGenericVar s) where+  fieldTypeName _ = genericly . cap . symbolVal $ Proxy @s+    where cap (x:xs) = toUpper x : xs+          cap []     = []++-- | This typeclass provides the ability to derive a TSType from any Generic data type+type TypeScriptDefinition :: Type -> Constraint+class TypeScriptDefinition a where+  gen :: TSType+  default gen ::+    ( TSType ~ GTypeScriptTail (Rep a)+    , GTypeScriptDef (Rep a)) => TSType+  gen = ggen $ Proxy @(Rep a)++-- | Custom error for missing TypeScriptDefinition's, as they can be a red herring+instance TypeError+  ('Text "No instance of TypeScriptDefinition found for: " ':<>: 'ShowType a ':$$:+   'Text "💠 If you are seeing this for a newtype of something primitive, derive FieldTypeName instead.") => TypeScriptDefinition a where+  gen = error "unreachable"++-- | Generic deriving mechanism for TypeScriptDefinition+type GTypeScriptDef :: a -> Constraint+class GTypeScriptDef a where+  type GTypeScriptTail a :: Type+  ggen :: Proxy a -> GTypeScriptTail a++-- | This is the top level of the Generic structure, D1, which holds top level 'Metadata+instance+  ( KnownSymbol name+  , KnownSymbol module'+  , KnownSymbol package+  , GTypeScriptDef u+  , GTypeScriptTail u ~ NonEmpty TSInterface+  , isNew `DegradesTo` Bool+  ) => GTypeScriptDef (D1 ('MetaData name module' package isNew) u) where+  type GTypeScriptTail (D1 ('MetaData name module' package isNew) u) = TSType+  ggen _ = TSType+    (symbolVal (Proxy @name))+    ("Defined in " <> symbolVal (Proxy @module') <> " of " <> symbolVal (Proxy @package))+    (ggen (Proxy @u))+    (if degrade (Proxy @isNew) then Newtype else Oldtype)++-- | Handler for Generic constructors, which we convert to @TSInterfaces@+instance+  ( KnownSymbol name+  , GTypeScriptDef u+  , GTypeScriptTail u ~ [TSField]+  ) => GTypeScriptDef (C1 ('MetaCons name fixity hasNames) u) where+  type GTypeScriptTail (C1 ('MetaCons name fixity hasNames) u) = NonEmpty TSInterface+  ggen _ = pure $ TSInterface (symbolVal (Proxy @name)) Nothing (checkTSFields $ ggen $ Proxy @u)+    where+    -- Sanity checker @TSField@, this is useful to ensure invariants assumed by JavaScript objects+    checkTSFields ts = let+        uniqueFieldNames = nubOrd $ fieldName <$> ts+      in if length uniqueFieldNames /= length ts+         && not (all (isNothing . fieldName) ts)+      then error $ "record field names are not unique : " <> show ts+      else ts++instance GTypeScriptDef U1 where+  type GTypeScriptTail U1 = [TSField]+  ggen _ = []++instance+  ( FieldTypeName u, w `DegradesTo` Maybe String+  )=> GTypeScriptDef (S1 ('MetaSel w x y z) u) where+  type GTypeScriptTail (S1 ('MetaSel w x y z) u) = [TSField]+  ggen _ = pure $ TSField+    { fieldName = degrade $ Proxy @w+    , fieldType = fieldTypeName $ Proxy @u+    }++instance+  ( GTypeScriptTail x ~ NonEmpty TSInterface+  , GTypeScriptTail y ~ NonEmpty TSInterface+  , GTypeScriptDef x, GTypeScriptDef y+  ) => GTypeScriptDef (x :+: y) where+  type GTypeScriptTail (x :+: y) = NonEmpty TSInterface+  ggen _ = ggen (Proxy @x) <> ggen (Proxy @y)++instance+  ( GTypeScriptTail a ~ [TSField]+  , GTypeScriptTail b ~ [TSField]+  , GTypeScriptDef a, GTypeScriptDef b+  ) => GTypeScriptDef (a :*: b) where+  type GTypeScriptTail (a :*: b) = [TSField]+  ggen _ = ggen (Proxy @a) <> ggen (Proxy @b)++printTS :: TSType -> String+printTS TSType{..} =+    [i|// #{tst_doc}+#{typeDecl}|] <> if isEnum || isPureProduct then "" else+      (if isSingleRecord then id else mappend "\n") interfaces+    where+      -- All the type variables found+      vars :: [TSField]+      vars = mconcat . toList $ tsi_members <$> tst_interfaces++      -- The generic variables in TypeScript syntax IE <A,B>+      -- These must be unique+      generics :: String+      generics = mkGenericVars $ nubOrd vars++      -- The constructors of the original haskell data type as our AST+      constructors :: [(String, [TSField])]+      constructors = toList $ (\x -> (tsi_constructor x,tsi_members x)) <$> tst_interfaces++      -- The interfaces associated with the constructors in TypeScript syntax+      interfaces :: String+      interfaces = if null tst_interfaces then "" else+        intercalate "\n" . toList $ printTSInterface . hackInTypeName <$> tst_interfaces++      -- Is this going to be a special variant for Aeson?+      isUnit, isSingleRecord, isPureProduct, isEnum :: Bool+      isUnit = length constructors == 1 && isEnum+      isSingleRecord = length constructors == 1 && all (all (isJust . fieldName) . tsi_members) tst_interfaces+      isPureProduct = length constructors == 1+        && all (all (isNothing . fieldName) . tsi_members) tst_interfaces+      isEnum = all (\TSInterface{..} -> null tsi_members) tst_interfaces++      -- When we are in a single record context, the interface gets named as the+      -- name of the type, not after the term constructor like normal. So we hack it in with an override+      hackInTypeName :: TSInterface -> TSInterface+      hackInTypeName face = if isSingleRecord then face { tsi_typeName = Just tst_constructor } else face++      -- The declaration of the type, if not a single record (which is just the inner interface)+      typeDecl :: String+      typeDecl = if isSingleRecord && not isUnit then "" :: String else+        [i|type #{tst_constructor}#{generics} = #{transObj};|] where+          transObj+            -- Aeson says so+            | isUnit = "[]"+            -- Its a newtype+            | tst_newtype == Newtype = case vars of+                              [TSField {..}] -> fs_wrapped fieldType+                              _ -> error $ "newtype wrong number of fields: " <> show vars+            -- This is a data type with mulitple fields and only one constructor, and so is a big tuple+            | isPureProduct = "[" <> intercalate ", " ((\TSField {..} -> fs_wrapped fieldType) <$> vars) <> "]"+            -- Its a union type+            | otherwise = intercalate " | "+                  . fmap (if isEnum then (\x ->  "\"" <> x <> "\"") else id)+                  $ (\(c,ms) -> c <> mkGenericVars ms) <$> constructors++printTSInterface :: TSInterface -> String+printTSInterface TSInterface{..} = [i|interface #{typeName}#{generics} {+  #{tag}readonly tag: "#{tsi_constructor}";|]+    <> (if null contents then "" else "\n" <> contents)+    <> "\n}" where+     -- The name of the type for use in the top of the interface declaration+     -- This is different if we are in a single record context. When we are a single+     -- record, the interface needs to be the type name, not the term constructor name+     typeName :: String+     typeName = fromMaybe tsi_constructor tsi_typeName++     -- If we are in the single record context, we leave the constructor name+     -- around as a code comment for debugging+     tag :: String+     tag = if isJust tsi_typeName then "// " else ""++     -- Make list of variables for contents+     unnamed :: String+     unnamed = flip mappend ";" $+       if length ms > 1 then "[" <> intercalate ", " ms <> "]" else mconcat ms+       where ms = fs_wrapped . fieldType <$> tsi_members++     -- Payload of the interface+     contents :: String+     contents = if isRecord then intercalate "\n" $ namedField <$> tsi_members+                            else "  readonly contents: " <> unnamed++     -- Do all fields have names?+     isRecord :: Bool+     isRecord = all (isJust . fieldName) tsi_members++     -- Build one named field+     namedField :: TSField -> String+     namedField TSField {..} = "  readonly " <> fieldName' <> ": " <> fieldType' <> ";"+       where fieldName' = fromMaybe (error "field name was not found") fieldName+             fieldType' = fs_wrapped fieldType++     -- Generics of the system+     generics :: String+     generics = mkGenericVars tsi_members++mkGenericVars :: [TSField] -> String+mkGenericVars xs = if null vars then "" else "<" <> intercalate "," vars <> ">"+  where+  vars = mapMaybe (go . fieldType) xs+  go = \case x@(FieldSpec GenericField _ _) -> Just $ fs_unwrapped x; _ -> Nothing++-- | degrade :: 'Maybe Symbol -> Maybe String+type Degrade :: k -> Constraint+class Degrade (w :: k) where+  type Degraded w :: Type+  degrade :: Proxy w -> Degraded w+instance KnownSymbol s => Degrade ('Just s) where+  type Degraded ('Just s) = Maybe String+  degrade _ = Just $ symbolVal $ Proxy @s+instance Degrade 'Nothing where+  type Degraded 'Nothing = Maybe String+  degrade _ = Nothing+instance Degrade 'True where+  type Degraded  'True = Bool+  degrade _ =     True+instance Degrade 'False where+  type Degraded  'False = Bool+  degrade _ =     False+instance Degrade 'Newtype where+  type Degraded  'Newtype = IsNewtype+  degrade _ =     Newtype+instance Degrade 'Oldtype where+  type Degraded  'Oldtype = IsNewtype+  degrade _ =     Oldtype++type DegradesTo :: k -> Type -> Constraint+type DegradesTo x t = (Degraded x ~ t, Degrade x)++-- | Get the TypeScriptDefinition as a String+getPrintedDefinition :: forall a. TypeScriptDefinition a => Proxy a -> String+getPrintedDefinition _ = printTS $ gen @a++-- This is present as debugging tool+type Foo :: Type -> Type+data Foo a = Foo+           | Bar Int+  deriving stock (Eq, Generic, Ord, Read, Show)+  deriving anyclass (TypeScriptDefinition)
+ test/Data/Aeson/Generics/TypeScript/ASTSpec.hs view
@@ -0,0 +1,157 @@+{-# OPTIONS_GHC -Wno-orphans #-}+module Data.Aeson.Generics.TypeScript.ASTSpec+  ( main+  , spec+  ) where++import           Data.Aeson.Generics.TypeScript+  ( FieldSpec (FieldSpec, fs_wrapped)+  , FieldType (ConcreteField, GenericField)+  , IsNewtype (Newtype, Oldtype)+  , TSField (TSField)+  , TSGenericVar+  , TSInterface (TSInterface)+  , TSType (TSType)+  , TypeScriptDefinition (..)+  , concretely+  , genericly+  )+import           Data.Aeson.Generics.TypeScript.Types+  ( CouldBe+  , GenericRecordInSum+  , HasEither+  , ItsEnum+  , ItsRecord+  , ItsRecordWithGeneric+  , MapParty+  , NewIdentity+  , Prod+  , RecordWithWrappedType+  , Sum+  , Unit+  , definedIn+  )+import           Data.List.NonEmpty (NonEmpty ((:|)))+import           Data.Time.Clock (UTCTime)+import           Test.Hspec (Spec, hspec, it, shouldBe)+import           Test.Hspec.QuickCheck (prop)+import           Test.QuickCheck (Arbitrary (..), arbitraryBoundedEnum)++instance Arbitrary FieldType where arbitrary = arbitraryBoundedEnum++-- | Form a list like thing as a product+(@:|) :: (Applicative f, Semigroup (f p)) => p -> p -> f p+x @:| y = pure x <> pure y++main :: IO ()+main = hspec spec++unitExpected :: TSType+unitExpected =+  TSType "Unit" definedIn+  (pure $ TSInterface "MkUnit" Nothing []) Oldtype++mkCouldBe :: FieldSpec -> TSType+mkCouldBe x =+    TSType "CouldBe" definedIn+    (   TSInterface "ForSure" Nothing [TSField Nothing x]+    @:| TSInterface "Nah" Nothing []) Oldtype++mkSum :: FieldSpec -> FieldSpec -> TSType+mkSum x y =+  TSType "Sum" definedIn+    (TSInterface "Foyst" Nothing [TSField Nothing x]+    @:| TSInterface "Loser" Nothing [TSField Nothing y]) Oldtype++mkProd :: FieldSpec -> FieldSpec -> FieldSpec -> TSType+mkProd x y z =+  TSType "Prod" definedIn+    (pure $ TSInterface "MkProd" Nothing [TSField Nothing x, TSField Nothing y, TSField Nothing z]) Oldtype++itsEnumExpected :: TSType+itsEnumExpected =+  TSType "ItsEnum" definedIn+    (    TSInterface "One" Nothing []+    :| [ TSInterface "Two" Nothing [], TSInterface "Three" Nothing [] ]) Oldtype++itsRecordExpected :: TSType+itsRecordExpected =+  TSType "ItsRecord" definedIn (pure+  $ TSInterface "MkItsRecord" Nothing+    [ TSField (Just "oneThing") (concretely "number")+    , TSField (Just "twoThing") (concretely "string")+    , TSField (Just "threeThing") (concretely "[]")+    ]) Oldtype++itsRecordWithGenericExpected :: TSType+itsRecordWithGenericExpected =+  TSType "ItsRecordWithGeneric" definedIn (pure+  $ TSInterface "MkItsRecordWithGeneric" Nothing+    [ TSField (Just "oneThing")   $ concretely "number"+    , TSField (Just "twoThing")   $ (concretely "string") { fs_wrapped = "string | null" }+    , TSField (Just "threeThing") $ genericly "A"+    ]) Oldtype++genericRecordInSumExpected :: TSType+genericRecordInSumExpected =+  TSType "GenericRecordInSum" definedIn+  ( TSInterface "OtherThing" Nothing []+  @:| TSInterface "MkGenericRecordInSum" Nothing+    [ TSField (Just "oneThing") (concretely "number")+    , TSField (Just "twoThing") (FieldSpec GenericField "Array<A>" "A")+    ]) Oldtype++recordWithWrappedTypeExpected :: TSType+recordWithWrappedTypeExpected =+  TSType "RecordWithWrappedType" definedIn+  (pure $ TSInterface "RecordWithWrappedType" Nothing+    [ TSField (Just "oneThing") (concretely "number")+    , TSField (Just "twoThing") (FieldSpec ConcreteField "Array<string>" "string")+    ]) Oldtype++digitalExpected :: TSType+digitalExpected =+  TSType "NewIdentity" definedIn+  ( pure $ TSInterface "NewIdentity" Nothing+    [ TSField Nothing (genericly "A")+    ]) Newtype++mapPartyIntStringExpected :: TSType+mapPartyIntStringExpected =+  TSType "MapParty" definedIn+  (pure $ TSInterface "MapParty" Nothing [+     TSField Nothing $ FieldSpec ConcreteField "{ [key: string]: string }" "number,string" ]) Newtype++hasEitherExpected :: TSType+hasEitherExpected =+  TSType "HasEither" definedIn+  (pure $ TSInterface "HasEither" Nothing [+    TSField (Just "notTheEither") $ FieldSpec ConcreteField "number" "number"+  , TSField (Just "theEither") $ FieldSpec ConcreteField+    "{ Left: string } | { Right: boolean }"+    "{ Left: string } | { Right: boolean }"+  ]) Oldtype++spec :: Spec+spec = do+  prop "FieldType Semigroup" \x y z -> x <> (y <> z) == (x <> y) <> (z :: FieldType)+  it "Unit" $ gen @Unit `shouldBe` unitExpected+  it "CouldBe a" $ gen @(CouldBe (TSGenericVar "a")) `shouldBe` mkCouldBe (genericly "A")+  it "CouldBe ()" $ gen @(CouldBe ()) `shouldBe` mkCouldBe (concretely "[]")+  it "CouldBe Int" $ gen @(CouldBe Int) `shouldBe` mkCouldBe (concretely "number")+  it "CouldBe String" $ gen @(CouldBe String) `shouldBe` mkCouldBe (concretely "string")+  it "CouldBe UTCTime" $ gen @(CouldBe UTCTime) `shouldBe` mkCouldBe (concretely "string")+  it "Sum Int ()" $ gen @(Sum Int ()) `shouldBe` mkSum (concretely "number") (concretely "[]")+  it "Sum () String" $ gen @(Sum () String) `shouldBe` mkSum (concretely "[]") (concretely "string")+  it "Sum a a" $ gen @(Sum (TSGenericVar "a") (TSGenericVar "a")) `shouldBe` mkSum (genericly "A") (genericly "A")+  it "Prod Int () String" $ gen @(Prod Int () String) `shouldBe` mkProd (concretely "number") (concretely "[]") (concretely "string")+  it "Prod a a a" $ gen @(Prod (TSGenericVar "a") (TSGenericVar "a") (TSGenericVar "a")) `shouldBe` mkProd (genericly "A") (genericly "A") (genericly "A")+  it "ItsEnum" $ gen @ItsEnum `shouldBe` itsEnumExpected+  it "ItsRecord" $ gen @ItsRecord `shouldBe` itsRecordExpected+  it "ItsRecordWithGeneric" $ gen @(ItsRecordWithGeneric (TSGenericVar "a")) `shouldBe` itsRecordWithGenericExpected+  it "GenericRecordInSum" $ gen @(GenericRecordInSum (TSGenericVar "a")) `shouldBe` genericRecordInSumExpected+  it "RecordWithWrappedType" $ gen @RecordWithWrappedType `shouldBe` recordWithWrappedTypeExpected+  it "NewIdentity" $ gen @(NewIdentity (TSGenericVar "a")) `shouldBe` digitalExpected+  it "MapParty Int String" $ gen @(MapParty Int String) `shouldBe` mapPartyIntStringExpected+  it "HasEither" $ gen @HasEither `shouldBe` hasEitherExpected+
+ test/Data/Aeson/Generics/TypeScript/AesonSpec.hs view
@@ -0,0 +1,208 @@+{-# LANGUAGE QuasiQuotes #-}++module Data.Aeson.Generics.TypeScript.AesonSpec+  ( main+  , spec+  ) where++import           Control.Monad (when)+import           Data.Aeson (encode)+import           Data.Aeson.Generics.TypeScript+  ( TSGenericVar+  , TypeScriptDefinition (..)+  , printTS+  )+import           Data.Aeson.Generics.TypeScript.Types+  ( CouldBe (ForSure)+  , GenericRecordInSum (MkGenericRecordInSum)+  , GotTime (..)+  , HasEither (HasEither)+  , ItsEnum (Two)+  , ItsRecord (MkItsRecord)+  , ItsRecordWithGeneric (MkItsRecordWithGeneric)+  , MapParty (..)+  , NewIdentity (..)+  , NewString (MkNewString)+  , Prod (..)+  , RecordWithWrappedType (RecordWithWrappedType)+  , Sum (Foyst)+  , Unit (..)+  )+import           Data.ByteString.Lazy.Char8 (unpack)+import           Data.List.Split (splitOn)+import           Data.Map (Map)+import qualified Data.Map as Map+import           Data.String.Interpolate (i)+import           Data.Time.Clock (getCurrentTime)+import           System.Directory (getTemporaryDirectory, removeFile)+import           System.Exit (ExitCode (ExitFailure))+import           System.FilePath ((<.>), (</>))+import           System.Process (readProcessWithExitCode)+import           System.Random (randomIO)+import           Test.Hspec (Spec, hspec, it, parallel, runIO)++main :: IO ()+main = hspec spec++spec :: Spec+spec = parallel do+  utcTime <- runIO getCurrentTime+  it "Unit" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @Unit)+    encoded = unpack (encode MkUnit)+    typeName = "Unit" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "CouldBe a" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @(CouldBe (TSGenericVar "a")))+    encoded = unpack (encode $ ForSure ())+    typeName = "CouldBe<[]>" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "GOTTime" . shouldSatisfyTypeScriptCompiler $ let+    encoded = unpack (encode $ GotTime utcTime)+    typeDecl = printTS (gen @GotTime)+    typename = "GotTime" :: String+    in [i|#{typeDecl}+const sample : #{typename} = #{encoded};|]++  it "Sum a b" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @(Sum (TSGenericVar "a") (TSGenericVar "b")))+    encoded = unpack (encode (Foyst 3 :: Sum Int String))+    typeName = "Sum<number,string>" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "Sum a a" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @(Sum (TSGenericVar "a") (TSGenericVar "a")))+    encoded = unpack (encode (Foyst 3 :: Sum Int Int))+    typeName = "Sum<number>" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "Prod a a a" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @(Prod (TSGenericVar "a") (TSGenericVar "a") (TSGenericVar "a")))+    encoded = unpack (encode (MkProd 3 4 5 :: Prod Int Int Int))+    typeName = "Prod<number>" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "ItsEnum" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @ItsEnum)+    encoded = unpack (encode Two)+    typeName = "ItsEnum" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "ItsRecord" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @ItsRecord)+    encoded = unpack (encode $ MkItsRecord 3 "wat" ())+    typeName = "ItsRecord" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "ItsRecordWithGeneric Just" $ do+   let+    typeDecl = printTS (gen @(ItsRecordWithGeneric (TSGenericVar "a")))+    encoded = unpack (encode $ MkItsRecordWithGeneric 3 (Just "foo") ())+    typeName = "ItsRecordWithGeneric<[]>" :: String+    thang = [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]+   shouldSatisfyTypeScriptCompiler thang++  it "ItsRecordWithGeneric Nothing" $ do+   let+    typeDecl = printTS (gen @(ItsRecordWithGeneric (TSGenericVar "a")))+    encoded = unpack (encode $ MkItsRecordWithGeneric 3 Nothing ())+    typeName = "ItsRecordWithGeneric<[]>" :: String+    thang = [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]+   shouldSatisfyTypeScriptCompiler thang++  it "GenericRecordInSum" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @(GenericRecordInSum (TSGenericVar "a")))+    encoded = encode $ MkGenericRecordInSum 3 ["foo" :: String,"bar"]+    typeName = "GenericRecordInSum<string>" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "RecordWithWrappedType" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @RecordWithWrappedType)+    encoded = encode $ RecordWithWrappedType 3 [MkNewString "foo", MkNewString "bar"]+    typeName = "RecordWithWrappedType" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "NewIdentity" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @(NewIdentity (TSGenericVar "a")))+    encoded = encode (NewIdentity 3 :: NewIdentity Int)+    typeName = "NewIdentity<number>" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "MapParty Int String" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @(MapParty Int String))+    encoded = encode $ MapParty (Map.fromList [(1,"foo"),(2,"bar"),(10,"baz")] :: Map Int String)+    typeName = "MapParty" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "MapParty a b" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @(MapParty (TSGenericVar "a") (TSGenericVar "b")))+    encoded = encode $ MapParty (Map.fromList [(1,"foo"),(2,"bar"),(10,"baz")] :: Map Int String)+    typeName = "MapParty<number,string>" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "MapParty memtpy" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @(MapParty (TSGenericVar "a") (TSGenericVar "b")))+    encoded = encode $ MapParty (Map.fromList [(3,"what"),(7,"frog"),(12,"wat")] :: Map Int String)+    typeName = "MapParty<number,string>" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "HasEither Left" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @HasEither)+    encoded = encode . HasEither 3 $ Left "foo"+    typeName = "HasEither" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++  it "HasEither Right" . shouldSatisfyTypeScriptCompiler $ let+    typeDecl = printTS (gen @HasEither)+    encoded = encode . HasEither 3 $ Right True+    typeName = "HasEither" :: String+    in [i|#{typeDecl}+const sample : #{typeName} = #{encoded};|]++showLineNumbers :: Bool+showLineNumbers = True++shouldSatisfyTypeScriptCompiler :: String -> IO ()+shouldSatisfyTypeScriptCompiler ts = do+  tmpDir :: FilePath <- getTemporaryDirectory+  now <- getCurrentTime+  rand <- randomIO :: IO Int+  let filePath = tmpDir </> show now <> show (rand * 1000000) <.> "ts"+  writeFile filePath ts+  res <- readProcessWithExitCode "tsc" [filePath, "--outFile", filePath <.> "js" ] ""+  removeFile filePath+  removeFile $ filePath <.> "js"+  case res of+    (ExitFailure ef, out, err) -> do+      putStrLn "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"+      putStrLn $ addLineNumbers ts+      putStrLn "┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈"+      putStrLn out+      when (not (null err)) $ putStrLn err+      putStrLn "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"+      error $ "TSC exited with " <> show ef+    _ -> return ()++addLineNumbers :: String -> String+addLineNumbers ts =+  if showLineNumbers then foldMap (\(x,ln) ->+    let lnf = show (ln :: Int) in "\n " <> lnf <> replicate (4 - length lnf) ' ' <> "|" <> x) $ zip (splitOn "\n" ts) [1..]+  else ts
+ test/Data/Aeson/Generics/TypeScript/PrintSpec.hs view
@@ -0,0 +1,165 @@+{-# LANGUAGE QuasiQuotes #-}++module Data.Aeson.Generics.TypeScript.PrintSpec+  ( main+  , spec+  ) where++import           Data.Aeson.Generics.TypeScript+  ( TSGenericVar+  , TypeScriptDefinition (..)+  , printTS+  )+import           Data.Aeson.Generics.TypeScript.Types+  ( CouldBe+  , GenericRecordInSum+  , GotTime+  , HasEither+  , ItsEnum+  , ItsRecord+  , ItsRecordWithGeneric+  , NewIdentity+  , Prod+  , RecordWithWrappedType+  , Sum+  , Unit+  , definedIn+  )+import           Data.String.Interpolate (i)+import           Test.Hspec (Spec, hspec, it, shouldBe)++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+  it "Unit" $ printTS (gen @Unit) `shouldBe`+      [i|// #{definedIn}+type Unit = [];|]++  it "GotTime" $ printTS (gen @GotTime) `shouldBe`+    [i|// #{definedIn}+interface GotTime {+  // readonly tag: "GotTime";+  readonly unGotTime: string;+}|]++  it "CouldeBe a" $ printTS (gen @(CouldBe (TSGenericVar "a"))) `shouldBe`+      [i|// #{definedIn}+type CouldBe<A> = ForSure<A> | Nah;+interface ForSure<A> {+  readonly tag: "ForSure";+  readonly contents: A;+}+interface Nah {+  readonly tag: "Nah";+}|]++  it "CouldeBe Int" $ printTS (gen @(CouldBe Int)) `shouldBe`+      [i|// #{definedIn}+type CouldBe = ForSure | Nah;+interface ForSure {+  readonly tag: "ForSure";+  readonly contents: number;+}+interface Nah {+  readonly tag: "Nah";+}|]++  it "Sum () String" $ printTS (gen @(Sum () String)) `shouldBe`+      [i|// #{definedIn}+type Sum = Foyst | Loser;+interface Foyst {+  readonly tag: "Foyst";+  readonly contents: [];+}+interface Loser {+  readonly tag: "Loser";+  readonly contents: string;+}|]++  it "Sum a b" $ printTS (gen @(Sum (TSGenericVar "a") (TSGenericVar "b"))) `shouldBe`+      [i|// #{definedIn}+type Sum<A,B> = Foyst<A> | Loser<B>;+interface Foyst<A> {+  readonly tag: "Foyst";+  readonly contents: A;+}+interface Loser<B> {+  readonly tag: "Loser";+  readonly contents: B;+}|]++  it "Sum a a" $ printTS (gen @(Sum (TSGenericVar "a") (TSGenericVar "a"))) `shouldBe`+      [i|// #{definedIn}+type Sum<A> = Foyst<A> | Loser<A>;+interface Foyst<A> {+  readonly tag: "Foyst";+  readonly contents: A;+}+interface Loser<A> {+  readonly tag: "Loser";+  readonly contents: A;+}|]++  it "Prod a b" $ printTS (gen @(Prod (TSGenericVar "a") (TSGenericVar "b") (TSGenericVar "c"))) `shouldBe`+      [i|// #{definedIn}+type Prod<A,B,C> = [A, B, C];|]++  it "Prod a a" $ printTS (gen @(Prod (TSGenericVar "a") (TSGenericVar "a") (TSGenericVar "a"))) `shouldBe`+      [i|// #{definedIn}+type Prod<A> = [A, A, A];|]++  it "ItsEnum" $ printTS (gen @ItsEnum) `shouldBe`+      [i|// #{definedIn}+type ItsEnum = "One" | "Two" | "Three";|]++  it "ItsRecord" $ printTS (gen @ItsRecord) `shouldBe`+      [i|// #{definedIn}+interface ItsRecord {+  // readonly tag: "MkItsRecord";+  readonly oneThing: number;+  readonly twoThing: string;+  readonly threeThing: [];+}|]++  it "ItsRecordWithGeneric" $ printTS (gen @(ItsRecordWithGeneric (TSGenericVar "a"))) `shouldBe`+      [i|// #{definedIn}+interface ItsRecordWithGeneric<A> {+  // readonly tag: "MkItsRecordWithGeneric";+  readonly oneThing: number;+  readonly twoThing: string | null;+  readonly threeThing: A;+}|]++  it "RecordWithWrappedType" $ printTS (gen @RecordWithWrappedType) `shouldBe`+      [i|// #{definedIn}+interface RecordWithWrappedType {+  // readonly tag: "RecordWithWrappedType";+  readonly oneThing: number;+  readonly twoThing: Array<string>;+}|]++  it "GenericRecordInSum" $ printTS (gen @(GenericRecordInSum (TSGenericVar "a"))) `shouldBe`+      [i|// #{definedIn}+type GenericRecordInSum<A> = OtherThing | MkGenericRecordInSum<A>;+interface OtherThing {+  readonly tag: "OtherThing";+}+interface MkGenericRecordInSum<A> {+  readonly tag: "MkGenericRecordInSum";+  readonly oneThing: number;+  readonly twoThing: Array<A>;+}|]++  it "HasEither" $ printTS (gen @HasEither) `shouldBe`+      [i|// #{definedIn}+interface HasEither {+  // readonly tag: "HasEither";+  readonly notTheEither: number;+  readonly theEither: { Left: string } | { Right: boolean };+}|]++  it "NewIdentity" $ printTS (gen @(NewIdentity (TSGenericVar "a"))) `shouldBe`+      [i|// #{definedIn}+type NewIdentity<A> = A;|]
+ test/Data/Aeson/Generics/TypeScript/Types.hs view
@@ -0,0 +1,98 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+module Data.Aeson.Generics.TypeScript.Types where++import           Data.Aeson (ToJSON)+import           Data.Aeson.Generics.TypeScript+  ( FieldTypeName+  , TypeScriptDefinition+  )+import           Data.Kind (Type)+import           Data.Map (Map)+import           Data.Time.Clock (UTCTime)+import           GHC.Generics (Generic)++definedIn :: String+definedIn = "Defined in Data.Aeson.Generics.TypeScript.Types of main"++newtype GotTime = GotTime { unGotTime :: UTCTime }+  deriving stock (Generic)+  deriving anyclass (ToJSON, TypeScriptDefinition)++data Unit+  = MkUnit+  deriving stock (Generic)+  deriving anyclass (ToJSON, TypeScriptDefinition)++data CouldBe a = ForSure a+               | Nah+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++data Sum a b = Foyst a+             | Loser b+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++data HasEither = HasEither+  { notTheEither :: Int+  , theEither    :: Either String Bool+  }+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++data Prod a b c = MkProd a b c+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++data ItsEnum+  = One+  | Two+  | Three+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++data ItsRecord = MkItsRecord+  { oneThing   :: Int+  , twoThing   :: String+  , threeThing :: ()+  }+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++data ItsRecordWithGeneric a = MkItsRecordWithGeneric+  { oneThing   :: Int+  , twoThing   :: Maybe String+  , threeThing :: a+  }+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++data GenericRecordInSum a = OtherThing+                          | MkGenericRecordInSum+  { oneThing :: Int+  , twoThing :: [a]+  }+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++type NewString :: Type+newtype NewString = MkNewString String+  deriving newtype (Eq, FieldTypeName, Generic, Ord, Show, ToJSON)++type RecordWithWrappedType :: Type+data RecordWithWrappedType = RecordWithWrappedType+  { oneThing :: Int+  , twoThing :: [NewString]+  }+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++type NewIdentity :: Type -> Type+newtype NewIdentity a = NewIdentity a+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)++type MapParty :: Type -> Type -> Type+newtype MapParty a b = MapParty (Map a b)+  deriving stock (Eq, Generic, Ord, Show)+  deriving anyclass (ToJSON, TypeScriptDefinition)
+ test/Main.hs view
@@ -0,0 +1,2 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}+module Main where