architect/_archive/2025-11-cleanup/platform-v2-cifra/archive/2025-11-10-restructure-v2/ABSTRACTION_THEMES_SECURITY.md

Уровни абстракции, темы и безопасность

Дата: 2025-11-10
Версия: 1.0.0
Цель: Каскадная система абстракций, тем и безопасности


КРАТКИЙ ОТВЕТ

5 ключевых систем:

1. DATA ABSTRACTION LAYERS (Уровни абстракции данных)
   Database  ORM  DTO  ViewModel  API Response

2. THEME SYSTEM (Система тем)
   Base Theme  Design Tokens  Theme Variants  Theme Builder

3. DESIGN SYSTEM (Система дизайна)
   Primitives  Components  Patterns  Templates

4. SECURITY (Безопасность)
   OAuth 2.0  OpenID Connect  RBAC  RLS  Field-level

5. CASCADE SYSTEM (Каскадность)
   Global  Theme  Component  Element  Inline

Часть 1: DATA ABSTRACTION LAYERS (Уровни абстракции данных)

Концепция: 6 уровней представления данных

┌─────────────────────────────────────────────────────────┐
  УРОВЕНЬ 1: DATABASE (База данных)                      
  Сырые данные в таблицах                                
├─────────────────────────────────────────────────────────┤
  users table:                                           
  | id | email          | password_hash | created_at |  
  | 1  | user@test.com  | $2b$12...     | 2025-11-10 |  
└─────────────────────────────────────────────────────────┘
              
┌─────────────────────────────────────────────────────────┐
  УРОВЕНЬ 2: ORM MODEL (Объектная модель)                
  SQLAlchemy / Django ORM                                
├─────────────────────────────────────────────────────────┤
  class User(Base):                                      
      id = Column(UUID, primary_key=True)                
      email = Column(String, unique=True)                
      password_hash = Column(String)                     
      created_at = Column(DateTime)                      
      # Relationships                                    
      orders = relationship('Order')                     
└─────────────────────────────────────────────────────────┘
              
┌─────────────────────────────────────────────────────────┐
  УРОВЕНЬ 3: DTO (Data Transfer Object)                  
  Чистые данные для передачи между слоями                
├─────────────────────────────────────────────────────────┤
  @dataclass                                             
  class UserDTO:                                         
      id: UUID                                           
      email: str                                         
      full_name: str                                     
      created_at: datetime                               
      # БЕЗ password_hash (безопасность!)                
      # БЕЗ relationships (производительность!)          
└─────────────────────────────────────────────────────────┘
              
┌─────────────────────────────────────────────────────────┐
  УРОВЕНЬ 4: VIEW MODEL (Модель представления)           
  Данные + логика для конкретного view                   
├─────────────────────────────────────────────────────────┤
  @dataclass                                             
  class UserProfileViewModel:                            
      user: UserDTO                                      
      orders_count: int                                  
      total_spent: Decimal                               
      last_login_ago: str  # "3 дня назад"              
      avatar_url: str                                    
      is_vip: bool                                       
                                                         
      # View logic                                       
      def display_name(self) -> str:                     
          return self.user.full_name or self.user.email  
└─────────────────────────────────────────────────────────┘
              
┌─────────────────────────────────────────────────────────┐
  УРОВЕНЬ 5: API RESPONSE (API ответ)                    
  JSON для клиента                                       
├─────────────────────────────────────────────────────────┤
  {                                                      
    "id": "123e4567-e89b-12d3-a456-426614174000",       
    "email": "user@test.com",                           
    "full_name": "Иван Петров",                         
    "stats": {                                           
      "orders_count": 42,                               
      "total_spent": "150000.00",                       
      "last_login_ago": "3 дня назад"                   
    },                                                   
    "avatar": "https://cdn.example.com/avatars/123.jpg",
    "badges": ["vip", "verified"]                       
  }                                                      
└─────────────────────────────────────────────────────────┘
              
┌─────────────────────────────────────────────────────────┐
  УРОВЕНЬ 6: UI COMPONENT (UI компонент)                 
  Визуализация для пользователя                          
