{"id":"jaeymo/classy","name":"classy","scope":"jaeymo","platform":"roblox","description":"A Classy is useful because writing compositional code for instances becomes much easier!","version":"2.2.0","latest":"2.2.0","versions":["1.0.0","1.0.1","1.1.0","1.2.0","1.3.0","1.4.0","1.5.0","1.6.0","1.7.0","1.7.1","1.7.2","1.7.3","1.7.4","1.7.5","1.7.6","1.7.7","1.7.8","1.7.9","2.0.0","2.1.0","2.1.1","2.1.2","2.1.3","2.1.4","2.1.5","2.2.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{"howmanysmall/janitor":{"version":"^1.18.3","alias":"Janitor"},"sleitnick/signal":{"version":"^2.0.3","alias":"Signal"}},"integrity":"d924bd8c540205b86a6f3d808d93149491f4798caf40f81ecec4847f38aa8a73","likes":0,"downloads":1,"install":"forest install jaeymo/classy","url":"https://forest.dev/p/roblox/jaeymo/classy","files":"https://api.forest.dev/ai/package/roblox/jaeymo/classy/files","readme":"<div align=\"center\">\r\n\r\n<img src=\"assets/Classy.png\" alt=\"Classy\" width=\"420\">\r\n\r\n### A lifecycle manager for tagged instances in Luau\r\n\r\nMake composition **and** OOP architecture feel good in Roblox.\r\n\r\n<p>\r\n  <a href=\"https://wally.run/package/jaeymo/classy\"><img alt=\"Wally\" src=\"https://img.shields.io/badge/wally-jaeymo%2Fclassy-orange?style=for-the-badge&logo=roblox&logoColor=white\"></a>\r\n  <a href=\"https://opensource.org/licenses/MIT\"><img alt=\"License: MIT\" src=\"https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge\"></a>\r\n  <a href=\"https://luau.org/\"><img alt=\"Luau\" src=\"https://img.shields.io/badge/Luau-strict-00A2FF?style=for-the-badge\"></a>\r\n</p>\r\n\r\n<p>\r\n  <img alt=\"Version\" src=\"https://img.shields.io/badge/version-2.2.0-blue?style=flat-square\">\r\n  <a href=\"https://github.com/jaeymo/classy/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaeymo/classy?style=flat-square&color=gold\"></a>\r\n  <a href=\"https://github.com/jaeymo/classy/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaeymo/classy?style=flat-square\"></a>\r\n  <a href=\"https://github.com/jaeymo/classy/commits/main\"><img alt=\"Last commit\" src=\"https://img.shields.io/github/last-commit/jaeymo/classy?style=flat-square\"></a>\r\n  <img alt=\"PRs welcome\" src=\"https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square\">\r\n</p>\r\n\r\n[**Quick Start**](#-quick-start) · [**Examples**](#-examples) · [**Settings**](#%EF%B8%8F-settings) · [**Lifecycle**](#-lifecycle) · [**API**](#-api) · [**Contributing**](#-contributing)\r\n\r\n</div>\r\n\r\n---\r\n\r\n> [!IMPORTANT]\r\n> Classy is actively being worked on and features are constantly being added!\r\n\r\n## ✨ What Is Classy?\r\n\r\n`Classy` is a lifecycle manager built on top of `CollectionService` that makes both **composition** and **OOP architecture** more appealing in [Luau](https://luau.org/).\r\n\r\nIt is the cleaner, strictly-typed, smarter iteration of the original package, [Wrapper](https://github.com/jaeymo/wrapper), which is now archived. If you're using Wrapper, switch to `Classy`.\r\n\r\n## 🚀 Why Use Classy?\r\n\r\n| | Feature | What you get |\r\n|---|---|---|\r\n| 🧩 | **Two construction styles** | Track instances with plain functions *or* full classes |\r\n| 🧹 | **Automatic cleanup** | Handles the class object's metatable and the provided [Janitor](https://github.com/howmanysmall/Janitor) |\r\n| 🔒 | **Full type safety** | Generics are preserved when accessing applied objects |\r\n| 🏛️ | **OOP-friendly** | Write classes for game objects, then track them with tags |\r\n| 🧱 | **Composition-friendly** | Register and fetch attached components (ClassyObjects) from an instance |\r\n| ⚡ | **Tiny API** | A whole system can fit in a few lines |\r\n\r\n## 👀 A Taste\r\n\r\nIsn't this great? A quick shorthand:\r\n\r\n```lua\r\n-- All parts with the \"KillPart\" tag will now kill anything with a humanoid!\r\nClassy.new(\"KillPart\", function(instance: BasePart, janitor: Classy.Janitor)\r\n\tjanitor:Add(instance.Touched:Connect(function(hit: BasePart)\r\n\t\tlocal humanoid = hit.Parent and hit.Parent:FindFirstChildWhichIsA(\"Humanoid\")\r\n\t\tif not humanoid then\r\n\t\t\treturn\r\n\t\tend\r\n\r\n\t\thumanoid.Health = 0\r\n\tend))\r\nend, {\r\n\tClassNames = { \"BasePart\" },\r\n\tAncestors = { workspace },\r\n}):Init()\r\n```\r\n\r\n## 📦 Quick Start\r\n\r\n### Install\r\n\r\n<details open>\r\n<summary><b>Wally</b> (recommended)</summary>\r\n\r\nAdd Classy to your `wally.toml`:\r\n\r\n```toml\r\n[dependencies]\r\nClassy = \"jaeymo/classy@2.2.0\"\r\n```\r\n\r\nThen run:\r\n\r\n```bash\r\nwally install\r\n```\r\n\r\nOr grab it from the [Wally package page](https://wally.run/package/jaeymo/classy?version=2.2.0).\r\n\r\n</details>\r\n\r\n### Dependencies\r\n\r\n| Package | Purpose |\r\n|---|---|\r\n| [Janitor](https://github.com/howmanysmall/Janitor) | Cleanup of connections and objects |\r\n| [Signal](https://github.com/Sleitnick/RbxUtil/blob/main/modules/signal/init.luau) | Events like `InstanceAdded` |\r\n\r\n## 🧪 Examples\r\n\r\nClassy's best feature is that you can use **function-based** or **class-based** construction, and both build the same underlying system. Here's the class used in the examples below:\r\n\r\n<details>\r\n<summary><b>📄 KillPartClass (click to expand)</b></summary>\r\n\r\n```lua\r\n--!strict\r\n\r\nlocal Classy = require(path.to.classy)\r\n\r\nlocal KillPartClass = {}\r\nKillPartClass.__index = KillPartClass\r\n\r\nexport type KillPart = typeof(setmetatable({} :: {\r\n\tPart: BasePart,\r\n\tJanitor: Classy.Janitor,\r\n}, KillPartClass))\r\n\r\nfunction KillPartClass.new(part: Instance, janitor: Classy.Janitor): KillPart\r\n\treturn setmetatable({\r\n\t\tPart = part :: BasePart,\r\n\t\tJanitor = janitor,\r\n\t}, KillPartClass)\r\nend\r\n\r\nfunction KillPartClass.Init(self: KillPart)\r\n\tself.Janitor:Add(self.Part.Touched:Connect(function(hit: BasePart)\r\n\t\tlocal humanoid = hit.Parent and hit.Parent:FindFirstChildWhichIsA(\"Humanoid\")\r\n\r\n\t\tif not humanoid then\r\n\t\t\treturn\r\n\t\tend\r\n\r\n\t\thumanoid.Health = 0\r\n\tend))\r\nend\r\n\r\nfunction KillPartClass.DoSomething(self: KillPart)\r\n\tprint(self)\r\nend\r\n\r\nfunction KillPartClass.Destroy(self: KillPart)\r\n\tprint(\"This object has been destroyed!\")\r\nend\r\n```\r\n\r\n</details>\r\n\r\n### 🏛️ Class-based construction\r\n\r\n```lua\r\nlocal KillPartClassy = Classy.newClass(\"KillPart\", KillPartClass, {\r\n\tClassNames = { \"BasePart\" },\r\n\tAncestors = { workspace },\r\n\tLogging = true,\r\n})\r\n```\r\n\r\n### 🧩 Function-based construction\r\n\r\n```lua\r\nlocal AnotherExample = Classy.new(\"KillPart\", function(instance: Instance, janitor: Classy.Janitor)\r\n\treturn KillPartClass.new(instance, janitor)\r\nend, {\r\n\tClassNames = { \"BasePart\" },\r\n\tAncestors = { workspace },\r\n\tLogging = true,\r\n})\r\n```\r\n\r\n### ▶️ Initialize\r\n\r\n```lua\r\nKillPartClassy:Init()\r\nAnotherExample:Init()\r\n```\r\n\r\n### 🔌 Interact with applied objects\r\n\r\nOnce initialized, you can interact with applied objects directly. `ObserveApplied` covers both instances that are already applied and ones added later:\r\n\r\n```lua\r\nKillPartClassy:ObserveApplied(function(instance, applied)\r\n\tapplied:GetData():DoSomething()\r\nend)\r\n```\r\n\r\nPrefer a raw signal? `KillPartClassy.InstanceAdded` fires only for instances applied from now on, and `InstanceRevoked` fires when they are removed.\r\n\r\n> [!TIP]\r\n> Building something simple that doesn't need a class? Use the shorthand from [A Taste](#-a-taste). It's the same system with less boilerplate.\r\n\r\n## ⚙️ Settings\r\n\r\nBoth `Classy.new` and `Classy.newClass` accept an optional `Config` table (`ClassyConfig`) as their last argument. Every option is optional:\r\n\r\n| Option | Type | Default | Description |\r\n|---|---|---|---|\r\n| `ClassNames` | `{ string }` | `{}` | Instances must pass `IsA` for **at least one** listed class name. Empty means no restriction |\r\n| `Ancestors` | `{ Instance }` | `{}` | Instances must be a descendant of **at least one** listed ancestor. Empty means no restriction |\r\n| `Predicate` | `(Instance) -> boolean` | `nil` | Custom check. Return `false` to skip an instance |\r\n| `Logging` | `boolean` | `false` | Prints when instances are applied, revoked, and cleaned up |\r\n| `RemoveTagOnCleanup` | `boolean` | `true` | Removes the tag from the instance when it is revoked (including when the Classy is destroyed) |\r\n| `Nuke` | `boolean` | `true` | When an applied object is destroyed, also clears the table your constructor returned and removes its metatable |\r\n| `NamingConvention` | `{ Init: string, Destroy: string }` | `{ Init = \"Init\", Destroy = \"Destroy\" }` | Method names Classy calls on your object for its lifecycle |\r\n| `WaitForMetadata` | `boolean` | `false` | Waits up to 7 seconds for an instance's [metadata attribute](#metadata) before applying it |\r\n| `Context` | `{ any }` | `{}` | Table handed to your constructor as its third argument |\r\n| `AutoInit` | `boolean` | `true` | Calls your object's `Init` method automatically after it is registered. When `false`, call it yourself. `Initialized` is then `true` as soon as the object is registered |\r\n\r\n```lua\r\nClassy.newClass(\"Door\", DoorClass, {\r\n\tClassNames = { \"Model\" },\r\n\tAncestors = { workspace.Doors },\r\n\tPredicate = function(instance)\r\n\t\treturn instance:GetAttribute(\"Enabled\") ~= false\r\n\tend,\r\n\tNamingConvention = { Init = \"Start\", Destroy = \"Cleanup\" },\r\n\tWaitForMetadata = true,\r\n\tLogging = true,\r\n}):Init()\r\n```\r\n\r\n> [!NOTE]\r\n> **Lifecycle methods are automatic.** After your object is constructed and registered, Classy calls its `Init` method (if it has one), and calls `Destroy` when the instance is revoked. Rename them with `NamingConvention`. Because the object is registered first, `Init` can find its own object with `getComponent` or `GetApplied`. See [Lifecycle](#-lifecycle) for the full order and what happens on failure.\r\n\r\n> [!NOTE]\r\n> **When checks run.** `ClassNames`, `Ancestors`, and `Predicate` are checked when an instance gets the tag (or joins the game with it) and again whenever an applied instance's ancestry changes. An instance that stops passing is revoked, and it is **not** re-applied automatically if it passes again later. Re-tag it or call `:Apply` yourself.\r\n\r\n## 🔄 Lifecycle\r\n\r\nApplying an instance (from its tag, `:Apply`, or `applyComponents`) runs these steps in order:\r\n\r\n1. **Construct**: your function, or your class's `new`, runs.\r\n2. **Register**: the object becomes available through `GetApplied`, `GetAll`, and `getComponent`.\r\n3. **Init**: your `Init` method runs (skipped if `AutoInit = false` or the method doesn't exist).\r\n4. **Initialized**: `Initialized` becomes `true` and `InstanceAdded` fires.\r\n\r\nYour constructor and `Init` are allowed to yield. Classy handles the cases that creates:\r\n\r\n| Situation | What happens |\r\n|---|---|\r\n| `:Apply` called again while the constructor is running | The second call waits and returns the same object |\r\n| `:Apply` called again while `Init` is running | Returns the existing object. Check `.Initialized` if you need it finished |\r\n| Revoked while the constructor is running | The apply is cancelled and the finished object is destroyed. `:Apply` returns `nil` |\r\n| Revoked while `Init` is running | Revoked normally. `:Apply` returns `nil` |\r\n| Constructor errors | Classy cleans up and rethrows the error |\r\n| `Init` errors | The object is revoked (your `Destroy` runs, the tag is removed) and the error is rethrown with a traceback |\r\n\r\n`ObserveApplied` only hands you initialized objects. Ones still in `Init` arrive through `InstanceAdded` when they finish.\r\n\r\n> [!NOTE]\r\n> If the instance is revoked while `Init` is running and `Init` then errors, the error is not rethrown and `:Apply` returns `nil`. The error is only logged as a warning when `Logging` is on.\r\n\r\n> [!TIP]\r\n> Since `Destroy` also runs after a failed `Init`, write it so it tolerates a half-built object.\r\n\r\n## 📚 API\r\n\r\n### Constructors\r\n\r\n| Function | Returns | Description |\r\n|---|---|---|\r\n| `Classy.new(Tag, Constructable, Config?)` | `Classy<T>` | Function-based construction |\r\n| `Classy.newClass(Tag, Constructable, Config?)` | `Classy<T>` | Class-based construction |\r\n\r\nYour function (or your class's `new`) is called with the same four arguments:\r\n\r\n| # | Argument | Description |\r\n|---|---|---|\r\n| 1 | `Instance` | The tagged instance |\r\n| 2 | `Janitor` | A Janitor that is cleaned up when the instance is revoked |\r\n| 3 | `Context` | The `Context` table from your config |\r\n| 4 | `Metadata?` | The metadata the instance was applied with, if any |\r\n\r\n```lua\r\nClassy.new(\"Door\", function(instance, janitor, context, metadata)\r\n\t-- ...\r\nend)\r\n\r\nfunction DoorClass.new(instance, janitor, context, metadata)\r\n\t-- ...\r\nend\r\n```\r\n\r\n### Working with components\r\n\r\nEvery applied object is registered on its instance, so you can fetch and drive an instance's components from anywhere.\r\n\r\n| Function | Returns | Description |\r\n|---|---|---|\r\n| `Classy.getComponent(Inst, Tag)` | `Applied<any>?` | Get the component applied to `Inst` under `Tag` |\r\n| `Classy.getComponents(Inst)` | `{ [string]: Applied<any> }?` | Get every component applied to `Inst`, keyed by tag |\r\n| `Classy.callOnAllComponents(Inst, MethodName, ...)` | | Calls `MethodName` (with `...`) on every component of `Inst` that has that method |\r\n| `Classy.applyComponents(Inst, Components)` | | Applies each listed component to `Inst` (and tags it), passing its metadata. Each tag must be [registered globally](#global-registry) |\r\n| `Classy.emit(Inst, TriggerName, Context?)` | | Fires a trigger; components that map `TriggerName` to a method get that method called with `Context` |\r\n\r\n```lua\r\n-- Grab a specific component and use it\r\nlocal killPart = Classy.getComponent(workspace.Lava, \"KillPart\")\r\nif killPart then\r\n\tkillPart:GetData():DoSomething()\r\nend\r\n\r\n-- Call a method on every component the instance has\r\nClassy.callOnAllComponents(workspace.Lava, \"DoSomething\")\r\n\r\n-- Attach components at runtime, with per-component metadata.\r\n-- The tag must be registered globally first!\r\nClassy.registerGlobal(\"KillPart\", KillPartClassy)\r\n\r\nClassy.applyComponents(workspace.Lava, {\r\n\tKillPart = { Damage = 25 },\r\n})\r\n\r\n-- Fire a trigger\r\nClassy.emit(workspace.Lava, \"Activated\", { Source = \"Lever\" })\r\n```\r\n\r\n> [!NOTE]\r\n> `getComponent` and `GetApplied` can return an object whose `Init` is still running. Check `.Initialized` if you need it finished.\r\n\r\n#### Triggers\r\n\r\nA component can declare triggers in its metadata under `Triggers`, mapping a trigger name to one of its methods. `Classy.emit` then calls that method with the context you pass:\r\n\r\n```lua\r\nClassy.registerGlobal(\"Door\", DoorClassy)\r\n\r\nClassy.applyComponents(door, {\r\n\tDoor = {\r\n\t\tTriggers = { Activated = \"Open\" },\r\n\t},\r\n})\r\n\r\n-- Calls door's Door component: Data:Open({ Source = \"Lever\" })\r\nClassy.emit(door, \"Activated\", { Source = \"Lever\" })\r\n```\r\n\r\n#### Component aliases\r\n\r\nMetadata can include `UseComponent` (or `useComponent`) naming another globally registered component. Classy builds a copy of that component's Classy for the new tag, so one constructor can power many differently-configured components:\r\n\r\n```lua\r\nClassy.registerGlobal(\"Inventory\", InventoryClassy)\r\nClassy.registerGlobal(\"Resource\", ResourceClassy)\r\n\r\nClassy.applyComponents(player, {\r\n\tInventory = { MaxItemAmount = 20 },\r\n\r\n\t-- Built from the Resource component instead of an \"Ammo\" Classy\r\n\tAmmo = {\r\n\t\tUseComponent = \"Resource\",\r\n\t\tStartingAmount = 0,\r\n\t\tMaxCount = 0,\r\n\t},\r\n})\r\n```\r\n\r\nThe alias reuses the base's constructor and config (`ClassNames`, `AutoInit`, `Ancestors`, `Predicate`, `NamingConvention`, `Logging`, `RemoveTagOnCleanup`, `Nuke`, `WaitForMetadata`) and gets its own tag. Its `Context` is a copy of the base's with `tag` set to the alias tag. If the base isn't registered, Classy warns and skips the component.\r\n\r\n#### Metadata\r\n\r\nMetadata passed to `Apply` or `applyComponents` is handed to your constructor as its fourth argument and saved on the instance as a JSON attribute named `_<lowercase tag>_metadata` (for example `_door_metadata`), so it must only contain JSON-serializable values (no functions or Instances). With `WaitForMetadata = true`, Classy waits up to 7 seconds for that attribute to exist before applying, which lets you tag an instance first and set its metadata attribute (as a JSON string) afterwards.\r\n\r\n### Global registry\r\n\r\n| Function | Returns | Description |\r\n|---|---|---|\r\n| `Classy.registerGlobal(Tag, ClassyInstance)` | | Register a Classy object globally under `Tag`. Required for `applyComponents` and `UseComponent` |\r\n| `Classy.getGloballyRegistered(Tag)` | `Classy<any>?` | Look up a globally registered Classy object |\r\n\r\n### Classy objects\r\n\r\nWhat `Classy.new` and `Classy.newClass` return.\r\n\r\n| Member | Returns | Description |\r\n|---|---|---|\r\n| `:Init()` | | Applies all currently tagged instances that pass the checks and starts listening for new and removed ones. Call it once |\r\n| `:CanBeApplied(Inst)` | `boolean` | Whether `Inst` passes `ClassNames`, `Ancestors`, and `Predicate` |\r\n| `:Apply(Inst, Metadata?)` | `Applied<T>?` | Applies `Inst`, bypassing `CanBeApplied`. Returns the existing one if already applied, and waits if another call is mid-construction. Returns `nil` if the instance is rev","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/jaeymo/roblox/classy/2.2.0/readme"}