diff --git a/.golden/kotlinGenericNewtypeSpec/golden b/.golden/kotlinGenericNewtypeSpec/golden
new file mode 100644
index 0000000..1d2744c
--- /dev/null
+++ b/.golden/kotlinGenericNewtypeSpec/golden
@@ -0,0 +1,3 @@
+@Parcelize
+@Serializable
+value class LTree(val value: List) : Parcelable
\ No newline at end of file
diff --git a/.golden/kotlinGenericStructSpec/golden b/.golden/kotlinGenericStructSpec/golden
new file mode 100644
index 0000000..f9eecf7
--- /dev/null
+++ b/.golden/kotlinGenericStructSpec/golden
@@ -0,0 +1,4 @@
+data class Tree(
+ val rootLabel: A,
+ val subForest: List>,
+) : Parcelable
\ No newline at end of file
diff --git a/.golden/swiftGenericNewtypeSpec/golden b/.golden/swiftGenericNewtypeSpec/golden
new file mode 100644
index 0000000..f721340
--- /dev/null
+++ b/.golden/swiftGenericNewtypeSpec/golden
@@ -0,0 +1,4 @@
+struct LTree: Hashable, Codable {
+ typealias LTreeTag = Tagged
+ let value: LTreeTag
+}
\ No newline at end of file
diff --git a/.golden/swiftGenericStructSpec/golden b/.golden/swiftGenericStructSpec/golden
new file mode 100644
index 0000000..6a750e0
--- /dev/null
+++ b/.golden/swiftGenericStructSpec/golden
@@ -0,0 +1,4 @@
+struct Tree: Hashable, Codable {
+ var rootLabel: A
+ var subForest: [Tree]
+}
\ No newline at end of file
diff --git a/.golden/swiftMultipleTypeVariableSpec/golden b/.golden/swiftMultipleTypeVariableSpec/golden
index 8b689eb..56343fa 100644
--- a/.golden/swiftMultipleTypeVariableSpec/golden
+++ b/.golden/swiftMultipleTypeVariableSpec/golden
@@ -1,4 +1,4 @@
-struct Data: CaseIterable, Hashable, Codable {
+struct Data: CaseIterable, Hashable, Codable {
var field0: A
var field1: B
}
\ No newline at end of file
diff --git a/.golden/swiftTypeVariableSpec/golden b/.golden/swiftTypeVariableSpec/golden
index acf7004..115a808 100644
--- a/.golden/swiftTypeVariableSpec/golden
+++ b/.golden/swiftTypeVariableSpec/golden
@@ -1,3 +1,3 @@
-struct Data: CaseIterable, Hashable, Codable {
+struct Data: CaseIterable, Hashable, Codable {
var field0: A
}
\ No newline at end of file
diff --git a/moat.cabal b/moat.cabal
index dfd9eab..95ef219 100644
--- a/moat.cabal
+++ b/moat.cabal
@@ -83,6 +83,8 @@ test-suite spec
DuplicateRecordFieldSpec
EnumValueClassDocSpec
EnumValueClassSpec
+ GenericNewtypeSpec
+ GenericStructSpec
MultipleTypeVariableSpec
StrictEnumsSpec
StrictFieldsSpec
diff --git a/src/Moat/Pretty/Swift.hs b/src/Moat/Pretty/Swift.hs
index 5ff3e8d..e7f5c58 100644
--- a/src/Moat/Pretty/Swift.hs
+++ b/src/Moat/Pretty/Swift.hs
@@ -29,7 +29,7 @@ prettySwiftDataWith indent = \case
MoatEnum {..} ->
prettyTypeDoc "" enumDoc []
++ "enum "
- ++ prettyMoatTypeHeader enumName enumTyVars
+ ++ prettyMoatTypeHeader enumName (addTyVarBounds enumTyVars enumProtocols)
++ prettyRawValueAndProtocols enumRawValue enumProtocols
++ " {"
++ newlineNonEmpty enumCases
@@ -42,7 +42,7 @@ prettySwiftDataWith indent = \case
MoatStruct {..} ->
prettyTypeDoc "" structDoc []
++ "struct "
- ++ prettyMoatTypeHeader structName structTyVars
+ ++ prettyMoatTypeHeader structName (addTyVarBounds structTyVars structProtocols)
++ prettyRawValueAndProtocols Nothing structProtocols
++ " {"
++ newlineNonEmpty structFields
@@ -55,13 +55,13 @@ prettySwiftDataWith indent = \case
MoatAlias {..} ->
prettyTypeDoc "" aliasDoc []
++ "typealias "
- ++ prettyMoatTypeHeader aliasName aliasTyVars
+ ++ prettyMoatTypeHeader aliasName (addTyVarBounds aliasTyVars [])
++ " = "
++ prettyMoatType aliasTyp
MoatNewtype {..} ->
prettyTypeDoc "" newtypeDoc []
++ "struct "
- ++ prettyMoatTypeHeader newtypeName newtypeTyVars
+ ++ prettyMoatTypeHeader newtypeName (addTyVarBounds newtypeTyVars newtypeProtocols)
++ prettyRawValueAndProtocols Nothing newtypeProtocols
++ " {\n"
++ indents
@@ -112,17 +112,17 @@ prettyRawValueAndProtocols Nothing ps = ": " ++ prettyProtocols ps
prettyRawValueAndProtocols (Just ty) [] = ": " ++ prettyMoatType ty
prettyRawValueAndProtocols (Just ty) ps = ": " ++ prettyMoatType ty ++ ", " ++ prettyProtocols ps
+prettyProtocol :: Protocol -> String
+prettyProtocol = \case
+ Hashable -> "Hashable"
+ Codable -> "Codable"
+ Equatable -> "Equatable"
+ OtherProtocol s -> s
+
prettyProtocols :: [Protocol] -> String
prettyProtocols = \case
[] -> ""
ps -> intercalate ", " (prettyProtocol <$> ps)
- where
- prettyProtocol :: Protocol -> String
- prettyProtocol = \case
- Hashable -> "Hashable"
- Codable -> "Codable"
- Equatable -> "Equatable"
- OtherProtocol s -> s
-- TODO: Need a plan to avoid @error@ in these pure functions
{-# ANN prettyTags "HLint: ignore" #-}
@@ -281,3 +281,28 @@ prettyPrivateTypes indents = go
onLast :: (a -> a) -> [a] -> [a]
onLast _ [] = []
onLast f (x : xs) = x : map f xs
+
+-- | Copy protocols from the parent type to upper bounds of generic type
+-- parameters.
+--
+-- This is needed for protocols with compiler-synthesized implementations
+-- (similar to 'deriving stock'), of which there are currently three:
+--
+-- - 'Equatable'
+-- - 'Hashable'
+-- - 'Codable'
+--
+-- See the [Swift documentation](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols#Adopting-a-Protocol-Using-a-Synthesized-Implementation).
+addTyVarBounds :: [String] -> [Protocol] -> [String]
+addTyVarBounds tyVars protos =
+ let isSynthesized :: Protocol -> Bool
+ isSynthesized = \case
+ Hashable -> True
+ Codable -> True
+ Equatable -> True
+ OtherProtocol _ -> False
+ synthesizedProtos = filter isSynthesized protos
+ bounds = ": " ++ intercalate " & " (map prettyProtocol synthesizedProtos)
+ in case synthesizedProtos of
+ [] -> tyVars
+ _ -> map (++ bounds) tyVars
diff --git a/test/GenericNewtypeSpec.hs b/test/GenericNewtypeSpec.hs
new file mode 100644
index 0000000..4ef2109
--- /dev/null
+++ b/test/GenericNewtypeSpec.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE DerivingStrategies #-}
+
+module GenericNewtypeSpec where
+
+import Common
+import Data.List.NonEmpty (NonEmpty)
+import Moat
+import Test.Hspec
+import Test.Hspec.Golden
+import Prelude
+
+newtype LTree a = LTree (NonEmpty a)
+ deriving stock (Show, Eq)
+
+mobileGenWith
+ ( defaultOptions
+ { dataAnnotations = [Parcelize, Serializable]
+ , dataInterfaces = [Parcelable]
+ , dataProtocols = [Hashable, Codable]
+ , dataRawValue = Just Str
+ , generateDocComments = False
+ }
+ )
+ ''LTree
+
+spec :: Spec
+spec =
+ describe "stays golden" $ do
+ let moduleName = "GenericNewtypeSpec"
+ it "swift" $
+ defaultGolden ("swift" <> moduleName) (showSwift @(LTree _))
+ it "kotlin" $
+ defaultGolden ("kotlin" <> moduleName) (showKotlin @(LTree _))
diff --git a/test/GenericStructSpec.hs b/test/GenericStructSpec.hs
new file mode 100644
index 0000000..dfbcc6d
--- /dev/null
+++ b/test/GenericStructSpec.hs
@@ -0,0 +1,28 @@
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module GenericStructSpec where
+
+import Common
+import Data.Tree (Tree)
+import Moat
+import Test.Hspec
+import Test.Hspec.Golden
+import Prelude
+
+mobileGenWith
+ ( defaultOptions
+ { dataInterfaces = [Parcelable]
+ , dataProtocols = [Hashable, Codable]
+ , generateDocComments = False
+ }
+ )
+ ''Tree
+
+spec :: Spec
+spec =
+ describe "stays golden" $ do
+ let moduleName = "GenericStructSpec"
+ it "swift" $
+ defaultGolden ("swift" <> moduleName) (showSwift @(Tree _))
+ it "kotlin" $
+ defaultGolden ("kotlin" <> moduleName) (showKotlin @(Tree _))