├─────────────────────────────────────────────────────────┤
  <UserProfileCard>                                      
    <Avatar src={user.avatar} />                        
    <h2>{user.full_name}</h2>                           
    <p>{user.email}</p>                                 
    <Stats>                                              
      <Stat label="Заказов" value={orders_count} />    
      <Stat label="Потрачено" value={total_spent} />   
    </Stats>                                             
    {user.is_vip && <VIPBadge />}                       
  </UserProfileCard>                                     
└─────────────────────────────────────────────────────────┘

Реализация в CIFRA:

# entities/user.cifra
entity:
  name: User
  table: users

  # УРОВЕНЬ 2: ORM Model
  fields:
    email: {type: email, unique: true}
    password_hash: {type: string, hidden: true}
    full_name: {type: string}
    created_at: {type: datetime, auto: true}

  # УРОВЕНЬ 3: DTO
  dto:
    fields: [id, email, full_name, created_at]
    exclude: [password_hash]  # Никогда не передаём наружу

  # УРОВЕНЬ 4: View Models
  view_models:
    profile:
      extends: dto
      computed:
        - orders_count: "COUNT(orders)"
        - total_spent: "SUM(orders.total)"
        - last_login_ago: "format_date_ago(last_login_at)"
        - avatar_url: "get_avatar_url(id)"
        - is_vip: "total_spent > 100000"

    list_item:
      fields: [id, email, full_name, created_at]
      computed:
        - display_name: "full_name OR email"

  # УРОВЕНЬ 5: API Response
  api:
    endpoints:
      - path: /users/me
        method: GET
        response: profile  # Использует view model
        auth: required

      - path: /users
        method: GET
        response: list_item[]
        auth: required
        permissions: [users:read]

  # УРОВЕНЬ 6: UI Components
  ui:
    components:
      - profile_card:
          template: UserProfileCard
          data: profile

Часть 2: THEME SYSTEM (Система тем)

2.1 Base Theme (Голая базовая тема)

Концепция: Минималистичная тема БЕЗ стилизации - только структура

# themes/base/base.theme.cifra
theme:
  id: base
  name: "Base Theme"
  description: "Голая тема - только структура, без стилей"
  version: 1.0.0

  # DESIGN TOKENS (токены дизайна)
  tokens:
    # Цвета (переменные)
    colors:
      primary: var(--color-primary)
      secondary: var(--color-secondary)
      success: var(--color-success)
      warning: var(--color-warning)
      error: var(--color-error)
      background: var(--color-background)
      surface: var(--color-surface)
      text:
        primary: var(--color-text-primary)
        secondary: var(--color-text-secondary)

    # Размеры
    spacing:
      unit: 4px  # Базовая единица
      xs: calc(var(--spacing-unit) * 1)    # 4px
      sm: calc(var(--spacing-unit) * 2)    # 8px
      md: calc(var(--spacing-unit) * 4)    # 16px
      lg: calc(var(--spacing-unit) * 6)    # 24px
      xl: calc(var(--spacing-unit) * 8)    # 32px

    # Типографика
    typography:
      font_family: var(--font-family)
      font_size:
        xs: 12px
        sm: 14px
        base: 16px
        lg: 18px
        xl: 20px
        "2xl": 24px
        "3xl": 30px
      font_weight:
        normal: 400
        medium: 500
        semibold: 600
        bold: 700
      line_height:
        tight: 1.25
        normal: 1.5
        relaxed: 1.75

    # Радиусы скругления
    radius:
      none: 0
      sm: 4px
      md: 6px
      lg: 8px
      xl: 12px
      full: 9999px

    # Тени
    shadows:
      none: none
      sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05)
      md: 0 4px 6px -1px rgba(0, 0, 0, 0.1)
      lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1)
      xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1)

    # Переходы
    transitions:
      fast: 150ms
      normal: 250ms
      slow: 400ms
      easing: cubic-bezier(0.4, 0, 0.2, 1)

    # Breakpoints (адаптивность)
    breakpoints:
      xs: 375px
      sm: 640px
      md: 768px
      lg: 1024px
      xl: 1280px
      "2xl": 1536px

  # LAYOUT (раскладка)
  layout:
    type: blank  # Без предустановленной раскладки
    regions: []  # Регионы определяются в дочерних темах

  # COMPONENTS (компоненты без стилей)
  components:
    button:
      class: "btn"
      variants: [primary, secondary, success, danger]
      sizes: [sm, md, lg]
      # Без конкретных стилей - только классы

    input:
      class: "input"
      variants: [default, error]
      sizes: [sm, md, lg]

    card:
      class: "card"
      variants: [default, bordered, elevated]

