@quenty/adorneedata 1.0.1-canary.433.80025dc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## 1.0.1-canary.433.80025dc.0 (2023-12-14)
7
+
8
+ **Note:** Version bump only for package @quenty/adorneedata
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2014-2023 James Onnen (Quenty)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ ## AdorneeData
2
+
3
+ <div align="center">
4
+ <a href="http://quenty.github.io/NevermoreEngine/">
5
+ <img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/docs.yml/badge.svg" alt="Documentation status" />
6
+ </a>
7
+ <a href="https://discord.gg/mhtGUS8">
8
+ <img src="https://img.shields.io/discord/385151591524597761?color=5865F2&label=discord&logo=discord&logoColor=white" alt="Discord" />
9
+ </a>
10
+ <a href="https://github.com/Quenty/NevermoreEngine/actions">
11
+ <img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/build.yml/badge.svg" alt="Build and release status" />
12
+ </a>
13
+ </div>
14
+
15
+ Bridges attributes and serialization
16
+
17
+ <div align="center"><a href="https://quenty.github.io/NevermoreEngine/api/AdorneeData">View docs →</a></div>
18
+
19
+ ## Installation
20
+
21
+ ```
22
+ npm install @quenty/adorneedata --save
23
+ ```
24
+
25
+ ## Requirements
26
+ These are the requirements for this attribute data library.
27
+
28
+ * Not like Tie in that it is focused on serialization/boundary between Roblox and our stuff (tie is interfaces separated by network boundary)
29
+ * Easy to transform between data and attributes
30
+ * No service bag requirements
31
+
32
+ ### Centralized definition with no server/client information
33
+ ```lua
34
+ return AdorneeData.new({
35
+ CurrencyColor = Color3.new();
36
+ CurrencyNameTranslationKey = "";
37
+ CurrencyFormatTranslationKey = "";
38
+ CurrencyImageId = "";
39
+ CurrencyShowType = CurrencyShowTypes.NONE;
40
+ GivePlayerCurrency = false;
41
+ CurrencySaves = false;
42
+ })
43
+ ```
44
+
45
+ ### Easy to write/author
46
+ We should be able to create new data that is validated
47
+
48
+ ```lua
49
+ CurrencyDefinitionData:CreateData({
50
+ CurrencyKey = CurrencyDefinitionConstants.DEFAULT_CURRENCY_KEY
51
+ CurrencyColor = Color3.fromRGB(55, 180, 74)
52
+ CurrencyTranslationKey = "currency.default.name"
53
+ CurrencyFormatTranslationKey = "currency.default.format"
54
+ DoesSave = true
55
+ ImageId = "rbxassetid://10049671651"
56
+ })
57
+ ```
58
+
59
+ We should be able to create partial data
60
+
61
+ ```lua
62
+ CurrencyDefinitionData:CreatePartialData({
63
+ CurrencyKey = CurrencyDefinitionConstants.DEFAULT_CURRENCY_KEY
64
+ CurrencyColor = Color3.fromRGB(55, 180, 74)
65
+ CurrencyTranslationKey = "currency.default.name"
66
+ CurrencyFormatTranslationKey = "currency.default.format"
67
+ DoesSave = true
68
+ ImageId = "rbxassetid://10049671651"
69
+ })
70
+ ```
71
+ #### Additional authorship requirements
72
+
73
+ * Should be able to set optional values
74
+
75
+ ### Easy to validate/assert/assign
76
+
77
+ We should be able to check data types
78
+
79
+ ```lua
80
+ function CurrencyService:InitCurrencyDefinition(currencyDefinitionData)
81
+ assert(CurrencyDefinitionData:IsData(currencyDefinitionData), "Bad currencyDefinitionData")
82
+
83
+ local currencyDefinition = CurrencyDefinition:Create("Folder")
84
+ CurrencyDefinitionData:SetAttributes(currencyDefinition, currencyDefinitionData)
85
+
86
+ return currencyDefinition
87
+ end
88
+ ```
89
+
90
+ ### Replacement files
91
+
92
+ Should replace following files:
93
+
94
+ * CurrencyDefinitionDataUtils - Files with `t` interface asserting type
95
+ * CurrencyDefinitionConstants - Files with constant attribute names
96
+ * CurrencyDefinitionUtils - File that sets certain properties or attributes and do validation
97
+
98
+ ### Easy to serialize
99
+
100
+ Should be able to flash to constant
101
+
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "adorneedata",
3
+ "tree": {
4
+ "$path": "src"
5
+ }
6
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@quenty/adorneedata",
3
+ "version": "1.0.1-canary.433.80025dc.0",
4
+ "description": "Bridges attributes and serialization",
5
+ "keywords": [
6
+ "Roblox",
7
+ "Nevermore",
8
+ "Lua",
9
+ "AdorneeData"
10
+ ],
11
+ "bugs": {
12
+ "url": "https://github.com/Quenty/NevermoreEngine/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/Quenty/NevermoreEngine.git",
17
+ "directory": "src/adorneedata/"
18
+ },
19
+ "funding": {
20
+ "type": "patreon",
21
+ "url": "https://www.patreon.com/quenty"
22
+ },
23
+ "license": "MIT",
24
+ "contributors": [
25
+ "Quenty"
26
+ ],
27
+ "dependencies": {
28
+ "@quenty/attributeutils": "9.1.2-canary.433.80025dc.0",
29
+ "@quenty/ducktype": "2.0.1-canary.433.80025dc.0",
30
+ "@quenty/loader": "7.0.1-canary.433.80025dc.0",
31
+ "@quenty/rx": "8.1.2-canary.433.80025dc.0",
32
+ "@quenty/rxsignal": "2.1.2-canary.433.80025dc.0",
33
+ "@quenty/table": "3.3.1-canary.433.80025dc.0",
34
+ "@quentystudios/t": "^3.0.0"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "gitHead": "80025dcd926765b502d322bb84e013b973409d8c"
40
+ }
@@ -0,0 +1,254 @@
1
+ --[=[
2
+ Bridges attributes and serializable data table. It's typical to need to define data in 3 ways.
3
+
4
+ 1. Attributes on an instance for replication
5
+ 2. Tables for Lua configuration
6
+ 3. Within AttributeValues for writing regular code
7
+
8
+ Providing all 3
9
+
10
+ @class AdorneeData
11
+ ]=]
12
+
13
+ local require = require(script.Parent.loader).load(script)
14
+
15
+ local AdorneeDataEntry = require("AdorneeDataEntry")
16
+ local AdorneeDataValue = require("AdorneeDataValue")
17
+ local AttributeUtils = require("AttributeUtils")
18
+ local t = require("t")
19
+
20
+ local AdorneeData = {}
21
+ AdorneeData.ClassName = "AdorneeData"
22
+ AdorneeData.__index = AdorneeData
23
+
24
+ --[=[
25
+ Attribute data specification
26
+
27
+ @param prototype any
28
+ @return AdorneeData<T>
29
+ ]=]
30
+ function AdorneeData.new(prototype)
31
+ local self = setmetatable({}, AdorneeData)
32
+
33
+ self._fullPrototype = assert(prototype, "Bad prototype")
34
+ self._attributePrototype = {}
35
+ self._valueObjectPrototype = {}
36
+
37
+ for key, item in pairs(self._fullPrototype) do
38
+ if AdorneeDataEntry.isAdorneeDataEntry(item) then
39
+ self._valueObjectPrototype[key] = item
40
+ else
41
+ self._attributePrototype[key] = item
42
+ end
43
+ end
44
+
45
+ return self
46
+ end
47
+
48
+ --[=[
49
+ Returns true if the data is valid data, otherwise returns false and an error.
50
+
51
+ @param data any
52
+ @return boolean
53
+ @return string -- Error message
54
+ ]=]
55
+ function AdorneeData:IsData(data)
56
+ return self:GetStrictTInterface()(data)
57
+ end
58
+
59
+ --[=[
60
+ Validates and creates a new data table for the data that is readonly and frozen
61
+
62
+ @param data T
63
+ @return T
64
+ ]=]
65
+ function AdorneeData:CreateData(data)
66
+ assert(self:IsData(data))
67
+
68
+ return table.freeze(table.clone(data))
69
+ end
70
+
71
+
72
+ --[=[
73
+ Validates and creates a new data table that is readonly and frozen, but for partial
74
+ data.
75
+
76
+ The partial data can just be part of the attributes.
77
+
78
+ @param partialData TPartial
79
+ @return TPartial
80
+ ]=]
81
+ function AdorneeData:CreatePartialData(partialData)
82
+ assert(self:IsPartialData(partialData))
83
+
84
+ return table.freeze(table.clone(partialData))
85
+ end
86
+
87
+
88
+ --[=[
89
+ Gets attribute table for the data
90
+
91
+ @param adornee Instance
92
+ @return AdorneeDataValue
93
+ ]=]
94
+ function AdorneeData:CreateAdorneeDataValue(adornee)
95
+ assert(typeof(adornee) == "Instance", "Bad adornee")
96
+
97
+ local attributeTableValue = AdorneeDataValue.new(adornee, self._fullPrototype)
98
+
99
+ return attributeTableValue
100
+ end
101
+
102
+ --[=[
103
+ Gets the attributes for the adornee
104
+
105
+ @param adornee Instance
106
+ @return T
107
+ ]=]
108
+ function AdorneeData:GetAttributes(adornee)
109
+ assert(typeof(adornee) == "Instance", "Bad adornee")
110
+
111
+ local data = {}
112
+ for key, value in pairs(self._attributePrototype) do
113
+ data[key] = adornee:GetAttribute(value)
114
+ end
115
+
116
+ -- TODO: Avoid additional allocation
117
+ for key, value in pairs(self._valueObjectPrototype) do
118
+ data[key] = value:CreateValueObject(adornee).Value
119
+ end
120
+
121
+ return self:CreateData(data)
122
+ end
123
+
124
+ --[=[
125
+ Sets the attributes for the adornee
126
+
127
+ @param adornee Instance
128
+ @param data T
129
+ ]=]
130
+ function AdorneeData:SetAttributes(adornee, data)
131
+ assert(typeof(adornee) == "Instance", "Bad adornee")
132
+ assert(self:IsData(data))
133
+
134
+ for key, _ in pairs(self._attributePrototype) do
135
+ adornee:SetAttribute(key, data[key])
136
+ end
137
+
138
+ -- TODO: Avoid additional allocation
139
+ for key, value in pairs(self._valueObjectPrototype) do
140
+ value:CreateValueObject(adornee).Value = data[key]
141
+ end
142
+ end
143
+
144
+ --[=[
145
+ Initializes the attributes for the adornee
146
+
147
+ @param adornee Instance
148
+ @param data T
149
+ ]=]
150
+ function AdorneeData:InitAttributes(adornee, data)
151
+ assert(typeof(adornee) == "Instance", "Bad adornee")
152
+ assert(self:IsData(data))
153
+
154
+ for key, _ in pairs(self._attributePrototype) do
155
+ if adornee:GetAttribute(key) == nil then
156
+ adornee:SetAttribute(key, data[key])
157
+ end
158
+ end
159
+
160
+ -- TODO: Avoid additional allocation
161
+ for key, value in pairs(self._valueObjectPrototype) do
162
+ local valueObject = value:CreateValueObject(adornee)
163
+ if valueObject == nil then
164
+ valueObject.Value = data[key]
165
+ end
166
+ end
167
+ end
168
+
169
+ --[=[
170
+ Sets partial attributes on the adornee
171
+
172
+ @param adornee Instance
173
+ @param partialData TPartial
174
+ ]=]
175
+ function AdorneeData:SetPartialAttributes(adornee, partialData)
176
+ assert(typeof(adornee) == "Instance", "Bad adornee")
177
+ assert(self:IsPartialData(partialData))
178
+
179
+ local attributeTable = self:CreateAdorneeDataValue(adornee)
180
+ for key, value in pairs(partialData) do
181
+ attributeTable[key].Value = value
182
+ end
183
+ end
184
+
185
+ --[=[
186
+ Gets a strict interface which will return true if the value is a partial interface and
187
+ false otherwise.
188
+
189
+ @return function
190
+ ]=]
191
+ function AdorneeData:GetStrictTInterface()
192
+ if self._fullInterface then
193
+ return self._fullInterface
194
+ end
195
+
196
+ self._fullInterface = t.strictInterface(self:_getOrCreateTypeInterfaceList())
197
+ return self._fullInterface
198
+ end
199
+
200
+ --[=[
201
+ Gets a [t] interface which will return true if the value is a partial interface, and
202
+ false otherwise.
203
+
204
+ @return function
205
+ ]=]
206
+ function AdorneeData:GetPartialTInterface()
207
+ if self._partialInterface then
208
+ return self._partialInterface
209
+ end
210
+
211
+ local interfaceList = {}
212
+ for key, value in pairs(self:_getOrCreateTypeInterfaceList()) do
213
+ interfaceList[key] = t.optional(value)
214
+ end
215
+
216
+ self._partialInterface = t.strictInterface(interfaceList)
217
+ return self._partialInterface
218
+ end
219
+
220
+
221
+ --[=[
222
+ Returns true if the data is valid partial data, otherwise returns false and an error.
223
+
224
+ @param data any
225
+ @return boolean
226
+ @return string -- Error message
227
+ ]=]
228
+ function AdorneeData:IsPartialData(data)
229
+ return self:GetPartialTInterface()(data)
230
+ end
231
+
232
+ function AdorneeData:_getOrCreateTypeInterfaceList()
233
+ if self._typeInterfaceList then
234
+ return self._typeInterfaceList
235
+ end
236
+
237
+ local interfaceList = {}
238
+
239
+ for key, value in pairs(self._fullPrototype) do
240
+ if AdorneeDataEntry.isAdorneeDataEntry(value) then
241
+ interfaceList[key] = value:GetStrictInterface()
242
+ else
243
+ local valueType = typeof(value)
244
+ assert(AttributeUtils.isValidAttributeType(valueType), "Not a valid value type")
245
+
246
+ interfaceList[key] = t.typeof(valueType)
247
+ end
248
+ end
249
+
250
+ self._typeInterfaceList = interfaceList
251
+ return interfaceList
252
+ end
253
+
254
+ return AdorneeData
@@ -0,0 +1,41 @@
1
+ --[=[
2
+ @class AdorneeDataEntry
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local DuckTypeUtils = require("DuckTypeUtils")
8
+ local t = require("t")
9
+
10
+ local AdorneeDataEntry = {}
11
+ AdorneeDataEntry.ClassName = "AdorneeDataEntry"
12
+ AdorneeDataEntry.__index = AdorneeDataEntry
13
+
14
+ function AdorneeDataEntry.new(dataType, createValueObject)
15
+ assert(type(dataType) == "string", "Bad dataType")
16
+ assert(type(createValueObject) == "function", "Bad createValueObject")
17
+
18
+ local self = setmetatable({}, AdorneeDataEntry)
19
+
20
+ self._dataType = dataType
21
+ self._createValueObject = createValueObject
22
+ self._strictInterface = t.typeof(self._dataType)
23
+
24
+ return self
25
+ end
26
+
27
+ function AdorneeDataEntry.isAdorneeDataEntry(data)
28
+ return DuckTypeUtils.isImplementation(AdorneeDataEntry, data)
29
+ end
30
+
31
+ function AdorneeDataEntry:CreateValueObject(adornee)
32
+ assert(typeof(adornee) == "Instance", "Bad adornee")
33
+
34
+ return self._createValueObject(adornee)
35
+ end
36
+
37
+ function AdorneeDataEntry:GetStrictInterface()
38
+ return self._strictInterface
39
+ end
40
+
41
+ return AdorneeDataEntry
@@ -0,0 +1,152 @@
1
+ --[=[
2
+ Allows access to an attribute like a ValueObject but in table form.
3
+
4
+ ```lua
5
+ local data = AdorneeDataValue.new(workspace, {
6
+ Version = "1.0.0";
7
+ CFrame = CFrame.new();
8
+ })
9
+ print(data.Version.Value) --> 1.0.0
10
+ print(workspace:GetAttribute("version")) --> 1.0.0
11
+
12
+ data.Version.Changed:Connect(function()
13
+ print(data.Version.Value)
14
+ end)
15
+
16
+ workspace:SetAttribute("1.1.0") --> 1.1.0
17
+ data.Value = "1.2.0" --> 1.2.0
18
+ ```
19
+
20
+ @class AdorneeDataValue
21
+ ]=]
22
+
23
+ local require = require(script.Parent.loader).load(script)
24
+
25
+ local AttributeValue = require("AttributeValue")
26
+ local RxSignal = require("RxSignal")
27
+ local Rx = require("Rx")
28
+ local AdorneeDataEntry = require("AdorneeDataEntry")
29
+
30
+ local AdorneeDataValue = {}
31
+ AdorneeDataValue.ClassName = "AdorneeDataValue"
32
+ AdorneeDataValue.__index = AdorneeDataValue
33
+
34
+ --[=[
35
+ Constructs a new AdorneeDataValue
36
+
37
+ @param adornee Instance
38
+ @param prototype table
39
+ @return AdorneeDataValue<T>
40
+ ]=]
41
+ function AdorneeDataValue.new(adornee, prototype)
42
+ assert(typeof(adornee) == "Instance", "Bad adornee")
43
+ assert(type(prototype) == "table", "Bad prototype")
44
+
45
+ local self = {
46
+ _adornee = adornee;
47
+ _defaultValues = prototype;
48
+ _valueObjects = {};
49
+ }
50
+
51
+ for key, value in pairs(prototype) do
52
+ if AdorneeDataEntry.isAdorneeDataEntry(value) then
53
+ self._valueObjects[key] = value:CreateValueObject(adornee)
54
+ else
55
+ self._valueObjects[key] = AttributeValue.new(adornee, key, value)
56
+ end
57
+ end
58
+
59
+ return setmetatable(self, AdorneeDataValue)
60
+ end
61
+
62
+ --[=[
63
+ The current property of the Attribute. Can be assigned to to write
64
+ the attribute.
65
+ @prop Value T
66
+ @within AdorneeDataValue
67
+ ]=]
68
+
69
+ --[=[
70
+ Signal that fires when the attribute changes
71
+ @readonly
72
+ @prop Changed Signal<()>
73
+ @within AdorneeDataValue
74
+ ]=]
75
+ function AdorneeDataValue:__index(index)
76
+ if index == "Value" then
77
+ local result = {}
78
+ for key, valueObject in pairs(self._valueObjects) do
79
+ result[key] = valueObject.Value
80
+ end
81
+ return result
82
+ elseif index == "Changed" then
83
+ return RxSignal.new(self:Observe():Pipe({
84
+ Rx.skip(1);
85
+ }))
86
+ elseif AdorneeDataValue[index] then
87
+ return AdorneeDataValue[index]
88
+ elseif type(index) == "string" then
89
+ local attributeValues = rawget(self, "_valueObjects")
90
+ if attributeValues[index] then
91
+ return attributeValues[index]
92
+ else
93
+ error(string.format("%q is not a member of AdorneeDataValue", tostring(index)))
94
+ end
95
+ else
96
+ error(string.format("%q is not a member of AdorneeDataValue", tostring(index)))
97
+ end
98
+ end
99
+
100
+ --[=[
101
+ Observes an attribute on an instance.
102
+ @return Observable<any>
103
+ ]=]
104
+ function AdorneeDataValue:Observe()
105
+ local current = rawget(self, "_defaultObservable")
106
+ if current then
107
+ return current
108
+ end
109
+
110
+ local attributeValues = rawget(self, "_valueObjects")
111
+
112
+ local observables = {}
113
+ for key, valueObject in pairs(attributeValues) do
114
+ observables[key] = valueObject:Observe()
115
+ end
116
+
117
+ local observable = Rx.combineLatest(observables)
118
+ rawset(self, "_defaultObservable", observable)
119
+ return observable
120
+ end
121
+
122
+ function AdorneeDataValue:__newindex(index, newValue)
123
+ if index == "Value" then
124
+ assert(type(newValue) == "table", "Bad newValue")
125
+ local attributeValues = rawget(self, "_valueObjects")
126
+
127
+ for key, value in pairs(newValue) do
128
+ if attributeValues[key] then
129
+ attributeValues[key].Value = value
130
+ else
131
+ error("%s is not a valid member of AttributeValue")
132
+ end
133
+ end
134
+ else
135
+ error(string.format("Use AttributeValue.%q.Value to set an attribute", tostring(index)))
136
+ end
137
+ end
138
+
139
+ function AdorneeDataValue:_getOrCreateAttributeValue(key)
140
+ local attributeValues = rawget(self, "_valueObjects")
141
+ if attributeValues[key] then
142
+ return attributeValues[key]
143
+ else
144
+ local prototype = rawget(self, "_defaultValues")
145
+ local adornee = rawget(self, "_adornee")
146
+
147
+ attributeValues[key] = AttributeValue.new(adornee, key, prototype[key])
148
+ return attributeValues
149
+ end
150
+ end
151
+
152
+ return AdorneeDataValue
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "node_modules",
3
+ "globIgnorePaths": [ "**/.package-lock.json" ],
4
+ "tree": {
5
+ "$path": { "optional": "../node_modules" }
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "AdorneeDataTest",
3
+ "tree": {
4
+ "$className": "DataModel",
5
+ "ServerScriptService": {
6
+ "adorneedata": {
7
+ "$path": ".."
8
+ }
9
+ }
10
+ }
11
+ }