إنشاء قوالب مخصصة
تعلم كيفية إنشاء قوالب مخصصة لـ FORGE لتناسب احتياجات مشروعك الخاصة.
نظرة عامة
┌─────────────────────────────────────────────────────────────────┐
│ إنشاء قالب مخصص │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. تحديد البنية │
│ └── manifest.yaml │
│ │
│ 2. إنشاء القوالب │
│ ├── backends/ │
│ ├── frontends/ │
│ └── database/ │
│ │
│ 3. تعريف الترحيلات │
│ └── migrations/*.sql.tera │
│ │
│ 4. إضافة البيانات الأولية │
│ └── seeders/*.sql.tera │
│ │
│ 5. الاختبار والنشر │
│ └── forge template validate │
│ │
└─────────────────────────────────────────────────────────────────┘البدء السريع
إنشاء قالب جديد
bash
# Create new template structure
forge template create my-template
# Or clone an existing template
forge template clone base my-templateبنية القالب
my-template/
├── manifest.yaml # Manifest file (required)
├── README.md # Template documentation
├── backends/
│ └── rust/
│ └── api/
│ └── src/
│ ├── models/
│ │ └── {{model}}.rs.tera
│ ├── handlers/
│ │ └── {{model}}.rs.tera
│ ├── services/
│ │ └── {{model}}.rs.tera
│ ├── routes/
│ │ └── {{model}}.rs.tera
│ └── dtos/
│ └── {{model}}.rs.tera
├── frontends/
│ └── next/
│ ├── admin/
│ │ └── src/
│ │ └── app/
│ │ └── [locale]/
│ │ └── admin/
│ │ └── {{model}}/
│ │ ├── page.tsx.tera
│ │ ├── columns.tsx.tera
│ │ └── [id]/
│ │ └── page.tsx.tera
│ └── web/
│ └── src/
│ └── app/
│ └── [locale]/
│ └── {{model}}/
│ └── page.tsx.tera
├── database/
│ ├── migrations/
│ │ └── {{timestamp}}_create_{{table}}.sql.tera
│ └── seeders/
│ └── {{name}}.sql.tera
└── contracts/
└── models/
└── {{model}}.yamlملف Manifest
ملف manifest.yaml يحدد البيانات الوصفية والتكوين للقالب:
yaml
# manifest.yaml
# Basic information
name: my-template
version: "1.0.0"
description: "Custom template description"
author: "Your Name <email@example.com>"
# Base template (optional)
extends: base
# Requirements
requires:
rust: ">=1.75.0"
node: ">=20.0.0"
postgres: ">=15.0"
# Included features
features:
- feature_one
- feature_two
# Additional dependencies
dependencies:
backend:
crates:
- name: custom-crate
version: "1.0"
frontend:
packages:
- name: custom-package
version: "^1.0.0"
# Required variables
variables:
custom_var:
type: string
required: true
description: "Variable description"
default: "default_value"
# Template files
templates:
backend:
- src: "backends/rust/api/src/models/{{model}}.rs.tera"
dest: "backend/src/models/{{model | snake_case}}.rs"
per_model: true
- src: "backends/rust/api/src/handlers/{{model}}.rs.tera"
dest: "backend/src/handlers/{{model | snake_case}}.rs"
per_model: true
frontend:
- src: "frontends/next/admin/src/app/[locale]/admin/{{model}}/page.tsx.tera"
dest: "frontend/admin/src/app/[locale]/admin/{{model | kebab_case}}/page.tsx"
per_model: true
database:
- src: "database/migrations/create_table.sql.tera"
dest: "database/migrations/{{timestamp}}_create_{{model | snake_case}}.sql"
per_model: true
# Hooks
hooks:
pre_generate:
- "echo 'Starting generation...'"
post_generate:
- "cargo fmt --manifest-path backend/Cargo.toml"
- "cd frontend && npm run format"
# Generation options
generation:
skip_existing: false
backup: trueقوالب النماذج
قالب نموذج Rust
jinja2
{# backends/rust/api/src/models/{{model}}.rs.tera #}
//! {{ model.description | default(value="") }}
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};
/// {{ model.name }} entity
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "{{ model.table_name }}")]
pub struct Model {
/// Primary key
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
{% for field in model.fields %}
{% if field.description %}
/// {{ field.description }}
{% endif %}
{% if field.nullable %}
pub {{ field.name }}: Option<{{ field.type | to_rust_type }}>,
{% else %}
pub {{ field.name }}: {{ field.type | to_rust_type }},
{% endif %}
{% endfor %}
/// Creation timestamp
pub created_at: DateTime<Utc>,
/// Last update timestamp
pub updated_at: DateTime<Utc>,
{% if model.soft_delete %}
/// Soft delete timestamp
pub deleted_at: Option<DateTime<Utc>>,
{% endif %}
}
/// Relation definitions
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
{% for relation in model.relations %}
#[sea_orm(
{% if relation.type == "belongs_to" %}
belongs_to = "super::{{ relation.model | snake_case }}::Entity",
from = "Column::{{ relation.foreign_key | pascal_case }}",
to = "super::{{ relation.model | snake_case }}::Column::Id"
{% elif relation.type == "has_many" %}
has_many = "super::{{ relation.model | snake_case }}::Entity"
{% elif relation.type == "has_one" %}
has_one = "super::{{ relation.model | snake_case }}::Entity"
{% endif %}
)]
{{ relation.name }},
{% endfor %}
}
{% for relation in model.relations %}
{% if relation.type == "belongs_to" %}
impl Related<super::{{ relation.model | snake_case }}::Entity> for Entity {
fn to() -> RelationDef {
Relation::{{ relation.name }}.def()
}
}
{% endif %}
{% endfor %}
impl ActiveModelBehavior for ActiveModel {}قالب المعالج (Handler)
jinja2
{# backends/rust/api/src/handlers/{{model}}.rs.tera #}
use axum::{
extract::{Path, Query, State},
Json,
};
use uuid::Uuid;
use crate::{
dto::{{ model.name | snake_case }}::{
Create{{ model.name }}Dto,
Update{{ model.name }}Dto,
{{ model.name }}Response,
{{ model.name }}ListResponse,
},
error::AppError,
services::{{ model.name | snake_case }}Service,
state::AppState,
utils::pagination::PaginationParams,
};
/// List {{ model.name | plural | lower }}
#[utoipa::path(
get,
path = "/api/admin/{{ model.name | kebab_case | plural }}",
responses(
(status = 200, description = "List of {{ model.name | plural | lower }}", body = {{ model.name }}ListResponse)
),
params(PaginationParams),
tag = "{{ model.name }}"
)]
pub async fn list(
State(state): State<AppState>,
Query(params): Query<PaginationParams>,
) -> Result<Json<{{ model.name }}ListResponse>, AppError> {
let result = {{ model.name | snake_case }}Service::list(&state.db, params).await?;
Ok(Json(result))
}
/// Get {{ model.name | lower }} by ID
#[utoipa::path(
get,
path = "/api/admin/{{ model.name | kebab_case | plural }}/{id}",
responses(
(status = 200, description = "{{ model.name }} details", body = {{ model.name }}Response),
(status = 404, description = "{{ model.name }} not found")
),
params(("id" = Uuid, Path, description = "{{ model.name }} ID")),
tag = "{{ model.name }}"
)]
pub async fn get(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Json<{{ model.name }}Response>, AppError> {
let result = {{ model.name | snake_case }}Service::get_by_id(&state.db, id).await?;
Ok(Json(result))
}
/// Create {{ model.name | lower }}
#[utoipa::path(
post,
path = "/api/admin/{{ model.name | kebab_case | plural }}",
request_body = Create{{ model.name }}Dto,
responses(
(status = 201, description = "{{ model.name }} created", body = {{ model.name }}Response),
(status = 400, description = "Validation error")
),
tag = "{{ model.name }}"
)]
pub async fn create(
State(state): State<AppState>,
Json(dto): Json<Create{{ model.name }}Dto>,
) -> Result<Json<{{ model.name }}Response>, AppError> {
let result = {{ model.name | snake_case }}Service::create(&state.db, dto).await?;
Ok(Json(result))
}
/// Update {{ model.name | lower }}
#[utoipa::path(
put,
path = "/api/admin/{{ model.name | kebab_case | plural }}/{id}",
request_body = Update{{ model.name }}Dto,
responses(
(status = 200, description = "{{ model.name }} updated", body = {{ model.name }}Response),
(status = 404, description = "{{ model.name }} not found")
),
params(("id" = Uuid, Path, description = "{{ model.name }} ID")),
tag = "{{ model.name }}"
)]
pub async fn update(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(dto): Json<Update{{ model.name }}Dto>,
) -> Result<Json<{{ model.name }}Response>, AppError> {
let result = {{ model.name | snake_case }}Service::update(&state.db, id, dto).await?;
Ok(Json(result))
}
/// Delete {{ model.name | lower }}
#[utoipa::path(
delete,
path = "/api/admin/{{ model.name | kebab_case | plural }}/{id}",
responses(
(status = 204, description = "{{ model.name }} deleted"),
(status = 404, description = "{{ model.name }} not found")
),
params(("id" = Uuid, Path, description = "{{ model.name }} ID")),
tag = "{{ model.name }}"
)]
pub async fn delete(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<(), AppError> {
{{ model.name | snake_case }}Service::delete(&state.db, id).await?;
Ok(())
}قالب صفحة React
jinja2
{# frontends/next/admin/src/app/[locale]/admin/{{model}}/page.tsx.tera #}
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { DataTable } from "@/components/ui/data-table";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import { columns } from "./columns";
import { use{{ model.name | plural | pascal_case }}Query } from "@/hooks/use-{{ model.name | kebab_case }}";
import { Create{{ model.name }}Dialog } from "@/components/{{ model.name | kebab_case }}/create-dialog";
import { PageHeader } from "@/components/layout/page-header";
import { PermissionGate } from "@/components/auth/permission-gate";
export default function {{ model.name | plural | pascal_case }}Page() {
const t = useTranslations("{{ model.name | plural | pascal_case }}");
const [isCreateOpen, setIsCreateOpen] = useState(false);
const { data, isLoading, error } = use{{ model.name | plural | pascal_case }}Query();
if (error) {
return <div className="text-destructive">{t("error.loading")}</div>;
}
return (
<div className="space-y-6">
<PageHeader
title={t("title")}
description={t("description")}
actions={
<PermissionGate permission="{{ model.name | snake_case | plural }}.create">
<Button onClick={() => setIsCreateOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
{t("actions.create")}
</Button>
</PermissionGate>
}
/>
<DataTable
columns={columns}
data={data?.items ?? []}
isLoading={isLoading}
pagination={{
pageIndex: 0,
pageSize: 10,
total: data?.total ?? 0,
}}
/>
<Create{{ model.name }}Dialog
open={isCreateOpen}
onOpenChange={setIsCreateOpen}
/>
</div>
);
}قوالب قاعدة البيانات
قالب الترحيل
jinja2
{# database/migrations/create_table.sql.tera #}
-- Migration: Create {{ model.table_name }} table
-- Generated: {{ now | date(format="%Y-%m-%d %H:%M:%S") }}
CREATE TABLE IF NOT EXISTS {{ model.table_name }} (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
{% for field in model.fields %}
{{ field.name }} {{ field.db_type }}{% if not field.nullable %} NOT NULL{% endif %}{% if field.default %} DEFAULT {{ field.default }}{% endif %},
{% endfor %}
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(){% if model.soft_delete %},
deleted_at TIMESTAMPTZ{% endif %}
{% for field in model.fields %}
{% if field.references %}
,CONSTRAINT fk_{{ model.table_name }}_{{ field.name }}
FOREIGN KEY ({{ field.name }})
REFERENCES {{ field.references.table }}({{ field.references.column }})
ON DELETE {{ field.references.on_delete | default(value="CASCADE") }}
{% endif %}
{% endfor %}
);
-- Indexes
{% for field in model.fields %}
{% if field.index %}
CREATE INDEX idx_{{ model.table_name }}_{{ field.name }} ON {{ model.table_name }}({{ field.name }});
{% endif %}
{% if field.unique %}
CREATE UNIQUE INDEX idx_{{ model.table_name }}_{{ field.name }}_unique ON {{ model.table_name }}({{ field.name }});
{% endif %}
{% endfor %}
-- Updated at trigger
CREATE TRIGGER set_{{ model.table_name }}_updated_at
BEFORE UPDATE ON {{ model.table_name }}
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Comments
COMMENT ON TABLE {{ model.table_name }} IS '{{ model.description | default(value="") }}';
{% for field in model.fields %}
{% if field.description %}
COMMENT ON COLUMN {{ model.table_name }}.{{ field.name }} IS '{{ field.description }}';
{% endif %}
{% endfor %}قالب البيانات الأولية
jinja2
{# database/seeders/default_data.sql.tera #}
-- Seeder: Default {{ model.name }} data
-- Generated: {{ now | date(format="%Y-%m-%d %H:%M:%S") }}
{% if model.seeds %}
INSERT INTO {{ model.table_name }} (
{% for field in model.fields %}
{{ field.name }}{% if not loop.last %},{% endif %}
{% endfor %}
) VALUES
{% for seed in model.seeds %}
(
{% for field in model.fields %}
{{ seed[field.name] | sql_value }}{% if not loop.last %},{% endif %}
{% endfor %}
){% if not loop.last %},{% endif %}
{% endfor %};
{% endif %}الفلاتر المخصصة
يمكنك إضافة فلاتر مخصصة عبر ملف filters.rs:
rust
// templates/my-template/filters.rs
use tera::{Result, Value};
use std::collections::HashMap;
pub fn custom_filter(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
// Custom filter implementation
Ok(value.clone())
}
// Register filters
pub fn register_filters(tera: &mut Tera) {
tera.register_filter("custom_filter", custom_filter);
}الماكرو المشتركة
إنشاء ماكرو قابلة لإعادة الاستخدام:
jinja2
{# templates/macros/forms.tera #}
{% macro form_field(field) %}
<div className="space-y-2">
<Label htmlFor="{{ field.name }}">
{t("fields.{{ field.name }}")}
{% if not field.nullable %}<span className="text-destructive">*</span>{% endif %}
</Label>
{% if field.type == "string" %}
<Input
id="{{ field.name }}"
{...register("{{ field.name }}")}
/>
{% elif field.type == "text" %}
<Textarea
id="{{ field.name }}"
{...register("{{ field.name }}")}
/>
{% elif field.type == "number" %}
<Input
id="{{ field.name }}"
type="number"
{...register("{{ field.name }}", { valueAsNumber: true })}
/>
{% elif field.type == "boolean" %}
<Switch
id="{{ field.name }}"
{...register("{{ field.name }}")}
/>
{% elif field.type == "date" %}
<DatePicker
id="{{ field.name }}"
{...register("{{ field.name }}")}
/>
{% elif field.type == "enum" %}
<Select {...register("{{ field.name }}")}>
{% for option in field.options %}
<SelectItem value="{{ option.value }}">{{ option.label }}</SelectItem>
{% endfor %}
</Select>
{% endif %}
{errors.{{ field.name }} && (
<p className="text-sm text-destructive">{errors.{{ field.name }}.message}</p>
)}
</div>
{% endmacro %}التحقق من القالب
bash
# Validate template
forge template validate my-template
# Test generation
forge template test my-template --output ./test-output
# Preview generated files
forge template preview my-template --model Productنشر القالب
النشر المحلي
bash
# Copy template to templates folder
cp -r my-template ~/.forge/templates/
# Verify registration
forge template listالنشر على GitHub
bash
# Create repository for template
gh repo create my-forge-template --public
# Push template
cd my-template
git init
git add .
git commit -m "Initial template"
git push origin main
# Usage
forge new my-project --template github:username/my-forge-templateالنشر على السجل
bash
# Login
forge login
# Publish template
forge template publish my-template
# Usage
forge new my-project --template registry:my-templateأفضل الممارسات
1. التوثيق الجيد
yaml
# manifest.yaml
documentation:
readme: README.md
examples:
- name: "Basic Usage"
file: examples/basic.md
- name: "Advanced Configuration"
file: examples/advanced.md2. الاختبارات
yaml
# manifest.yaml
tests:
- name: "Generate basic model"
command: "forge generate model Product"
expect:
files:
- "backend/src/models/product.rs"
- "frontend/admin/src/app/[locale]/admin/products/page.tsx"3. الإصدارات
yaml
# manifest.yaml
version: "1.0.0"
changelog:
- version: "1.0.0"
date: "2025-01-01"
changes:
- "Initial release"4. التوافقية
yaml
# manifest.yaml
compatibility:
forge: ">=1.0.0"
templates:
base: ">=1.0.0"المزيد من المعلومات
- نظام القوالب - نظرة عامة على القوالب
- القالب الأساسي - القالب الافتراضي
- القوالب المتوفرة - القوالب الجاهزة