2.2 Theme Variants (Варианты на основе Base)

# themes/admin_light.theme.cifra
theme:
  id: admin_light
  name: "Admin Light"
  extends: base  # Наследуем Base Theme

  # Переопределяем токены
  tokens:
    colors:
      primary: "#3B82F6"
      secondary: "#64748B"
      success: "#10B981"
      warning: "#F59E0B"
      error: "#EF4444"
      background: "#F8FAFC"
      surface: "#FFFFFF"
      text:
        primary: "#1E293B"
        secondary: "#64748B"

    typography:
      font_family: "'Inter', system-ui, sans-serif"

  # Добавляем раскладку
  layout:
    type: sidebar_left
    regions:
      - sidebar:
          width: 280px
          background: surface
          border_right: 1px solid #E2E8F0

      - topbar:
          height: 64px
          background: surface
          border_bottom: 1px solid #E2E8F0

      - content:
          padding: md
          background: background

  # Переопределяем компоненты
  components:
    button:
      extends: base.button
      styles:
        default:
          padding: "8px 16px"
          border_radius: radius.md
          font_weight: font_weight.medium
          transition: all transitions.normal transitions.easing

        variants:
          primary:
            background: colors.primary
            color: white
            hover:
              background: darken(colors.primary, 10%)

          secondary:
            background: colors.secondary
            color: white

2.3 Theme Builder (Конструктор тем)

# theme_builder.cifra
theme_builder:
  # Визуальный редактор тем
  editor:
    sections:
      # 1. Цвета
      - colors:
          primary:
            type: color_picker
            default: "#3B82F6"

          secondary:
            type: color_picker
            default: "#64748B"

          # Авто-генерация оттенков
          generate_shades: true  # 50, 100, 200... 900

      # 2. Типографика
      - typography:
          font_family:
            type: font_select
            options: [Inter, Roboto, "Open Sans", ...]

          scale:
            type: slider
            min: 0.8
            max: 1.2
            step: 0.1

      # 3. Размеры
      - spacing:
          unit:
            type: number
            default: 4
            unit: px

          scale:
            type: select
            options: [linear, geometric, fibonacci]

      # 4. Компоненты
      - components:
          button:
            border_radius:
              type: slider
              min: 0
              max: 24
              unit: px

            shadow:
              type: select
              options: [none, sm, md, lg]

  # Live preview
  preview:
    enabled: true
    screens:
      - desktop: 1440px
      - tablet: 768px
      - mobile: 375px

  # Export
  export:
    formats:
      - css_variables  # :root { --color-primary: #3B82F6; }
      - scss_variables # $color-primary: #3B82F6;
      - js_object      # { colors: { primary: '#3B82F6' } }
      - cifra_theme    # theme.cifra файл

Часть 3: DESIGN SYSTEM (Система дизайна)

3.1 Design Tokens (Токены дизайна)

Из: Design Tokens Community Group (W3C)

{
  "color": {
    "brand": {
      "primary": {
        "value": "#3B82F6",
        "type": "color"
      },
      "secondary": {
        "value": "#64748B",
        "type": "color"
      }
    },
    "semantic": {
      "success": {
        "value": "{color.green.500}",
        "type": "color"
      },
      "error": {
        "value": "{color.red.500}",
        "type": "color"
      }
    }
  },
  "spacing": {
    "scale": {
      "0": {"value": "0", "type": "dimension"},
      "1": {"value": "4px", "type": "dimension"},
      "2": {"value": "8px", "type": "dimension"},
      "4": {"value": "16px", "type": "dimension"}
    }
  },
  "typography": {
    "heading": {
      "h1": {
        "fontSize": {"value": "{font.size.3xl}"},
        "fontWeight": {"value": "{font.weight.bold}"},
        "lineHeight": {"value": "{line.height.tight}"}
      }
    }
  }
}

