Swift Macros — kompletny przewodnik po tworzeniu własnych makr w Swift

Przewodnik po makrach w Swift — od pierwszego makra freestanding po zaawansowane attached macros. Twórz, testuj i debuguj makra z SwiftSyntax, eliminując boilerplate w projektach iOS.

Wprowadzenie — czym są makra w Swift i po co w ogóle się nimi zajmować

Jeśli kiedykolwiek pisałeś w Swift i złapałeś się na kopiowaniu tych samych CodingKeys po raz piętnasty, ręcznym implementowaniu Equatable dla kolejnej klasy, albo na budowaniu boilerplate'u dla UserDefaults — to mam dobrą wiadomość. Makra w Swift zostały stworzone dokładnie po to, żeby tego nie robić.

Wprowadzone w Swift 5.9 (propozycje SE-0382, SE-0389 i SE-0397), makra pozwalają generować kod w czasie kompilacji. Koniec z powtarzaniem się. Koniec z literówkami w dwudziestym enum case.

I tu ważna uwaga — to nie są makra znane z C czy C++, gdzie preprocesor robi głupie kopiuj-wklej tekstu. Makra w Swift są bezpieczne typowo, działają jako zewnętrzne programy podczas fazy budowania i operują na pełnym drzewie składniowym (AST) dzięki bibliotece SwiftSyntax. Mówimy tu o metaprogramowaniu na poziomie kompilatora, z pełną kontrolą nad tym, co zostanie wygenerowane.

W tym przewodniku przejdziemy przez wszystko krok po kroku: typy makr, tworzenie pierwszego makra od zera, testowanie, debugowanie i najlepsze praktyki. Wszystkie przykłady to działający kod — możesz go wziąć i użyć w swoim projekcie od razu.

Rodzaje makr w Swift — freestanding i attached

Swift dzieli makra na dwie główne kategorie. Każda ma inne zastosowanie i inną składnię.

Makra freestanding (wolnostojące)

Makra wolnostojące pojawiają się samodzielnie w kodzie i zawsze zaczynają się od znaku #. Działają jak wyrażenia lub deklaracje, które kompilator rozwija w czasie kompilacji.

Najprostszy przykład? Wbudowane makro #stringify:

// Freestanding expression macro
let (result, code) = #stringify(2 + 3)
// result == 5
// code == "2 + 3"

Inne popularne przykłady to #warning("TODO: dokończyć implementację") czy #URL("https://api.example.com/users") — makro, które waliduje URL jeszcze zanim aplikacja się uruchomi. Przyznam, że to jedno z moich ulubionych zastosowań.

Makra attached (dołączone)

Makra dołączone działają jak atrybuty — umieszczasz je przed deklaracją ze znakiem @. Mogą modyfikować, rozszerzać lub uzupełniać deklarację, do której zostały dołączone.

// Attached macro — dodaje nowych członków do struktury
@AutoCodable
struct User {
    let name: String
    let email: String
    let age: Int
}

Makra dołączone dzielą się na kilka podtypów, zwanych rolami:

  • @attached(member) — dodaje nowe właściwości, metody lub typy zagnieżdżone do deklaracji
  • @attached(accessor) — dodaje gettery i settery do właściwości
  • @attached(memberAttribute) — stosuje atrybuty do istniejących członków typu
  • @attached(peer) — generuje nowe deklaracje obok istniejącej (np. asynchroniczną wersję funkcji)
  • @attached(extension) — dodaje zgodności z protokołami lub rozszerzenia do typu
  • @attached(body) — wprowadza lub modyfikuje ciało funkcji (SE-0415, zaakceptowane w 2024)

Co ciekawe, jedno makro może pełnić kilka ról jednocześnie — np. być zarówno @attached(member), jak i @attached(extension). To daje sporą elastyczność.

Tworzenie pierwszego makra krok po kroku

No dobra, pora na praktykę. Zbudujemy makro #URL, które waliduje adresy URL w czasie kompilacji — jeśli podany string nie jest prawidłowym URL-em, kompilator zgłosi błąd. Żadnych crashy w runtime.

Krok 1: Utwórz pakiet Swift Macro

