diff --git a/README.ja.md b/README.ja.md index 04dbfcf..4896578 100644 --- a/README.ja.md +++ b/README.ja.md @@ -10,5 +10,11 @@ - **馴染みの関連 API で高速参照**: `belongs_to` / `has_many` 風のインターフェースを DB ではなくオンメモリ上で処理し、N+1 を気にしなくてよい速度で動作。 - **COW フレンドリーで多プロセス共有**: レコードは freeze され、Copy-on-Write を活かしてフォークプロセス間でメモリを効率共有できる。 +## ドキュメント +- 導入ガイド: [English](docs/simple_master_guide_en.md) / [日本語](docs/simple_master_guide_ja.md) +- カラム仕様: [English](docs/simple_master_columns_en.md) / [日本語](docs/simple_master_columns_ja.md) +- Dataset / Table: [English](docs/simple_master_dataset_en.md) / [日本語](docs/simple_master_dataset_ja.md) +- Association: [English](docs/simple_master_associations_en.md) / [日本語](docs/simple_master_associations_ja.md) + ## ライセンス MIT ライセンスです。詳細は [LICENSE](LICENSE) を参照してください。 diff --git a/README.md b/README.md index 8336936..04ddf7c 100644 --- a/README.md +++ b/README.md @@ -9,5 +9,11 @@ In game development and other domains, configuration/definition datasets are oft - **Familiar associations, very fast**: `belongs_to` / `has_many`-style API resolved in memory, fast enough that N+1 is rarely a concern. - **COW-friendly for multi-process**: records are frozen, making Copy-on-Write efficient when sharing memory across forked processes. +## Documentation +- Getting Started Guide: [English](docs/simple_master_guide_en.md) / [日本語](docs/simple_master_guide_ja.md) +- Columns: [English](docs/simple_master_columns_en.md) / [日本語](docs/simple_master_columns_ja.md) +- Dataset / Table: [English](docs/simple_master_dataset_en.md) / [日本語](docs/simple_master_dataset_ja.md) +- Associations: [English](docs/simple_master_associations_en.md) / [日本語](docs/simple_master_associations_ja.md) + ## License MIT License. See [LICENSE](LICENSE) for details. diff --git a/docs/simple_master_associations_en.md b/docs/simple_master_associations_en.md new file mode 100644 index 0000000..0f4873a --- /dev/null +++ b/docs/simple_master_associations_en.md @@ -0,0 +1,51 @@ +# SimpleMaster Associations (English) + +> 日本語版: [simple_master_associations_ja.md](simple_master_associations_ja.md) + +## Overview +SimpleMaster provides `belongs_to`, `has_one`, `has_many`, and `has_many :through`. + +## Definitions +```ruby +class Player < ApplicationRecord + belongs_to :level, foreign_key: :lv, primary_key: :lv + has_many :player_items +end +``` + +```ruby +class Reward < ApplicationMaster + belongs_to :enemy + belongs_to :reward, polymorphic: true +end +``` + +The lookup path depends on whether the target is `SimpleMaster::Master` +or `ActiveRecord::Base`. + +- Master to Master: use `all_by` / `find_by` + - These are fast, so values are fetched on each call. Cache in a variable if used often. +- ActiveRecord: use `simple_master_connection` + - `belongs_to_store` / `has_many_store` (RequestStore) caches per request. + +## Common options +### `class_name:` +- Example: `belongs_to :reward, class_name: "Weapon"` +- Explicitly sets the target class. + +### `foreign_key:` +- Example: `has_many :players, foreign_key: :lv` +- Sets the foreign key column. + +### `primary_key:` +- Example: `belongs_to :level, primary_key: :lv` +- Sets the target primary key (default is `:id`). + +## Association types +- `belongs_to` : `belongs_to :enemy` +- `belongs_to (polymorphic)` : `belongs_to :reward, polymorphic: true` + - Requires `def_column :reward_type, polymorphic_type: true` +- `has_one` : `has_one :profile` +- `has_many` : `has_many :players, foreign_key: :lv` +- `has_many :through` : `has_many :items, through: :player_items` + - `source:` can rename the target association diff --git a/docs/simple_master_associations_ja.md b/docs/simple_master_associations_ja.md new file mode 100644 index 0000000..e4bd552 --- /dev/null +++ b/docs/simple_master_associations_ja.md @@ -0,0 +1,50 @@ +# SimpleMaster Association 仕様 (日本語) + +> English version: [simple_master_associations_en.md](simple_master_associations_en.md) + +## 全体説明 +SimpleMaster の Association は `belongs_to` / `has_one` / `has_many` / `has_many :through` を提供します。 + +## 定義方法 +```ruby +class Player < ApplicationRecord + belongs_to :level, foreign_key: :lv, primary_key: :lv + has_many :player_items +end +``` + +```ruby +class Reward < ApplicationMaster + belongs_to :enemy + belongs_to :reward, polymorphic: true +end +``` + +対象が `SimpleMaster::Master` か `ActiveRecord::Base` かで参照方法が変わります。 + +- Master 同士: `all_by` / `find_by` を使った参照 + - 取得が高速なため、都度引き直しとなります。利用時に呼ぶ回数多いなら、変数に格納してください。 +- ActiveRecord: `simple_master_connection` で DB 参照 + - `belongs_to_store` / `has_many_store` (RequestStore) に保持されるため、リクエストごとにキャッシュが効きます。 + +## 共通オプション +### `class_name:` +- 例: `belongs_to :reward, class_name: "Weapon"` +- 明示的に参照先クラスを指定します。 + +### `foreign_key:` +- 例: `has_many :players, foreign_key: :lv` +- 外部キー名を指定します。 + +### `primary_key:` +- 例: `belongs_to :level, primary_key: :lv` +- 参照先のキーを指定します(デフォルトは `:id`)。 + +## Association 種別 +- `belongs_to` : `belongs_to :enemy` +- `belongs_to (polymorphic)` : `belongs_to :reward, polymorphic: true` + - 前提: `def_column :reward_type, polymorphic_type: true` +- `has_one` : `has_one :profile` +- `has_many` : `has_many :players, foreign_key: :lv` +- `has_many :through` : `has_many :items, through: :player_items` + - `source:` を指定すると参照先の名前を変更できます。 diff --git a/docs/simple_master_columns_en.md b/docs/simple_master_columns_en.md new file mode 100644 index 0000000..82f3412 --- /dev/null +++ b/docs/simple_master_columns_en.md @@ -0,0 +1,199 @@ +# SimpleMaster Columns (English) + +> 日本語版: [simple_master_columns_ja.md](simple_master_columns_ja.md) + +## Overview +SimpleMaster columns are defined with `def_column`. At load time, type conversion, +cache helpers, and accessor methods are generated. +The behavior depends on `type` and DSL options. + +```ruby +class Weapon < ApplicationMaster + def_column :id + def_column :name, type: :string + def_column :attack, type: :float + def_column :rarity + + enum :rarity, { common: 0, rare: 1, epic: 2 } +end +``` + +## Common options +### `type:` +- Example: `def_column :attack, type: :float` +- See the column type list below. + +### `group_key:` +- Example: `def_column :lv, type: :integer, group_key: true` +- You can also use `group_key :lv`. + +### `db_column_name:` +- Use when the DB column name differs. +- Example: `def_column :start_at, type: :time, db_column_name: :start_time` + +### `globalize:` +- Adds locale-aware values using `I18n.locale`. +- Example: `def_column :name, globalize: true` +- You can also use `globalize :name`. +- Translation values live in `@_globalized_name` like `{ en: "Storm Edge", ja: "..." }`. +- Not supported on `id` / `enum` / `bitmask` / `sti` / `polymorphic_type`. +- Cannot be used with `group_key`. + +## Column types + +### id (IdColumn) +**Usage** +```ruby +def_column :id +``` +**Behavior** +- Converts to `to_i` on assignment. +- In tests, updates `id_hash` when changed. + +### integer +**Usage** +```ruby +def_column :lv, type: :integer +``` +**Behavior** +- Converts to `to_i` on assignment (empty string becomes `nil`). + +### float +**Usage** +```ruby +def_column :attack, type: :float +``` +**Behavior** +- Converts to `to_f` on assignment (empty string becomes `nil`). + +### string +**Usage** +```ruby +def_column :name, type: :string +``` +**Behavior** +- Converts to `to_s` on assignment. +- Values are cached to reuse identical objects (`object_cache`). + +### symbol +**Usage** +```ruby +def_column :kind, type: :symbol +``` +**Behavior** +- Converts to `to_s` + `to_sym` on assignment. +- SQL/CSV output uses a string. + +### boolean +**Usage** +```ruby +def_column :is_boss, type: :boolean +``` +**Behavior** +- Integers use 0/1, strings accept "true" or "1". +- Adds a `name?` predicate. +- SQL/CSV output is 0/1. + +### json +**Usage** +```ruby +def_column :info, type: :json +``` +**Options** +- `symbolize_names: true` converts JSON keys to symbols. + +**Behavior** +- Parses string values with `JSON.parse`. +- SQL/CSV output uses `JSON.generate`. +- Non-string assignments are not transformed by `symbolize_names`. + +### time +**Usage** +```ruby +def_column :start_at, type: :time +``` +**Options** +- `db_type: :time` outputs `HH:MM:SS` only. + +**Behavior** +- Parses strings with `Date._parse` into `Time`. +- Sub-seconds are truncated. + +### enum +**Usage** +```ruby +def_column :rarity, enum: { common: 0, rare: 1, epic: 2 } +# or +def_column :rarity +enum :rarity, { common: 0, rare: 1, epic: 2 } +``` +**Options** +- `prefix`, `suffix` add a prefix/suffix to predicates. + - `prefix: true` => `rarity_common?` + - `suffix: :rarity` => `common_rarity?` + +**Behavior** +- Values are stored as symbols. +- Adds `rarities` and `rarity_before_type_cast`. +- Predicate methods (e.g. `common?`) are generated. + +### bitmask +**Usage** +```ruby +def_column :flags, type: :integer +bitmask :flags, as: [:tradeable, :soulbound, :limited] +``` +**Behavior** +- Accepts array/symbol/integer and converts to bit integer. +- `flags` returns an array of symbols. +- Adds `flags_value` / `flags_value=` for raw integer bits. + +### sti (STI type column) +**Usage** +```ruby +def_column :type, sti: true +``` +**Behavior** +- Converts `type` to a string. +- Defines `sti_base_class` and `sti_column`. +- Loader should resolve classes by `type`. + +### polymorphic_type +**Usage** +```ruby +def_column :reward_type, polymorphic_type: true +``` +**Behavior** +- Used for `belongs_to polymorphic` type columns. +- Stores a class name string and sets `reward_type_class`. +- Empty strings become `nil`. + +## Custom column types +Define custom columns by subclassing `SimpleMaster::Master::Column`. +If the class name ends with `Column`, the `type` is auto-registered. + +```ruby +class MoneyColumn < SimpleMaster::Master::Column + private + + def code_for_conversion + <<-RUBY + value = value&.to_i + RUBY + end + + def code_for_sql_value + <<-RUBY + #{name} + RUBY + end +end + +class Product < ApplicationMaster + def_column :price, type: :money +end +``` + +- Ensure the file is loaded before use. +- Override `init` if you need custom methods. +- See [lib/simple_master/master/column.rb](lib/simple_master/master/column.rb). diff --git a/docs/simple_master_columns_ja.md b/docs/simple_master_columns_ja.md new file mode 100644 index 0000000..f11f745 --- /dev/null +++ b/docs/simple_master_columns_ja.md @@ -0,0 +1,199 @@ +# SimpleMaster カラム仕様 (日本語) + +> English version: [simple_master_columns_en.md](simple_master_columns_en.md) + +## 全体説明 +SimpleMaster のカラムは `def_column` で定義し、ロード時に型変換・キャッシュ・補助メソッドを自動生成します。 +`type` や各種 DSL によって、変換ルールや追加メソッドが決まります。 + +```ruby +class Weapon < ApplicationMaster + def_column :id + def_column :name, type: :string + def_column :attack, type: :float + def_column :rarity + + enum :rarity, { common: 0, rare: 1, epic: 2 } +end +``` + +## 共通オプション +### `type:` +- 例: `def_column :attack, type: :float` +- 対応タイプは「カラムタイプ別一覧」を参照してください。 + +### `group_key:` +- 例: `def_column :lv, type: :integer, group_key: true` +- もしくは `group_key :lv` でも指定できます。 + +### `db_column_name:` +- DB 側のカラム名が異なる場合に使います。 +- 例: `def_column :start_at, type: :time, db_column_name: :start_time` + +### `globalize:` +- 言語による差分が定義でき、`I18n.locale` に応じた値を返すようになります。 +- 例: `def_column :name, globalize: true` +- もしくは `globalize :name` でも指定できます。 +- `@_globalized_name` に翻訳文が `{ en: "Storm Edge", ja: "ストームエッジ" }` のように入ります。 +- `id` / `enum` / `bitmask` / `sti` / `polymorphic_type` では利用できません。 +- `group_key` とは併用できません。 + +## カラムタイプ別一覧 + +### id (IdColumn) +**指定方法** +```ruby +def_column :id +``` +**挙動** +- 代入時に `to_i` で変換。 +- テスト用の更新時に `id_hash` を再構築するための処理が入ります。 + +### integer +**指定方法** +```ruby +def_column :lv, type: :integer +``` +**挙動** +- 代入時に nil 以外は `to_i` で変換されます(空文字は `nil` に)。 + +### float +**指定方法** +```ruby +def_column :attack, type: :float +``` +**挙動** +- 代入時に nil 以外は `to_f` で変換されます(空文字は `nil` に)。 + +### string +**指定方法** +```ruby +def_column :name, type: :string +``` +**挙動** +- 代入時に nil 以外は `to_s` で変換されます。 +- メモリ節約のために、オブジェクトはキャッシュされ、同じ値ならオブジェクトは流用されます。(object_cache) + +### symbol +**指定方法** +```ruby +def_column :kind, type: :symbol +``` +**挙動** +- 代入時に nil 以外は `to_s` + `to_sym` で変換されます。 +- SQL/CSV 用には文字列として出力されます。 + +### boolean +**指定方法** +```ruby +def_column :is_boss, type: :boolean +``` +**挙動** +- `Integer` は 0/1、`String` は "true" / "1" で判定。 +- `name?` のメソッドが追加されます。 +- SQL/CSV 出力時は 0/1 に変換されます。 + +### json +**指定方法** +```ruby +def_column :info, type: :json +``` +**オプション** +- `symbolize_names: true` を指定すると JSON 文字列をシンボルキーに変換します。 + +**挙動** +- 文字列の場合は `JSON.parse`。 +- SQL/CSV 出力時は `JSON.generate` で文字列化されます。 +- 注意点: 文字列以外の代入は、`symbolize_names` によるキー変換は行われません。 + +### time +**指定方法** +```ruby +def_column :start_at, type: :time +``` +**オプション** +- `db_type: :time` を指定すると時刻だけの形式 (`HH:MM:SS`) で出力します。 + +**挙動** +- 文字列を `Date._parse` で解析して `Time` に変換します。 +- 小数秒は切り捨てられます。 + +### enum +**指定方法** +```ruby +def_column :rarity, enum: { common: 0, rare: 1, epic: 2 } +# or +def_column :rarity +enum :rarity, { common: 0, rare: 1, epic: 2 } +``` +**オプション** +- `prefix`, `suffix`: 述語メソッドに prefix / suffix を付けられます。 + - `prefix: true` で `rarity_common?` のようになります。 + - `suffix: :rarity` で `common_rarity?` のようになります。 + +**挙動** +- 値は `Symbol` として扱われます。 +- `rarities` クラスメソッドと `rarity_before_type_cast` が追加されます。 +- 述語メソッド (例: `common?`) が自動生成されます。 + +### bitmask +**指定方法** +```ruby +def_column :flags, type: :integer +bitmask :flags, as: [:tradeable, :soulbound, :limited] +``` +**挙動** +- 配列/シンボル/整数を受け取り、内部では整数ビットに変換します。 +- `flags` はシンボル配列として返ります。 +- `flags_value` / `flags_value=` が追加されます。ビット列の数値が返ります。 + +### sti (STIタイプカラム) +**指定方法** +```ruby +def_column :type, sti: true +``` +**挙動** +- `type` を文字列に変換します。 +- Loader 側で `type` を見てクラス分岐する運用になります。 +- `sti_base_class` と `sti_column` が定義されます。 + +### polymorphic_type +**指定方法** +```ruby +def_column :reward_type, polymorphic_type: true +``` +**挙動** +- `belongs_to polymorphic` のタイプカラムとして使います。 +- `reward_type` を文字列として保持し、`reward_type_class` を自動で設定します。 +- 空文字は `nil` に変換されます。 + +## カラムのカスタム定義 +独自のカラム型を追加する場合は `SimpleMaster::Master::Column` を継承します。 +クラス名の末尾が `Column` であれば、自動で `type` が登録されます。 + +```ruby +class MoneyColumn < SimpleMaster::Master::Column + private + + def code_for_conversion + <<-RUBY + value = value&.to_i + RUBY + end + + def code_for_sql_value + <<-RUBY + #{name} + RUBY + end +end + +# 利用側 +class Product < ApplicationMaster + def_column :price, type: :money +end +``` + +- カスタムカラムのファイルはロード対象に含めてください。 +- `init` をオーバーライドすると、独自メソッドの生成も可能です。 +- 詳しくは [lib/simple_master/master/column.rb](lib/simple_master/master/column.rb) 定義ファイルを直接ご覧ください diff --git a/docs/simple_master_dataset_en.md b/docs/simple_master_dataset_en.md new file mode 100644 index 0000000..812cb19 --- /dev/null +++ b/docs/simple_master_dataset_en.md @@ -0,0 +1,122 @@ +# SimpleMaster Dataset / Table (English) + +> 日本語版: [simple_master_dataset_ja.md](simple_master_dataset_ja.md) + +## Overview +In SimpleMaster, the dataset holds the actual data, and each master class maps to a table. +The loader reads external data, and the table keeps records and caches. + +``` +Dataset + ├─ Table (Weapon) + ├─ Table (Armor) + └─ Table (Level) +``` + +## Dataset +### Role +- Load each `Table` via `loader` +- Keep `cache` for class/instance caches +- Provide diff overrides via `diff` + +### Basic usage +```ruby +loader = SimpleMaster::Loader::QueryLoader.new +dataset = SimpleMaster::Storage::Dataset.new(loader: loader) +dataset.load + +SimpleMaster.use_dataset(dataset) do + # work with this dataset +end +``` + +### Main API +- `load` : load all target tables and update caches +- `reload` : reload or unload depending on table class +- `unload` : clear tables and cache +- `duplicate(diff: nil)` : duplicate a dataset (diff included) +- `table(klass)` : fetch table for a class + +### diff +You can layer changes on top of loader data. +Set `dataset.diff` to a JSON/Hash and the diff is applied after load. +`Table.apply_diff` updates `id_hash` and overrides records. + +```ruby +dataset = SimpleMaster::Storage::Dataset.new + +dataset.diff = { + "weapons" => { + "1" => { "name" => "Updated Name" }, + "2" => nil + } +} + +dataset.load +``` + +### Dataset cache +Provides `cache_read` / `cache_fetch` / `cache_write` / `cache_delete`. +Use it for lightweight external caches. Data stays in memory, so mind the size. + +## Table +### Role +- Hold record array (`all`) +- Build `id_hash` / `grouped_hash` +- Update class/instance caches +- Keep STI sub tables + +### Main data +- `all` : array of records +- `id_hash` : `id` => record +- `grouped_hash` : `group_key` => grouped records +- `class_method_cache` : results of `cache_class_method` +- `method_cache` : results of `cache_method` + +### STI and sub tables +When a class uses STI, `sub_table` returns a table per subclass. +`update_sub_tables` extracts subclasses from `all` and registers them. + +## Table types +### Table (default) +- Loads all records when the dataset loads +- Builds `all` / `id_hash` / `grouped_hash` on load +- Records are frozen, so Copy-on-Write works well + +### OndemandTable +- Builds `all` / `id_hash` / `grouped_hash` on first access +- Useful for large data or on-demand access + +```ruby +dataset = SimpleMaster::Storage::Dataset.new( + table_class: SimpleMaster::Storage::OndemandTable +) +``` + +### TestTable +- Lightweight table for tests +- Assumes `update` / `record_updated` diffs + +```ruby +dataset = SimpleMaster::Storage::Dataset.new( + table_class: SimpleMaster::Storage::TestTable +) +``` + +## Loader +A loader implements `read_raw` and `build_records`. +Besides `QueryLoader` and `MarshalLoader`, you can define your own. + +```ruby +class JsonLoader < SimpleMaster::Loader + FIXTURE_DIR = Rails.root.join("fixtures/masters") + + def read_raw(table) + File.read(FIXTURE_DIR.join("#{table.klass.table_name}.json")) + end + + def build_records(klass, raw) + JSON.parse(raw).map { |attrs| klass.new(attrs) } + end +end +``` diff --git a/docs/simple_master_dataset_ja.md b/docs/simple_master_dataset_ja.md new file mode 100644 index 0000000..73e5f4e --- /dev/null +++ b/docs/simple_master_dataset_ja.md @@ -0,0 +1,122 @@ +# SimpleMaster Dataset / Table 仕様 (日本語) + +> English version: [simple_master_dataset_en.md](simple_master_dataset_en.md) + +## 全体説明 +SimpleMaster ではデータの実体を `Dataset` が持ち、各 Master クラスごとに `Table` が対応します。 +`Loader` が外部データを読み込み、`Table` がレコードと各種キャッシュを保持します。 + +``` +Dataset + ├─ Table (Weapon) + ├─ Table (Armor) + └─ Table (Level) +``` + +## Dataset +### 役割 +- `loader` を使って各 `Table` をロードする +- `cache` を保持し、クラス/インスタンスのキャッシュに利用する +- `diff` による差分上書きを提供する + +### 基本の使い方 +```ruby +loader = SimpleMaster::Loader::QueryLoader.new +dataset = SimpleMaster::Storage::Dataset.new(loader: loader) +dataset.load + +SimpleMaster.use_dataset(dataset) do + # dataset を使った処理 +end +``` + +### 主なAPI +- `load` : 全対象テーブルをロードし、キャッシュを更新 +- `reload` : `Table` の種類に応じて再ロード/アンロードを実行 +- `unload` : テーブルとキャッシュをクリア +- `duplicate(diff: nil)` : dataset を複製 (diff も継承) +- `table(klass)` : 対象クラスの `Table` を取得 + +### 差分 (diff) +Loader から取得するデータの上にさらに変更を自動的に加えられる仕組みです。 +`dataset.diff` に JSON/Hash を設定するとロード後に差分が適用されます。 +`Table.apply_diff` が `id_hash` を更新し、差分レコードを上書きします。 + +```ruby +dataset = SimpleMaster::Storage::Dataset.new + +dataset.diff = { + "weapons" => { + "1" => { "name" => "Updated Name" }, + "2" => nil + } +} + +dataset.load +``` + +### Dataset キャッシュ +`cache_read` / `cache_fetch` / `cache_write` / `cache_delete` を用意しています。 +外部参照用の軽量キャッシュに使えます。ただし、メモリに保存されるので、容量にご注意ください。 + +## Table +### 役割 +- 対象クラスのレコード配列 (`all`) を保持 +- `id_hash` / `grouped_hash` を構築 +- クラス/インスタンスキャッシュを更新 +- STI サブクラスのサブテーブルを保持 + +### 主なデータ +- `all` : レコードの配列 +- `id_hash` : `id` => record +- `grouped_hash` : `group_key` => grouped records +- `class_method_cache` : `cache_class_method` の結果 +- `method_cache` : `cache_method` の結果 + +### STI とサブテーブル +STI を使うクラスでは、`sub_table` がサブクラスごとの `Table` を返します。 +`update_sub_tables` が `all` からサブクラスを抽出して登録します。 + +## Table の種類 +### Table (デフォルト) +- `Dataset` 読み込み時に全件をロードする +- `load` のタイミングで `all` / `id_hash` / `grouped_hash` を構築 +- 基本的に中身は freeze されるので、Copy-on-Write が効きやすい + +### OndemandTable +- `all` / `id_hash` / `grouped_hash` を初回アクセス時に構築 +- 大規模データやオンデマンド参照で有効 + +```ruby +dataset = SimpleMaster::Storage::Dataset.new( + table_class: SimpleMaster::Storage::OndemandTable +) +``` + +### TestTable +- テスト向けの軽量テーブル +- `update` / `record_updated` による差分更新を前提とする + +```ruby +dataset = SimpleMaster::Storage::Dataset.new( + table_class: SimpleMaster::Storage::TestTable +) +``` + +## Loader +`Loader` は `read_raw` と `build_records` を実装して使います。 +既存の `QueryLoader` / `MarshalLoader` のほか、アプリケーションの要件に応じて Loader を作れます。 + +```ruby +class JsonLoader < SimpleMaster::Loader + FIXTURE_DIR = Rails.root.join("fixtures/masters") + + def read_raw(table) + File.read(FIXTURE_DIR.join("#{table.klass.table_name}.json")) + end + + def build_records(klass, raw) + JSON.parse(raw).map { |attrs| klass.new(attrs) } + end +end +``` diff --git a/docs/simple_master_guide_en.md b/docs/simple_master_guide_en.md new file mode 100644 index 0000000..b72e4b4 --- /dev/null +++ b/docs/simple_master_guide_en.md @@ -0,0 +1,142 @@ +# SimpleMaster Getting Started Guide + +> 日本語版: [simple_master_guide_ja.md](simple_master_guide_ja.md) + +## Purpose +- Handle master data fast without relying on Rails/ActiveRecord +- Reference records as Ruby objects and define associations and caches +- Use without a DB and switch datasets by use case + +## Installation +Add to Gemfile and bundle. + +```ruby +gem "simple_master" +``` + +```bash +bundle install +``` + +## Initialization +Initialize SimpleMaster at boot and load a dataset. + +```ruby +# config/initializers/simple_master.rb +Rails.application.config.after_initialize do + Rails.application.eager_load! + + SimpleMaster.init(for_test: Rails.env.test?) + + loader = SimpleMaster::Loader::QueryLoader.new + $current_dataset = SimpleMaster::Storage::Dataset.new(loader: loader) + $current_dataset.load +end +``` + +If you load JSON fixtures, use the `JsonLoader` described below. + +## Defining Master Classes +Create masters based on `ApplicationMaster`. + +```ruby +# app/models/application_master.rb +class ApplicationMaster < SimpleMaster::Master + self.abstract_class = true +end +``` + +```ruby +# app/models/weapon.rb +class Weapon < ApplicationMaster + def_column :id + def_column :type, sti: true + def_column :name + def_column :attack, type: :float + def_column :rarity, type: :integer + + enum :rarity, { common: 0, rare: 1, epic: 2 } + bitmask :flags, as: [:tradeable, :soulbound, :limited] + + validates :name, presence: true + validates :attack, numericality: { greater_than_or_equal_to: 0 } +end +``` + +## Data Loading (DB / Fixture) +### Load from DB +Use the default `QueryLoader` to load from DB tables. + +```ruby +loader = SimpleMaster::Loader::QueryLoader.new +$current_dataset = SimpleMaster::Storage::Dataset.new(loader: loader) +$current_dataset.load +``` + +### Build a Loader (JSON fixtures) +Example: implement a loader to read JSON. + +```ruby +class JsonLoader < SimpleMaster::Loader + FIXTURE_DIR = Rails.root.join("fixtures/masters") + + def read_raw(table) + File.read(FIXTURE_DIR.join("#{table.klass.table_name}.json")) + end + + def build_records(klass, raw) + JSON.parse(raw).map { |attrs| klass.new(attrs) } + end +end +``` + +If you use STI, add a branch that resolves the class from `type` +(see [dummy/lib/json_loader.rb](dummy/lib/json_loader.rb)). + +```ruby +loader = JsonLoader.new +$current_dataset = SimpleMaster::Storage::Dataset.new(loader: loader) +$current_dataset.load +``` + +## ActiveRecord Integration +Use the Extension to reference masters from ActiveRecord models. + +```ruby +# app/models/application_record.rb +class ApplicationRecord < ActiveRecord::Base + include SimpleMaster::ActiveRecord::Extension +end +``` + +```ruby +class Player < ApplicationRecord + belongs_to :level, foreign_key: :lv, primary_key: :lv + has_many :player_items +end +``` + +## Test Setup +Create a dataset per example to reset state. +In tests, `SimpleMaster::Master::Editable` and `TestTable` are useful. +Example with RSpec: + +```ruby +ApplicationMaster.prepend(SimpleMaster::Master::Editable) + +RSpec.configure do |config| + config.around do |example| + dataset = SimpleMaster::Storage::Dataset.new(table_class: SimpleMaster::Storage::TestTable) + SimpleMaster.use_dataset(dataset) { example.run } + end +end +``` + +## Useful Methods +- `SimpleMaster.use_dataset(dataset) { ... }` : temporarily switch dataset +- `cache_method` / `cache_class_method` : define fast caches +- `enum` / `bitmask` / `globalize` : column extensions + +## Notes +- Do not use `SimpleMaster::Master::Editable` in production; it is for tests +- Swap the `Loader` based on your data source diff --git a/docs/simple_master_guide_ja.md b/docs/simple_master_guide_ja.md new file mode 100644 index 0000000..6541cc3 --- /dev/null +++ b/docs/simple_master_guide_ja.md @@ -0,0 +1,141 @@ +# SimpleMaster 導入ガイド + +> English version: [simple_master_guide_en.md](simple_master_guide_en.md) + +## 目的 +- マスターデータを Rails/ActiveRecord とは別に高速に扱う +- Ruby オブジェクトとして参照でき、関連づけやキャッシュを定義できる +- DB がなくても扱え、用途に応じて複数の dataset を切り替えられる + +## インストール +Gemfile に追加して bundle します。 + +```ruby +gem "simple_master" +``` + +```bash +bundle install +``` + +## 初期化 +アプリ起動時に SimpleMaster を初期化し、データセットを読み込ませます。 + +```ruby +# config/initializers/simple_master.rb +Rails.application.config.after_initialize do + Rails.application.eager_load! + + SimpleMaster.init(for_test: Rails.env.test?) + + loader = SimpleMaster::Loader::QueryLoader.new + $current_dataset = SimpleMaster::Storage::Dataset.new(loader: loader) + $current_dataset.load +end +``` + +※ JSON fixture を読み込む場合は後述の `JsonLoader` を使います。 + +## Master クラス定義 +`ApplicationMaster` をベースに Master を作ります。 + +```ruby +# app/models/application_master.rb +class ApplicationMaster < SimpleMaster::Master + self.abstract_class = true +end +``` + +```ruby +# app/models/weapon.rb +class Weapon < ApplicationMaster + def_column :id + def_column :type, sti: true + def_column :name + def_column :attack, type: :float + def_column :rarity, type: :integer + + enum :rarity, { common: 0, rare: 1, epic: 2 } + bitmask :flags, as: [:tradeable, :soulbound, :limited] + + validates :name, presence: true + validates :attack, numericality: { greater_than_or_equal_to: 0 } +end +``` + +## データロード (DB / Fixture) +### DB から読み込む +標準の `QueryLoader` で DB のテーブルから読み込みます。 + +```ruby +loader = SimpleMaster::Loader::QueryLoader.new +$current_dataset = SimpleMaster::Storage::Dataset.new(loader: loader) +$current_dataset.load +``` + +### Loader を自作する +例:独自ローダを用意して JSON を読み込みます。 + +```ruby +class JsonLoader < SimpleMaster::Loader + FIXTURE_DIR = Rails.root.join("fixtures/masters") + + def read_raw(table) + File.read(FIXTURE_DIR.join("#{table.klass.table_name}.json")) + end + + def build_records(klass, raw) + JSON.parse(raw).map { |attrs| klass.new(attrs) } + end +end +``` + +※ STI を使う場合は `type` を見てクラス分岐する実装を追加してください(例: [dummy/lib/json_loader.rb](dummy/lib/json_loader.rb))。 + +```ruby +loader = JsonLoader.new +$current_dataset = SimpleMaster::Storage::Dataset.new(loader: loader) +$current_dataset.load +``` + +## ActiveRecord 連携 +ActiveRecord のモデルから Master を参照する場合は Extension を使います。 + +```ruby +# app/models/application_record.rb +class ApplicationRecord < ActiveRecord::Base + include SimpleMaster::ActiveRecord::Extension +end +``` + +```ruby +class Player < ApplicationRecord + belongs_to :level, foreign_key: :lv, primary_key: :lv + has_many :player_items +end +``` + +## テスト用設定 +テストケースごとにリセットするため、Dataset を都度作るように設定します。 +またテストでは通常と違い、データの一時保存や関連付け保存等が必要なため、`SimpleMaster::Master::Editable` と `TestTable` 利用するとよりスムーズとなります。 +例えば、RSpec ではこのように設定します。 + +```ruby +ApplicationMaster.prepend(SimpleMaster::Master::Editable) + +RSpec.configure do |config| + config.around do |example| + dataset = SimpleMaster::Storage::Dataset.new(table_class: SimpleMaster::Storage::TestTable) + SimpleMaster.use_dataset(dataset) { example.run } + end +end +``` + +## 便利メソッド +- `SimpleMaster.use_dataset(dataset) { ... }` : 一時的に dataset を差し替える +- `cache_method` / `cache_class_method` : 高速なキャッシュを定義 +- `enum` / `bitmask` / `globalize` : カラム拡張 + +## 補足 +- 本番では `SimpleMaster::Master::Editable` は使わず、テスト用途のみ推奨 +- データソースに合わせて `Loader` を差し替える運用を想定