3.2 Cascading (Каскадность) - как в CSS

# Каскад стилей (от общего к частному)

# УРОВЕНЬ 1: Global (глобальные стили)
global:
  colors:
    primary: "#3B82F6"
  typography:
    font_family: "Inter"

    ↓ наследуется

# УРОВЕНЬ 2: Theme (тема)
theme: admin_light
  colors:
    primary: "#1E40AF"  # Переопределяет global

    ↓ наследуется

# УРОВЕНЬ 3: Component (компонент)
component: Button
  colors:
    primary: "#2563EB"  # Переопределяет theme

    ↓ наследуется

# УРОВЕНЬ 4: Variant (вариант)
variant: button.large
  padding: "12px 24px"  # Дополняет component

    ↓ наследуется

# УРОВЕНЬ 5: State (состояние)
state: button:hover
  colors:
    primary: "#1D4ED8"  # Переопределяет при наведении

    ↓ наследуется

# УРОВЕНЬ 6: Inline (inline стили)
inline:
  colors:
    primary: "#1E3A8A"  # Наивысший приоритет

3.3 Реализация каскада в CIFRA:

# cifra/core/cascade.py
class CascadeResolver:
    """
    Разрешает каскад стилей (как в CSS)
    """

    def __init__(self):
        self.layers = [
            'global',      # Приоритет 1 (низкий)
            'theme',       # Приоритет 2
            'component',   # Приоритет 3
            'variant',     # Приоритет 4
            'state',       # Приоритет 5
            'inline'       # Приоритет 6 (высокий)
        ]

    def resolve(self, property: str, context: dict) -> Any:
        """
        Разрешает значение свойства с учётом каскада

        Args:
            property: "colors.primary" или "typography.font_family"
            context: {
                'global': {...},
                'theme': {...},
                'component': {...},
                'variant': {...},
                'state': {...},
                'inline': {...}
            }

        Returns:
            Значение с наивысшим приоритетом
        """
        value = None

        # Идём от низкого приоритета к высокому
        for layer in self.layers:
            layer_value = self._get_nested(context.get(layer, {}), property)
            if layer_value is not None:
                value = layer_value

        return value

    def _get_nested(self, obj: dict, path: str) -> Any:
        """Получить вложенное свойство: colors.primary"""
        keys = path.split('.')
        for key in keys:
            if isinstance(obj, dict):
                obj = obj.get(key)
            else:
                return None
        return obj


# Использование:
resolver = CascadeResolver()

context = {
    'global': {
        'colors': {'primary': '#3B82F6'}
    },
    'theme': {
        'colors': {'primary': '#1E40AF'}  # Переопределяет
    },
    'component': {
        'colors': {'primary': '#2563EB'}  # Переопределяет ещё раз
    }
}

color = resolver.resolve('colors.primary', context)
# → '#2563EB' (из component, т.к. высший приоритет)

Часть 4: SECURITY (Безопасность)

4.1 OAuth 2.0 + OpenID Connect