Makra w Swift muszą być tworzone jako oddzielne pakiety. Masz dwa sposoby na start:

Sposób A — w Xcode:

Wybierz File → New → Package... i szablon Swift Macro. Xcode wygeneruje całą strukturę projektu za Ciebie.

Sposób B — z terminala:

mkdir URLMacro
cd URLMacro
swift package init --type macro

Po utworzeniu pakietu powinieneś zobaczyć mniej więcej taką strukturę:

URLMacro/
├── Package.swift
├── Sources/
│   ├── URLMacro/           // Deklaracja makra (sygnatura)
│   │   └── URLMacro.swift
│   └── URLMacroMacros/     // Implementacja makra (logika)
│       └── URLMacroMacro.swift
└── Tests/
    └── URLMacroTests/      // Testy
        └── URLMacroTests.swift

Krok 2: Zdefiniuj sygnaturę makra

W pliku URLMacro.swift deklarujesz publiczny interfejs — czyli to, co będą widzieć użytkownicy Twojego makra:

/// Makro walidujące URL w czasie kompilacji.
/// Zwraca `URL` — gwarantowany prawidłowy adres.
@freestanding(expression)
public macro URL(_ stringLiteral: String) -> URL =
    #externalMacro(module: "URLMacroMacros", type: "URLMacroImplementation")

Kilka rzeczy, na które warto zwrócić uwagę:

  • @freestanding(expression) — oznacza, że to makro wolnostojące zwracające wyrażenie
  • #externalMacro — wskazuje moduł i typ, w którym siedzi implementacja
  • Sygnatura definiuje typ parametrów i wartość zwracaną — jak w zwykłej funkcji

Krok 3: Zaimplementuj logikę makra

Teraz najciekawsza część. W pliku URLMacroMacro.swift piszesz właściwą implementację, korzystając z protokołu ExpressionMacro i biblioteki SwiftSyntax:

import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

public struct URLMacroImplementation: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // 1. Pobierz argument (string literal)
        guard let argument = node.arguments.first?.expression,
              let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
              segments.count == 1,
              case .stringSegment(let literalSegment)? = segments.first
        else {
            throw MacroError.requiresStaticStringLiteral
        }

        let urlString = literalSegment.content.text

        // 2. Waliduj URL w czasie kompilacji
        guard URL(string: urlString) != nil else {
            throw MacroError.invalidURL(urlString)
        }

        // 3. Zwróć bezpieczne wyrażenie
        return "URL(string: \(argument))!"
    }
}

enum MacroError: CustomStringConvertible, Error {
    case requiresStaticStringLiteral
    case invalidURL(String)

    var description: String {
        switch self {
        case .requiresStaticStringLiteral:
            return "#URL wymaga statycznego literału string"
        case .invalidURL(let url):
            return "Nieprawidłowy URL: \"\(url)\""
        }
    }
}

// Rejestracja makra w pluginie kompilatora
@main
struct URLMacroPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        URLMacroImplementation.self,
    ]
}

Krok 4: Użyj makra w kodzie

Po zbudowaniu pakietu możesz użyć makra w swoim projekcie — i tu zaczyna się magia:

import URLMacro

// Kompiluje się poprawnie — URL jest prawidłowy
let apiEndpoint = #URL("https://api.example.com/users")

// Błąd kompilacji! Nieprawidłowy URL
let badURL = #URL("to nie jest url")
// error: Nieprawidłowy URL: "to nie jest url"

Widzisz różnicę? Bez makra błędny URL powodowałby crash w runtime — może u użytkownika, może w najgorszym momencie. Z makrem kompilator łapie problem jeszcze zanim aplikacja zostanie uruchomiona. Szczerze mówiąc, po tym jak zacząłem tego używać, nie wyobrażam sobie wracać do URL(string:)! z nadzieją, że string jest poprawny.

Makro attached — praktyczny przykład z @AutoEquatable

Czas na coś bardziej zaawansowanego. Zbudujemy makro dołączone, które automatycznie generuje zgodność z protokołem Equatable dla klas. Struktury w Swift dostają to za darmo, ale klasy? Niestety nie — i pisanie operatora == ręcznie dla klasy z dziesięcioma polami to nie jest nic przyjemnego.

