From 10b09dd045679e7278752fac9e025c4a0d5a1529 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Tue, 28 Nov 2023 11:13:00 -0800 Subject: [PATCH 1/2] Propagate 'Codable' bounds through MoatData tyvars --- .golden/kotlinGenericNewtypeSpec/golden | 3 ++ .golden/kotlinGenericStructSpec/golden | 4 +++ .golden/swiftGenericNewtypeSpec/golden | 4 +++ .golden/swiftGenericStructSpec/golden | 4 +++ .golden/swiftMultipleTypeVariableSpec/golden | 2 +- .golden/swiftTypeVariableSpec/golden | 2 +- moat.cabal | 2 ++ src/Moat/Pretty/Swift.hs | 13 +++++--- test/GenericNewtypeSpec.hs | 33 ++++++++++++++++++++ test/GenericStructSpec.hs | 28 +++++++++++++++++ 10 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 .golden/kotlinGenericNewtypeSpec/golden create mode 100644 .golden/kotlinGenericStructSpec/golden create mode 100644 .golden/swiftGenericNewtypeSpec/golden create mode 100644 .golden/swiftGenericStructSpec/golden create mode 100644 test/GenericNewtypeSpec.hs create mode 100644 test/GenericStructSpec.hs 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..d1b73d2 --- /dev/null +++ b/.golden/swiftGenericNewtypeSpec/golden @@ -0,0 +1,4 @@ +struct LTree: CaseIterable, 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..06b832c --- /dev/null +++ b/.golden/swiftGenericStructSpec/golden @@ -0,0 +1,4 @@ +struct Tree: 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..e9be510 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..9b39a6f 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 db93ab3..f7e1bfd 100644 --- a/src/Moat/Pretty/Swift.hs +++ b/src/Moat/Pretty/Swift.hs @@ -28,7 +28,7 @@ prettySwiftDataWith indent = \case MoatEnum {..} -> prettyTypeDoc "" enumDoc [] ++ "enum " - ++ prettyMoatTypeHeader enumName enumTyVars + ++ prettyMoatTypeHeader enumName (addTyVarBounds enumTyVars enumProtocols) ++ prettyRawValueAndProtocols enumRawValue enumProtocols ++ " {" ++ newlineNonEmpty enumCases @@ -41,7 +41,7 @@ prettySwiftDataWith indent = \case MoatStruct {..} -> prettyTypeDoc "" structDoc [] ++ "struct " - ++ prettyMoatTypeHeader structName structTyVars + ++ prettyMoatTypeHeader structName (addTyVarBounds structTyVars structProtocols) ++ prettyRawValueAndProtocols Nothing structProtocols ++ " {" ++ newlineNonEmpty structFields @@ -54,13 +54,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 @@ -268,3 +268,8 @@ prettyPrivateTypes indents = go onLast :: (a -> a) -> [a] -> [a] onLast _ [] = [] onLast f (x : xs) = x : map f xs + +addTyVarBounds :: [String] -> [Protocol] -> [String] +addTyVarBounds tyVars protos + | Codable `elem` protos = map (++ ": Codable") tyVars + | otherwise = tyVars diff --git a/test/GenericNewtypeSpec.hs b/test/GenericNewtypeSpec.hs new file mode 100644 index 0000000..7e069e9 --- /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 = [OtherProtocol "CaseIterable", 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..4a87795 --- /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 = [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 _)) From 87c52edf10a78e118fcc4862cc0fe2f6c64b1f2c Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Tue, 30 Jan 2024 14:20:43 -0800 Subject: [PATCH 2/2] swift: Propagate all protos with synthesized impls --- .golden/swiftGenericNewtypeSpec/golden | 2 +- .golden/swiftGenericStructSpec/golden | 2 +- .golden/swiftMultipleTypeVariableSpec/golden | 2 +- .golden/swiftTypeVariableSpec/golden | 2 +- src/Moat/Pretty/Swift.hs | 40 +++++++++++++++----- test/GenericNewtypeSpec.hs | 2 +- test/GenericStructSpec.hs | 2 +- 7 files changed, 36 insertions(+), 16 deletions(-) diff --git a/.golden/swiftGenericNewtypeSpec/golden b/.golden/swiftGenericNewtypeSpec/golden index d1b73d2..f721340 100644 --- a/.golden/swiftGenericNewtypeSpec/golden +++ b/.golden/swiftGenericNewtypeSpec/golden @@ -1,4 +1,4 @@ -struct LTree: CaseIterable, Hashable, Codable { +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 index 06b832c..6a750e0 100644 --- a/.golden/swiftGenericStructSpec/golden +++ b/.golden/swiftGenericStructSpec/golden @@ -1,4 +1,4 @@ -struct Tree: Codable { +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 e9be510..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 9b39a6f..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/src/Moat/Pretty/Swift.hs b/src/Moat/Pretty/Swift.hs index f7e1bfd..06b09b1 100644 --- a/src/Moat/Pretty/Swift.hs +++ b/src/Moat/Pretty/Swift.hs @@ -109,17 +109,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" #-} @@ -269,7 +269,27 @@ 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 - | Codable `elem` protos = map (++ ": Codable") tyVars - | otherwise = tyVars +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 index 7e069e9..4ef2109 100644 --- a/test/GenericNewtypeSpec.hs +++ b/test/GenericNewtypeSpec.hs @@ -16,7 +16,7 @@ mobileGenWith ( defaultOptions { dataAnnotations = [Parcelize, Serializable] , dataInterfaces = [Parcelable] - , dataProtocols = [OtherProtocol "CaseIterable", Hashable, Codable] + , dataProtocols = [Hashable, Codable] , dataRawValue = Just Str , generateDocComments = False } diff --git a/test/GenericStructSpec.hs b/test/GenericStructSpec.hs index 4a87795..dfbcc6d 100644 --- a/test/GenericStructSpec.hs +++ b/test/GenericStructSpec.hs @@ -12,7 +12,7 @@ import Prelude mobileGenWith ( defaultOptions { dataInterfaces = [Parcelable] - , dataProtocols = [Codable] + , dataProtocols = [Hashable, Codable] , generateDocComments = False } )