# security/oauth.cifra
security:
  authentication:
    # Локальная аутентификация
    local:
      enabled: true
      password:
        min_length: 8
        require_uppercase: true
        require_lowercase: true
        require_numbers: true
        require_special: false

    # OAuth 2.0 провайдеры
    oauth:
      # Google
      - provider: google
        enabled: true
        client_id: "{env.GOOGLE_CLIENT_ID}"
        client_secret: "{env.GOOGLE_CLIENT_SECRET}"
        authorization_endpoint: https://accounts.google.com/o/oauth2/v2/auth
        token_endpoint: https://oauth2.googleapis.com/token
        userinfo_endpoint: https://www.googleapis.com/oauth2/v3/userinfo
        scopes: [openid, email, profile]
        button_text: "Войти через Google"
        button_icon: google

      # GitHub
      - provider: github
        enabled: true
        client_id: "{env.GITHUB_CLIENT_ID}"
        client_secret: "{env.GITHUB_CLIENT_SECRET}"
        authorization_endpoint: https://github.com/login/oauth/authorize
        token_endpoint: https://github.com/login/oauth/access_token
        userinfo_endpoint: https://api.github.com/user
        scopes: [read:user, user:email]
        button_text: "Войти через GitHub"

      # Microsoft (Azure AD)
      - provider: microsoft
        enabled: true
        tenant: common  # или конкретный tenant ID
        client_id: "{env.MICROSOFT_CLIENT_ID}"
        client_secret: "{env.MICROSOFT_CLIENT_SECRET}"
        authorization_endpoint: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
        token_endpoint: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
        scopes: [openid, email, profile]

      # Custom OAuth provider
      - provider: custom_sso
        enabled: true
        name: "Company SSO"
        client_id: "{env.SSO_CLIENT_ID}"
        client_secret: "{env.SSO_CLIENT_SECRET}"
        authorization_endpoint: https://sso.company.com/oauth/authorize
        token_endpoint: https://sso.company.com/oauth/token
        userinfo_endpoint: https://sso.company.com/oauth/userinfo
        scopes: [openid, email, profile]

    # OpenID Connect
    openid_connect:
      enabled: true
      discovery_url: https://auth.example.com/.well-known/openid-configuration
      # Автоматически загружает endpoints из discovery

  # JWT настройки
  jwt:
    secret: "{env.JWT_SECRET}"
    algorithm: HS256
    access_token_expire: 3600  # 1 час
    refresh_token_expire: 2592000  # 30 дней

  # Session
  session:
    provider: redis
    cookie_name: cifra_session
    cookie_secure: true
    cookie_httponly: true
    cookie_samesite: lax

4.2 Полная интеграция OAuth

# cifra/security/oauth.py
from authlib.integrations.httpx_client import AsyncOAuth2Client

class OAuthProvider:
    """Провайдер OAuth 2.0"""

    def __init__(self, config: dict):
        self.provider_name = config['provider']
        self.client_id = config['client_id']
        self.client_secret = config['client_secret']
        self.authorization_endpoint = config['authorization_endpoint']
        self.token_endpoint = config['token_endpoint']
        self.userinfo_endpoint = config['userinfo_endpoint']
        self.scopes = config.get('scopes', [])

    async def get_authorization_url(self, redirect_uri: str, state: str) -> str:
        """
        Получить URL для редиректа пользователя

        Returns:
            https://accounts.google.com/o/oauth2/v2/auth?
                client_id=...&
                redirect_uri=...&
                scope=openid email profile&
                state=random_state&
                response_type=code
        """
        client = AsyncOAuth2Client(
            client_id=self.client_id,
            redirect_uri=redirect_uri,
            scope=' '.join(self.scopes)
        )

        auth_url, state = client.create_authorization_url(
            self.authorization_endpoint,
            state=state
        )

        return auth_url

    async def exchange_code_for_token(
        self,
        code: str,
        redirect_uri: str
    ) -> dict:
        """
        Обменять authorization code на access token

        Returns:
            {
                'access_token': '...',
                'token_type': 'Bearer',
                'expires_in': 3600,
                'refresh_token': '...',
                'id_token': '...'  # если OpenID Connect
            }
        """
        client = AsyncOAuth2Client(
            client_id=self.client_id,
            client_secret=self.client_secret,
            redirect_uri=redirect_uri
        )

        token = await client.fetch_token(
            self.token_endpoint,
            code=code
        )

        return token

    async def get_user_info(self, access_token: str) -> dict:
        """
        Получить информацию о пользователе

        Returns:
            {
                'sub': 'google_user_id',
                'email': 'user@gmail.com',
                'name': 'John Doe',
                'picture': 'https://...'
            }
        """
        client = AsyncOAuth2Client(token={'access_token': access_token})
        response = await client.get(self.userinfo_endpoint)
        return response.json()


# FastAPI integration
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import RedirectResponse

router = APIRouter()

