|
1 | 1 | # ClassPlus |
2 | 2 |
|
| 3 | +A Motoko library designed to reduce boilerplate when instantiating and managing class-like objects within actor classes. ClassPlus enables developers to create modular, upgrade-friendly classes that leverage stable variables for persistence across upgrades. |
3 | 4 |
|
4 | | -A simple library to reduce boilerplate when instantiating ClassPlus Objects in Motoko. |
| 5 | +--- |
5 | 6 |
|
6 | | -# Requirements |
| 7 | +## Requirements |
7 | 8 |
|
8 | | -At least DFX 0.24.0 |
| 9 | +- **DFX Version**: Requires DFX 0.24.0 or later. |
9 | 10 |
|
| 11 | +--- |
10 | 12 |
|
11 | | -# Usage |
| 13 | +## Installation |
12 | 14 |
|
13 | | -ClassPlus classes should be used from actor classes. They simplify boilerplate for constructing class objects that *virtually* survive upgrades. Upon each upgrade they are reconstituted from stable variables in the canister. |
| 15 | +`mops add class-plus` |
14 | 16 |
|
15 | | -ClassPlus objects need to be created with the signature: |
| 17 | +## Overview |
16 | 18 |
|
17 | | -`public class AClass(stored: ?State, caller: Principal, canister: Principal, _environment: ?Environment)` |
| 19 | +ClassPlus simplifies the process of defining and managing objects in actor classes by: |
18 | 20 |
|
19 | | -The State and Environment Objects can very and are handled with generic typing so you can define those to be what every you need. Typically you'll want to use a migration pattern for your state structure and it needs to be made up of stable compatible objects. |
| 21 | +1. **Reducing Boilerplate**: It minimizes repetitive code for constructing and maintaining objects. |
| 22 | +2. **Supporting Upgrades**: Ensures objects can be reconstituted from stable variables after an upgrade. |
| 23 | +3. **Encapsulating Complexity**: Provides a unified interface for initialization, state management, and environment configuration. |
20 | 24 |
|
21 | | -In your class will need to define: |
| 25 | +ClassPlus objects are instantiated with a predefined structure and integrate seamlessly into actor classes. |
22 | 26 |
|
23 | | -- `public type State = {}` - What is the shape of your state. These all need to be stable compatible variables. |
24 | | -- `public let initialState = {}` - What are the defaults? You can update these in the initialization step if they are not ready. |
25 | | -- `public type Environment = {}` - Your class can support environment style variables unique to your class instantiation. |
| 27 | +--- |
26 | 28 |
|
| 29 | +## Usage |
27 | 30 |
|
28 | | -Instantiating your class in your actor is accomplished as follows: |
| 31 | +### Core Concepts |
29 | 32 |
|
30 | | -``` |
| 33 | +1. **State**: The shape of the class's state, stored in stable variables, must be composed of stable-compatible types. |
| 34 | +2. **Environment**: Optional environment variables passed to the class for contextual operations. |
| 35 | +3. **Initialization**: Initialization logic, including setup and configuration, can be provided during class creation. |
| 36 | + |
| 37 | +### Class Definition |
| 38 | + |
| 39 | +To define a class compatible with ClassPlus, follow this structure: |
| 40 | + |
| 41 | +#### Example Class Definition |
| 42 | + |
| 43 | +```motoko |
| 44 | +public class AClass(stored: ?State, caller: Principal, canister: Principal, args: ?InitArgs, _environment: ?Environment, onStateChange: (State) -> ()) { |
| 45 | + // Define the initial state. |
| 46 | + public let state = switch(stored) { |
| 47 | + case (?val) val; |
| 48 | + case (null) initialState(); |
| 49 | + }; |
| 50 | +
|
| 51 | + // Notify about state changes. |
| 52 | + onStateChange(state); |
31 | 53 |
|
32 | | - let initManager = ClassPlus.ClassPlusInitializationManager(); |
33 | | -
|
34 | | - stable let aClass_state : AClass.State = AClass.initialState; |
35 | | -
|
36 | | - let aClass = ClassPlus.ClassPlus<system, |
37 | | - AClass.AClass, |
38 | | - AClass.State, |
39 | | - AClass.Environment>( |
40 | | - _owner, //typically the msg.caller from your canister creation |
41 | | - actor(Principal.toText(Principal.fromActor(this))), //important - helps capture the current canister you are running on. |
42 | | - aClass_state, |
43 | | - AClass.AClass, |
44 | | - initManager, |
45 | | - // set up any environment settings here |
46 | | - ?(func() : AClass.Environment { |
47 | | - { |
48 | | - //you can set up any post install references here. If you need references to other Class Plus items here you can reference them here as long as they are initialized before hand. Order is important. |
49 | | - thisActor = actor(Principal.toText(Principal.fromActor(this))); |
| 54 | + // Capture environment settings. |
| 55 | + let environment: Environment = switch(_environment) { |
| 56 | + case (?val) val; |
| 57 | + case (null) D.trap("No Environment Set"); |
| 58 | + }; |
| 59 | +
|
| 60 | + // Apply initial arguments, if provided. |
| 61 | + switch (args) { |
| 62 | + case (?val) { |
| 63 | + if (state.message == "Uninitialized") { |
| 64 | + state.message := val.messageModifier; |
| 65 | + } |
50 | 66 | }; |
51 | | - }), |
52 | | - //any initialization code |
53 | | - ?(func () : async* () { |
54 | | - D.print("Initializing AClass"); |
55 | | - //do any work here necessary for initialization |
56 | | - }) |
57 | | - ).get; |
58 | | -
|
59 | | - public shared func getMessage() : async Text { |
60 | | - //this is how you use your class |
61 | | - aClass().message(); |
62 | | - } |
63 | | -``` |
| 67 | + case (null) {}; |
| 68 | + }; |
| 69 | +
|
| 70 | + // Define class methods. |
| 71 | + public func message(): Text { |
| 72 | + state.message # " from canister " # Principal.toText(canister) # " created by " # Principal.toText(caller); |
| 73 | + }; |
| 74 | +
|
| 75 | + public func setMessage(x: Text): () { |
| 76 | + state.message := x; |
| 77 | + }; |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +#### Required Definitions |
| 82 | + |
| 83 | +1. **`State`**: Define the structure of the class's state. |
| 84 | + |
| 85 | + ```motoko |
| 86 | + public type State = { |
| 87 | + var message: Text; |
| 88 | + }; |
| 89 | + ``` |
| 90 | + |
| 91 | +2. **`Environment`**: Define any environment variables (optional). |
| 92 | + |
| 93 | + ```motoko |
| 94 | + public type Environment = { |
| 95 | + thisActor: actor { |
| 96 | + auto_init: () -> async (); |
| 97 | + }; |
| 98 | + }; |
| 99 | + ``` |
| 100 | + |
| 101 | +3. **`initialState`**: Define default state values. |
| 102 | + |
| 103 | + ```motoko |
| 104 | + public func initialState(): State = { |
| 105 | + var message = "Uninitialized"; |
| 106 | + }; |
| 107 | + ``` |
| 108 | + |
| 109 | +4. **`InitArgs`**: Define any arguments required for initialization (optional). |
| 110 | + |
| 111 | + ```motoko |
| 112 | + public type InitArgs = { |
| 113 | + messageModifier: Text; |
| 114 | + }; |
| 115 | + ``` |
| 116 | + |
| 117 | +### Instantiating the Class in an Actor |
| 118 | + |
| 119 | +Use the `ClassPlus` library to simplify instantiation and initialization within an actor. |
| 120 | + |
| 121 | +#### Example Actor Definition |
| 122 | + |
| 123 | +```motoko |
| 124 | +import AClassLib "aclass"; |
| 125 | +import ClassPlus "../"; |
| 126 | +
|
| 127 | +shared ({ caller = _owner }) actor class Token () = this { |
| 128 | + type AClass = AClassLib.AClass; |
| 129 | + type State = AClassLib.State; |
| 130 | + type InitArgs = AClassLib.InitArgs; |
| 131 | + type Environment = AClassLib.Environment; |
| 132 | +
|
| 133 | + let initManager = ClassPlus.ClassPlusInitializationManager(_owner, Principal.fromActor(this), true); |
| 134 | +
|
| 135 | + stable var aClass_state: State = AClassLib.initialState(); |
| 136 | +
|
| 137 | + let aClass = AClassLib.Init<system>({ |
| 138 | + manager = initManager; |
| 139 | + initialState = aClass_state; |
| 140 | + args = ?({ messageModifier = "Hello World" }); |
| 141 | + pullEnvironment = ?(func() : Environment { |
| 142 | + { |
| 143 | + thisActor = actor(Principal.toText(Principal.fromActor(this))); |
| 144 | + }; |
| 145 | + }); |
| 146 | + onInitialize = ?(func(newClass: AClassLib.AClass): async* () { |
| 147 | + D.print("Initializing AClass"); |
| 148 | + }); |
| 149 | + onStorageChange = func(new_state: State) { |
| 150 | + aClass_state := new_state; |
| 151 | + } |
| 152 | + }); |
| 153 | +
|
| 154 | + public shared func getMessage(): async Text { |
| 155 | + aClass().message(); |
| 156 | + }; |
| 157 | +
|
| 158 | + public shared func SetMessage(x: Text): async () { |
| 159 | + aClass().setMessage(x); |
| 160 | + }; |
| 161 | +
|
| 162 | + private shared func initStuff(): async* (){ |
| 163 | + //add init logic here |
| 164 | + } |
| 165 | +
|
| 166 | + initManager.calls.add(initStuff); |
| 167 | +}; |
| 168 | +``` |
| 169 | + |
| 170 | +--- |
| 171 | + |
| 172 | +## ClassPlus Library API |
| 173 | + |
| 174 | +### **Modules and Classes** |
| 175 | + |
| 176 | +#### **`ClassPlusInitializationManager`** |
| 177 | + |
| 178 | +Handles initialization and tracking of ClassPlus objects. |
| 179 | + |
| 180 | +- **Constructor**: `ClassPlusInitializationManager(_owner: Principal, _canister: Principal, autoTimer: Bool)` |
| 181 | + |
| 182 | + - `_owner`: The principal of the actor owner. |
| 183 | + - `_canister`: The principal of the canister where the object resides. |
| 184 | + - `autoTimer`: Automatically initialize objects on a timer. |
| 185 | + |
| 186 | +- **Methods**: |
| 187 | + |
| 188 | + - `initialize(): async* ()` |
| 189 | + - Executes initialization logic for all registered classes. |
| 190 | + |
| 191 | +- Members |
| 192 | + |
| 193 | + - calls: Buffer.Buffer(() ->async\*() |
| 194 | + - queue up functions to call during initialization by adding them to the calls buffer. They will be executed in the order you add them. |
| 195 | + |
| 196 | +#### **`ClassPlus`** |
| 197 | + |
| 198 | +Encapsulates logic for creating and managing a class instance. |
| 199 | + |
| 200 | +- **Constructor**: `ClassPlus<system, T, S, A, E>(config: {...})` |
| 201 | + |
| 202 | + - `manager`: Instance of `ClassPlusInitializationManager`. |
| 203 | + - `initialState`: Initial state of the class. |
| 204 | + - `constructor`: Constructor function for the class. |
| 205 | + - `args`: Optional initialization arguments. |
| 206 | + - `pullEnvironment`: Function to retrieve environment variables. |
| 207 | + - `onInitialize`: Optional initialization logic. |
| 208 | + - `onStorageChange`: Callback for state updates. |
| 209 | + |
| 210 | +- **Methods**: |
| 211 | + |
| 212 | + - `get(): T` |
| 213 | + - Retrieves the class instance, creating it if necessary. |
| 214 | + - `initialize(): async* ()` |
| 215 | + - Performs any setup logic for the class. |
| 216 | + - `getState(): S` |
| 217 | + - Retrieves the current state. |
| 218 | + - `getEnvironment(): ?E` |
| 219 | + - Retrieves the environment, initializing it if necessary. |
| 220 | + |
| 221 | +### **Helper Functions** |
| 222 | + |
| 223 | +#### **`ClassPlusGetter`** |
| 224 | + |
| 225 | +Simplifies retrieval of a class instance. |
| 226 | + |
| 227 | +```motoko |
| 228 | +public func ClassPlusGetter<T, S, A, E>(x: ?ClassPlus<T, S, A, E>): () -> T; |
| 229 | +``` |
| 230 | + |
| 231 | +#### **`BuildInit`** |
| 232 | + |
| 233 | +Constructs initialization logic for a class. |
| 234 | + |
| 235 | +```motoko |
| 236 | +public func BuildInit<system, T, S, A, E>(Constructor: (...)): (...) -> (); |
| 237 | +``` |
| 238 | + |
| 239 | +--- |
| 240 | + |
| 241 | +## Advantages of ClassPlus |
| 242 | + |
| 243 | +- **Reduced Boilerplate**: Eliminates repetitive code in actor classes. |
| 244 | +- **Upgrade-Safe**: Ensures class objects can be reconstituted from stable variables. |
| 245 | +- **Modular and Organized**: Provides a clear structure for defining and managing classes. |
| 246 | +- **Automatic Initialization**: Built-in timer management simplifies initialization. |
| 247 | + |
| 248 | +--- |
| 249 | + |
| 250 | +This library is ideal for projects requiring modular, upgrade-friendly object management in Motoko. By leveraging ClassPlus, developers can focus more on functionality and less on boilerplate code. |
| 251 | + |
0 commit comments