Deklaracja makra

@attached(extension, conformances: Equatable)
@attached(member, names: named(==))
public macro AutoEquatable() = #externalMacro(
    module: "AutoEquatableMacros",
    type: "AutoEquatableMacro"
)

Zwróć uwagę na dwie role: extension (dodaje zgodność z protokołem) i member (dodaje operator ==). Jedno makro, dwa zadania.

Implementacja makra

import SwiftSyntax
import SwiftSyntaxMacros

public struct AutoEquatableMacro: MemberMacro, ExtensionMacro {

    // Generowanie rozszerzenia z Equatable
    public static func expansion(
        of node: AttributeSyntax,
        attachedTo declaration: some DeclGroupSyntax,
        providingExtensionsOf type: some TypeSyntaxProtocol,
        conformingTo protocols: [TypeSyntax],
        in context: some MacroExpansionContext
    ) throws -> [ExtensionDeclSyntax] {
        let extensionDecl = try ExtensionDeclSyntax(
            "extension \(type.trimmed): Equatable {}"
        )
        return [extensionDecl]
    }

    // Generowanie operatora ==
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        conformingTo protocols: [TypeSyntax],
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // Pobierz nazwę typu
        guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
            throw MacroError.onlyApplicableToClass
        }

        let className = classDecl.name.text

        // Znajdź wszystkie stored properties
        let properties = classDecl.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .filter { $0.bindings.first?.accessorBlock == nil }
            .compactMap { $0.bindings.first?.pattern.as(
                IdentifierPatternSyntax.self
            )?.identifier.text }

        // Wygeneruj porównania
        let comparisons = properties
            .map { "lhs.\($0) == rhs.\($0)" }
            .joined(separator: " &&\n            ")

        let equalOperator: DeclSyntax = """
            public static func == (lhs: \(raw: className), rhs: \(raw: className)) -> Bool {
                \(raw: comparisons)
            }
            """

        return [equalOperator]
    }
}

enum MacroError: CustomStringConvertible, Error {
    case onlyApplicableToClass

    var description: String {
        switch self {
        case .onlyApplicableToClass:
            return "@AutoEquatable można stosować tylko do klas"
        }
    }
}

Użycie w kodzie

@AutoEquatable
class UserProfile {
    let id: UUID
    let name: String
    let email: String

    init(id: UUID, name: String, email: String) {
        self.id = id
        self.name = name
        self.email = email
    }
}

// Kompilator automatycznie wygeneruje:
// extension UserProfile: Equatable {
//     public static func == (lhs: UserProfile, rhs: UserProfile) -> Bool {
//         lhs.id == rhs.id &&
//         lhs.name == rhs.name &&
//         lhs.email == rhs.email
//     }
// }

let user1 = UserProfile(id: uuid, name: "Jan", email: "[email protected]")
let user2 = UserProfile(id: uuid, name: "Jan", email: "[email protected]")
print(user1 == user2) // true

Trzy pola to jeszcze nie tragedia, ale wyobraź sobie klasę z piętnaście właściwościami. Albo dwadzieścia. Makro oszczędza realny czas i eliminuje ryzyko, że zapomnisz dodać nowego pola do porównania po refaktorze.

Makro @attached(peer) — generowanie asynchronicznej wersji funkcji

Makra peer tworzą nowe deklaracje obok istniejącej. Szczerze? To chyba jeden z najbardziej praktycznych typów makr, jeśli pracujesz z kodem, który wciąż korzysta z completion handlerów.

@attached(peer, names: overloaded)
public macro AddAsync() = #externalMacro(
    module: "AddAsyncMacros",
    type: "AddAsyncMacro"
)

// Użycie:
@AddAsync
func fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {
    // implementacja z completion handlerem
}

// Makro automatycznie wygeneruje:
func fetchUser(id: String) async throws -> User {
    try await withCheckedThrowingContinuation { continuation in
        fetchUser(id: id) { result in
            continuation.resume(with: result)
        }
    }
}