@router.get('/auth/{provider}/login')
async def oauth_login(provider: str):
    """Инициировать OAuth flow"""
    oauth_provider = get_oauth_provider(provider)

    # Генерируем random state для защиты от CSRF
    state = generate_random_state()
    save_state_to_session(state)

    # Получаем authorization URL
    redirect_uri = f"{settings.BASE_URL}/auth/{provider}/callback"
    auth_url = await oauth_provider.get_authorization_url(
        redirect_uri=redirect_uri,
        state=state
    )

    return RedirectResponse(auth_url)


@router.get('/auth/{provider}/callback')
async def oauth_callback(
    provider: str,
    code: str,
    state: str
):
    """Callback после успешной авторизации"""
    # Проверяем state (защита от CSRF)
    if not verify_state(state):
        raise HTTPException(400, "Invalid state")

    oauth_provider = get_oauth_provider(provider)

    # Обмениваем code на token
    redirect_uri = f"{settings.BASE_URL}/auth/{provider}/callback"
    token = await oauth_provider.exchange_code_for_token(
        code=code,
        redirect_uri=redirect_uri
    )

    # Получаем информацию о пользователе
    user_info = await oauth_provider.get_user_info(
        access_token=token['access_token']
    )

    # Создаём или обновляем пользователя
    user = await create_or_update_user_from_oauth(
        provider=provider,
        provider_user_id=user_info['sub'],
        email=user_info['email'],
        name=user_info.get('name'),
        avatar=user_info.get('picture')
    )

    # Создаём JWT токен для нашей системы
    jwt_token = create_jwt_token(user)

    # Редиректим на фронтенд с токеном
    return RedirectResponse(
        f"{settings.FRONTEND_URL}?token={jwt_token}"
    )

4.3 RBAC + RLS + Field-level (из предыдущих документов)

security:
  # Role-Based Access Control
  rbac:
    roles:
      - admin: {permissions: ["*"]}
      - manager: {permissions: [contacts:*, deals:*]}
      - user: {permissions: [contacts:read, deals:read:own]}

  # Row-Level Security
  rls:
    Contact:
      - policy: own_contacts
        condition: "owner_id == current_user.id"
        roles: [user]

  # Field-Level Security
  field_security:
    Contact:
      phone:
        read: [admin, manager]
        write: [admin]

Часть 5: DRUPAL-ПОДОБНЫЕ КОНЦЕПЦИИ

5.1 Render Arrays (Массивы рендеринга)

Концепция из Drupal: Данные для рендеринга в виде массивов

# cifra/core/render.py

def render_user_profile(user):
    """
    Возвращает render array (не HTML!)

    Позволяет другим модулям изменить данные до рендеринга
    """
    render_array = {
        '#theme': 'user_profile',
        '#user': user,
        '#cache': {
            'keys': ['user', user.id],
            'tags': ['user:' + str(user.id)],
            'max_age': 3600
        },
        '#attributes': {
            'class': ['user-profile'],
            'data-user-id': user.id
        },
        'avatar': {
            '#theme': 'image',
            '#uri': user.avatar_url,
            '#alt': user.full_name,
            '#attributes': {'class': ['user-avatar']}
        },
        'name': {
            '#markup': f'<h2>{user.full_name}</h2>',
            '#weight': 10
        },
        'email': {
            '#markup': f'<p>{user.email}</p>',
            '#weight': 20
        }
    }

    # Позволяем другим модулям изменить
    alter_hooks.invoke_all('user_profile_alter', render_array, user)

    return render_array

5.2 Theme Hooks (Хуки тем)

# cifra/themes/hooks.py

def hook_theme():
    """
    Регистрирует все theme hooks

    Аналог hook_theme() в Drupal
    """
    return {
        'user_profile': {
            'template': 'user-profile',
            'variables': {
                'user': None,
                'show_avatar': True,
                'show_stats': True
            }
        },
        'button': {
            'template': 'button',
            'variables': {
                'label': '',
                'type': 'button',
                'variant': 'primary',
                'size': 'md',
                'disabled': False
            }
        }
    }


def theme_user_profile(variables):
    """
    Рендерит user profile

    Может быть переопределена в дочерней теме
    """
    user = variables['user']
    show_avatar = variables['show_avatar']

    return f"""
    <div class="user-profile">
        {f'<img src="{user.avatar}" />' if show_avatar else ''}
        <h2>{user.full_name}</h2>
        <p>{user.email}</p>
    </div>
    """

