cat swift/laboratorio-libreria-swift-pubblicabile.md

Lezione 4790 min

Laboratorio: libreria Swift pubblicabile

Laboratorio del Modulo 6: trasformare il dominio coupon in un package Swift con target, test, documentazione, CLI demo è CI.

Questo laboratorio chiude il modulo sul tooling professionale. L’obiettivo è creare una libreria Swift che non sia solo codice funzionante, ma un piccolo prodotto tecnico:

  • package organizzato;
  • target chiari;
  • test;
  • documentazione;
  • demo CLI;
  • comandi riproducibili;
  • CI di base.

Obiettivo

Costruire CouponKit.

Struttura finale:

CouponKit/
  Package.swift
  README.md
  Sources/
    CouponCore/
    CouponCLI/
  Tests/
    CouponCoreTests/
  .github/
    workflows/
      swift.yml

Passo 1: creare package

mkdir CouponKit
cd CouponKit
swift package init --type library

Poi riorganizza:

Sources/CouponCore
Tests/CouponCoreTests

Aggiorna i nomi nel manifest.

Passo 2: manifest

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
  name: "CouponKit",
  platforms: [
    .macOS(.v14)
  ],
  products: [
    .library(name: "CouponCore", targets: ["CouponCore"]),
    .executable(name: "coupon-cli", targets: ["CouponCLI"])
  ],
  targets: [
    .target(name: "CouponCore"),
    .executableTarget(
      name: "CouponCLI",
      dependencies: ["CouponCore"]
    ),
    .testTarget(
      name: "CouponCoreTests",
      dependencies: ["CouponCore"]
    )
  ]
)

Passo 3: dominio pubblico minimo

In Sources/CouponCore:

public struct Coupon: Equatable, Sendable {
  public let codice: String
  public let sconto: Sconto
  public let regole: [RegolaCoupon]

  public init(codice: String, sconto: Sconto, regole: [RegolaCoupon]) {
    self.codice = codice
    self.sconto = sconto
    self.regole = regole
  }
}

Ricorda: in una libreria, public va dichiarato intenzionalmente. Non tutto deve essere pubblico.

Passo 4: engine

public struct CouponEngine: Sendable {
  public init() {}

  public func applica(_ coupon: Coupon, a carrello: Carrello) throws -> Carrello {
    try valida(coupon, a: carrello)
    return carrello.applicando(coupon.sconto)
  }
}

La API deve leggere bene.

Passo 5: test

Con Swift Testing:

import Testing
@testable import CouponCore

@Test
func couponPercentualeRiduceTotale() throws {
  let carrello = Carrello.fixture(totaleInCent: 10_000)
  let coupon = Coupon.percentuale10

  let aggiornato = try CouponEngine().applica(coupon, a: carrello)

  #expect(aggiornato.totaleInCent == 9_000)
}

Crea fixture leggibili:

extension Coupon {
  static let percentuale10 = Coupon(...)
}

Passo 6: CLI demo

In Sources/CouponCLI/main.swift:

import CouponCore

let engine = CouponEngine()
let carrello = Carrello.demo
let coupon = Coupon.demo

do {
  let aggiornato = try engine.applica(coupon, a: carrello)
  print("Totale: \(aggiornato.totaleInCent)")
} catch {
  print("Coupon non valido: \(error)")
}

Esegui:

swift run coupon-cli

Passo 7: README

Un README utile contiene:

# CouponKit

Typed coupon modeling and validation for Swift packages.

## Install

Add this package with Swift Package Manager.

## Usage

```swift
let updated = try CouponEngine().applica(coupon, a: carrello)

Development

swift build
swift test
swift run coupon-cli

Il README non deve essere romanzo. Deve permettere di iniziare.

## Passo 8: DocC

Crea:

```text
Sources/CouponCore/CouponCore.docc/CouponCore.md

Con:

# CouponCore

Model coupon rules with strongly typed Swift APIs.

## Overview

Use `CouponEngine` to validate and apply coupons to carts.

Poi documenta i simboli pubblici principali con ///.

Passo 9: CI

name: Swift

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: macos-15

    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: swift build

      - name: Test
        run: swift test

Passo 10: checklist release

Prima di considerare il package pubblicabile:

  • swift build passa;
  • swift test passa;
  • swift run coupon-cli funziona;
  • README spiega installazione e uso;
  • DocC esiste;
  • API pubbliche hanno commenti;
  • errori pubblici sono documentati;
  • CI è configurata;
  • nessuna dipendenza inutile nel core;
  • i nomi dei target sono chiari.

Esercizi

Base · facile

Scrivi un Package.swift con un target libreria e un target di test, e verifica che swift build e swift test passino.

Soluzione
// swift-tools-version:6.0
import PackageDescription

let package = Package(
    name: "CouponKit",
    products: [.library(name: "CouponKit", targets: ["CouponKit"])],
    targets: [
        .target(name: "CouponKit"),
        .testTarget(name: "CouponKitTests", dependencies: ["CouponKit"]),
    ]
)
swift build   # compila la libreria
swift test    # esegue i test del testTarget

Il manifest dichiara cosa il package produce (products) e i targets che lo compongono. Il testTarget dipende dalla libreria per poterla testare.

Intermedio · medio

Aggiungi un target eseguibile per una CLI demo che usa la libreria, separato dal core. Spiega perché conviene tenere CLI e libreria in target distinti.

Soluzione
targets: [
    .target(name: "CouponKit"),                                   // core riusabile
    .executableTarget(name: "coupon-cli", dependencies: ["CouponKit"]), // demo
    .testTarget(name: "CouponKitTests", dependencies: ["CouponKit"]),
]
swift run coupon-cli --codice SCONTO10 --totale 50

Separare i target tiene il core libreria privo di codice da riga di comando (parsing argomenti, stampa): chi importa CouponKit in un’app non si porta dietro la CLI né le sue dipendenze. La CLI diventa un semplice consumatore della libreria, utile anche come esempio d’uso vivo.

Sfida · difficile

Documenta le API pubbliche con DocC e configura una CI di base che esegue build, test e documentazione a ogni push. Spiega cosa rende un package “un piccolo prodotto” e non solo codice funzionante.

Soluzione
/// Applica un coupon a un totale.
/// - Parameters:
///   - codice: il codice del coupon (non vuoto).
///   - totale: importo positivo del carrello.
/// - Returns: l'esito con totale finale ed eventuali errori.
/// - Throws: ``ErroreCoupon`` se il codice o il totale non sono validi.
public func applica(codice: String, a totale: Double) throws -> EsitoCoupon { /* ... */ }
# .github/workflows/ci.yml
on: [push, pull_request]
jobs:
  build-test:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - run: swift build
      - run: swift test

Un package è “un prodotto” quando un’altra persona può adottarlo senza chiederti niente: API pubbliche documentate (DocC), README con installazione e uso, errori pubblici spiegati, e una CI che garantisce che build e test passino a ogni modifica. Il codice che “gira sulla mia macchina” diventa software affidabile quando è documentato, testato in automatico e riproducibile.

Estensioni

Per rendere il package più professionale:

  • aggiungi semantic versioning;
  • aggiungi changelog;
  • aggiungi badge CI nel README;
  • aggiungi esempi DocC;
  • aggiungi test Linux in matrix;
  • aggiungi benchmark se le performance contano;
  • separa CouponHTTP come target futuro.