fosmvvm-viewmodel-generator
Generate FOSMVVM ViewModels for SwiftUI screens, pages, and components. Scaffolds RequestableViewModel, localization bindings, and stub factories.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-fosmvvm-viewmodel-generator
Repository
Skill path: skills/foscomputerservices/fosmvvm-viewmodel-generator
Generate FOSMVVM ViewModels for SwiftUI screens, pages, and components. Scaffolds RequestableViewModel, localization bindings, and stub factories.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install fosmvvm-viewmodel-generator into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding fosmvvm-viewmodel-generator to shared team environments
- Use fosmvvm-viewmodel-generator for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: fosmvvm-viewmodel-generator
description: Generate FOSMVVM ViewModels for SwiftUI screens, pages, and components. Scaffolds RequestableViewModel, localization bindings, and stub factories.
homepage: https://github.com/foscomputerservices/FOSUtilities
metadata: {"clawdbot": {"emoji": "ποΈ", "os": ["darwin", "linux"]}}
---
# FOSMVVM ViewModel Generator
Generate ViewModels following FOSMVVM architecture patterns.
## Conceptual Foundation
> For full architecture context, see [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) | [OpenClaw reference]({baseDir}/references/FOSMVVMArchitecture.md)
A **ViewModel** is the bridge in the Model-View-ViewModel architecture:
```
βββββββββββββββ βββββββββββββββββββ βββββββββββββββ
β Model β ββββΊ β ViewModel β ββββΊ β View β
β (Data) β β (The Bridge) β β (SwiftUI) β
βββββββββββββββ βββββββββββββββββββ βββββββββββββββ
```
**Key insight:** In FOSMVVM, ViewModels are:
- **Created by a Factory** (either server-side or client-side)
- **Localized during encoding** (resolves all `@LocalizedString` references)
- **Consumed by Views** which just render the localized data
---
## First Decision: Hosting Mode
**This is a per-ViewModel decision.** An app can mix both modes - for example, a standalone iPhone app with server-based sign-in.
**The key question: Where does THIS ViewModel's data come from?**
| Data Source | Hosting Mode | Factory |
|-------------|--------------|---------|
| Server/Database | Server-Hosted | Hand-written |
| Local state/preferences | Client-Hosted | Macro-generated |
| **ResponseError (caught error)** | **Client-Hosted** | Macro-generated |
### Server-Hosted Mode
When data comes from a server:
- Factory is **hand-written** on server (`ViewModelFactory` protocol)
- Factory queries database, builds ViewModel
- Server localizes during JSON encoding
- Client receives fully localized ViewModel
**Examples:** Sign-in screen, user profile from API, dashboard with server data
### Client-Hosted Mode
When data is local to the device:
- Use `@ViewModel(options: [.clientHostedFactory])`
- Macro **auto-generates** factory from init parameters
- Client bundles YAML resources
- Client localizes during encoding
**Examples:** Settings screen, onboarding, offline-first features, **error display**
### Error Display Pattern
Error display is a classic client-hosted scenario. You already have the data from `ResponseError` - just wrap it in a **specific** ViewModel for that error:
```swift
// Specific ViewModel for MoveIdeaRequest errors
@ViewModel(options: [.clientHostedFactory])
struct MoveIdeaErrorViewModel {
let message: LocalizableString
let errorCode: String
public var vmId = ViewModelId()
// Takes the specific ResponseError
init(responseError: MoveIdeaRequest.ResponseError) {
self.message = responseError.message
self.errorCode = responseError.code.rawValue
}
}
```
Usage:
```swift
catch let error as MoveIdeaRequest.ResponseError {
let vm = MoveIdeaErrorViewModel(responseError: error)
return try await req.view.render("Shared/ToastView", vm)
}
```
**Each error scenario gets its own ViewModel:**
- `MoveIdeaErrorViewModel` for `MoveIdeaRequest.ResponseError`
- `CreateIdeaErrorViewModel` for `CreateIdeaRequest.ResponseError`
- `SettingsValidationErrorViewModel` for settings form errors
Don't create a generic "ToastViewModel" or "ErrorViewModel" - that's unified error architecture, which we avoid.
**Key insights:**
- No server request needed - you already caught the error
- The `LocalizableString` properties in `ResponseError` are **already localized** (server did it)
- Standard ViewModel β View encoding chain handles this correctly; already-localized strings pass through unchanged
- Client-hosted ViewModel wraps existing data; the macro generates the factory
### Hybrid Apps
Many apps use both:
```
βββββββββββββββββββββββββββββββββββββββββββββββββ
β iPhone App β
βββββββββββββββββββββββββββββββββββββββββββββββββ€
β SettingsViewModel β Client-Hosted β
β OnboardingViewModel β Client-Hosted β
β MoveIdeaErrorViewModel β Client-Hosted β β Error display
β SignInViewModel β Server-Hosted β
β UserProfileViewModel β Server-Hosted β
βββββββββββββββββββββββββββββββββββββββββββββββββ
```
**Same ViewModel patterns work in both modes** - only the factory creation differs.
### Core Responsibility: Shaping Data
A ViewModel's job is **shaping data for presentation**. This happens in two places:
1. **Factory** - *what* data is needed, *how* to transform it
2. **Localization** - *how* to present it in context (including locale-aware ordering)
**The View just renders** - it should never compose, format, or reorder ViewModel properties.
### What a ViewModel Contains
A ViewModel answers: **"What does the View need to display?"**
| Content Type | How It's Represented | Example |
|--------------|---------------------|---------|
| Static UI text | `@LocalizedString` | Page titles, button labels (fixed text) |
| Dynamic enum values | `LocalizableString` (stored) | Status/state display (see Enum Localization Pattern) |
| Dynamic data in text | `@LocalizedSubs` | "Welcome, %{name}!" with substitutions |
| Composed text | `@LocalizedCompoundString` | Full name from pieces (locale-aware order) |
| Formatted dates | `LocalizableDate` | `createdAt: LocalizableDate` |
| Formatted numbers | `LocalizableInt` | `totalCount: LocalizableInt` |
| Dynamic data | Plain properties | `content: String`, `count: Int` |
| Nested components | Child ViewModels | `cards: [CardViewModel]` |
### What a ViewModel Does NOT Contain
- Database relationships (`@Parent`, `@Siblings`)
- Business logic or validation (that's in Fields protocols)
- Raw database IDs exposed to templates (use typed properties)
- Unlocalized strings that Views must look up
### Anti-Pattern: Composition in Views
```swift
// β WRONG - View is composing
Text(viewModel.firstName) + Text(" ") + Text(viewModel.lastName)
// β
RIGHT - ViewModel provides shaped result
Text(viewModel.fullName) // via @LocalizedCompoundString
```
If you see `+` or string interpolation in a View, the shaping belongs in the ViewModel.
## ViewModel Protocol Hierarchy
```swift
public protocol ViewModel: ServerRequestBody, RetrievablePropertyNames, Identifiable, Stubbable {
var vmId: ViewModelId { get }
}
public protocol RequestableViewModel: ViewModel {
associatedtype Request: ViewModelRequest
}
```
**ViewModel** provides:
- `ServerRequestBody` - Can be sent over HTTP as JSON
- `RetrievablePropertyNames` - Enables `@LocalizedString` binding (via `@ViewModel` macro)
- `Identifiable` - Has `vmId` for SwiftUI identity
- `Stubbable` - Has `stub()` for testing/previews
**RequestableViewModel** adds:
- Associated `Request` type for fetching from server
## Two Categories of ViewModels
### 1. Top-Level (RequestableViewModel)
Represents a full page or screen. Has:
- An associated `ViewModelRequest` type
- A `ViewModelFactory` that builds it from database
- Child ViewModels embedded within it
```swift
@ViewModel
public struct DashboardViewModel: RequestableViewModel {
public typealias Request = DashboardRequest
@LocalizedString public var pageTitle
public let cards: [CardViewModel] // Children
public var vmId: ViewModelId = .init()
}
```
### 2. Child (plain ViewModel)
Nested components built by their parent's factory. No Request type.
```swift
@ViewModel
public struct CardViewModel: Codable, Sendable {
public let id: ModelIdType
public let title: String
public let createdAt: LocalizableDate
public var vmId: ViewModelId = .init()
}
```
---
## Display vs Form ViewModels
ViewModels serve two distinct purposes:
| Purpose | ViewModel Type | Adopts Fields? |
|---------|----------------|----------------|
| **Display data** (read-only) | Display ViewModel | No |
| **Collect user input** (editable) | Form ViewModel | Yes |
### Display ViewModels
For showing data - cards, rows, lists, detail views:
```swift
@ViewModel
public struct UserCardViewModel {
public let id: ModelIdType
public let name: String
@LocalizedString public var roleDisplayName
public let createdAt: LocalizableDate
public var vmId: ViewModelId = .init()
}
```
**Characteristics:**
- Properties are `let` (read-only)
- No validation needed
- No FormField definitions
- Just projects Model data for display
### Form ViewModels
For collecting input - create forms, edit forms, settings:
```swift
@ViewModel
public struct UserFormViewModel: UserFields { // β Adopts Fields!
public var id: ModelIdType?
public var email: String
public var firstName: String
public var lastName: String
public let userValidationMessages: UserFieldsMessages
public var vmId: ViewModelId = .init()
}
```
**Characteristics:**
- Properties are `var` (editable)
- **Adopts a Fields protocol** for validation
- Gets FormField definitions from Fields
- Gets validation logic from Fields
- Gets localized error messages from Fields
### The Connection
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β UserFields Protocol β
β (defines editable properties + validation) β
β β
β Adopted by: β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β
β β CreateUserReq β β UserFormVM β β User (Model) β β
β β .RequestBody β β (UI form) β β (persistence) β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β
β β
β Same validation logic everywhere! β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
### Quick Decision Guide
**The key question: "Is the user editing data in this ViewModel?"**
- **No** β Display ViewModel (no Fields)
- **Yes** β Form ViewModel (adopt Fields)
| ViewModel | User Edits? | Adopt Fields? |
|-----------|-------------|---------------|
| `UserCardViewModel` | No | No |
| `UserRowViewModel` | No | No |
| `UserDetailViewModel` | No | No |
| `UserFormViewModel` | Yes | `UserFields` |
| `CreateUserViewModel` | Yes | `UserFields` |
| `EditUserViewModel` | Yes | `UserFields` |
| `SettingsViewModel` | Yes | `SettingsFields` |
---
## When to Use This Skill
- Creating a new page or screen
- Adding a new UI component (card, row, modal, etc.)
- Displaying data from the database in a View
- Following an implementation plan that requires new ViewModels
## What This Skill Generates
### Server-Hosted: Top-Level ViewModel (4 files)
| File | Location | Purpose |
|------|----------|---------|
| `{Name}ViewModel.swift` | `{ViewModelsTarget}/` | The ViewModel struct |
| `{Name}Request.swift` | `{ViewModelsTarget}/` | The ViewModelRequest type |
| `{Name}ViewModel.yml` | `{ResourcesPath}/` | Localization strings |
| `{Name}ViewModel+Factory.swift` | `{WebServerTarget}/` | Factory that builds from DB |
### Client-Hosted: Top-Level ViewModel (2 files)
| File | Location | Purpose |
|------|----------|---------|
| `{Name}ViewModel.swift` | `{ViewModelsTarget}/` | ViewModel with `clientHostedFactory` option |
| `{Name}ViewModel.yml` | `{ResourcesPath}/` | Localization strings (bundled in app) |
*No Request or Factory files needed - macro generates them!*
### Child ViewModels (1-2 files, either mode)
| File | Location | Purpose |
|------|----------|---------|
| `{Name}ViewModel.swift` | `{ViewModelsTarget}/` | The ViewModel struct |
| `{Name}ViewModel.yml` | `{ResourcesPath}/` | Localization (if has `@LocalizedString`) |
**Note:** If child is only used by one parent and represents a summary/reference (not a full ViewModel), nest it inside the parent file instead. See **Nested Child Types Pattern** under Key Patterns.
## Project Structure Configuration
| Placeholder | Description | Example |
|-------------|-------------|---------|
| `{ViewModelsTarget}` | Shared ViewModels SPM target | `ViewModels` |
| `{ResourcesPath}` | Localization resources | `Sources/Resources` |
| `{WebServerTarget}` | Server-side target | `WebServer`, `AppServer` |
## How to Use This Skill
**Invocation:**
/fosmvvm-viewmodel-generator
**Prerequisites:**
- View requirements understood from conversation context
- Data source determined (server/database vs local state)
- Display vs Form decision made (if user input involved, Fields protocol exists)
**Workflow integration:**
This skill is typically used after discussing View requirements or reading specification files. The skill references conversation context automaticallyβno file paths or Q&A needed. For Form ViewModels, run fosmvvm-fields-generator first to create the Fields protocol.
## Pattern Implementation
This skill references conversation context to determine ViewModel structure:
### Hosting Mode Detection
From conversation context, the skill identifies:
- **Data source** (server/database vs local state/preferences)
- Server-hosted β Hand-written factory, server-side localization
- Client-hosted β Macro-generated factory, client-side localization
### ViewModel Design
From requirements already in context:
- **View purpose** (page, modal, card, row component)
- **Data needs** (from database query, from AppState, from caught error)
- **Static UI text** (titles, labels, buttons requiring @LocalizedString)
- **Child ViewModels** (nested components)
- **Hierarchy level** (top-level RequestableViewModel vs child ViewModel)
### Property Planning
Based on View requirements:
- **Display properties** (data to render)
- **Localization requirements** (which properties use @LocalizedString)
- **Identity strategy** (singleton vmId vs instance-based vmId)
- **Form adoption** (whether ViewModel adopts Fields protocol)
### File Generation
**Server-Hosted Top-Level:**
1. ViewModel struct (with `RequestableViewModel`)
2. Request type
3. YAML localization
4. Factory implementation
**Client-Hosted Top-Level:**
1. ViewModel struct (with `clientHostedFactory` option)
2. YAML localization
**Child (either mode):**
1. ViewModel struct
2. YAML localization (if needed)
### Context Sources
Skill references information from:
- **Prior conversation**: View requirements, data sources discussed with user
- **Specification files**: If Claude has read UI specs or feature docs into context
- **Fields protocols**: From codebase or previous fosmvvm-fields-generator invocation
## Key Patterns
### The @ViewModel Macro
Always use the `@ViewModel` macro - it generates the `propertyNames()` method required for localization binding.
**Server-Hosted** (basic macro):
```swift
@ViewModel
public struct MyViewModel: RequestableViewModel {
public typealias Request = MyRequest
@LocalizedString public var title
public var vmId: ViewModelId = .init()
public init() {}
}
```
**Client-Hosted** (with factory generation):
```swift
@ViewModel(options: [.clientHostedFactory])
public struct SettingsViewModel {
@LocalizedString public var pageTitle
public var vmId: ViewModelId = .init()
public init(theme: Theme, notifications: NotificationSettings) {
// Init parameters become AppState properties
}
}
// Macro auto-generates:
// - typealias Request = ClientHostedRequest
// - struct AppState { let theme: Theme; let notifications: NotificationSettings }
// - class ClientHostedRequest: ViewModelRequest { ... }
// - static func model(context:) async throws -> Self { ... }
```
### Stubbable Pattern
All ViewModels must support `stub()` for testing and SwiftUI previews:
```swift
public extension MyViewModel {
static func stub() -> Self {
.init(/* default values */)
}
}
```
### Identity: vmId
Every ViewModel needs a `vmId` for SwiftUI's identity system:
**Singleton** (one per page): `vmId = .init(type: Self.self)`
**Instance** (multiple per page): `vmId = .init(id: id)` where `id: ModelIdType`
### Localization
Static UI text uses `@LocalizedString`:
```swift
@LocalizedString public var pageTitle
```
With corresponding YAML:
```yaml
en:
MyViewModel:
pageTitle: "Welcome"
```
### Dates and Numbers
Never send pre-formatted strings. Use localizable types:
```swift
public let createdAt: LocalizableDate // NOT String
public let itemCount: LocalizableInt // NOT String
```
The client formats these according to user's locale and timezone.
### Enum Localization Pattern
For dynamic enum values (status, state, category), use a **stored `LocalizableString`** - NOT `@LocalizedString`.
`@LocalizedString` always looks up the same key (the property name). A stored `LocalizableString` carries the dynamic key from the enum case.
```swift
// Enum provides localizableString
public enum SessionState: String, CaseIterable, Codable, Sendable {
case pending, running, completed, failed
public var localizableString: LocalizableString {
.localized(for: Self.self, propertyName: rawValue)
}
}
// ViewModel stores it (NOT @LocalizedString)
@ViewModel
public struct SessionCardViewModel {
public let state: SessionState // Raw enum for data attributes
public let stateDisplay: LocalizableString // Localized display text
public init(session: Session) {
self.state = session.state
self.stateDisplay = session.state.localizableString
}
}
```
```yaml
# YAML keys match enum type and case names
en:
SessionState:
pending: "Pending"
running: "Running"
completed: "Completed"
failed: "Failed"
```
**Constraint:** `LocalizableString` only works in ViewModels encoded with `localizingEncoder()`. Do not use in Fluent JSONB fields or other persisted types.
### Child ViewModels
Top-level ViewModels contain their children:
```swift
@ViewModel
public struct BoardViewModel: RequestableViewModel {
public let columns: [ColumnViewModel]
public let cards: [CardViewModel]
}
```
The Factory builds all children when building the parent.
#### Nested Child Types Pattern
When a child type is **only used by one parent** and represents a summary or reference (not a full ViewModel), nest it inside the parent:
```swift
@ViewModel
public struct GovernancePrincipleCardViewModel: Codable, Sendable, Identifiable {
// Properties come first
public let versionHistory: [GovernancePrincipleVersionSummary]?
public let referencingDecisions: [GovernanceDecisionReference]?
// MARK: - Nested Types
/// Summary of a principle version for display in version history.
public struct GovernancePrincipleVersionSummary: Codable, Sendable, Identifiable, Stubbable {
public let id: ModelIdType
public let version: Int
public let createdAt: Date
public init(id: ModelIdType, version: Int, createdAt: Date) {
self.id = id
self.version = version
self.createdAt = createdAt
}
}
/// Reference to a decision that cites this principle.
public struct GovernanceDecisionReference: Codable, Sendable, Identifiable, Stubbable {
public let id: ModelIdType
public let title: String
public let decisionNumber: String
public let createdAt: Date
public init(id: ModelIdType, title: String, decisionNumber: String, createdAt: Date) {
self.id = id
self.title = title
self.decisionNumber = decisionNumber
self.createdAt = createdAt
}
}
// vmId and parent init follow
public let vmId: ViewModelId
// ...
}
```
**Reference:** `Sources/KairosModels/Governance/GovernancePrincipleCardViewModel.swift`
**Placement rules:**
1. Nested types go AFTER the properties that reference them
2. Before `vmId` and the parent's init
3. Use `// MARK: - Nested Types` section marker
4. Each nested type gets its own doc comment
**Conformances for nested types:**
- `Codable` - for ViewModel encoding
- `Sendable` - for Swift 6 concurrency
- `Identifiable` - for SwiftUI ForEach if used in arrays
- `Stubbable` - for testing/previews
**Two-Tier Stubbable Pattern:**
Nested types use fully qualified names in their extensions:
```swift
public extension GovernancePrincipleCardViewModel.GovernancePrincipleVersionSummary {
// Tier 1: Zero-arg convenience (ALWAYS delegates to tier 2)
static func stub() -> Self {
.stub(id: .init())
}
// Tier 2: Full parameterized with defaults
static func stub(
id: ModelIdType = .init(),
version: Int = 1,
createdAt: Date = .now
) -> Self {
.init(id: id, version: version, createdAt: createdAt)
}
}
public extension GovernancePrincipleCardViewModel.GovernanceDecisionReference {
static func stub() -> Self {
.stub(id: .init())
}
static func stub(
id: ModelIdType = .init(),
title: String = "A Title",
decisionNumber: String = "DEC-12345",
createdAt: Date = .now
) -> Self {
.init(id: id, title: title, decisionNumber: decisionNumber, createdAt: createdAt)
}
}
```
**Why two tiers:**
- Tests often just need `[.stub()]` without caring about values
- Other tests need specific values: `.stub(name: "Specific Name")`
- Zero-arg ALWAYS calls parameterized version (single source of truth)
**When to nest vs keep top-level:**
| Nest Inside Parent | Keep Top-Level |
|-------------------|----------------|
| Child is ONLY used by this parent | Child is shared across multiple parents |
| Child represents subset/summary | Child is a full ViewModel |
| Child has no @ViewModel macro | Child has @ViewModel macro |
| Child is not RequestableViewModel | Child is RequestableViewModel |
| Example: VersionSummary, Reference | Example: CardViewModel, ListViewModel |
**Examples:**
Card with nested summaries:
```swift
@ViewModel
public struct TaskCardViewModel {
public let assignees: [AssigneeSummary]?
public struct AssigneeSummary: Codable, Sendable, Identifiable, Stubbable {
public let id: ModelIdType
public let name: String
public let avatarUrl: String?
// ...
}
}
```
List with nested references:
```swift
@ViewModel
public struct ProjectListViewModel {
public let relatedProjects: [ProjectReference]?
public struct ProjectReference: Codable, Sendable, Identifiable, Stubbable {
public let id: ModelIdType
public let title: String
public let status: String
// ...
}
}
```
### Codable and Computed Properties
Swift's synthesized `Codable` only encodes **stored properties**. Since ViewModels are serialized (for JSON transport, Leaf rendering, etc.), computed properties won't be available.
```swift
// Computed - NOT encoded, invisible after serialization
public var hasCards: Bool { !cards.isEmpty }
// Stored - encoded, available after serialization
public let hasCards: Bool
```
**When to pre-compute:**
For Leaf templates, you can often use Leaf's built-in functions directly:
- `#if(count(cards) > 0)` - no need for `hasCards` property
- `#count(cards)` - no need for `cardCount` property
Pre-compute only when:
- Direct array subscripts needed (`firstCard` - array indexing not documented in Leaf)
- Complex logic that's cleaner in Swift than in template
- Performance-sensitive repeated calculations
See [fosmvvm-leaf-view-generator](../fosmvvm-leaf-view-generator/SKILL.md) for Leaf template patterns.
## File Templates
See [reference.md](reference.md) for complete file templates.
## Naming Conventions
| Concept | Convention | Example |
|---------|------------|---------|
| ViewModel struct | `{Name}ViewModel` | `DashboardViewModel` |
| Request class | `{Name}Request` | `DashboardRequest` |
| Factory extension | `{Name}ViewModel+Factory.swift` | `DashboardViewModel+Factory.swift` |
| YAML file | `{Name}ViewModel.yml` | `DashboardViewModel.yml` |
## See Also
- [Architecture Patterns](../shared/architecture-patterns.md) - Mental models (errors are data, type safety, etc.)
- [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) - Full FOSMVVM architecture
- [fosmvvm-fields-generator](../fosmvvm-fields-generator/SKILL.md) - For form validation
- [fosmvvm-fluent-datamodel-generator](../fosmvvm-fluent-datamodel-generator/SKILL.md) - For Fluent persistence layer
- [fosmvvm-leaf-view-generator](../fosmvvm-leaf-view-generator/SKILL.md) - For Leaf templates that render ViewModels
- [reference.md](reference.md) - Complete file templates
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2024-12-24 | Initial skill |
| 2.0 | 2024-12-26 | Complete rewrite from architecture; generalized from Kairos-specific |
| 2.1 | 2024-12-26 | Added Client-Hosted mode support; per-ViewModel hosting decision |
| 2.2 | 2024-12-26 | Added shaping responsibility, @LocalizedSubs/@LocalizedCompoundString, anti-pattern |
| 2.3 | 2025-12-27 | Added Display vs Form ViewModels section; clarified Fields adoption |
| 2.4 | 2026-01-08 | Added Codable/computed properties section. Clarified when to pre-compute vs use Leaf built-ins. |
| 2.5 | 2026-01-19 | Added Enum Localization Pattern section. Clarified @LocalizedString is for static text only; stored LocalizableString for dynamic enum values. |
| 2.6 | 2026-01-24 | Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths. |
| 2.7 | 2026-01-25 | Added Nested Child Types Pattern section with two-tier Stubbable pattern, placement rules, conformances, and decision criteria for when to nest vs keep top-level. |
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### ../fosmvvm-leaf-view-generator/SKILL.md
```markdown
---
name: fosmvvm-leaf-view-generator
description: Generate Leaf templates for FOSMVVM WebApps. Create full-page views and HTML-over-the-wire fragments that render ViewModels.
homepage: https://github.com/foscomputerservices/FOSUtilities
metadata: {"clawdbot": {"emoji": "π", "os": ["darwin", "linux"]}}
---
# FOSMVVM Leaf View Generator
Generate Leaf templates that render ViewModels for web clients.
> **Architecture context:** See [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) | [OpenClaw reference]({baseDir}/references/FOSMVVMArchitecture.md)
---
## The View Layer for WebApps
In FOSMVVM, Leaf templates are the **View** in M-V-VM for web clients:
```
Model β ViewModel β Leaf Template β HTML
β β
(localized) (renders it)
```
**Key principle:** The ViewModel is already localized when it reaches the template. The template just renders what it receives.
---
## Core Principle: View-ViewModel Alignment
**The Leaf filename should match the ViewModel it renders.**
```
Sources/
{ViewModelsTarget}/
ViewModels/
{Feature}ViewModel.swift ββββ
{Entity}CardViewModel.swift ββββΌββ Same names
β
{WebAppTarget}/ β
Resources/Views/ β
{Feature}/ β
{Feature}View.leaf βββββ€ (renders {Feature}ViewModel)
{Entity}CardView.leaf βββββ (renders {Entity}CardViewModel)
```
This alignment provides:
- **Discoverability** - Find the template for any ViewModel instantly
- **Consistency** - Same naming discipline as SwiftUI
- **Maintainability** - Changes to ViewModel are reflected in template location
---
## Two Template Types
### Full-Page Templates
Render a complete page with layout, navigation, CSS/JS includes.
```
{Feature}View.leaf
βββ Extends base layout
βββ Includes <html>, <head>, <body>
βββ Renders {Feature}ViewModel
βββ May embed fragment templates for components
```
**Use for:** Initial page loads, navigation destinations.
### Fragment Templates
Render a single component - no layout, no page structure.
```
{Entity}CardView.leaf
βββ NO layout extension
βββ Single root element
βββ Renders {Entity}CardViewModel
βββ Has data-* attributes for state
βββ Returned to JS for DOM swapping
```
**Use for:** Partial updates, HTML-over-the-wire responses.
---
## The HTML-Over-The-Wire Pattern
For dynamic updates without full page reloads:
```
JS Event β WebApp Route β ServerRequest.processRequest() β Controller
β
ViewModel
β
HTML β JS DOM swap β WebApp returns β Leaf renders ββββββββββ
```
**The WebApp route:**
```swift
app.post("move-{entity}") { req async throws -> Response in
let body = try req.content.decode(Move{Entity}Request.RequestBody.self)
let serverRequest = Move{Entity}Request(requestBody: body)
guard let response = try await serverRequest.processRequest(baseURL: app.serverBaseURL) else {
throw Abort(.internalServerError)
}
// Render fragment template with ViewModel
return try await req.view.render(
"{Feature}/{Entity}CardView",
["card": response.viewModel]
).encodeResponse(for: req)
}
```
**JS receives HTML and swaps it into the DOM** - no JSON parsing, no client-side rendering.
---
## When to Use This Skill
- Creating a new page template (full-page)
- Creating a new card, row, or component template (fragment)
- Adding data attributes for JS event handling
- Troubleshooting Localizable types not rendering correctly
- Setting up templates for HTML-over-the-wire responses
---
## Key Patterns
### Pattern 1: Data Attributes for State
Fragments must embed all state that JS needs for future actions:
```html
<div class="{entity}-card"
data-{entity}-id="#(card.id)"
data-status="#(card.status)"
data-category="#(card.category)"
draggable="true">
```
**Rules:**
- `data-{entity}-id` for the primary identifier
- `data-{field}` for state values (kebab-case)
- Store **raw values** (enum cases), not localized display names
- JS reads these to build ServerRequest payloads
```javascript
const request = {
{entity}Id: element.dataset.{entity}Id,
newStatus: targetColumn.dataset.status
};
```
### Pattern 2: Localizable Types in Leaf
FOSMVVM's `LeafDataRepresentable` conformance handles Localizable types automatically.
**In templates, just use the property:**
```html
<span class="date">#(card.createdAt)</span>
<!-- Renders: "Dec 27, 2025" (localized) -->
```
**If Localizable types render incorrectly** (showing `[ds: "2", ls: "...", v: "..."]`):
1. Ensure FOSMVVMVapor is imported
2. Check `Localizable+Leaf.swift` exists with conformances
3. Clean build: `swift package clean && swift build`
### Pattern 3: Display Values vs Identifiers
ViewModels should provide both raw values (for data attributes) and localized strings (for display). For enum localization, see the [Enum Localization Pattern](../fosmvvm-viewmodel-generator/SKILL.md#enum-localization-pattern).
```swift
@ViewModel
public struct {Entity}CardViewModel {
public let id: ModelIdType // For data-{entity}-id
public let status: {Entity}Status // Raw enum for data-status
public let statusDisplay: LocalizableString // Localized (stored, not @LocalizedString)
}
```
```html
<div data-status="#(card.status)"> <!-- Raw: "queued" for JS -->
<span class="badge">#(card.statusDisplay)</span> <!-- Localized: "In Queue" -->
</div>
```
### Pattern 4: Fragment Structure
Fragments are minimal - just the component:
```html
<!-- {Entity}CardView.leaf -->
<div class="{entity}-card"
data-{entity}-id="#(card.id)"
data-status="#(card.status)">
<div class="card-content">
<p class="text">#(card.contentPreview)</p>
</div>
<div class="card-footer">
<span class="creator">#(card.creatorName)</span>
<span class="date">#(card.createdAt)</span>
</div>
</div>
```
**Rules:**
1. NO `#extend("base")` - fragments don't use layouts
2. **Single root element** - makes DOM swapping clean
3. All required state in data-* attributes
4. Localized values from ViewModel properties
### Pattern 5: Full-Page Structure
Full pages extend a base layout:
```html
<!-- {Feature}View.leaf -->
#extend("base"):
#export("content"):
<div class="{feature}-container">
<header class="{feature}-header">
<h1>#(viewModel.title)</h1>
</header>
<main class="{feature}-content">
#for(card in viewModel.cards):
#extend("{Feature}/{Entity}CardView")
#endfor
</main>
</div>
#endexport
#endextend
```
### Pattern 6: Conditional Rendering
```html
#if(card.isHighPriority):
<span class="priority-badge">#(card.priorityLabel)</span>
#endif
#if(card.assignee):
<div class="assignee">
<span class="name">#(card.assignee.name)</span>
</div>
#else:
<div class="unassigned">#(card.unassignedLabel)</div>
#endif
```
### Pattern 7: Looping with Embedded Fragments
```html
<div class="column" data-status="#(column.status)">
<div class="column-header">
<h3>#(column.displayName)</h3>
<span class="count">#(column.count)</span>
</div>
<div class="column-cards">
#for(card in column.cards):
#extend("{Feature}/{Entity}CardView")
#endfor
#if(column.cards.count == 0):
<div class="empty-state">#(column.emptyMessage)</div>
#endif
</div>
</div>
```
---
## File Organization
```
Sources/{WebAppTarget}/Resources/Views/
βββ base.leaf # Base layout (all pages extend this)
βββ {Feature}/
β βββ {Feature}View.leaf # Full page β {Feature}ViewModel
β βββ {Entity}CardView.leaf # Fragment β {Entity}CardViewModel
β βββ {Entity}RowView.leaf # Fragment β {Entity}RowViewModel
β βββ {Modal}View.leaf # Fragment β {Modal}ViewModel
βββ Shared/
βββ HeaderView.leaf # Shared components
βββ FooterView.leaf
```
---
## Leaf Built-in Functions
Leaf provides useful functions for working with arrays:
```html
<!-- Count items -->
#if(count(cards) > 0):
<p>You have #count(cards) cards</p>
#endif
<!-- Check if array contains value -->
#if(contains(statuses, "active")):
<span class="badge">Active</span>
#endif
```
### Loop Variables
Inside `#for` loops, Leaf provides progress variables:
```html
#for(item in items):
#if(isFirst):<span class="first">#endif
#(item.name)
#if(!isLast):, #endif
#endfor
```
| Variable | Description |
|----------|-------------|
| `isFirst` | True on first iteration |
| `isLast` | True on last iteration |
| `index` | Current iteration (0-based) |
### Array Index Access
Direct array subscripts (`array[0]`) are not documented in Leaf. For accessing specific elements, pre-compute in the ViewModel:
```swift
public let firstCard: CardViewModel?
public init(cards: [CardViewModel]) {
self.cards = cards
self.firstCard = cards.first
}
```
---
## Codable and Computed Properties
Swift's synthesized `Codable` only encodes **stored properties**. Since ViewModels are passed to Leaf via Codable encoding, computed properties won't be available.
```swift
// Computed property - NOT encoded by Codable, invisible in Leaf
public var hasCards: Bool { !cards.isEmpty }
// Stored property - encoded by Codable, available in Leaf
public let hasCards: Bool
```
If you need a derived value in a Leaf template, calculate it in `init()` and store it:
```swift
public let hasCards: Bool
public let cardCount: Int
public init(cards: [CardViewModel]) {
self.cards = cards
self.hasCards = !cards.isEmpty
self.cardCount = cards.count
}
```
---
## ViewModelId Initialization - CRITICAL
**IMPORTANT:** Even though Leaf templates don't use `vmId` directly, the ViewModels being rendered must initialize `vmId` correctly for SwiftUI clients.
**β WRONG - Never use this:**
```swift
public var vmId: ViewModelId = .init() // NO! Generic identity
```
**β
MINIMUM - Use type-based identity:**
```swift
public var vmId: ViewModelId = .init(type: Self.self)
```
**β
IDEAL - Use data-based identity when available:**
```swift
public struct TaskCardViewModel {
public let id: ModelIdType
public var vmId: ViewModelId
public init(id: ModelIdType, /* other params */) {
self.id = id
self.vmId = .init(id: id) // Ties view identity to data identity
// ...
}
}
```
**Why this matters for Leaf ViewModels:**
- ViewModels are shared between Leaf (web) and SwiftUI (native) clients
- SwiftUI uses `.id(vmId)` to determine when to recreate vs update views
- Wrong identity = SwiftUI views don't update when data changes
- Data-based identity (`.init(id:)`) is best practice
---
## Common Mistakes
### Missing Data Attributes
```html
<!-- BAD - JS can't identify this element -->
<div class="{entity}-card">
<!-- GOOD - JS reads data-{entity}-id -->
<div class="{entity}-card" data-{entity}-id="#(card.id)">
```
### Storing Display Names Instead of Identifiers
```html
<!-- BAD - localized string can't be sent to server -->
<div data-status="#(card.statusDisplayName)">
<!-- GOOD - raw enum value works for requests -->
<div data-status="#(card.status)">
```
### Using Layout in Fragments
```html
<!-- BAD - fragment should not extend layout -->
#extend("base"):
#export("content"):
<div class="card">...</div>
#endexport
#endextend
<!-- GOOD - fragment is just the component -->
<div class="card">...</div>
```
### Hardcoding Text
```html
<!-- BAD - not localizable -->
<span class="status">Queued</span>
<!-- GOOD - ViewModel provides localized value -->
<span class="status">#(card.statusDisplayName)</span>
```
### Concatenating Localized Values
```html
<!-- BAD - breaks RTL languages and locale-specific word order -->
#(conversation.messageCount) #(conversation.messagesLabel)
<!-- GOOD - ViewModel composes via @LocalizedSubs -->
#(conversation.messageCountDisplay)
```
Template-level concatenation assumes left-to-right order. Use `@LocalizedSubs` in the ViewModel so YAML can define locale-appropriate ordering:
```yaml
en:
ConversationViewModel:
messageCountDisplay: "%{messageCount} %{messagesLabel}"
ar:
ConversationViewModel:
messageCountDisplay: "%{messagesLabel} %{messageCount}"
```
### Formatting Dates in Templates
```html
<!-- BAD - hardcoded format, not locale-aware, concatenation issue -->
<span>#(content.createdPrefix) #date(content.createdAt, "MMM d, yyyy")</span>
<!-- GOOD - LocalizableDate handles locale formatting, @LocalizedSubs composes -->
<span>#(content.createdDisplay)</span>
```
Use `LocalizableDate` in the ViewModel - it formats according to user locale. If combining with a prefix, use `@LocalizedSubs`:
```swift
public let createdAt: LocalizableDate
@LocalizedSubs(\.createdPrefix, \.createdAt)
public var createdDisplay
```
### Mismatched Filenames
```
<!-- BAD - filename doesn't match ViewModel -->
ViewModel: UserProfileCardViewModel
Template: ProfileCard.leaf
<!-- GOOD - aligned names -->
ViewModel: UserProfileCardViewModel
Template: UserProfileCardView.leaf
```
### Incorrect ViewModelId Initialization
```swift
// β BAD - Generic identity (breaks SwiftUI clients)
public var vmId: ViewModelId = .init()
// β
MINIMUM - Type-based identity
public var vmId: ViewModelId = .init(type: Self.self)
// β
IDEAL - Data-based identity (when id available)
public init(id: ModelIdType) {
self.id = id
self.vmId = .init(id: id)
}
```
ViewModels rendered by Leaf are often shared with SwiftUI clients. Correct `vmId` initialization is critical for SwiftUI's view identity system.
---
## Rendering Errors in Leaf Templates
When a WebApp route catches an error, the error type is **known at compile time**. You don't need generic "ErrorViewModel" patterns:
```swift
// WebApp route - you KNOW the request type, so you KNOW the error type
app.post("move-idea") { req async throws -> Response in
let body = try req.content.decode(MoveIdeaRequest.RequestBody.self)
let serverRequest = MoveIdeaRequest(requestBody: body)
do {
try await serverRequest.processRequest(mvvmEnv: req.application.mvvmEnv)
// success path...
} catch let error as MoveIdeaRequest.ResponseError {
// I KNOW this is MoveIdeaRequest.ResponseError
// I KNOW it has .code and .message
return try await req.view.render(
"Shared/ToastView",
["message": error.message.value, "type": "error"]
).encodeResponse(for: req)
}
}
```
**The anti-pattern (JavaScript brain):**
```swift
// β WRONG - treating errors as opaque
catch let error as ServerRequestError {
// "How do I extract the message? The protocol doesn't guarantee it!"
// This is wrong thinking. You catch the CONCRETE type.
}
```
Each route handles its own specific error type. There's no mystery about what properties are available.
---
## How to Use This Skill
**Invocation:**
/fosmvvm-leaf-view-generator
**Prerequisites:**
- ViewModel structure understood from conversation context
- Template type determined (full-page vs fragment)
- Data attributes needed for JS interactions identified
- HTML-over-the-wire pattern understood if using fragments
**Workflow integration:**
This skill is used when creating Leaf templates for web clients. The skill references conversation context automaticallyβno file paths or Q&A needed. Typically follows fosmvvm-viewmodel-generator.
## Pattern Implementation
This skill references conversation context to determine template structure:
### ViewModel Analysis
From conversation context, the skill identifies:
- **ViewModel type** (from prior discussion or server implementation)
- **Properties** (what data the template will display)
- **Localization** (which properties are Localizable types)
- **Nested ViewModels** (child components)
### Template Type Detection
From ViewModel purpose:
- **Page content** β Full-page template (extends layout)
- **List item/Card** β Fragment (no layout, single root)
- **Modal content** β Fragment
- **Inline component** β Fragment
### Property Mapping
For each ViewModel property:
- **`id: ModelIdType`** β `data-{entity}-id="#(vm.id)"` (for JS)
- **Raw enum** β `data-{field}="#(vm.field)"` (for state)
- **`LocalizableString`** β `#(vm.displayName)` (display text)
- **`LocalizableDate`** β `#(vm.createdAt)` (formatted date)
- **Nested ViewModel** β Embed fragment or access properties
### Data Attributes Planning
Based on JS interaction needs:
- **Entity identifier** (for operations)
- **State values** (enum raw values for requests)
- **Drag/drop attributes** (if interactive)
- **Category/grouping** (for filtering/sorting)
### Template Generation
**Full-page:**
1. Layout extension
2. Content export
3. Embedded fragments for components
**Fragment:**
1. Single root element
2. Data attributes for state
3. Localized text from ViewModel
4. No layout extension
### Context Sources
Skill references information from:
- **Prior conversation**: Template requirements, user flows discussed
- **ViewModel**: If Claude has read ViewModel code into context
- **Existing templates**: From codebase analysis of similar views
---
## See Also
- [Architecture Patterns](../shared/architecture-patterns.md) - Mental models (errors are data, type safety, etc.)
- [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) - Full architecture
- [fosmvvm-viewmodel-generator](../fosmvvm-viewmodel-generator/SKILL.md) - Creates the ViewModels this skill renders
- [fosmvvm-serverrequest-generator](../fosmvvm-serverrequest-generator/SKILL.md) - Creates requests that return ViewModels
- [reference.md](reference.md) - Complete template examples
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2025-12-24 | Initial Kairos-specific skill |
| 2.0 | 2025-12-27 | Generalized for FOSMVVM, added View-ViewModel alignment principle, full-page templates, architecture connection |
| 2.1 | 2026-01-08 | Added Leaf Built-in Functions section (count, contains, loop variables). Clarified Codable/computed properties. Corrected earlier false claims about #count() not working. |
| 2.2 | 2026-01-19 | Updated Pattern 3 to use stored LocalizableString for dynamic enum displays; linked to Enum Localization Pattern. Added anti-patterns for concatenating localized values and formatting dates in templates. |
| 2.3 | 2026-01-20 | Added "Rendering Errors in Leaf Templates" section - error types are known at compile time, no need for generic ErrorViewModel patterns. Prevents JavaScript-brain thinking about runtime type discovery. |
| 2.4 | 2026-01-24 | Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths. |
```
### reference.md
```markdown
# FOSMVVM ViewModel Generator - Reference Templates
Complete file templates for generating ViewModels.
> **Conceptual context:** See [SKILL.md](SKILL.md) for when and why to use this skill.
> **Architecture context:** See [ViewModelArchitecture.md](../../docs/ViewModelArchitecture.md) for full FOSMVVM understanding.
## Placeholders
| Placeholder | Replace With | Example |
|-------------|--------------|---------|
| `{Name}` | ViewModel name (PascalCase, without "ViewModel" suffix) | `Dashboard`, `Card` |
| `{ViewModelsTarget}` | Your ViewModels SPM target | `ViewModels` |
| `{ResourcesPath}` | Your localization resources path | `Sources/Resources` |
| `{WebServerTarget}` | Your server-side target (server-hosted only) | `WebServer` |
---
# Server-Hosted Templates
Use these templates for apps with a backend server.
---
## Template 1: Top-Level ViewModel (RequestableViewModel)
For pages or screens that are fetched directly via API.
**Location:** `Sources/{ViewModelsTarget}/{Feature}/{Name}ViewModel.swift`
```swift
import FOSFoundation
import FOSMVVM
import Foundation
/// ViewModel for the {Name} screen.
///
/// This is a top-level ViewModel - it has an associated Request type
/// and is built by a ViewModelFactory on the server.
@ViewModel
public struct {Name}ViewModel: RequestableViewModel {
public typealias Request = {Name}Request
// MARK: - Localized UI Text
@LocalizedString public var pageTitle
// Add more @LocalizedString properties for static UI text
// MARK: - Data
// Add data properties the View needs to display
// public let items: [ItemViewModel]
// MARK: - Child ViewModels
// Add nested ViewModels for components
// public let createModal: CreateModalViewModel
// MARK: - Identity
public var vmId: ViewModelId = .init()
// MARK: - Initialization
public init(/* parameters */) {
// Initialize all properties
}
}
// MARK: - Stubbable
public extension {Name}ViewModel {
static func stub() -> Self {
.init(/* default values for previews */)
}
}
```
---
## Template 2: Child ViewModel (Instance)
For components that appear multiple times (cards, rows, list items).
**Location:** `Sources/{ViewModelsTarget}/{Feature}/{Name}ViewModel.swift`
```swift
import FOSFoundation
import FOSMVVM
import Foundation
/// ViewModel for a {Name} component.
///
/// This is a child ViewModel - built by its parent's Factory.
/// Each instance represents a different data entity.
@ViewModel
public struct {Name}ViewModel: Codable, Sendable {
// MARK: - Data Identity
/// The database entity ID - enables round-trip to server
public let id: ModelIdType
// MARK: - Content
public let title: String
// Add more content properties
// MARK: - Formatted Values
public let createdAt: LocalizableDate // NOT String - formatted client-side
// MARK: - Identity
public var vmId: ViewModelId
// MARK: - Initialization
public init(
id: ModelIdType,
title: String,
createdAt: Date
) {
self.id = id
self.title = title
self.createdAt = LocalizableDate(value: createdAt)
self.vmId = .init(id: id) // Instance identity from data ID
}
}
// MARK: - Stubbable
public extension {Name}ViewModel {
static func stub() -> Self {
.stub(id: .init())
}
static func stub(
id: ModelIdType = .init(),
title: String = "Sample Title",
createdAt: Date = .now
) -> Self {
.init(
id: id,
title: title,
createdAt: createdAt
)
}
}
```
---
## Template 3: Child ViewModel (Singleton)
For components that appear once (modals, headers, toolbars).
**Location:** `Sources/{ViewModelsTarget}/{Feature}/{Name}ViewModel.swift`
```swift
import FOSFoundation
import FOSMVVM
import Foundation
/// ViewModel for the {Name} component.
///
/// This is a singleton child ViewModel - only one instance per parent.
@ViewModel
public struct {Name}ViewModel: Codable, Sendable {
// MARK: - Localized UI Text
@LocalizedString public var title
@LocalizedString public var submitButtonLabel
@LocalizedString public var cancelButtonLabel
// MARK: - Identity
public var vmId: ViewModelId = .init()
// MARK: - Initialization
public init() {}
}
// MARK: - Stubbable
public extension {Name}ViewModel {
static func stub() -> Self {
.init()
}
}
```
---
## Template 4: ViewModel with Nested Child Types
For ViewModels that contain child types used only by this parent. Shows proper placement, conformances, and two-tier Stubbable pattern.
**Reference:** `Sources/KairosModels/Governance/GovernancePrincipleCardViewModel.swift`
**File:** `Sources/{Module}/{Feature}/{Name}ViewModel.swift`
```swift
// {Name}ViewModel.swift
//
// Copyright 2026 {YourOrganization}
// Licensed under the Apache License, Version 2.0
import FOSFoundation
import FOSMVVM
import Foundation
@ViewModel
public struct {Name}ViewModel: Codable, Sendable, Identifiable {
// MARK: - Localized Strings
@LocalizedString public var {field}Label
// MARK: - Data Identity
public let id: ModelIdType
// MARK: - Content
public let title: String
public let description: String
// MARK: - Collections (referencing nested types)
/// Array of child summaries (only populated when expanded).
public let childSummaries: [ChildSummary]?
/// Related items that reference this entity.
public let relatedItems: [RelatedItemReference]?
// MARK: - Nested Types
/// Summary of a child item for display in lists.
public struct ChildSummary: Codable, Sendable, Identifiable, Stubbable {
public let id: ModelIdType
public let name: String
public let createdAt: Date
public init(id: ModelIdType, name: String, createdAt: Date) {
self.id = id
self.name = name
self.createdAt = createdAt
}
}
/// Reference to a related item.
public struct RelatedItemReference: Codable, Sendable, Identifiable, Stubbable {
public let id: ModelIdType
public let title: String
public let status: String
public init(id: ModelIdType, title: String, status: String) {
self.id = id
self.title = title
self.status = status
}
}
// MARK: - View Identity
public let vmId: ViewModelId
public init(
id: ModelIdType,
title: String,
description: String,
childSummaries: [ChildSummary]? = nil,
relatedItems: [RelatedItemReference]? = nil
) {
self.vmId = .init(id: id)
self.id = id
self.title = title
self.description = description
self.childSummaries = childSummaries
self.relatedItems = relatedItems
}
}
// MARK: - Parent Stubbable
public extension {Name}ViewModel {
// Tier 1: Zero-arg (delegates to tier 2)
static func stub() -> Self {
.stub(id: .init())
}
// Tier 2: Parameterized with defaults
static func stub(
id: ModelIdType = .init(),
title: String = "A Title",
description: String = "A Description",
childSummaries: [ChildSummary]? = [.stub()],
relatedItems: [RelatedItemReference]? = [.stub()]
) -> Self {
.init(
id: id,
title: title,
description: description,
childSummaries: childSummaries,
relatedItems: relatedItems
)
}
}
// MARK: - Nested Type Stubbable Extensions (fully qualified names)
public extension {Name}ViewModel.ChildSummary {
// Tier 1: Zero-arg (delegates to tier 2)
static func stub() -> Self {
.stub(id: .init())
}
// Tier 2: Parameterized with defaults
static func stub(
id: ModelIdType = .init(),
name: String = "A Name",
createdAt: Date = .now
) -> Self {
.init(id: id, name: name, createdAt: createdAt)
}
}
public extension {Name}ViewModel.RelatedItemReference {
// Tier 1: Zero-arg (delegates to tier 2)
static func stub() -> Self {
.stub(id: .init())
}
// Tier 2: Parameterized with defaults
static func stub(
id: ModelIdType = .init(),
title: String = "A Title",
status: String = "Active"
) -> Self {
.init(id: id, title: title, status: status)
}
}
```
**Key Points:**
- Nested types placed AFTER properties that reference them
- Nested types placed BEFORE `vmId` and parent init
- Each nested type conforms to: `Codable, Sendable, Identifiable, Stubbable`
- Extensions use fully qualified names: `{Parent}.{NestedType}`
- Two-tier Stubbable: zero-arg always delegates to parameterized
- Section markers: `// MARK: - Nested Types`
---
## Template 5: ViewModelRequest
For top-level ViewModels - the Request type for fetching from server.
**Location:** `Sources/{ViewModelsTarget}/{Feature}/{Name}Request.swift`
```swift
import FOSFoundation
import FOSMVVM
import Foundation
/// Request to fetch the {Name}ViewModel from the server.
public final class {Name}Request: ViewModelRequest, @unchecked Sendable {
public typealias Query = EmptyQuery
public typealias ResponseError = EmptyError
public var responseBody: {Name}ViewModel?
public init(
query: EmptyQuery? = nil,
fragment: EmptyFragment? = nil,
requestBody: EmptyBody? = nil,
responseBody: {Name}ViewModel? = nil
) {
self.responseBody = responseBody
}
}
```
---
## Template 6: ViewModelFactory
For top-level ViewModels - builds the ViewModel from database.
**Location:** `Sources/{WebServerTarget}/ViewModelFactories/{Name}ViewModel+Factory.swift`
```swift
import Fluent
import FOSFoundation
import FOSMVVM
import FOSMVVMVapor
import Foundation
import Vapor
import ViewModels
/// Factory that builds {Name}ViewModel from database.
extension {Name}ViewModel: VaporViewModelFactory {
public typealias VMRequest = {Name}Request
public static func model(context: VaporModelFactoryContext<VMRequest>) async throws -> Self {
let db = context.req.db
// Query database for required data
// let items = try await Item.query(on: db).all()
// Build child ViewModels
// let itemViewModels = items.map { item in
// ItemViewModel(
// id: item.id!,
// title: item.title,
// createdAt: item.createdAt ?? .now
// )
// }
return .init(
// Pass built children
)
}
}
```
---
## Template 7: Localization YAML
**Location:** `{ResourcesPath}/ViewModels/{Feature}/{Name}ViewModel.yml`
```yaml
en:
{Name}ViewModel:
pageTitle: "Page Title"
headerText: "Welcome"
submitButtonLabel: "Submit"
cancelButtonLabel: "Cancel"
```
---
# Client-Hosted Templates
Use these templates for standalone apps without a backend server.
---
## Template 8: Client-Hosted Top-Level ViewModel
For standalone apps - the macro generates the factory automatically.
**Location:** `Sources/{ViewModelsTarget}/{Feature}/{Name}ViewModel.swift`
```swift
import FOSFoundation
import FOSMVVM
import Foundation
/// ViewModel for the {Name} screen.
///
/// Client-hosted: Factory is auto-generated from init parameters.
/// The AppState struct is derived from the init signature.
@ViewModel(options: [.clientHostedFactory])
public struct {Name}ViewModel {
// MARK: - Localized UI Text
@LocalizedString public var pageTitle
// Add more @LocalizedString properties for static UI text
// MARK: - Data (from AppState)
// Properties populated from init parameters
// public let settings: UserSettings
// public let items: [ItemViewModel]
// MARK: - Identity
public var vmId: ViewModelId = .init()
// MARK: - Initialization
/// Parameters here become AppState properties.
/// The macro generates:
/// - struct AppState { let settings: UserSettings; let items: [ItemViewModel] }
/// - static func model(context:) that builds Self from context.appState
public init(settings: UserSettings, items: [ItemViewModel]) {
self.settings = settings
self.items = items
}
}
// MARK: - Stubbable
public extension {Name}ViewModel {
static func stub() -> Self {
.init(
settings: .stub(),
items: [.stub()]
)
}
}
```
**What the macro generates:**
```swift
// Auto-generated by @ViewModel(options: [.clientHostedFactory])
extension {Name}ViewModel {
public typealias Request = ClientHostedRequest
public struct AppState: Hashable, Sendable {
public let settings: UserSettings
public let items: [ItemViewModel]
public init(settings: UserSettings, items: [ItemViewModel]) {
self.settings = settings
self.items = items
}
}
public final class ClientHostedRequest: ViewModelRequest, @unchecked Sendable {
public var responseBody: {Name}ViewModel?
public typealias ResponseError = EmptyError
public init(...) { ... }
}
public static func model(
context: ClientHostedModelFactoryContext<Request, AppState>
) async throws -> Self {
.init(
settings: context.appState.settings,
items: context.appState.items
)
}
}
```
---
## Template 9: Client-Hosted Complete Example
A settings screen for a standalone iPhone app.
### SettingsViewModel.swift
```swift
import FOSFoundation
import FOSMVVM
import Foundation
@ViewModel(options: [.clientHostedFactory])
public struct SettingsViewModel {
// MARK: - Localized UI Text
@LocalizedString public var pageTitle
@LocalizedString public var themeLabel
@LocalizedString public var notificationsLabel
@LocalizedString public var saveButtonLabel
// MARK: - Data
public let currentTheme: Theme
public let notificationsEnabled: Bool
// MARK: - Identity
public var vmId: ViewModelId = .init()
// MARK: - Initialization
public init(currentTheme: Theme, notificationsEnabled: Bool) {
self.currentTheme = currentTheme
self.notificationsEnabled = notificationsEnabled
}
}
public extension SettingsViewModel {
static func stub() -> Self {
.init(currentTheme: .light, notificationsEnabled: true)
}
}
public enum Theme: String, Codable, Sendable {
case light, dark, system
}
```
### SettingsViewModel.yml
```yaml
en:
SettingsViewModel:
pageTitle: "Settings"
themeLabel: "Theme"
notificationsLabel: "Notifications"
saveButtonLabel: "Save Changes"
```
### Usage in SwiftUI View
```swift
struct SettingsView: View {
@State private var viewModel: SettingsViewModel?
var body: some View {
// Render viewModel
}
func loadViewModel() async {
// Create AppState from local storage/preferences
let appState = SettingsViewModel.AppState(
currentTheme: UserDefaults.standard.theme,
notificationsEnabled: UserDefaults.standard.notificationsEnabled
)
// Build context with localization
let context = ClientHostedModelFactoryContext<
SettingsViewModel.Request,
SettingsViewModel.AppState
>(appState: appState, localizationStore: myYamlStore)
// Get localized ViewModel
viewModel = try await SettingsViewModel.model(context: context)
}
}
```
---
# Server-Hosted Complete Example
## Complete Example: Dashboard with Cards
### DashboardViewModel.swift
```swift
import FOSFoundation
import FOSMVVM
import Foundation
@ViewModel
public struct DashboardViewModel: RequestableViewModel {
public typealias Request = DashboardRequest
@LocalizedString public var pageTitle
@LocalizedString public var emptyStateMessage
public let cards: [CardViewModel]
public let totalCount: LocalizableInt
public var vmId: ViewModelId = .init()
public init(cards: [CardViewModel], totalCount: Int) {
self.cards = cards
self.totalCount = LocalizableInt(value: totalCount)
}
}
public extension DashboardViewModel {
static func stub() -> Self {
.init(
cards: [.stub(), .stub()],
totalCount: 2
)
}
}
```
### CardViewModel.swift
```swift
import FOSFoundation
import FOSMVVM
import Foundation
@ViewModel
public struct CardViewModel: Codable, Sendable {
public let id: ModelIdType
public let title: String
public let description: String
public let createdAt: LocalizableDate
public var vmId: ViewModelId
public init(
id: ModelIdType,
title: String,
description: String,
createdAt: Date
) {
self.id = id
self.title = title
self.description = description
self.createdAt = LocalizableDate(value: createdAt)
self.vmId = .init(id: id)
}
}
public extension CardViewModel {
static func stub() -> Self {
.stub(id: .init())
}
static func stub(
id: ModelIdType = .init(),
title: String = "Sample Card",
description: String = "This is a sample card for previews.",
createdAt: Date = .now
) -> Self {
.init(
id: id,
title: title,
description: description,
createdAt: createdAt
)
}
}
```
### DashboardRequest.swift
```swift
import FOSFoundation
import FOSMVVM
import Foundation
public final class DashboardRequest: ViewModelRequest, @unchecked Sendable {
public typealias Query = EmptyQuery
public typealias ResponseError = EmptyError
public var responseBody: DashboardViewModel?
public init(
query: EmptyQuery? = nil,
fragment: EmptyFragment? = nil,
requestBody: EmptyBody? = nil,
responseBody: DashboardViewModel? = nil
) {
self.responseBody = responseBody
}
}
```
### DashboardViewModel+Factory.swift
```swift
import Fluent
import FOSFoundation
import FOSMVVM
import FOSMVVMVapor
import Foundation
import Vapor
import ViewModels
extension DashboardViewModel: VaporViewModelFactory {
public typealias VMRequest = DashboardRequest
public static func model(context: VaporModelFactoryContext<VMRequest>) async throws -> Self {
let db = context.req.db
let items = try await Item.query(on: db)
.sort(\.$createdAt, .descending)
.all()
let cardViewModels = items.map { item in
CardViewModel(
id: item.id!,
title: item.title,
description: item.description,
createdAt: item.createdAt ?? .now
)
}
return .init(
cards: cardViewModels,
totalCount: items.count
)
}
}
```
### DashboardViewModel.yml
```yaml
en:
DashboardViewModel:
pageTitle: "Dashboard"
emptyStateMessage: "No items yet. Create your first one!"
```
---
## Quick Reference: Property Types
| Data Type | ViewModel Property Type | Why |
|-----------|------------------------|-----|
| Static UI text | `@LocalizedString` | Resolved from YAML |
| Dynamic data in text | `@LocalizedSubs` | Substitutions like "Hello, %{name}!" |
| Composed text | `@LocalizedCompoundString` | Joins pieces with locale-aware ordering |
| User content | `String` | Already localized or raw data |
| Database ID | `ModelIdType` | Type-safe round trips |
| Date/time | `LocalizableDate` | Client formats for locale/timezone |
| Count/number | `LocalizableInt` | Client formats with grouping |
| Child component | `ChildViewModel` | Nested ViewModel |
| List of children | `[ChildViewModel]` | Array of nested ViewModels |
---
## Contextual Localization Examples
### @LocalizedSubs - Dynamic Data in Text
When you need to embed dynamic values in localized text:
```swift
@ViewModel
public struct WelcomeViewModel {
@LocalizedSubs(substitutions: \.subs) var welcomeMessage
private let userName: String
private let userIndex: LocalizableInt
private var subs: [String: any Localizable] {
[
"userName": LocalizableString.constant(userName),
"userIndex: userIndex
],
}
}
```
```yaml
en:
WelcomeViewModel:
welcomeMessage: "Welcome back, %{userName}:%{userIndex}!"
ja:
WelcomeViewModel:
welcomeMessage: "γεΈ°γγͺγγγ%{userName}:%{userIndex}γγοΌ"
```
The `%{userName}` and `%{userIndex}` substitution points are placed correctly per locale.
Use LocalizableInt and not Int to ensure proper localization of numbers in all locales.
### @LocalizedCompoundString - Composed Text
When you need to join multiple pieces with locale-aware ordering:
```swift
@ViewModel
public struct UserNameViewModel {
@LocalizedStrings var namePieces // Array of strings from YAML
@LocalizedString var separator
@LocalizedCompoundString(pieces: \._namePieces, separator: \._separator) var fullName
}
```
This handles RTL languages and locales where name order differs (e.g., family name first).
---
## Checklists
### All ViewModels:
- [ ] `@ViewModel` macro applied
- [ ] `vmId: ViewModelId` property
- [ ] `stub()` method for testing/previews
- [ ] `Codable, Sendable` conformance
### Server-Hosted Top-Level:
- [ ] `: RequestableViewModel` conformance
- [ ] `typealias Request = {Name}Request`
- [ ] Request file created
- [ ] Factory file created
- [ ] YAML file created
### Client-Hosted Top-Level:
- [ ] `@ViewModel(options: [.clientHostedFactory])` macro
- [ ] Init parameters define the AppState
- [ ] YAML file created (bundled in app)
- [ ] No Request or Factory files needed
### Instance ViewModels (either mode):
- [ ] `id: ModelIdType` property
- [ ] `vmId = .init(id: id)` in init
### ViewModels with Localization (either mode):
- [ ] `@LocalizedString` for static text
- [ ] YAML file with matching keys
```
### ../fosmvvm-fields-generator/SKILL.md
```markdown
---
name: fosmvvm-fields-generator
description: Generate FOSMVVM Fields protocols with validation rules, FormField definitions, and localized messages. Define form contracts once, validate everywhere.
homepage: https://github.com/foscomputerservices/FOSUtilities
metadata: {"clawdbot": {"emoji": "π", "os": ["darwin", "linux"]}}
---
# FOSMVVM Fields Generator
Generate Form Specifications following FOSMVVM patterns.
## Conceptual Foundation
> For full architecture context, see [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) | [OpenClaw reference]({baseDir}/references/FOSMVVMArchitecture.md)
A **Form Specification** (implemented as a `{Name}Fields` protocol) is the **single source of truth** for user input. It answers:
1. **What data** can the user provide? (properties)
2. **How should it be presented?** (FormField with type, keyboard, autofill semantics)
3. **What constraints apply?** (validation rules)
4. **What messages should be shown?** (localized titles, placeholders, errors)
### Why This Matters
The Form Specification is **defined once, used everywhere**:
```swift
// Same protocol adopted by different consumers:
struct CreateIdeaRequestBody: ServerRequestBody, IdeaFields { ... } // HTTP transmission
@ViewModel struct IdeaFormViewModel: IdeaFields { ... } // Form rendering
final class Idea: Model, IdeaFields { ... } // Persistence validation
```
This ensures:
- **Consistent validation** - Same rules on client and server
- **Shared localization** - One YAML file, used everywhere
- **Single source of truth** - Change once, applies everywhere
### Connection to FOSMVVM
Form Specifications integrate with:
- **Localization System** - FormField titles/placeholders and validation messages use `LocalizableString`
- **Validation System** - Implements `ValidatableModel` protocol
- **Request System** - RequestBody types adopt Fields for validated transmission
- **ViewModel System** - ViewModels adopt Fields for form rendering
## When to Use This Skill
- Defining a new form (create, edit, filter, search)
- Adding validation to a request body
- Any type that needs to conform to `ValidatableModel`
- When `fosmvvm-fluent-datamodel-generator` needs form fields for a DataModel
## What This Skill Generates
A complete Form Specification consists of **3 files**:
| File | Purpose |
|------|---------|
| `{Name}Fields.swift` | Protocol + FormField definitions + validation methods |
| `{Name}FieldsMessages.swift` | `@FieldValidationModel` struct with `@LocalizedString` properties |
| `{Name}FieldsMessages.yml` | YAML localization (titles, placeholders, error messages) |
## Project Structure Configuration
Replace placeholders with your project's actual paths:
| Placeholder | Description | Example |
|-------------|-------------|---------|
| `{ViewModelsTarget}` | Shared ViewModels SPM target | `ViewModels`, `SharedViewModels` |
| `{ResourcesPath}` | Localization resources path | `Sources/Resources` |
**Expected Structure:**
```
Sources/
{ViewModelsTarget}/
FieldModels/
{Name}Fields.swift
{Name}FieldsMessages.swift
{ResourcesPath}/
FieldModels/
{Name}FieldsMessages.yml
```
## How to Use This Skill
**Invocation:**
/fosmvvm-fields-generator
**Prerequisites:**
- Form purpose understood from conversation context
- Field requirements discussed (names, types, constraints)
- Entity relationship identified (what is this form creating/editing)
**Workflow integration:**
This skill is used when defining form validation and user input contracts. The skill references conversation context automaticallyβno file paths or Q&A needed. Often precedes fosmvvm-fluent-datamodel-generator for form-backed models.
## Pattern Implementation
This skill references conversation context to determine Fields protocol structure:
### Form Analysis
From conversation context, the skill identifies:
- **Form purpose** (create, edit, filter, login, settings)
- **Entity relation** (User, Idea, Document - what's being created/edited)
- **Protocol naming** (CreateIdeaFields, UpdateProfile, LoginCredentials)
### Field Design
For each field from requirements:
- **Property specification** (name, type, optional vs required)
- **Presentation type** (FormFieldType: text, textArea, select, checkbox)
- **Input semantics** (FormInputType: email, password, tel, date)
- **Constraints** (required, length range, value range, date range)
- **Localization** (title, placeholder, validation error messages)
### File Generation Order
1. Fields protocol with FormField definitions and validation
2. FieldsMessages struct with @LocalizedString properties
3. FieldsMessages YAML with localized strings
### Context Sources
Skill references information from:
- **Prior conversation**: Form requirements, field specifications discussed
- **Specification files**: If Claude has read form specs into context
- **Existing patterns**: From codebase analysis of similar Fields protocols
## Key Patterns
### Protocol Structure
```swift
public protocol {Name}Fields: ValidatableModel, Codable, Sendable {
var fieldName: FieldType { get set }
var {name}ValidationMessages: {Name}FieldsMessages { get }
}
```
### FormField Definition
```swift
static var contentField: FormField<String?> { .init(
fieldId: .init(id: "content"),
title: .localized(for: {Name}FieldsMessages.self, propertyName: "content", messageKey: "title"),
placeholder: .localized(for: {Name}FieldsMessages.self, propertyName: "content", messageKey: "placeholder"),
type: .textArea(inputType: .text),
options: [
.required(value: true)
] + FormInputOption.rangeLength(contentRange)
) }
```
### FormField Types Reference
| FormFieldType | Use Case |
|---------------|----------|
| `.text(inputType:)` | Single-line input |
| `.textArea(inputType:)` | Multi-line input |
| `.checkbox` | Boolean toggle |
| `.select` | Dropdown selection |
| `.colorPicker` | Color selection |
### FormInputType Reference (common ones)
| FormInputType | Keyboard/Autofill |
|---------------|-------------------|
| `.text` | Default keyboard |
| `.emailAddress` | Email keyboard, email autofill |
| `.password` | Secure entry |
| `.tel` | Phone keyboard |
| `.url` | URL keyboard |
| `.date`, `.datetimeLocal` | Date picker |
| `.givenName`, `.familyName` | Name autofill |
### Validation Method Pattern
```swift
internal func validateContent(_ fields: [FormFieldBase]?) -> [ValidationResult]? {
guard fields == nil || (fields?.contains(Self.contentField) == true) else {
return nil
}
var result = [ValidationResult]()
if content.isEmpty {
result.append(.init(
status: .error,
field: Self.contentField,
message: {name}ValidationMessages.contentRequiredMessage
))
} else if !Self.contentRange.contains(NSString(string: content).length) {
result.append(.init(
status: .error,
field: Self.contentField,
message: {name}ValidationMessages.contentOutOfRangeMessage
))
}
return result.isEmpty ? nil : result
}
```
### Messages Struct Pattern
```swift
@FieldValidationModel public struct {Name}FieldsMessages {
@LocalizedString("content", messageGroup: "validationMessages", messageKey: "required")
public var contentRequiredMessage
@LocalizedString("content", messageGroup: "validationMessages", messageKey: "outOfRange")
public var contentOutOfRangeMessage
}
```
### YAML Structure
```yaml
en:
{Name}FieldsMessages:
content:
title: "Content"
placeholder: "Enter your content..."
validationMessages:
required: "Content is required"
outOfRange: "Content must be between 1 and 10,000 characters"
```
## Naming Conventions
| Concept | Convention | Example |
|---------|------------|---------|
| Protocol | `{Name}Fields` | `IdeaFields`, `CreateIdeaFields` |
| Messages struct | `{Name}FieldsMessages` | `IdeaFieldsMessages` |
| Messages property | `{name}ValidationMessages` | `ideaValidationMessages` |
| Field definition | `{fieldName}Field` | `contentField` |
| Range constant | `{fieldName}Range` | `contentRange` |
| Validate method | `validate{FieldName}` | `validateContent` |
| Required message | `{fieldName}RequiredMessage` | `contentRequiredMessage` |
| OutOfRange message | `{fieldName}OutOfRangeMessage` | `contentOutOfRangeMessage` |
## See Also
- [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) - Full FOSMVVM architecture reference
- [fosmvvm-viewmodel-generator](../fosmvvm-viewmodel-generator/SKILL.md) - For ViewModels that adopt Fields
- [fosmvvm-fluent-datamodel-generator](../fosmvvm-fluent-datamodel-generator/SKILL.md) - For Fluent DataModels that implement Fields
- [reference.md](reference.md) - Complete file templates
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2024-12-24 | Initial skill |
| 2.0 | 2024-12-26 | Rewritten with conceptual foundation; generalized from Kairos-specific |
| 2.1 | 2026-01-24 | Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths. |
```
### ../fosmvvm-fluent-datamodel-generator/SKILL.md
```markdown
---
name: fosmvvm-fluent-datamodel-generator
description: Generate Fluent DataModels for FOSMVVM server-side persistence. Scaffolds models, migrations, and tests for database-backed entities.
homepage: https://github.com/foscomputerservices/FOSUtilities
metadata: {"clawdbot": {"emoji": "ποΈ", "os": ["darwin", "linux"]}}
---
# FOSMVVM Fluent DataModel Generator
Generate Fluent DataModels for server-side persistence following FOSMVVM architecture.
> **Dependency:** This skill uses [fosmvvm-fields-generator](../fosmvvm-fields-generator/SKILL.md) for the Fields layer (protocol, messages, YAML). Run that skill first for form-backed models.
## Scope Guard
This skill is specifically for **Fluent** persistence layer (typically in Vapor server apps).
**STOP and ask the user if:**
- The project doesn't use Fluent
- The target is iOS-only with CoreData, SwiftData, or Realm
- The user mentions a non-Fluent ORM or persistence layer
- You're unsure whether Fluent is the persistence layer
**Check for Fluent indicators:**
- `Package.swift` imports `fluent`, `fluent-postgres-driver`, `fluent-sqlite-driver`, etc.
- Existing models use `@ID`, `@Field`, `@Parent`, `@Children`, `@Siblings` property wrappers
- A `Migrations/` directory exists with Fluent migration patterns
- Imports include `FluentKit` or `Fluent`
If Fluent isn't present, inform the user: *"This skill generates Fluent DataModels for server-side persistence. Your project doesn't appear to use Fluent. How would you like to proceed?"*
---
## When to Use This Skill
- User asks to create a new model/entity/table
- User wants to add a database-backed type (Users, Ideas, Documents, etc.)
- User mentions needing CRUD operations for a new concept
- Creating the persistence layer for a new entity
## Architecture Context
In FOSMVVM, the **Model** is the center - the source of truth that reads and writes flow through.
See [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) | [OpenClaw reference]({baseDir}/references/FOSMVVMArchitecture.md) for full context.
### DataModel in the Architecture
```
βββββββββββββββββββββββββββββββββββββββ
β Fluent DataModel β
β (implements Model + Fields) β
β β
β β’ All fields (system + user) β
β β’ Relationships (@Parent, etc.) β
β β’ Timestamps, audit fields β
β β’ Persistence logic β
ββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββββββββΌβββββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β ViewModelFactoryβ β CreateRequest β β UpdateRequest β
β (projector) β β RequestBody β β RequestBody β
β β β β β β
β β ViewModel β β β persists to β β β updates β
β (projection) β β DataModel β β DataModel β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
```
### Fields vs DataModels
**Fields protocol** = Form input (user-editable subset)
- What users type into forms
- Validation, labels, placeholders
- NO relationships, NO system-assigned fields
**DataModel** = Complete entity (Fluent implementation)
- All fields including system-assigned (createdBy, timestamps)
- All relationships (@Parent, @Siblings, @Children)
- Fluent property wrappers, migrations, seeds
**Not all entities need Fields:**
- Session: system auth, no user form β DataModel-only
- Audit records: system-generated β DataModel-only
- Junction tables: pure storage β DataModel-only
---
## File Structure
Each form-backed model requires files across multiple targets:
```
ββ fosmvvm-fields-generator ββββββββββββββββββββββββββββββββββ
{ViewModelsTarget}/ (shared protocol layer)
FieldModels/
{Model}Fields.swift β Protocol + Enum + Validation
{Model}FieldsMessages.swift β Localization message struct
{ResourcesPath}/ (localization resources)
FieldModels/
{Model}FieldsMessages.yml β YAML localization strings
ββ fosmvvm-fluent-datamodel-generator (this skill) βββββββββββ
{WebServerTarget}/ (server implementation)
DataModels/
{Model}.swift β Fluent model (implements protocol)
Migrations/
{Model}+Schema.swift β Table creation migration
{Model}+Seed.swift β Seed data migration
Tests/
{ViewModelsTarget}Tests/
FieldModels/
{Model}FieldsTests.swift β Unit tests
database.swift β Register migrations
```
---
## How to Use This Skill
**Invocation:**
/fosmvvm-fluent-datamodel-generator
**Prerequisites:**
- Model structure understood from conversation context
- Fields protocol exists (if form-backed model) via fosmvvm-fields-generator
- Relationships and system-assigned fields identified
- Fluent confirmed as the persistence layer
**Workflow integration:**
This skill is used for server-side persistence with Fluent. For form-backed models, run fosmvvm-fields-generator first to create the Fields protocol. The skill references conversation context automaticallyβno file paths or Q&A needed.
## Pattern Implementation
This skill references conversation context to determine DataModel structure:
### Model Type Detection
From conversation context, the skill identifies:
- **Entity purpose** (user data, system records, audit logs, junction table)
- **User input involvement** (form-backed vs system-generated)
- **Fields protocol requirement** (if user edits this data)
### Relationship Analysis
From requirements already in context:
- **One-to-many relationships** (@Parent in DataModel, not in Fields)
- **Many-to-many relationships** (Junction table + @Siblings, NOT UUID arrays)
- **Relationship naming** (self-documenting names, not vague references)
### Field Classification
Based on data source:
- **User-editable fields** (from Fields protocol)
- **System-assigned fields** (createdBy, timestamps, status - DataModel only)
- **Computed relationships** (@Parent, @Children, @Siblings)
### File Generation Order
**If form-backed model (Fields protocol exists):**
1. Fields layer already created via fosmvvm-fields-generator
2. DataModel implementation referencing Fields
3. Schema migration
4. Seed data migration
5. Tests
6. Migration registration
**If system-only model (no Fields):**
1. DataModel struct
2. Schema migration
3. Seed data migration (if needed)
4. Tests
5. Migration registration
### Design Validation
Before generating, the skill validates:
1. **Form requirement** - System-generated entities skip Fields
2. **Relationship patterns** - Junction tables for many-to-many, @Parent for foreign keys
3. **Naming clarity** - Relationships have self-documenting names
4. **Field separation** - User fields in protocol, system fields in DataModel only
### Context Sources
Skill references information from:
- **Prior conversation**: Model requirements, relationships discussed
- **Fields protocol**: If Claude has read Fields protocol into context or just created it
- **Database schema**: From codebase analysis of existing models
- **Migration patterns**: From existing migrations in project
---
## File Templates
See [reference.md](reference.md) for complete file templates with all patterns.
---
## Key Patterns
### Fluent DataModel
```swift
import FluentKit
import FOSFoundation
import FOSMVVM
import FOSMVVMVapor
import Foundation
final class {Model}: DataModel, {Model}Fields, Hashable, @unchecked Sendable {
static let schema = "{models}" // snake_case plural
@ID(key: .id) var id: ModelIdType?
// Fields from protocol
@Field(key: "field_name") var fieldName: FieldType
// Validation messages
let {model}ValidationMessages: {Model}FieldsMessages
// Timestamps
@Timestamp(key: "created_at", on: .create) var createdAt: Date?
@Timestamp(key: "updated_at", on: .update) var updatedAt: Date?
// CRITICAL: Initialize validationMessages FIRST
init() {
self.{model}ValidationMessages = .init()
}
init(id: ModelIdType? = nil, fieldName: FieldType) {
self.{model}ValidationMessages = .init() // FIRST!
self.id = id
self.fieldName = fieldName
}
}
```
### Relationships (Associated Types Pattern)
**PRINCIPLE: Existential types (`any Protocol`) are a code smell.** Always ask "Is there any other way?" before using them.
For required relationships, use **associated types** in the protocol:
```swift
public protocol IdeaFields: ValidatableModel, Codable, Sendable {
associatedtype User: UserFields
var createdBy: User { get set }
}
```
In the Fluent model, `@Parent` directly satisfies the protocol:
```swift
final class Idea: DataModel, IdeaFields, Hashable, @unchecked Sendable {
@Parent(key: "created_by") var createdBy: User
// No computed property needed - @Parent satisfies the associated type directly
}
```
In schema: `.field("created_by", .uuid, .required, .references(User.schema, "id", onDelete: .cascade))`
**When to use each pattern:**
- **Associated type** (`associatedtype User: UserFields`): Required relationships
- **Optional associated type**: Not supported - use `ModelIdType?` for optional FKs
- **Plain `ModelIdType`**: Optional FKs, external system references
### Migrations
- Schema migration named: `"{Model.schema}-initial"`
- Seed migration named: `"{Model.schema}-seed"`
- Seed is environment-aware (debug, test, release)
- Seed is idempotent: `guard count() == 0`
### Raw SQL in Migrations (PostgreSQL Features)
For PostgreSQL-specific features (tsvector, LTREE, etc.), use SQLKit:
```swift
import Fluent
import SQLKit // Required for raw SQL
// In prepare():
guard let sql = database as? any SQLDatabase else { return }
let schema = Model.schema
try await sql.raw(SQLQueryString("ALTER TABLE \(unsafeRaw: schema) ADD COLUMN search_vector tsvector")).run()
```
Key points:
- Import `SQLKit` (not just `Fluent`)
- Cast database: `database as? any SQLDatabase`
- Use `SQLQueryString` with `\(unsafeRaw:)` for identifiers
- These columns are database-only (not in protocol or Fluent model)
### Tests
- Use `@Suite` annotation with descriptive name
- Conform to `LocalizableTestCase`
- Test all form fields
- Test validation with `@Test(arguments:)`
- Create private test struct implementing the Fields protocol
**Test structs with associated types:**
```swift
private struct TestIdea: IdeaFields {
typealias User = TestUser // Satisfy the associated type
var id: ModelIdType?
var createdBy: TestUser // Concrete type, not existential
}
private struct TestUser: UserFields {
var id: ModelIdType? = .init()
var firstName: String = "Test"
// ... other required fields with defaults
}
```
---
## Naming Conventions
| Concept | Convention | Example |
|---------|------------|---------|
| Model class | PascalCase singular | `User`, `Idea` |
| Table name | snake_case plural | `users`, `ideas` |
| Field keys | snake_case | `created_at`, `user_id` |
| Enum cases | camelCase | `searchLanguage`, `inProgress` |
| Enum raw values | snake_case | `"search_language"`, `"in_progress"` |
| Protocol | `{Model}Fields` | `UserFields`, `IdeaFields` |
| Messages struct | `{Model}FieldsMessages` | `UserFieldsMessages` |
## Common Field Types
| Swift Type | Fluent Type | Database |
|------------|-------------|----------|
| `String` | `.string` | `VARCHAR/TEXT` |
| `Int` | `.int` | `INTEGER` |
| `Bool` | `.bool` | `BOOLEAN` |
| `Date` | `.datetime` | `TIMESTAMPTZ` |
| `UUID` | `.uuid` | `UUID` |
| `[UUID]` | `.array(of: .uuid)` | `UUID[]` |
| Custom Enum | `.string` | `VARCHAR` (stored as raw value) |
| `JSONB` | `.json` | `JSONB` |
---
## See Also
- [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) - Full FOSMVVM architecture
- [fosmvvm-fields-generator](../fosmvvm-fields-generator/SKILL.md) - For form validation (Fields protocols)
- [fosmvvm-viewmodel-generator](../fosmvvm-viewmodel-generator/SKILL.md) - For ViewModels that project from DataModels
- [reference.md](reference.md) - Complete file templates
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2025-12-23 | Initial skill based on SystemConfig pattern |
| 1.1 | 2025-12-23 | Added relationship patterns (@Parent), initialization order, imports list |
| 1.2 | 2025-12-23 | Associated types for relationships (not existentials), raw SQL patterns, test struct patterns |
| 1.3 | 2025-12-24 | Factored out Fields layer to fields-generator skill |
| 2.0 | 2025-12-26 | Renamed to fosmvvm-fluent-datamodel-generator, added Scope Guard, generalized from Kairos-specific to FOSMVVM patterns, added architecture context |
| 2.1 | 2026-01-24 | Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths. |
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "foscomputerservices",
"slug": "fosmvvm-viewmodel-generator",
"displayName": "FOSMVVM ViewModel Generator",
"latest": {
"version": "2.0.6",
"publishedAt": 1771086496740,
"commit": "https://github.com/openclaw/skills/commit/b07151b3d9d819e3e1c1462a800e6ec954d772d9"
},
"history": []
}
```