Ten wzorzec jest nieoceniony podczas migracji starszego kodu opartego na callbackach do async/await. Zamiast ręcznie pisać wrappery dla dziesiątek (albo setek) funkcji, jedno makro robi robotę za Ciebie.

Makra body — modyfikowanie ciała funkcji (SE-0415)

To najnowszy typ makr, zaakceptowany w 2024 roku. Makra body pozwalają wprowadzać lub modyfikować ciało funkcji — idealne do logowania, tracingu czy instrumentacji.

@attached(body)
public macro Logged() = #externalMacro(
    module: "LoggingMacros",
    type: "LoggedMacro"
)

// Użycie:
@Logged
func processPayment(amount: Decimal, currency: String) -> PaymentResult {
    // Twoja logika biznesowa
    return PaymentResult.success
}

// Makro wstrzykuje logowanie:
func processPayment(amount: Decimal, currency: String) -> PaymentResult {
    log("→ processPayment(amount: \(amount), currency: \(currency))")
    defer { log("← processPayment zakończone") }
    // Twoja logika biznesowa
    return PaymentResult.success
}

Jeśli znasz dekoratory z Pythona — to dokładnie ten sam koncept. Opakowujesz istniejącą logikę dodatkowym zachowaniem bez dotykania oryginalnego kodu. Czysto i elegancko.

Testowanie makr — klucz do niezawodności

Makra operują na drzewie składniowym, więc ich testowanie jest w gruncie rzeczy dość przyjemne — sprowadza się do porównywania wejściowego kodu z oczekiwanym rozwinięciem. SwiftSyntax udostępnia do tego funkcję assertMacroExpansion.

import SwiftSyntaxMacrosTestSupport
import XCTest

final class URLMacroTests: XCTestCase {

    let testMacros: [String: Macro.Type] = [
        "URL": URLMacroImplementation.self,
    ]

    func testValidURL() throws {
        assertMacroExpansion(
            """
            #URL("https://example.com")
            """,
            expandedSource: """
            URL(string: "https://example.com")!
            """,
            macros: testMacros
        )
    }

    func testInvalidURL() throws {
        assertMacroExpansion(
            """
            #URL("nieprawidłowy url")
            """,
            expandedSource: """
            #URL("nieprawidłowy url")
            """,
            diagnostics: [
                DiagnosticSpec(
                    message: "Nieprawidłowy URL: \"nieprawidłowy url\"",
                    line: 1, column: 1
                )
            ],
            macros: testMacros
        )
    }
}

Warto też sprawdzić bibliotekę swift-macro-testing od Point-Free — oferuje automatyczne snapshoty rozwinięć makr, co znacznie upraszcza utrzymywanie testów przy zmianach w implementacji.

Na co zwracać uwagę przy testowaniu

  • Pozytywne scenariusze — upewnij się, że makro generuje poprawny kod dla typowych danych wejściowych
  • Walidacja błędów — sprawdź, czy makro poprawnie reaguje na nieprawidłowe użycie
  • Przypadki graniczne — puste dane, nietypowe typy, wielokrotne zastosowanie makra na tym samym typie
  • Expand Macro w Xcode — kliknij prawym przyciskiem na makro i wybierz „Expand Macro", żeby podejrzeć wygenerowany kod na żywo

Debugowanie makr w Xcode

Debugowanie makr potrafi być frustrujące — w końcu działają w czasie kompilacji, nie w runtime. Ale są sposoby, żeby sobie z tym poradzić.

Expand Macro — podgląd wygenerowanego kodu

Najszybszy sposób na sprawdzenie, co Twoje makro wygenerowało: kliknij prawym przyciskiem na użycie makra w edytorze Xcode i wybierz „Expand Macro". Xcode pokaże dokładnie, jaki kod został stworzony — z pełnym podświetlaniem składni. To zawsze mój pierwszy krok, kiedy coś działa nie tak, jak powinno.

Breakpointy w implementacji makra

Ponieważ makra to oddzielne programy, możesz ustawić breakpointy bezpośrednio w ich implementacji. Kiedy projekt używający makra się kompiluje, debugger zatrzyma się w Twoim kodzie. To działa naprawdę dobrze, choć wymaga chwili, żeby się do tego przyzwyczaić.

