Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import PackageDescription

let package = Package(
name: "platform-swift",
name: "primitives-swift",
platforms: [
.iOS(.v16),
.macOS(.v13),
Expand All @@ -14,7 +14,6 @@ let package = Package(
.library(name: "Filtering", targets: ["Filtering"]),
.library(name: "APIErrors", targets: ["APIErrors"]),
.library(name: "Retry", targets: ["Retry"]),
.library(name: "Identifiers", targets: ["Identifiers"]),
.library(name: "RandomKit", targets: ["RandomKit"]),
.library(name: "Numbers", targets: ["Numbers"]),
.library(name: "Bitmask", targets: ["Bitmask"]),
Expand Down Expand Up @@ -112,15 +111,6 @@ let package = Package(
dependencies: ["Retry", "DurationWire"],
swiftSettings: [.swiftLanguageMode(.v6)]
),
.target(
name: "Identifiers",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "IdentifiersTests",
dependencies: ["Identifiers"],
swiftSettings: [.swiftLanguageMode(.v6)]
),
.target(
name: "RandomKit",
swiftSettings: [.swiftLanguageMode(.v6)]
Expand Down Expand Up @@ -323,12 +313,12 @@ let package = Package(
),
.target(
name: "Fake",
dependencies: ["RandomKit", "Identifiers"],
dependencies: ["RandomKit"],
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "FakeTests",
dependencies: ["Fake", "RandomKit", "Identifiers"],
dependencies: ["Fake", "RandomKit"],
swiftSettings: [.swiftLanguageMode(.v6)]
),
.target(
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A Swift port of [`primitives-go`](https://github.com/primandproper/primitives-go)'s toolkit — the same
conceptual API across languages, expressed idiomatically for Swift concurrency and iOS. It ships as a
set of **independent products** (33 libraries: observability, HTTP, event streams, crypto/auth,
set of **independent products** (32 libraries: observability, HTTP, event streams, crypto/auth,
analytics, feature flags, in-app purchase, and more) that you adopt à la carte. The design rules
are thin/native/no-third-party-SPM-SDK: URLSession + Codable, CryptoKit, StoreKit, UserNotifications,
with protocol seams shaped so a native or vendor adapter can wrap later.
Expand All @@ -14,6 +14,11 @@ there is. That makes this port unusual among the primitives ports in how *little
API key on a handset, or that only makes sense beside server infrastructure. Talking to a service built
on `platform-go` is `platform-client-swift`'s job, not this package's.

Identifiers are the case worth naming, since this package used to carry a full xid
implementation: a server issues IDs and a client receives opaque strings, so minting one here
only risks having it rejected by the service you send it to. ``Fake/opaqueID()`` produces an
ID-*shaped* string for test data and deliberately parses nothing.

The keystone — and the deepest port — is **observability**. Its **Observer / Operation** abstraction is
a per-component bundle of a named logger and tracer, where `op.set(key, value)` records to **both** the
active span and a trace-enriched logger at once. On Apple platforms it lights up Instruments (spans) and
Expand Down Expand Up @@ -344,7 +349,6 @@ module.
| `Bitmask` | pure-logic | 🟢 bitmask set ops, full Go parity |
| `Numbers` | pure-logic | 🟢 numeric helpers + range clamping |
| `Version` | pure-logic | 🟢 build/version info, JSON + text rendering |
| `Identifiers` | pure-logic | 🟢 `XID` (Go xid wire-compatible, wrap-safe) |
| `RandomKit` | pure-logic | 🟢 `SecRandomCopyBytes` generator, Base32, slice helpers; noop |
| `APIErrors` | pure-logic | 🟢 `APIResponse` / `ErrorCode` types |
| `Filtering` | pure-logic | 🟡 pagination / query-filter / RFC3339; `FromParams`/`ToPagination` **not ported** |
Expand Down
10 changes: 6 additions & 4 deletions Sources/Fake/Fake.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
import Identifiers
import RandomKit

/// # Fake
Expand Down Expand Up @@ -96,9 +95,12 @@ public enum Fake: Sendable {
UUID().uuidString
}

/// A random 20-character xid string. Delegates to ``Identifiers/Identifier/new()``.
public static func xid() -> String {
Identifier.new()
/// A random 20-character lowercase base32-hex string, shaped like a server-issued ID
/// without being one. A client never mints a server's identifiers; this exists so tests
/// have a plausible stand-in for one the server would have sent.
public static func opaqueID() -> String {
var source = SystemFakeSource()
return FakeEngine.opaqueID(using: &source)
}

/// A random `Bool`.
Expand Down
11 changes: 11 additions & 0 deletions Sources/Fake/FakeEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ enum FakeEngine {
return String(format: "(%03d) %03d-%04d", area, exchange, line)
}

/// The base32-hex alphabet (lowercased), which is what server-issued IDs happen to look
/// like. Nothing here parses or validates an ID — this produces a string of the right
/// *shape* for test data, and deliberately carries no timestamp or machine identity.
private static let opaqueIDAlphabet = Array("0123456789abcdefghijklmnopqrstuv")

/// A 20-character lowercase base32-hex string, drawn entirely from `source` so a seeded
/// generator reproduces it.
static func opaqueID<S: FakeSource>(using source: inout S) -> String {
String((0..<20).map { _ in source.pick(from: opaqueIDAlphabet) ?? "0" })
}

static func url<S: FakeSource>(using source: inout S) -> String {
let host = word(using: &source)
let tld = source.pick(from: FakeCorpus.topLevelDomains) ?? "com"
Expand Down
18 changes: 8 additions & 10 deletions Sources/Fake/FakeGenerator.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
import Identifiers

/// A seeded, reproducible counterpart to ``Fake``, for callers that need the *same* fixture data
/// every time — most notably SwiftUI `#Preview` bodies, which re-render on every canvas refresh
Expand All @@ -15,11 +14,9 @@ import Identifiers
/// non-deterministic one — so two generators constructed with the same seed and driven through the
/// same call sequence always produce identical output.
///
/// **Where seeding is not feasible.** ``uuid()`` and ``xid()`` delegate to `Foundation.UUID` and
/// ``Identifiers/Identifier/new()`` respectively, neither of which exposes a seedable API (xid in
/// particular mixes in wall-clock time, machine ID, and process ID by design) — so these two
/// remain non-deterministic even on a seeded generator. Every other generator here is fully
/// reproducible.
/// **Where seeding is not feasible.** ``uuid()`` delegates to `Foundation.UUID`, which exposes no
/// seedable API, so it remains non-deterministic even on a seeded generator. Every other generator
/// here is fully reproducible — including ``opaqueID()``, which replaced an `xid()` that was not.
public struct FakeGenerator: Sendable {
private var source: SeededFakeSource

Expand Down Expand Up @@ -87,10 +84,11 @@ public struct FakeGenerator: Sendable {
UUID().uuidString
}

/// A random 20-character xid string. Not seed-reproducible — see the type-level discussion
/// above.
public func xid() -> String {
Identifier.new()
/// A reproducible 20-character lowercase base32-hex string, shaped like a server-issued
/// ID without being one. Unlike the `xid()` this replaces, it is drawn from the seeded
/// source, so a seeded generator reproduces it.
public mutating func opaqueID() -> String {
FakeEngine.opaqueID(using: &source)
}

/// A reproducible random `Bool`.
Expand Down
41 changes: 0 additions & 41 deletions Sources/Identifiers/Identifier.swift

This file was deleted.

144 changes: 0 additions & 144 deletions Sources/Identifiers/XID.swift

This file was deleted.

11 changes: 7 additions & 4 deletions Tests/FakeTests/FakeTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
import Identifiers
import Testing

@testable import Fake
Expand Down Expand Up @@ -76,9 +75,11 @@ struct FakeStaticGeneratorTests {
#expect(UUID(uuidString: Fake.uuid()) != nil)
}

@Test("xid is a valid Identifiers xid")
func xid() {
#expect(Identifier.isValid(Fake.xid()))
@Test("opaqueID is 20 lowercase base32-hex characters")
func opaqueID() {
let id = Fake.opaqueID()
#expect(id.count == 20)
#expect(id.allSatisfy { "0123456789abcdefghijklmnopqrstuv".contains($0) })
}

@Test("bool returns without trapping")
Expand Down Expand Up @@ -139,6 +140,8 @@ struct FakeGeneratorSeededTests {
#expect(a.int(in: 0...1000) == b.int(in: 0...1000))
#expect(a.double(in: 0...1000) == b.double(in: 0...1000))
#expect(a.pick(from: [1, 2, 3, 4, 5]) == b.pick(from: [1, 2, 3, 4, 5]))
// opaqueID replaced an xid() that could not be seeded; this is the difference.
#expect(a.opaqueID() == b.opaqueID())
}

@Test("different seeds are very likely to diverge")
Expand Down
Loading