Skip to content

إنشاء قوالب مخصصة

تعلم كيفية إنشاء قوالب مخصصة لـ 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.md

2. الاختبارات

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"

المزيد من المعلومات

Released under the MIT License.