context.makeUniqueName() — unikanie konfliktów nazw

Podczas generowania kodu w makrach nie koduj na sztywno nazw zmiennych. Użyj metody context.makeUniqueName("prefix"), która generuje unikalne nazwy gwarantujące brak kolizji:

let tempVarName = context.makeUniqueName("temp")
// Generuje np. __macro_local_temp_0 — gwarantowana unikalność

Najlepsze praktyki przy tworzeniu makr

Po kilku miesiącach pracy z makrami i śledzeniu dyskusji w społeczności Swift, wyłania się kilka kluczowych zasad. Oto te, które naprawdę mają znaczenie:

1. Generuj tylko to, co konieczne

Makra powinny eliminować powtarzalny boilerplate, nie zastępować każdy fragment kodu. Jeśli jakiś pattern pojawia się w dwóch miejscach — nie twórz makra, to przesada. Ale jeśli w dwudziestu? Wtedy zdecydowanie warto.

2. Zapewnij czytelne komunikaty błędów

Kiedy ktoś użyje Twojego makra nieprawidłowo, powinien od razu wiedzieć, co zrobił źle. Zamiast generycznego „expansion failed" — podaj konkretny opis problemu i podpowiedź, jak go naprawić. Twoi współpracownicy (i przyszły Ty) będą wdzięczni.

3. Nie ukrywaj zbyt wiele logiki

Makro, które z jednej adnotacji generuje 200 linii kodu, brzmi fajnie w teorii. W praktyce? Utrudnia zrozumienie, co tak naprawdę robi Twój program. Staraj się, by wygenerowany kod był przewidywalny i łatwy do podejrzenia przez „Expand Macro".

4. Dokumentuj wygenerowany kod

Dodaj komentarze DocC do deklaracji makra. Wyjaśnij, jaki kod zostanie wygenerowany i jakie są wymagania wobec typu, na którym makro jest stosowane.

5. Stabilizuj wzorzec przed tworzeniem makra

Nie twórz makra dla czegoś, nad czym wciąż pracujesz. Serio. Makra powinny kodyfikować ustalone, sprawdzone wzorce — nie eksperymentalne pomysły, które mogą się jutro zmienić.

Ekosystem gotowych makr — co warto znać

Społeczność Swift zdążyła już stworzyć sporo gotowych makr, które możesz od razu dodać do swoich projektów. Kilka najciekawszych:

  • CodingKeys — automatyczne generowanie CodingKeys z konwersją snake_case na camelCase
  • MemberwiseInit — ulepszone inicjalizatory memberwise z kontrolą dostępu (bo wbudowany init bywa zbyt ograniczony)
  • EnvironmentValues — eliminuje boilerplate przy definiowaniu kluczy i wartości SwiftUI Environment
  • SFSymbols Macro — typowo-bezpieczne odwoływanie się do SF Symbols z walidacją w czasie kompilacji
  • URLMacro — walidacja URL w czasie kompilacji, zero narzutu w runtime
  • SmartLogMacro — strukturalne logowanie z obsługą prywatności i przekierowaniem do bibliotek logowania

Warto zajrzeć na repozytorium krzysztofzablocki/Swift-Macros na GitHubie — to chyba najlepsza zbiorka dostępnych makr z podziałem na kategorie. Świetne źródło inspiracji i gotowych rozwiązań.

Kiedy NIE używać makr

Makra to potężne narzędzie, ale — jak każde potężne narzędzie — nie jest odpowiedzią na wszystko. Oto sytuacje, w których lepiej poszukać innej drogi:

  • Prosty protokół wystarczy — jeśli domyślna implementacja protokołu rozwiązuje problem, nie komplikuj życia makrem
  • Generics załatwią sprawę — parametryzacja typów często eliminuje powtarzalność bez generowania kodu
  • Wzorzec jest niestabilny — jeśli API wciąż się zmienia, makro będzie wymagało ciągłych poprawek (a debugowanie makr nie jest zabawne)
  • Debugowanie jest priorytetem — wygenerowany kod jest trudniejszy do śledzenia krok po kroku niż napisany ręcznie
  • Zespół nie zna SwiftSyntax — utrzymanie makra wymaga znajomości tej biblioteki, co podnosi próg wejścia dla nowych osób w projekcie

