---
title: 'RoR stílusútmutató'
url: 'https://wiki.teletypegames.org/hu/development/ror-styleguide'
markdown: 'https://wiki.teletypegames.org/hu/development/ror-styleguide.md'
lang: hu
date: '2026-08-23'
description: 'Ezek azok a szabályok, amelyeket a Rails-kódunk követ. Két helyről jönnek: a Growing Rails Applications in Practice (makandra) könyvből, és abból a két alkalmazásból, amelyet már rájuk építve üzemeltetünk — a nyilvános oldalból a WarpEngine katalógusmotorral, és a Teletype Orbitból. Semmi itt nem …'
taxonomy:
  tag:
    - howto
---

# RoR stílusútmutató

> Ezek azok a szabályok, amelyeket a Rails-kódunk követ. Két helyről jönnek: a *Growing Rails Applications in Practice* (makandra) könyvből, és abból a két alkalmazásból, amelyet már rájuk építve üzemeltetünk — a [nyilvános oldalból](https://wiki.teletypegames.org/hu/services/public-site) a [WarpEngine](https://wiki.teletypegames.org/hu/engines/warp-engine) katalógusmotorral, és a [Teletype Orbitból](https://wiki.teletypegames.org/hu/services/teletype-orbit). Semmi itt nem stíluspreferencia. Ha itt Railst írsz, ezeket követed.

Egy mondat, amit érdemes megjegyezni: **a nagy alkalmazások nagyok**. Ezt nem tudjuk eltüntetni. Amit tehetünk: úgy rendezzük a kódot, hogy a kétszer annyi modell ne jelentsen kétszer annyi problémát.

# 1. Hova kerül a kód

Minden kódrésznek egy otthona van. Ha nem tudod megnevezni az otthonát, akkor az osztály, amire szükséged van, még nem létezik — hozd létre.

- **Kontroller** (`app/controllers/`) — csak HTTP: paramétereket olvas, form objektumot vagy szolgáltatást hív, rendel vagy átirányít. Üzleti logika nincs benne.
- **Form objektum** (`app/forms/`) — minden olyan űrlap, amely egynél több modellbe ír, vagy amely mögött egyáltalán nincs modell (belépés, pénztár, feltöltés). Sima `ActiveModel::Model` validációkkal.
- **Szolgáltatás** (`app/services/`) — egy használati eset, egy publikus metódus beszédes névvel (`place`, `issue`, `revoke`, `publish`). A tranzakciók, döntések és mellékhatások itt élnek. A metódust soha ne nevezd `call`-nak.
- **Query objektum** (`app/queries/`) — bármi, ami bonyolultabb egy scope-nál: keresés, szűrők, diagramok, riportok. Relációt vagy sorok tömbjét adja vissza.
- **Presenter** (`app/presenters/`) — formázás a nézetnek, hogy a sablonokban ne legyen logika.
- **DTO** (`app/dtos/`) — az az eredmény, amit egy szolgáltatás visszaad. `Struct`-szerű érték, validációk nélkül.
- **Szerializáló** (`app/serializers/`) — Blueprinter, kizárólag a JSON API-hoz. A HTML-oldalak nézeteket használnak.
- **Modell** (`app/models/`) — asszociációk, validációk, scope-ok.
- **Job** (`app/jobs/`) — aszinkron mellékhatások: levél, aggregálás, webhook-feldolgozás.

Két szabály a rétegek között:

- Egy szolgáltatás hívhat másik szolgáltatást. **Egy modell soha nem hív szolgáltatást.**
- Ha egy modell-callback levelet küldene vagy jogosultságot adna, akkor rossz helyen van. Tedd át egy szolgáltatásba.

# 2. Kontrollerek

- **Csak a hét szabványos akció létezik**: `index`, `show`, `new`, `create`, `edit`, `update`, `destroy`. Nincs `approve`, `publish`, `add_to_cart`, `redeem`.
- Egy új interakció **új kontrollert jelent, amelynek az erőforrása maga az interakció**:

| Ehelyett | Ezt írd |
|---|---|
| `CartsController#add_item` | `Store::CartItemsController#create` |
| `SubmissionsController#approve` | `Moderation::ApprovalsController#create` |
| `ProductsController#publish` | `Studio::PublicationsController#create` |
| `ReviewsController#upvote` | `Community::ReviewVotesController#create` |
| `SessionsController#logout` | `Accounts::SessionsController#destroy` |

- Egy akció három-hat sor. Nulla üzleti logika, és **nulla `if` egy modell állapotára**.
- **Minden bemenetet engedélyezőlistázz.** A modellattribútumok `permit`-en mennek át. Azok a szolgáltatásbemenetek, amelyek nem modellattribútumok, form objektumba valók — a „csak két mező" mondattal kezdődik a következő pokoli kontroller.
- **Soha ne hívj `constantize`-t paraméterre.** Előbb ellenőrizd egy engedélyezőlista-konstanshoz (már megvan a `Support::Flag::FLAGGABLES` és a `Community::Follow::FOLLOWABLES`).
- **Egyetlen privát metódus az egyetlen ajtó a modellhez.** Ahol egy erőforrás felhasználóhoz, kiadóhoz vagy tokenhez van szűkítve, azon a szűkítésen keresztül töltsd be, ne előbb keresd meg és utána ellenőrizd.
- **Az ismétlődő `before_action` törzsek az alapkontrollerbe valók**, lehetőleg kis osztálymakróként. Ugyanannak a „keresd meg a kiadót és engedélyezz" párnak öt másolata öt esély az elcsúszásra.
- A szolgáltatások saját hibaosztályokat dobnak (`Store::CheckoutService::Error`, `Library::DownloadService::Denied`). A kontroller **azokat** kapja el, soha nem a `StandardError`-t.
- A közös válaszformálás — 404, 403, a JSON hibatörzs — egyszer él, az alapkontroller `rescue_from` blokkjában.

# 3. Modellek és ActiveRecord

- **Egy invariánst validációban és callbackben mondj ki, ne segédmetódusban.** Egy `invite.accept!` metódust tucatnyi módon meg lehet kerülni (`update!`, `create!`, attribútum-beállító). Csak a validációkról és a callbackekről garantált, hogy lefutnak.
- A kényelmi metódusok jöhetnek. Csak soha ne támaszkodj arra, hogy más kód meghívja őket.
- **Egy callback konzisztensen tartja az adatot. Soha nem csinál üzletet.** Slug vagy token generálása, státusz tükrözése, csak olvasható sor őrzése: igen. Levélküldés, jogosultság adása, webhook hívása: nem.
- **Az `update_column`, az `update_columns` és az `update_all` megkerüli a saját szerződésedet.** Csak házimunkára használd őket — egy `last_used_at` érintés, egy számláló, egy kötegelt söprés. Bármi, amit egy ember ténynek nevezne (kié ez, listázva van-e, melyik a fő kép), `save`/`update!` hívásokon megy át.
- **Soha ne nyúlj a külvilághoz, mielőtt a sor commitolódott.** Egy `before_save`-ben írt fájl vagy egy `before_validation`-ben mentett másik rekord árvákat hagy maga után, ha a mentés elbukik. Használj `after_commit`-et, vagy csináld egy szolgáltatásban, tranzakción belül.
- **Egy értékkészlet a modellen konstansként él, mellette `inclusion` validációval.** A validáció nélküli konstans javaslat, nem szabály — és soha ne másold be az értékeket egy ActiveAdmin-szűrőbe, hivatkozz a konstansra.
- **Az interakcióspecifikus kód nem a mag-modellbe való.** Egy validáció, amire csak egy űrlapnak van szüksége, egy virtuális attribútum egyetlen képernyőhöz, egy callback egyetlen használati esethez: az form objektum vagy szolgáltatás.
- **A modulok és concernek nem karcsúsítanak egy modellt.** Ezek fájlszervezés; a callbackek és a metódusok továbbra is betöltődnek az osztályba.
- A `default_scope` a soft delete szűrőjére való (`where(deleted_at: nil)`), és semmi másra. **Soha ne tegyél `order`-t `default_scope`-ba** — átszivárog minden asszociációba és aggregátumba, és a végén `unscope(:order)` hívásokat fogsz írni, hogy kikerülj belőle.
- Minden modell, amelyhez adminoldal tartozik, explicit `ransackable_attributes` és `ransackable_associations` listát deklarál.

# 4. Nevek és névterek

- **Minden domain egy Ruby-modul, és nincsenek legfelső szintű modellek.** `Accounts`, `Store`, `Library`, `Studio`, `Community`, `Support`, `Finance`, `Ops`.
- A modul viszi a tábla-előtagot, így a leképezés automatikus:

```ruby
# app/models/store.rb
module Store
  def self.table_name_prefix = "store_"
end
# Store::Product -> store_products
```

Ez nem dekoráció: a WarpEngine saját táblái (`softwares`, `releases`, `images`) előtag nélkül ülnek **ugyanabban az adatbázisban**, tehát az előtag az, ami szerkezetileg megakadályozza az ütközést.

- **A motor szavai foglaltak.** A `Software`, `Release`, `ReleaseAsset`, `Download`, `SoftwareImage`, `ExternalLink`, `PlatformLink`, `ApplicationToken`, `Pipeline` a kódunkban soha nem jelent mást. Ahol a bolt fogalma eltér, használj másik szót: az eladható dolog `Store::Product`, egy fórumszál `Community::Topic` (a `Thread` Ruby-osztály), egy tartalombejelentés `Support::Flag` (a „report" az értékesítési riport).
- Kerülendő továbbá: `Process`, `Set`, `Data`, `Method`, `File`, `Dir`, `Range`, `Signal`, `Random`, `Comparable`.
- **Névterezz agresszíven, és a szolgáltatásokat fogalom szerint névterezd**, ne utótag szerint: `Software::Publish`, `Rss::BlogFeed`, `Store::CheckoutService` — nem egy lapos mappa tizenhét `_service.rb` végű fájllal.
- **Használj mindenhol ugyanazt a szerkezetet**: a modellek, kontrollerek, nézetek, helperek és specek tükrözik egymást. Egy request spec a `spec/requests` alatt él, egy model spec a `spec/models` alatt — még akkor is, ha a fájl máshonnan is futna.
- A névterezés kétszer térül meg: olvashatóvá teszi a mappát, és nyilvánvaló helyet ad az új osztályoknak — pontosan ez akadályozza meg, hogy a modellek elhízzanak.

# 5. A WarpEngine határa

- **Minden hoszt-specifikus rész egy adapter alapértékkel, és az alapérték a mai viselkedés.** Így marad a motor önmagában is használható, miközben a bolt beteszi a saját szabályait: tárolás, hozzáférési szabályzat, képek, CI, tokenforrás, alanyfeloldás.
- **Bolt-specifikus kód soha nem kerül a motorba.** A motor közöl és konfigurál; a hoszt dönt.
- **A motor kifelé `ActiveSupport::Notifications` eseményekkel beszél**, nem a hosztba visszahívó callbackekkel. A hoszt feliratkozik (`warp_engine.publish`, `warp_engine.download`).
- **Egy motormodellt a load hookján keresztül bővíts** (`ActiveSupport.run_load_hooks(:warp_engine_software, self)`), soha ne az osztály újranyitásával.
- **A motor URL-jei foglaltak**: `/api/software*`, `/api/builds`, `/api/download`, `/api/service`, `/api/auth/*`, `/api/ci/*`, `/build/*`, `/file/*`. A saját JSON API-nk verziózott (`/api/v1`), tehát nem ütközhet. A `mount WarpEngine::Engine` **mindig a `routes.rb` utolsó sora**.
- Ha szándékosan árnyékolsz egy motorútvonalat, az útvonalhoz komment kell arról, hogy miért, **és** annak a regressziós specnek a neve, amely védi.
- **Egy motormodellbe úgy írj, ahogy a motor várja**: `update!` hívással, hogy a validációi és callbackjei lefussanak. A motor verziózott függőség — egy oszlopírás, ami ma működik, némán kihagyja azt a callbacket, amit a következő kiadásban kap.
- **Amikor egy seam leváltja a régi kódot, töröld a régi kódot.** Miután a tárolóadapter beérkezett, még négy másik hely számolta a fájlok alapútvonalát; kettő közülük már halott volt.

# 6. Konfiguráció

- **Minden `ENV`-olvasás egy helyen történik**, a `config/application.rb` fájlban, a `config.x` alá:

```ruby
config.x.downloads.grant_ttl = 15.minutes
config.x.images.container_path = ENV.fetch("IMAGE_CONTAINER_PATH", "/images")
```

- A kód a `Rails.configuration.x` értékeit olvassa, soha nem közvetlenül az `ENV`-et. Egyetlen hely mutatja meg, mit vár az alkalmazás a környezetétől, és egy teszt felül tudja írni.
- **Egy dolognak egy neve.** Két környezeti változó ugyanarra a wikire, amelyek közül az egyiket a compose-fájl soha nem állítja be, olyan hiba, amely a péntekre vár.
- A motorbeállítások a motor saját konfigurációs objektumába kerülnek alapértékekkel, a hoszt pedig egyetlen initializerben állítja be őket.
- **Semmi monkey patch initializerből.** Az `ApplicationController` újranyitása egy `to_prepare` blokkban elrejti a viselkedést az osztályt deklaráló fájl elől — tedd concernbe, és include-old.

# 7. Nézetek és stílusok

- Tailwind, és **elnevezett osztály mindenre, ami kétszernél többször előfordul**. Ha a `text-2xl font-semibold` 81-szer szerepel, akkor az egy címsorosztály, amely meg akar születni.
- **Szemantikus színtokenek, nem nyers palettanevek.** `surface`, `line`, `muted`, `accent` — így egy témaváltás egy fájl, nem 134 nézet.
- **Egy téma tokencsere.** Más blokkok `!important`-tal való felülírásától válik karbantarthatatlanná egy stíluslap; egy blokk a saját módosítóosztályán keresztül dönti el, hogyan néz ki egy témában.
- **Egy blokk soha nem stílusoz egy másikat.** A `.sidebar .article {}` kizárva; vagy az elem a blokkhoz tartozik (`.sidebar__article`), vagy a blokk kap egy módosítót (`.article.is_summary`).
- Megengedett pragmatikus kivételek: összetett HTML gyerekei (`table`, `dl`, `ul`), `:before`/`:after`, pszeudoszelektorok, médiaquery-k, markdown- vagy WYSIWYG-tartalom egyetlen konténeren belül (`.wiki-content`), és külső könyvtár által generált markup.
- **Nézetben nincs lekérdezés**, és `includes` sem — az adatot a kontroller vagy a szolgáltatás készíti elő. Egy lekérdezés egy partial ciklusában N+1 probléma, extra lépésekkel.
- **Nincs soron belüli `style:` attribútum ActiveAdmin-oldalakon**, ha a projektnek már van adminstíluslapja. Az ismétlődő markup osztályt kap; egy hosszú ikon-`case` presentert.
- Az a nézet, amelynek hat példányváltozó kell öt szolgáltatásból, nem `show` — az egy munkalap, és presentert vagy query objektumot kér.

# 8. JSON API

- Blueprinter-szerializálók, **camelCase kulcsok**, és szerializálók kizárólag az API-hoz — soha nem HTML-oldalakhoz.
- **A nyilvános végpontokat apipie-jal annotáld** (`api`, `param`, `returns`, `error`), felsorolva a válasz tulajdonságait. Az `/api/docs` és a swagger-oldal ebből épül.
- **Írd le, miért nyilvános egy végpont.** A „egy kliensnek nincs kinek belépnie" a végpont leírásába való, nem egy commitüzenetbe.
- A saját API-nk verziózott. Az a válasz, amelyre egy kliens támaszkodik, verziófejlécet visz, egy szolgáltatásleíró végpont pedig még bármilyen hitelesítő adat előtt megmondja a kliensnek, mit kínál ez a telepítés.
- Ha a projektben vannak szerializálók, egy kontrollerben kézzel összerakott hash az az út, amelynek a végén ugyanannak a mezőnek két neve lesz egy válaszban.

# 9. Tesztek

- **Az egységtesztek és a teljes vertikumú tesztek térülnek meg.** Ne adj hozzá kontroller- és nézetspecet ahhoz, amit ez a kettő már lefed.
- A sötét ház szabálya: az alkalmazás egy sötét ház, az integrációs tesztek a háttérvilágítás, az egységtesztek a reflektorok. Nem kell mindenhova sebészi fény — arra van szükség, hogy biztos lehess benne: egy sötét sarokban sem nő semmi.
- **`spec/models` minden invariánsra**: az állapotgép, az utolsó tulajdonos szabálya, a csak olvasható sor, az egyediség, ami számít. Ezek a legolcsóbb és leghosszabb életű tesztek, amiket írni fogsz.
- **A `spec/services` a súlypont** — minden ág, minden hibaút.
- **`spec/requests` a jogosultságokra, a státuszkódokra és az átirányításokra.** A letöltési kapunak kötelező regressziós spece van.
- **Minden képernyőt megérint legalább egy teljes vertikumú teszt.** Egy-két út rajta keresztül, nem minden peremeset. Hotwire mellett ez nem opcionális.
- **Egy parancs futtat mindent**, külön beállítás nélkül, a tesztadatot pedig maguk a tesztek hozzák létre.
- **Semmi nem kerül commitba bukó vagy függőben lévő teszttel.** Amint a tesztkészlet nem zöld, nem tudja többé megmondani, hogy elrontott-e valamit a változtatásod, a léc pedig egyre lejjebb csúszik.
- A factory-k minimálisak maradnak, traitekkel. Nincs istenfactory, ami az adatbázis felét behúzza.
- Tartsd unalmasan a teszteket: az okosság a tesztelt kódba való.

# 10. Gemek és frissítések

- **Rögzíts minden gemet**, és írd oda az indoklást minden kockázatos választás mellé. A Gemfile jó hely egy mondatnak, ami megspórol egy délutánt.
- **Nincs git-ágas függőség.** Egy `branch: master` gem azt jelenti, hogy két `bundle update` között bármi megérkezhet, a lockfile pedig revíziót nevez meg, nem verziót. Ha még nincs kiadás, rögzíts egy `ref:` értéket, és szándékosan emeld.
- **Nincs `:latest` konténerimage a CI-ban.** Egy build eszközlánca nem változhat commit nélkül.
- Egy gem hozzáadása azt jelenti, hogy **a tiéd lesz**: a viselkedése terhelés alatt, a biztonsági frissítései, a frissítése, amikor a Rails továbblép, és a függőségei. Válassz inkább alacsony szintű könyvtárat, mint mini keretrendszert, amely mélyen beleakaszkodik a Rails belsőségeibe.
- **Ne élj a vérző élen.** Várd meg, amíg egy főverzió elér néhány javítószintet.
- **Használd ki a meglévő eszközeidet.** Egy újabb tábla a Redis előtt, `LIKE` a keresőszerver előtt, egy cron a sorkezelő előtt — és rejtsd a döntést egy kis API mögé (`Article::Search.find`), hogy a későbbi csere egyetlen osztályt érintsen.
- **A monkey patchekért frissítéskor fizetsz.** Forkolj, javítsd teszttel, és nyiss pull requestet.

# 11. Hogyan dolgozunk

- \**A terv a repó gyökerében lévő `NOTES\_*.md` fájlban él\*\* (gitignore-olva), nem commitüzenetek között szétszórva: döntések, alternatívák és a miértek. A kód szakaszra hivatkozva utalhat rá.
- **Repónkénti kommentkonvenció.** A `teletype-orbit` repóban a miértet a nem nyilvánvaló kód mellé írjuk; a `teletypegames` monorepóban nem használunk magyarázó kommentet, a miért a commitba megy. Mindkettő szándékos — kövesd azt a repót, amelyikben vagy.
- **A kiadói műveleteket táblába auditáld**, ne naplófájlba: ki, mit, mikor, melyik rekorddal.
- **A seedek két szinten jönnek**: alapadatok, amelyek élesben is lefutnak, és példaadatok fejlesztéshez, amelyek a **valódi** úton mennek végig (studio, beküldés, jóváhagyás, kosár, fizetési webhook), így a seed a domaint gyakorolja, nem sorokat tol be oldalról.
- **Az új mintákat megítéljük, nem átvesszük.** Írj át egy kis részt az új stílusban, hasonlítsd össze a kettőt, és mondd ki hangosan, mi a kompromisszum. A következetesség többet ér egy félúton átvett, marginálisan jobb technikánál.

'; var prevLevel = 2; for (var i = 0; i < headings.length; i++) { var h = headings\[i\]; var text = h.textContent.trim(); var slug = text.toLowerCase() .replace(/\[áàâä\]/g,'a').replace(/\[éèêë\]/g,'e').replace(/\[íìîï\]/g,'i') .replace(/\[óòôöő\]/g,'o').replace(/\[úùûüű\]/g,'u') .replace(/\[^a-z0-9\]+/g, '-').replace(/^-|-$/g, ''); if (!slug) slug = 'heading-' + i; if (used\[slug\]) { slug += '-' + i; } used\[slug\] = true; h.id = slug; var level = parseInt(h.tagName.charAt(1)); var cls = level > 2 ? ' class="toc-child"' : ''; html += '- [' + text + '](#'%20+%20slug%20+%20')
'; } html += '
'; toc.innerHTML = html; // Scroll-spy: highlight current heading var links = toc.querySelectorAll('a'); var ids = \[\]; for (var j = 0; j < links.length; j++) ids.push(links\[j\].getAttribute('href').slice(1)); function updateActive() { var current = ''; for (var k = 0; k < ids.length; k++) { var el = document.getElementById(ids\[k\]); if (el && el.getBoundingClientRect().top <= 80) current = ids\[k\]; } for (var m = 0; m < links.length; m++) { links\[m\].parentNode.classList.toggle('active', links\[m\].getAttribute('href') === '#' + current); } } window.addEventListener('scroll', updateActive, { passive: true }); updateActive(); })();

---

## Navigation

- Parent: [Fejlesztés](https://wiki.teletypegames.org/hu/development.md)
- Previous: [RDoc](https://wiki.teletypegames.org/hu/development/rdoc.md)
- Next: [Hasznos anyagok](https://wiki.teletypegames.org/hu/development/useful-materials.md)