5.3 Preprocess Functions (Предобработка)

# cifra/themes/preprocess.py

def preprocess_user_profile(variables):
    """
    Предобработка перед рендерингом

    Добавляет дополнительные переменные
    """
    user = variables['user']

    # Добавляем вычисляемые поля
    variables['display_name'] = user.full_name or user.email
    variables['member_since'] = format_date(user.created_at)
    variables['is_vip'] = user.total_spent > 100000

    # Добавляем CSS классы
    variables['attributes']['class'].append('user-type-' + user.role)
    if variables['is_vip']:
        variables['attributes']['class'].append('user-vip')


def preprocess_button(variables):
    """Предобработка кнопки"""
    # Комбинируем классы
    classes = ['btn']
    classes.append(f"btn-{variables['variant']}")
    classes.append(f"btn-{variables['size']}")

    if variables['disabled']:
        classes.append('btn-disabled')

    variables['attributes']['class'] = classes

5.4 Field Formatters (Форматтеры полей)

Как в Drupal: Разные способы отображения одного поля

# entities/product.cifra
entity:
  name: Product

  fields:
    price:
      type: decimal

      # Разные форматы отображения
      formatters:
        - default:
            format: "number"
            decimals: 2
            prefix: "$"

        - with_currency:
            format: "currency"
            currency: "USD"
            locale: "en_US"

        - short:
            format: "abbreviated"
            # $1.5K вместо $1,500.00

        - comparison:
            format: "comparison"
            # Показывает старую цену зачёркнутую
            show_discount_percent: true

    description:
      type: text

      formatters:
        - default:
            format: "plain_text"

        - teaser:
            format: "summary"
            length: 200
            suffix: "..."

        - full:
            format: "html"
            allowed_tags: [p, strong, em, ul, li]

Часть 6: ПОЛНЫЙ ПРИМЕР

Создание страницы с каскадными стилями:

# pages/user_profile.page.cifra
page:
  id: user_profile
  path: /users/{user_id}
  title: "Профиль пользователя"

  # Data (уровни абстракции)
  data:
    user:
      source: entity
      entity: User
      view_model: profile
      id: "{params.user_id}"

  # Theme (каскад стилей)
  theme:
    extends: admin_light

    # Переопределяем для этой страницы
    overrides:
      layout:
        type: centered
        max_width: 800px

  # Components (с каскадом)
  components:
    header:
      type: user_header
      data: "{page.user}"
      styles:
        background: colors.surface
        padding: spacing.lg
        # → Разрешается через каскад

    stats:
      type: stats_grid
      data:
        - label: "Заказов"
          value: "{user.orders_count}"
        - label: "Потрачено"
          value: "{user.total_spent}"
          format: currency

    orders_list:
      type: data_table
      data:
        source: entity
        entity: Order
        filters:
          customer_id: "{user.id}"
        limit: 10

  # Security
  security:
    auth: required
    permissions:
      - own: "user_id == current_user.id"
      - or: "users:read"

Результат: Страница с данными через 6 уровней абстракции, стилями через каскад, OAuth авторизацией и проверкой прав!


Заключение

Что получили:

✅ DATA ABSTRACTION (6 уровней)
   DB → ORM → DTO → ViewModel → API → UI

✅ THEME SYSTEM (каскадная)
   Base → Tokens → Variants → Builder → Cascade

✅ DESIGN SYSTEM
   Tokens → Components → Patterns → Templates

✅ SECURITY (полная)
   OAuth 2.0 → OpenID → RBAC → RLS → Field-level

✅ DRUPAL CONCEPTS
   Render Arrays → Theme Hooks → Preprocess → Formatters

✅ CASCADE (универсальная)
   Global → Theme → Component → Variant → State → Inline

Всё работает вместе:

Один .cifra файл → генерирует:
- Все уровни данных
- Все уровни стилей
- Всю безопасность
- Все хуки и форматтеры

Полная каскадность и универсальность! 🚀