Konfiguracja Package.swift — zależności i kompatybilność

Prawidłowa konfiguracja pakietu to fundament. Oto minimalny Package.swift, od którego możesz zacząć:

// swift-tools-version: 5.9
import PackageDescription
import CompilerPluginSupport

let package = Package(
    name: "MyMacros",
    platforms: [.macOS(.v10_15), .iOS(.v13)],
    products: [
        .library(name: "MyMacros", targets: ["MyMacros"]),
    ],
    dependencies: [
        .package(
            url: "https://github.com/swiftlang/swift-syntax.git",
            from: "600.0.0"
        ),
    ],
    targets: [
        // Biblioteka z deklaracjami makr
        .target(name: "MyMacros", dependencies: ["MyMacrosPlugin"]),

        // Plugin kompilatora z implementacjami
        .macro(
            name: "MyMacrosPlugin",
            dependencies: [
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
            ]
        ),

        // Testy
        .testTarget(
            name: "MyMacrosTests",
            dependencies: [
                "MyMacrosPlugin",
                .product(
                    name: "SwiftSyntaxMacrosTestSupport",
                    package: "swift-syntax"
                ),
            ]
        ),
    ]
)

Wersja SwiftSyntax powinna odpowiadać wersji Swift, której używasz: 509 dla Swift 5.9, 510 dla Swift 5.10, 600 dla Swift 6.0 i 602 dla najnowszych wydań. Niezgodność wersji to jeden z najczęstszych problemów przy rozpoczynaniu pracy z makrami.

Często zadawane pytania (FAQ)

Czy makra Swift działają w runtime czy w czasie kompilacji?

Wyłącznie w czasie kompilacji. Kompilator uruchamia plugin makra jako oddzielny proces, który analizuje drzewo składniowe (AST), generuje nowy kod i wstrzykuje go do projektu przed właściwą fazą kompilacji. W gotowej aplikacji nie ma żadnego narzutu — wygenerowany kod jest identyczny z kodem, który mógłbyś napisać ręcznie.

Jakie narzędzia są potrzebne do tworzenia makr?

Potrzebujesz Xcode 15 lub nowszego (Swift 5.9+) oraz biblioteki SwiftSyntax jako zależności w Swift Package Manager. Makra muszą być osobnymi pakietami — nie mogą być częścią głównego targetu aplikacji. Opcjonalnie (ale naprawdę polecam) zainstaluj Swift AST Explorer — to narzędzie webowe do podglądu drzewa składniowego kodu, nieocenione przy pisaniu transformacji.

Czy mogę używać makr w istniejącym projekcie iOS?

Tak, i to dość prosto. Makra dodajesz jako zależność Swift Package — lokalną (w ramach workspace'u) albo zdalną (z repozytorium Git). Po dodaniu pakietu wystarczy zaimportować moduł i używać makr jak zwykłych atrybutów czy wyrażeń. Minimalne wymaganie dla projektu docelowego to iOS 13 / macOS 10.15.

Czym makra Swift różnią się od makr w C/C++?

W skrócie — praktycznie wszystkim. Makra w C/C++ to proste zastępowanie tekstu przez preprocesor. Nie mają pojęcia o typach ani kontekście. Makra Swift działają na pełnym drzewie składniowym (AST), są bezpieczne typowo, uruchamiane w sandboxie i mają dostęp do kontekstu kompilacji. Błędy wyświetlają się z precyzyjnymi komunikatami, a nie jako kryptyczne wiadomości preprocesora.

Czy makra wpływają na czas kompilacji?

Tak, mogą go wydłużyć — kompilator musi uruchomić oddzielny proces dla każdego rozwinięcia makra. W praktyce wpływ jest minimalny dla prostych makr, ale może być odczuwalny przy dużej liczbie złożonych rozwinięć. Warto monitorować czas budowania w Xcode (Product → Perform Action → Build With Timing Summary) i stosować makra tam, gdzie oszczędność boilerplate'u rzeczywiście uzasadnia ten koszt.

O Autorze Editorial Team

Our team of expert writers and editors.