Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
SwiftSyntaxMacrosに入門してみた
Search
Toshiya Kobayashi (とち)
September 26, 2023
Programming
440
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
SwiftSyntaxMacrosに入門してみた
Toshiya Kobayashi (とち)
September 26, 2023
More Decks by Toshiya Kobayashi (とち)
See All by Toshiya Kobayashi (とち)
ユーザー数10万人規模のアプリで挑んだトップ画面のUI刷新
tochi86
0
2.5k
Swift Zoomin' #9 報告会
tochi86
1
590
Other Decks in Programming
See All in Programming
Are APIs Still Relevant in the AI Era?
soyuka
0
440
【加筆修正版】Laravel のアプリケーションをどこにデプロイするか #phpcon_ehime
akase244
0
150
GemmaをJevのように使ってみる / Use Gemma like Jev
kishida
5
740
CodeRabbitの効果検証と過ごしてみた3ヶ月
armondando
0
200
Jetpack Compose Mechanisms
skydoves
2
650
SREの越境 / SRE Collaboration
y0hgi
2
300
Vue Fes Japan の “作る側”に関わってみて
naokihaba
0
140
速く作れる。その次は、速く確かめられる開発へ 〜AIネイティブ開発を支える、Shift Down〜 / Can build fast. Next, moving to development where we can verify fast.
rkaga
8
5.6k
難しいけど、読めた。- OSSの入口に立った話。
sts11142
0
140
Kubernetes祭り #2 OP/ED
bells17
0
110
コードで差別化できない時代の ソフトウェアエンジニアリング / Software Engineering in an Era Where Code Doesn’t Differentiate
kishida
2
130
そのリトライ、死んだコネクションを使い回していませんか ── GoのHTTPクライアントとHTTP/2を実プロダクト障害から学び直す
myus4a
1
410
Featured
See All Featured
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
260
How to Talk to Developers About Accessibility
jct
2
570
Data-driven link building: lessons from a $708K investment (BrightonSEO talk)
szymonslowik
1
1.4k
The Mindset for Success: Future Career Progression
greggifford
PRO
0
530
What’s in a name? Adding method to the madness
productmarketing
PRO
24
4.2k
Into the Great Unknown - MozCon
thekraken
41
2.8k
Automating Front-end Workflow
addyosmani
1369
210k
Technical Leadership for Architectural Decision Making
baasie
3
600
Building AI with AI
inesmontani
PRO
1
1.3k
So, you think you're a good person
axbom
PRO
2
2.2k
Accessibility Awareness
sabderemane
1
240
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
720
Transcript
4XJGU4ZOUBY.BDSPTʹ ೖͯ͠Έͨ 5PTIJZB,PCBZBTIJ ͱͪ!UPDIJ@
ϚΫϩʹೖͨ͠Ϟνϕʔγϣϯ w QPJOUGSFFDPTXJGUEFQFOEFODJFTͱ͍͏ϥΠϒϥϦͰɺQSPUPDPMͰͳ͘ TUSVDUΛར༻ͯ͠ΠϯλʔϑΣʔεΛهड़͢Δ͜ͱ͕ਪ͞Ε͍ͯΔ protocol APIClient { func fetchUserName(userId:
Int) async throws -> String func setUserFlag(userId: Int, flag: Bool) async throws } struct APIClient { var fetchUserName: @Sendable (_ userId: Int) async throws -> String var setUserFlag: @Sendable (_ userId: Int, _ flag: Bool) async throws -> Void } w VCFSNPDLPMPͷΑ͏ͳɺϞοΫͷίʔυΛࣗಈੜ͢ΔΈΛ TUSVDUελΠϧͷΠϯλʔϑΣʔεͰར༻ͨ͘͠ͳͬͨ
ࠓճ࡞͢Δ!(FOFSBUF.PDLϚΫϩ @GenerateMock struct APIClient { var fetch: @Sendable (Int) async
throws -> String } struct APIClient { var fetch: @Sendable (Int) async throws -> String static func mock(_ mock: Mock) -> Self { Self (fetch: mock.fetch) } class Mock { private(set) var fetchCallCount = 0 private(set) var fetchArgValues: [(Int)] = [] var fetchHandler: ((Int) async throws -> String)? @Sendable fileprivate func fetch(_ arg0: Int) async throws -> String { fetchCallCount += 1 fetchArgValues.append((arg0)) return try await fetchHandler!(arg0) } } } ϚΫϩΛ͚Δͱɺͭͷϝϯόʔ͕TUSVDUʹՃ͞ΕΔ ϞοΫΦϒδΣΫτΛੜ͢ΔTUBUJDؔNPDL @ ֤Ϋϩʔδϟ͕ ɾݺΕͨճ ɾݺΕͨ࣌ͷҾ ɾݺΕͨΒ࣮ߦ͍ͨ͠ॲཧ Λอ࣋͢ΔΫϥε.PDL
public struct GenerateMockMacro: MemberMacro { public static func expansion( of
node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [DeclSyntax("")] } } w .FNCFS.BDSPͷFYQBOTJPOϝιουʹɺϚΫϩͷ࣮Λॻ͍͍ͯ͘ ϚΫϩΛ৽ن࡞͢Δ w 9DPEFͰ1BDLBHFΛ৽ن࡞ͯ͠4XJGU.BDSPςϯϓϨʔτΛબ ͜͜Ͱฦͨ͠4ZOUBY͕ɺϚΫϩͰల։͞ΕΔ @attached(member, names: named(mock(_:)), named(Mock)) public macro GenerateMock() = #externalMacro(module: "GenerateMockMacros", type: "GenerateMockMacro") w ࠓճTUSVDUʹϝϯόʔΛՃ͍ͨ͠ͷͰ!BUUBDIFE NFNCFS Λઃఆ
ςετΛ༻ҙ͢Δ w ςετΛ܁Γฦ࣮͠ߦ͠ͳ͕Βɺগͣͭ͠ϚΫϩΛΈཱ͍ͯͯ͘ final class GenerateMockTests: XCTestCase { func testMacro()
throws { assertMacroExpansion(#""" @GenerateMock struct APIClient { var fetch: @Sendable (Int) async throws -> String } """#, expandedSource: #""" struct APIClient { var fetch: @Sendable (Int) async throws -> String } """#, macros: ["GenerateMock": GenerateMockMacro.self] ) } } ·ͩԿ࣮͍ͯ͠ͳ͍ͷͰɺ TUSVDUΛͦͷ··ग़ྗ͢Ε ςετ͕௨Δ
ςετΛॻ͖͑Δ w ςετʹTUBUJDؔΛՃͯ͠ɺظ௨ΓͷࠩͰࣦഊ͢Δ͜ͱΛ֬ೝ assertMacroExpansion(#""" @GenerateMock struct APIClient { var fetch:
@Sendable (Int) async throws -> String } """#, expandedSource: #""" struct APIClient { var fetch: @Sendable (Int) async throws -> String static func mock(_ mock: Mock) -> Self { Self (fetch: mock.fetch) } } """#, macros: testMacros )
4XJGU"45&YQMPSFSͰߏจΛௐΔ w IUUQTTXJGUBTUFYQMPSFSDPN ͳΔ΄Ͳʜɻ ·ͣ'VODUJPO%FDM͔Β ࢝ΊΕྑͦ͞͏ͩͳ🧐
'VODUJPO%FDM4ZOUBYΛߏங͢Δ w ͱΓ͋͑ͣ'VODUJPO%FDM4ZOUBYΛฦͯ͠ΈΔ return [ DeclSyntax( FunctionDeclSyntax( name: <#T##TokenSyntax#>, signature:
<#T##FunctionSignatureSyntax#> ) ) ] name: TokenSyntax(stringLiteral: “mock"), signature: FunctionSignatureSyntax( parameterClause: <#T##FunctionParameterClauseSyntax#> ) w OBNFͱTJHOBUVSFͱ͍͏ύϥϝʔλΛٻΊΒΕͨͷͰɺຒΊͯΈΔ w TJHOBUVSFɺ͞ΒʹQBSBNFUFS$MBVTFͱ͍͏ͷΛٻΊ͖ͯͨ
QBSBNFUFS$MBVTFΛຒΊΔ w ίʔυิʹཔΓͳ͕Βɺ4XJGU"45&YQMPSFSͰௐͨ༰ΛຒΊ͍ͯ͘ parameterClause: FunctionParameterClauseSyntax( parameters: FunctionParameterListSyntax([ FunctionParameterSyntax(stringLiteral: "_ mock:
Mock") ]) ) ࣍ઌ಄ʹTUBUJDΛ͚ͯΈΑ͏
TUBUJDΛ͚Δ w 'VODUJPO%FDM4ZOUBYʹNPEJ fi FSTΛՃ͢Δ ࣍4FMGΛฦͯ͠ΈΑ͏ FunctionDeclSyntax( modifiers: DeclModifierListSyntax([ DeclModifierSyntax(name:
"static") ]), name: TokenSyntax(stringLiteral: "mock"), signature: FunctionSignatureSyntax(…) )
4FMGΛฦ͢ w 'VODUJPO4JHOBUVSF4ZOUBYʹSFUVSO$MBVTFΛՃ͢Δ ͋ͱؔʹίʔυϒϩοΫΛՃ͢Δ͚ͩʂ signature: FunctionSignatureSyntax( parameterClause: FunctionParameterClauseSyntax(…), returnClause: ReturnClauseSyntax(
type: IdentifierTypeSyntax(name: "Self") ) )
͜Μͳײ͡ͷϊϦͰΈཱ͍ͯͯ͘ΜͰ͕͢ʜ struct APIClient { var fetch: @Sendable (Int) async throws
-> String static func mock(_ mock: Mock) -> Self { Self (fetch: mock.fetch) } } w ϚΫϩͷೖྗΛղੳ͢Δඞཁ͕͋Δ w ͜ΕΒͷ໊લʢGFUDIʣ ͔͜͜Βऔ͖͍ͬͯͨ
ϚΫϩೖྗΛղੳ͢Δ w EFCVH%FTDSJQUJPOΛQSJOU͢Δ StructDeclSyntax ├─attributes: AttributeListSyntax │ ╰─[0]: AttributeSyntax │
├─atSign: atSign │ ╰─attributeName: IdentifierTypeSyntax │ ╰─name: identifier("GenerateMock") ├─modifiers: DeclModifierListSyntax ├─structKeyword: keyword(SwiftSyntax.Keyword.struct) ├─name: identifier("APIClient") ╰─memberBlock: MemberBlockSyntax ├─leftBrace: leftBrace ├─members: MemberBlockItemListSyntax │ ╰─[0]: MemberBlockItemSyntax │ ╰─decl: VariableDeclSyntax │ ├─attributes: AttributeListSyntax │ ├─modifiers: DeclModifierListSyntax │ ├─bindingSpecifier: keyword(SwiftSyntax.Keyword.var) │ ╰─bindings: PatternBindingListSyntax │ ╰─[0]: PatternBindingSyntax │ ├─pattern: IdentifierPatternSyntax │ │ ╰─identifier: identifier("fetch") │ ╰─typeAnnotation: TypeAnnotationSyntax │ ├─colon: colon │ ╰─type: AttributedTypeSyntax │ ├─attributes: AttributeListSyntax │ │ ╰─[0]: AttributeSyntax │ │ ├─atSign: atSign │ │ ╰─attributeName: IdentifierTypeSyntax │ │ ╰─name: identifier("Sendable") │ ╰─baseType: FunctionTypeSyntax │ ├─leftParen: leftParen │ ├─parameters: TupleTypeElementListSyntax │ │ ╰─[0]: TupleTypeElementSyntax │ │ ╰─type: IdentifierTypeSyntax │ │ ╰─name: identifier("Int") │ ├─rightParen: rightParen │ ├─effectSpecifiers: TypeEffectSpecifiersSyntax │ │ ├─asyncSpecifier: keyword(SwiftSyntax.Keyword.async) │ │ ╰─throwsSpecifier: keyword(SwiftSyntax.Keyword.throws) │ ╰─returnClause: ReturnClauseSyntax │ ├─arrow: arrow │ ╰─type: IdentifierTypeSyntax │ ╰─name: identifier("String") ╰─rightBrace: rightBrace public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { print(declaration.debugDescription) return [DeclSyntax(…)] } w ϚΫϩʹೖྗ͞Εͨίʔυͷߏจ͕ ΞεΩʔΞʔτܗࣜͰग़ྗ͞ΕΔ
Ϋϩʔδϟͷม໊Λऔಘ͢Δ StructDeclSyntax ╰─memberBlock: MemberBlockSyntax ├─members: MemberBlockItemListSyntax │ ╰─[0]: MemberBlockItemSyntax │
╰─decl: VariableDeclSyntax │ ╰─bindings: PatternBindingListSyntax │ ╰─[0]: PatternBindingSyntax │ ├─pattern: IdentifierPatternSyntax │ │ ╰─identifier: identifier("fetch") guard let structDecl = declaration.as(StructDeclSyntax.self), let variable = structDecl.memberBlock.members.first?.decl.as(VariableDeclSyntax.self), let identifierPattern = variable.bindings.first?.pattern.as(IdentifierPatternSyntax.self) else { throw CustomError.message("...") } print(identifierPattern.identifier) // => "fetch" w దٓΩϟετΛར༻͠ͳ͕Βɺͯͷཁૉ·ͰͨͲ͍ͬͯ͘
ϚΫϩ͕ w ͜͜·Ͱʹհͨ͠ςΫχοΫΛۦ͠ͳ͕Βʜ w ςετΛॻ͖ w 4XJGU"45&YQMPSFSΛࢀߟʹϚΫϩग़ྗΛΈཱͯ w EFCVH%FTDSJQUJPOΛࢀߟʹϚΫϩೖྗΛղੳͯ͠ w
ͱΓ͋͑ͣಈ͖ͦ͏ͳϚΫϩ͕͠·ͨ͠ʙʂ🎉 w IUUQTHJUIVCDPNUPDIJ(FOFSBUF.PDL
࣮ࡍʹͬͯΈͨ w "1*$MJFOUͱTXJGUEFQFOEFODJFTͰར༻͢ΔͨΊͷ͓·͡ͳ͍Λهड़ @GenerateMock struct APIClient { var fetchUserName: @Sendable
(_ userId: Int) async throws -> String var setUserFlag: @Sendable (_ userId: Int, _ flag: Bool) async throws -> Void } extension APIClient: DependencyKey { static let liveValue = Self( fetchUserName: { "Live user for \($0)" }, setUserFlag: { _, _ in try await Task.sleep(nanoseconds: NSEC_PER_SEC) } ) } extension DependencyValues { var apiClient: APIClient { get { self[APIClient.self] } set { self[APIClient.self] = newValue } } }
࣮ࡍʹͬͯΈͨ w "1*$MJFOUΛར༻͢Δ7JFX.PEFMΛهड़ @MainActor final class ViewModel: ObservableObject { @Published
private(set) var text: String? @Published private(set) var isLoading = false @Dependency(\.apiClient) private var apiClient private let userId: Int init(userId: Int) { self.userId = userId } func buttonTapped() async { text = nil isLoading = true; defer { isLoading = false } do { text = try await apiClient.fetchUserName(userId) try await apiClient.setUserFlag(userId, true) } catch { text = "Error!" } } }
࣮ࡍʹͬͯΈͨ w ςετͷTFU6QͰɺϚΫϩʹΑΓੜ͞Εͨ"1*$MJFOUͷϞοΫʹࠩ͠ସ͑ @MainActor final class ViewModelTests: XCTestCase { var
sut: ViewModel! var apiClientMock: APIClient.Mock! override func setUp() { super.setUp() apiClientMock = .init() sut = withDependencies { $0.apiClient = .mock(apiClientMock) } operation: { ViewModel(userId: 1234) } } ... }
࣮ࡍʹͬͯΈͨ w ςετ͜Μͳײ͡Ͱ ॻ͚Δ func testButtonTapped_Success() async { apiClientMock.fetchUserNameHandler
= { "Mock user for \($0)" } apiClientMock.setUserFlagHandler = { _, _ in } await sut.buttonTapped() XCTAssertEqual(sut.text, "Mock user for 1234") XCTAssertEqual(apiClientMock.fetchUserNameCallCount, 1) XCTAssertEqual(apiClientMock.fetchUserNameArgValues, [1234]) XCTAssertEqual(apiClientMock.setUserFlagCallCount, 1) XCTAssertEqual(apiClientMock.setUserFlagArgValues.map(\.userId), [1234]) XCTAssertEqual(apiClientMock.setUserFlagArgValues.map(\.flag), [true]) } func testButtonTapped_Failure() async { apiClientMock.fetchUserNameHandler = { _ in struct SomeError: Error {} throw SomeError() } await sut.buttonTapped() XCTAssertEqual(sut.text, "Error!") XCTAssertEqual(apiClientMock.fetchUserNameCallCount, 1) XCTAssertEqual(apiClientMock.fetchUserNameArgValues, [1234]) XCTAssertEqual(apiClientMock.setUserFlagCallCount, 0) } w GFUDIʹޭͨ͠߹ ϑϥάઃఆ͕ݺΕΔ w GFUDIʹࣦഊͨ͠߹ ϑϥάઃఆ͕ݺΕͳ͍
࣮ࡍʹͬͯΈͨ w Ұࣦഊͯ͠ɺϦτϥΠͨ͠Β ޭ͢ΔΑ͏ͳςετॻ͚Δ func testButtonTapped_RetryWithLoading() async { await
withMainSerialExecutor { apiClientMock.fetchUserNameHandler = { _ in await Task.yield() struct SomeError: Error {} throw SomeError() } let task1 = Task { await sut.buttonTapped() } await Task.yield() XCTAssertTrue(sut.isLoading) XCTAssertNil(sut.text) await task1.value XCTAssertFalse(sut.isLoading) XCTAssertEqual(sut.text, "Error!") apiClientMock.fetchUserNameHandler = { await Task.yield() return "Mock user for \($0)" } apiClientMock.setUserFlagHandler = { _, _ in } let task2 = Task { await sut.buttonTapped() } await Task.yield() XCTAssertTrue(sut.isLoading) XCTAssertNil(sut.text) await task2.value XCTAssertFalse(sut.isLoading) XCTAssertEqual(sut.text, "Mock user for 1234") } } w ͍ͭͰʹJT-PBEJOHͷΓସ͑ςετ w 5BTLZJFME Λ༻͢Δςετɺ XJUI.BJO4FSJBM&YFDVUPS\^Ͱ ғΘͳ͍ͱෆ҆ఆʹͳΔʢࣦഊʣ w ৄ͘͠1PJOU'SFFͷ υΩϡϝϯτΛ͝ཡ͍ͩ͘͞
·ͱΊ w ςετΛগͣͭ͠Ճ͠ͳ͕ΒɺϚΫϩͷ։ൃΛਐΊΔ w 4XJGU"45&YQMPSFSͱิʹཔΓͳ͕ΒɺϚΫϩग़ྗΛΈཱ͍ͯͯ͘ w EFCVH%FTDSJQUJPOΛࢀߟʹ͠ͳ͕ΒɺϚΫϩೖྗͷඞཁͳཁૉΛղੳ͢Δ w 9DPEFͷҠߦ͕ྃͨ͠ΒɺͥͻϚΫϩΛ׆༻ͯ͠
շదͳ4XJGUϓϩάϥϛϯάੜ׆ΛૹΓ·͠ΐ͏ʙ👍👍