Merge branch 'master' of https://github.com/boatbomber/rojo into headless-api

This commit is contained in:
boatbomber 2025-05-22 10:57:29 -07:00
commit 5f4c2a7a3f
82 changed files with 10139 additions and 2429 deletions

View file

@ -53,7 +53,7 @@ jobs:
# https://doc.rust-lang.org/rustc/platform-support.html
include:
- host: linux
os: ubuntu-20.04
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
label: linux-x86_64

4
.gitignore vendored
View file

@ -19,3 +19,7 @@
# Snapshot files from the 'insta' Rust crate
**/*.snap.new
# Macos file system junk
._*
.DS_STORE

View file

@ -3,6 +3,20 @@
## Unreleased Changes
* Added headless API for Studio companion plugins. ([#639])
[#639]: https://github.com/rojo-rbx/rojo/pull/639
## 7.5.1 - April 25th, 2025
* Fixed output spam related to `Instance.Capabilities` in the plugin
## 7.5.0 - April 25th, 2025
* Fixed an edge case that caused model pivots to not be built correctly in some cases ([#1027])
* Add `blockedPlaceIds` project config field to allow blocking place ids from being live synced ([#1021])
* Adds support for `.plugin.lua(u)` files - this applies the `Plugin` RunContext. ([#1008])
* Added support for Roblox's `Content` type. This replaces the old `Content` type with `ContentId` to reflect Roblox's change.
If you were previously using the fully-qualified syntax for `Content` you will need to switch it to `ContentId`.
* Added support for `Enum` attributes
* Significantly improved performance of `.rbxm` parsing
* Support for a `$schema` field in all special JSON files (`.project.json`, `.model.json`, and `.meta.json`) ([#974])
* Projects may now manually link `Ref` properties together using `Attributes`. ([#843])
This has two parts: using `id` or `$id` in JSON files or a `Rojo_Target` attribute, an Instance
@ -61,7 +75,7 @@
Additionally, the `exclude` field allows files to be excluded from the sync rule if they match a pattern specified by it. If it's not present, all files that match `pattern` will be modified using the sync rule.
The `use` field corresponds to one of the potential file type that Rojo will currently include in a project. Files that match the provided pattern will be treated as if they had the file extension for that file type. A full list is below:
The `use` field corresponds to one of the potential file type that Rojo will currently include in a project. Files that match the provided pattern will be treated as if they had the file extension for that file type.
| `use` value | file extension |
|:---------------|:----------------|
@ -78,9 +92,18 @@
| `project` | `.project.json` |
| `ignore` | None! |
Additionally, there are `use` values for specific script types ([#909]):
| `use` value | script type |
|:-------------------------|:---------------------------------------|
| `legacyServerScript` | `Script` with `Enum.RunContext.Legacy` |
| `legacyClientScript` | `LocalScript` |
| `runContextServerScript` | `Script` with `Enum.RunContext.Server` |
| `runContextClientScript` | `Script` with `Enum.RunContext.Client` |
| `pluginScript` | `Script` with `Enum.RunContext.Plugin` |
**All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced!
[#639]: https://github.com/rojo-rbx/rojo/pull/639
[#813]: https://github.com/rojo-rbx/rojo/pull/813
[#832]: https://github.com/rojo-rbx/rojo/pull/832
[#834]: https://github.com/rojo-rbx/rojo/pull/834
@ -89,11 +112,19 @@
[#843]: https://github.com/rojo-rbx/rojo/pull/843
[#883]: https://github.com/rojo-rbx/rojo/pull/883
[#886]: https://github.com/rojo-rbx/rojo/pull/886
[#909]: https://github.com/rojo-rbx/rojo/pull/909
[#911]: https://github.com/rojo-rbx/rojo/pull/911
[#915]: https://github.com/rojo-rbx/rojo/pull/915
[#974]: https://github.com/rojo-rbx/rojo/pull/974
[#987]: https://github.com/rojo-rbx/rojo/pull/987
[#988]: https://github.com/rojo-rbx/rojo/pull/988
[#1008]: https://github.com/rojo-rbx/rojo/pull/1008
[#1021]: https://github.com/rojo-rbx/rojo/pull/1021
[#1027]: https://github.com/rojo-rbx/rojo/pull/1027
## [7.4.4] - August 22nd, 2024
* Fixed issue with reading attributes from `Lighting` in new place files
* `Instance.Archivable` will now default to `true` when building a project into a binary (`rbxm`/`rbxl`) file rather than `false`.
## [7.4.3] - August 6th, 2024
* Fixed issue with building binary files introduced in 7.4.2

180
Cargo.lock generated
View file

@ -17,6 +17,19 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if 1.0.0",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.2"
@ -171,6 +184,10 @@ name = "cc"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723"
dependencies = [
"jobserver",
"libc",
]
[[package]]
name = "cfg-if"
@ -492,7 +509,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"redox_syscall 0.4.1",
"windows-sys 0.52.0",
]
@ -935,6 +952,15 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "jod-thread"
version = "0.1.2"
@ -962,9 +988,9 @@ dependencies = [
[[package]]
name = "lazy_static"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lazycell"
@ -986,7 +1012,7 @@ checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
dependencies = [
"bitflags 2.4.2",
"libc",
"redox_syscall",
"redox_syscall 0.4.1",
]
[[package]]
@ -1001,6 +1027,16 @@ version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.21"
@ -1241,6 +1277,29 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall 0.5.10",
"smallvec",
"windows-targets 0.52.4",
]
[[package]]
name = "paste"
version = "1.0.14"
@ -1310,6 +1369,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plotters"
version = "0.3.5"
@ -1498,10 +1563,11 @@ dependencies = [
[[package]]
name = "rbx_binary"
version = "0.7.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b85057e8ff75a1ce99248200c4b3c7b481a3d52f921f1053ecd67921dcc7930"
checksum = "9573fee5e073d7b303f475c285197fdc8179468de66ca60ee115a58fbac99296"
dependencies = [
"ahash",
"log",
"lz4",
"profiling",
@ -1509,23 +1575,26 @@ dependencies = [
"rbx_reflection",
"rbx_reflection_database",
"thiserror",
"zstd",
]
[[package]]
name = "rbx_dom_weak"
version = "2.9.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcd2a17d09e46af0805f8b311a926402172b97e8d9388745c9adf8f448901841"
checksum = "04425cf6e9376e5486f4fb35906c120d1b1b45618a490318cf563fab1fa230a9"
dependencies = [
"ahash",
"rbx_types",
"serde",
"ustr",
]
[[package]]
name = "rbx_reflection"
version = "4.7.0"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8118ac6021d700e8debe324af6b40ecfd2cef270a00247849dbdfeebb0802677"
checksum = "1b6d0d62baa613556b058a5f94a53b01cf0ccde0ea327ce03056e335b982e77e"
dependencies = [
"rbx_types",
"serde",
@ -1534,9 +1603,9 @@ dependencies = [
[[package]]
name = "rbx_reflection_database"
version = "0.2.12+roblox-638"
version = "1.0.3+roblox-670"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e29381d675420e841f8c02db5755cbb2545ed3e13f56c539546dc58702b512a"
checksum = "e22c05ef92528c0fb0cc580592a65ca178d3ea9beb07a1d9ca0a2503c4f3721c"
dependencies = [
"lazy_static",
"rbx_reflection",
@ -1546,9 +1615,9 @@ dependencies = [
[[package]]
name = "rbx_types"
version = "1.10.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e30f49b2a3bb667e4074ba73c2dfb8ca0873f610b448ccf318a240acfdec6c73"
checksum = "78e4fdde46493def107e5f923d82e813dec9b3eef52c2f75fbad3a716023eda2"
dependencies = [
"base64 0.13.1",
"bitflags 1.3.2",
@ -1561,10 +1630,11 @@ dependencies = [
[[package]]
name = "rbx_xml"
version = "0.13.5"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b14b3027bc9ccd82e2fc854c8bcd25ed58318e570c355bf2cf63df9cdbd5ba8"
checksum = "bb623833c31cc43bbdaeb32f5e91db8ecd63fc46e438d0d268baf9e61539cf1c"
dependencies = [
"ahash",
"base64 0.13.1",
"log",
"rbx_dom_weak",
@ -1582,6 +1652,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
dependencies = [
"bitflags 2.4.2",
]
[[package]]
name = "redox_users"
version = "0.4.4"
@ -1752,7 +1831,7 @@ dependencies = [
[[package]]
name = "rojo"
version = "7.5.0-prealpha"
version = "7.5.1"
dependencies = [
"anyhow",
"backtrace",
@ -1896,6 +1975,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
version = "0.7.1"
@ -2393,6 +2478,19 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "ustr"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18b19e258aa08450f93369cf56dd78063586adf19e92a75b338a800f799a0208"
dependencies = [
"ahash",
"byteorder",
"lazy_static",
"parking_lot",
"serde",
]
[[package]]
name = "uuid"
version = "1.7.0"
@ -2784,3 +2882,51 @@ name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 2.0.52",
]
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.15+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
dependencies = [
"cc",
"pkg-config",
]

View file

@ -1,8 +1,12 @@
[package]
name = "rojo"
version = "7.5.0-prealpha"
version = "7.5.1"
rust-version = "1.70.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
authors = [
"Lucien Greathouse <me@lpghatguy.com>",
"Micah Reid <git@dekkonot.com>",
"Ken Loeffler <kenloef@gmail.com>",
]
description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0"
homepage = "https://rojo.space"
@ -51,11 +55,11 @@ memofs = { version = "0.3.0", path = "crates/memofs" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "0.7.7"
rbx_dom_weak = "2.9.0"
rbx_reflection = "4.7.0"
rbx_reflection_database = "0.2.12"
rbx_xml = "0.13.5"
rbx_binary = "1.0.0"
rbx_dom_weak = "3.0.0"
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.3"
rbx_xml = "1.0.0"
anyhow = "1.0.80"
backtrace = "0.3.69"

View file

@ -2,7 +2,11 @@
name = "memofs"
description = "Virtual filesystem with configurable backends."
version = "0.3.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
authors = [
"Lucien Greathouse <me@lpghatguy.com>",
"Micah Reid <git@dekkonot.com>",
"Ken Loeffler <kenloef@gmail.com>",
]
edition = "2018"
readme = "README.md"
license = "MIT"

View file

@ -1 +1 @@
7.5.0-prealpha
7.5.1

View file

@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
elseif valueType == "table" then
local valueMeta = getmetatable(value)
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
-- This type implement's the metamethod we made up to line up with
-- Rust's 'Debug' trait.
@ -242,4 +242,4 @@ return {
debugOutputBuffer = debugOutputBuffer,
fmt = fmt,
debugify = debugify,
}
}

View file

@ -31,4 +31,4 @@ function Response:json()
return HttpService:JSONDecode(self.body)
end
return Response
return Response

View file

@ -2,4 +2,4 @@ return function()
it("should load", function()
require(script.Parent)
end)
end
end

View file

@ -57,4 +57,4 @@ function Log.error(template, ...)
error(Fmt.fmt(template, ...))
end
return Log
return Log

View file

@ -2,4 +2,4 @@ return function()
it("should load", function()
require(script.Parent)
end)
end
end

View file

@ -188,6 +188,38 @@ types = {
},
Content = {
fromPod = function(pod): Content
if type(pod) == "string" then
if pod == "None" then
return Content.none
else
error(`unexpected Content value '{pod}'`)
end
else
local ty, value = next(pod)
if ty == "Uri" then
return Content.fromUri(value)
elseif ty == "Object" then
error("Object deserializing is not currently implemented")
else
error(`Unknown Content type '{ty}' (could not deserialize)`)
end
end
end,
toPod = function(roblox: Content)
if roblox.SourceType == Enum.ContentSourceType.None then
return "None"
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
return { Uri = roblox.Uri }
elseif roblox.SourceType == Enum.ContentSourceType.Object then
error("Object serializing is not currently implemented")
else
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
end
end,
},
ContentId = {
fromPod = identity,
toPod = identity,
},
@ -205,6 +237,19 @@ types = {
end,
},
EnumItem = {
fromPod = function(pod)
return Enum[pod.type]:FromValue(pod.value)
end,
toPod = function(roblox)
return {
type = tostring(roblox.EnumType),
value = roblox.Value,
}
end,
},
Faces = {
fromPod = function(pod)
local faces = {}
@ -300,7 +345,12 @@ types = {
local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, keypoint.value, keypoint.envelope)
-- TODO: Add a test for NaN or Infinity values and envelopes
-- Right now it isn't possible because it'd fail the roundtrip.
-- It's more important that it works right now, though.
local value = keypoint.value or 0
local envelope = keypoint.envelope or 0
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
end
return NumberSequence.new(keypoints)

View file

@ -5,6 +5,7 @@ Error.Kind = {
UnknownProperty = "UnknownProperty",
PropertyNotReadable = "PropertyNotReadable",
PropertyNotWritable = "PropertyNotWritable",
CannotParseBinaryString = "CannotParseBinaryString",
Roblox = "Roblox",
}

View file

@ -15,6 +15,12 @@
0.0
]
},
"TestEnumItem": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"TestNumber": {
"Float64": 1337.0
},
@ -170,9 +176,23 @@
},
"ty": "ColorSequence"
},
"Content": {
"ContentId": {
"value": {
"Content": "rbxassetid://12345"
"ContentId": "rbxassetid://12345"
},
"ty": "ContentId"
},
"Content_None": {
"value": {
"Content": "None"
},
"ty": "Content"
},
"Content_Uri": {
"value": {
"Content": {
"Uri": "rbxasset://abc/123.rojo"
}
},
"ty": "Content"
},
@ -182,6 +202,15 @@
},
"ty": "Enum"
},
"EnumItem": {
"value": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"ty": "EnumItem"
},
"Faces": {
"value": {
"Faces": [

View file

@ -1,6 +1,8 @@
local CollectionService = game:GetService("CollectionService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local Error = require(script.Parent.Error)
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
local TERRAIN_MATERIAL_COLORS = {
Enum.Material.Grass,
@ -51,6 +53,10 @@ return {
return true, instance:GetAttributes()
end,
write = function(instance, _, value)
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
local existing = instance:GetAttributes()
local didAllWritesSucceed = true
@ -160,9 +166,14 @@ return {
return true, colors
end,
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
for material, color in value do
instance:SetMaterialColor(material, color)
end
return true
end,
},

File diff suppressed because it is too large Load diff

View file

@ -45,14 +45,7 @@ end
local function rejectWrongPlaceId(infoResponseBody)
if infoResponseBody.expectedPlaceIds ~= nil then
local foundId = false
for _, id in ipairs(infoResponseBody.expectedPlaceIds) do
if id == game.PlaceId then
foundId = true
break
end
end
local foundId = table.find(infoResponseBody.expectedPlaceIds, game.PlaceId)
if not foundId then
local idList = {}
@ -62,10 +55,30 @@ local function rejectWrongPlaceId(infoResponseBody)
local message = (
"Found a Rojo server, but its project is set to only be used with a specific list of places."
.. "\nYour place ID is %s, but needs to be one of these:"
.. "\nYour place ID is %u, but needs to be one of these:"
.. "\n%s"
.. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
):format(tostring(game.PlaceId), table.concat(idList, "\n"))
):format(game.PlaceId, table.concat(idList, "\n"))
return Promise.reject(message)
end
end
if infoResponseBody.unexpectedPlaceIds ~= nil then
local foundId = table.find(infoResponseBody.unexpectedPlaceIds, game.PlaceId)
if foundId then
local idList = {}
for _, id in ipairs(infoResponseBody.unexpectedPlaceIds) do
table.insert(idList, "- " .. tostring(id))
end
local message = (
"Found a Rojo server, but its project is set to not be used with a specific list of places."
.. "\nYour place ID is %u, but needs to not be one of these:"
.. "\n%s"
.. "\n\nTo change this list, edit 'blockedPlaceIds' in your .project.json file."
):format(game.PlaceId, table.concat(idList, "\n"))
return Promise.reject(message)
end

View file

@ -16,6 +16,7 @@ local invariant = require(script.Parent.Parent.invariant)
local decodeValue = require(script.Parent.decodeValue)
local reify = require(script.Parent.reify)
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
local setProperty = require(script.Parent.setProperty)
local function applyPatch(instanceMap, patch)
@ -29,6 +30,11 @@ local function applyPatch(instanceMap, patch)
-- Tracks any portions of the patch that could not be applied to the DOM.
local unappliedPatch = PatchSet.newEmpty()
-- Contains a list of all of the ref properties that we'll need to assign.
-- It is imperative that refs are assigned after all instances are created
-- to ensure that referents can be mapped to instances correctly.
local deferredRefs = {}
for _, removedIdOrInstance in ipairs(patch.removed) do
local removeInstanceSuccess = pcall(function()
if Types.RbxId(removedIdOrInstance) then
@ -78,7 +84,7 @@ local function applyPatch(instanceMap, patch)
)
end
local failedToReify = reify(instanceMap, patch.added, id, parentInstance)
local failedToReify = reifyInstance(deferredRefs, instanceMap, patch.added, id, parentInstance)
if not PatchSet.isEmpty(failedToReify) then
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
@ -143,7 +149,7 @@ local function applyPatch(instanceMap, patch)
[update.id] = mockVirtualInstance,
}
local failedToReify = reify(instanceMap, mockAdded, update.id, instance.Parent)
local failedToReify = reifyInstance(deferredRefs, instanceMap, mockAdded, update.id, instance.Parent)
local newInstance = instanceMap.fromIds[update.id]
@ -206,6 +212,18 @@ local function applyPatch(instanceMap, patch)
if update.changedProperties ~= nil then
for propertyName, propertyValue in pairs(update.changedProperties) do
-- Because refs may refer to instances that we haven't constructed yet,
-- we defer applying any ref properties until all instances are created.
if next(propertyValue) == "Ref" then
table.insert(deferredRefs, {
id = update.id,
instance = instance,
propertyName = propertyName,
virtualValue = propertyValue,
})
continue
end
local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap)
if not decodeSuccess then
unappliedUpdate.changedProperties[propertyName] = propertyValue
@ -230,6 +248,8 @@ local function applyPatch(instanceMap, patch)
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
end
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
return unappliedPatch
end

View file

@ -151,7 +151,24 @@ local function diff(instanceMap, virtualInstances, rootId)
if getProperySuccess then
local existingValue = existingValueOrErr
local decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
local decodeSuccess, decodedValue
-- If `virtualValue` is a ref then instead of decoding it to an instance,
-- we change `existingValue` to be a ref. This is because `virtualValue`
-- may point to an Instance which doesn't exist yet and therefore
-- decoding it may throw an error.
if next(virtualValue) == "Ref" then
decodeSuccess, decodedValue = true, virtualValue
if existingValue and typeof(existingValue) == "Instance" then
local existingValueRef = instanceMap.fromInstances[existingValue]
if existingValueRef then
existingValue = { Ref = existingValueRef }
end
end
else
decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
end
if decodeSuccess then
if not trueEquals(existingValue, decodedValue) then

View file

@ -7,26 +7,6 @@ local PatchSet = require(script.Parent.Parent.PatchSet)
local setProperty = require(script.Parent.setProperty)
local decodeValue = require(script.Parent.decodeValue)
local reifyInner, applyDeferredRefs
local function reify(instanceMap, virtualInstances, rootId, parentInstance)
-- Create an empty patch that will be populated with any parts of this reify
-- that could not happen, like instances that couldn't be created and
-- properties that could not be assigned.
local unappliedPatch = PatchSet.newEmpty()
-- Contains a list of all of the ref properties that we'll need to assign
-- after all instances are created. We apply refs in a second pass, after
-- we create as many instances as we can, so that we ensure that referents
-- can be mapped to instances correctly.
local deferredRefs = {}
reifyInner(instanceMap, virtualInstances, rootId, parentInstance, unappliedPatch, deferredRefs)
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
return unappliedPatch
end
--[[
Add the given ID and all of its descendants in virtualInstances to the given
PatchSet, marked for addition.
@ -40,10 +20,21 @@ local function addAllToPatch(patchSet, virtualInstances, id)
end
end
function reifyInstance(deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
-- Create an empty patch that will be populated with any parts of this reify
-- that could not happen, like instances that couldn't be created and
-- properties that could not be assigned.
local unappliedPatch = PatchSet.newEmpty()
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
return unappliedPatch
end
--[[
Inner function that defines the core routine.
]]
function reifyInner(instanceMap, virtualInstances, id, parentInstance, unappliedPatch, deferredRefs)
function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, id, parentInstance)
local virtualInstance = virtualInstances[id]
if virtualInstance == nil then
@ -102,7 +93,7 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
end
for _, childId in ipairs(virtualInstance.Children) do
reifyInner(instanceMap, virtualInstances, childId, instance, unappliedPatch, deferredRefs)
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, childId, instance)
end
instance.Parent = parentInstance
@ -143,6 +134,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
end
local targetInstance = instanceMap.fromIds[refId]
if targetInstance == nil then
markFailed(entry.id, entry.propertyName, entry.virtualValue)
continue
@ -155,4 +147,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
end
end
return reify
return {
reifyInstance = reifyInstance,
applyDeferredRefs = applyDeferredRefs,
}

View file

@ -1,5 +1,6 @@
return function()
local reify = require(script.Parent.reify)
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
local PatchSet = require(script.Parent.Parent.PatchSet)
local InstanceMap = require(script.Parent.Parent.InstanceMap)
@ -20,7 +21,11 @@ return function()
it("should throw when given a bogus ID", function()
expect(function()
reify(InstanceMap.new(), {}, "Hi, mom!", game)
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, {}, "Hi, mom!", game)
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
end).to.throw()
end)
@ -34,8 +39,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT", nil)
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT", nil)
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(instanceMap:size() == 0, "expected instanceMap to be empty")
@ -60,8 +68,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -90,8 +101,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -122,8 +136,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
expect(size(unappliedPatch.added)).to.equal(1)
expect(unappliedPatch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
@ -153,8 +170,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
local instance = instanceMap.fromIds["ROOT"]
expect(instance.ClassName).to.equal("StringValue")
@ -196,8 +216,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -223,13 +246,16 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local existing = Instance.new("Folder")
existing.Name = "Existing"
instanceMap:insert("EXISTING", existing)
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -268,8 +294,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -307,8 +336,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -332,8 +364,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(not PatchSet.hasRemoves(unappliedPatch), "expected no removes")
assert(not PatchSet.hasAdditions(unappliedPatch), "expected no additions")

View file

@ -6,6 +6,7 @@ expression: contents
<Item class="Model" referent="0">
<Properties>
<string name="Name">init_meta_class_name</string>
<bool name="NeedsPivotMigration">false</bool>
</Properties>
</Item>
</roblox>

View file

@ -1,7 +1,6 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
@ -25,6 +24,7 @@ expression: contents
<R21>0</R21>
<R22>1</R22>
</CoordinateFrame>
<bool name="NeedsPivotMigration">false</bool>
<Ref name="PrimaryPart">null</Ref>
<BinaryString name="Tags"></BinaryString>
</Properties>

View file

@ -1,7 +1,6 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="DataModel" referent="0">
@ -22,6 +21,7 @@ expression: contents
<Item class="Workspace" referent="2">
<Properties>
<string name="Name">Workspace</string>
<bool name="NeedsPivotMigration">false</bool>
</Properties>
<Item class="BoolValue" referent="3">
<Properties>

View file

@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
@ -22,7 +21,8 @@ instances:
ignoreUnknownInstances: false
Name: test
Parent: id-2
Properties: {}
Properties:
NeedsPivotMigration:
Bool: false
messageCursor: 1
sessionId: id-1

View file

@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
---
messageCursor: 1
messages:
@ -14,8 +13,9 @@ messages:
ignoreUnknownInstances: false
Name: test
Parent: id-2
Properties: {}
Properties:
NeedsPivotMigration:
Bool: false
removed: []
updated: []
sessionId: id-1

View file

@ -0,0 +1,64 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children:
- id-3
ClassName: DataModel
Id: id-2
Metadata:
ignoreUnknownInstances: true
Name: pivot_migration
Parent: "00000000000000000000000000000000"
Properties: {}
id-3:
Children:
- id-4
- id-5
- id-6
ClassName: Workspace
Id: id-3
Metadata:
ignoreUnknownInstances: true
Name: Workspace
Parent: id-2
Properties:
NeedsPivotMigration:
Bool: false
id-4:
Children: []
ClassName: Model
Id: id-4
Metadata:
ignoreUnknownInstances: true
Name: Model
Parent: id-3
Properties:
NeedsPivotMigration:
Bool: false
id-5:
Children: []
ClassName: Tool
Id: id-5
Metadata:
ignoreUnknownInstances: false
Name: Tool
Parent: id-3
Properties:
NeedsPivotMigration:
Bool: false
id-6:
Children: []
ClassName: Actor
Id: id-6
Metadata:
ignoreUnknownInstances: true
Name: Actor
Parent: id-3
Properties:
NeedsPivotMigration:
Bool: false
messageCursor: 1
sessionId: id-1

View file

@ -0,0 +1,27 @@
---
source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
---
messageCursor: 1
messages:
- added:
id-6:
Children: []
ClassName: Actor
Id: id-6
Metadata:
ignoreUnknownInstances: true
Name: Actor
Parent: id-3
Properties:
NeedsPivotMigration:
Bool: false
removed: []
updated:
- changedClassName: ~
changedMetadata:
ignoreUnknownInstances: true
changedName: ~
changedProperties: {}
id: id-3
sessionId: id-1

View file

@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,6 +1,5 @@
---
source: tests/tests/serve.rs
assertion_line: 316
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,6 +1,5 @@
---
source: tests/tests/serve.rs
assertion_line: 335
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,6 +1,5 @@
---
source: tests/tests/serve.rs
assertion_line: 351
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -0,0 +1,52 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children:
- id-3
ClassName: DataModel
Id: id-2
Metadata:
ignoreUnknownInstances: true
Name: pivot_migration
Parent: "00000000000000000000000000000000"
Properties: {}
id-3:
Children:
- id-4
- id-5
ClassName: Workspace
Id: id-3
Metadata:
ignoreUnknownInstances: true
Name: Workspace
Parent: id-2
Properties:
NeedsPivotMigration:
Bool: false
id-4:
Children: []
ClassName: Model
Id: id-4
Metadata:
ignoreUnknownInstances: true
Name: Model
Parent: id-3
Properties:
NeedsPivotMigration:
Bool: false
id-5:
Children: []
ClassName: Tool
Id: id-5
Metadata:
ignoreUnknownInstances: false
Name: Tool
Parent: id-3
Properties:
NeedsPivotMigration:
Bool: false
messageCursor: 0
sessionId: id-1

View file

@ -0,0 +1,13 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: pivot_migration
protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -31,6 +31,8 @@ instances:
Attributes:
Rojo_Target_PrimaryPart:
String: project target
NeedsPivotMigration:
Bool: false
PrimaryPart:
Ref: id-9
id-2:
@ -55,7 +57,9 @@ instances:
ignoreUnknownInstances: true
Name: Workspace
Parent: id-2
Properties: {}
Properties:
NeedsPivotMigration:
Bool: false
id-4:
Children: []
ClassName: ObjectValue
@ -124,6 +128,8 @@ instances:
Attributes:
Rojo_Target_PrimaryPart:
String: model target 2
NeedsPivotMigration:
Bool: false
PrimaryPart:
Ref: id-7
id-9:
@ -138,4 +144,3 @@ instances:
Properties: {}
messageCursor: 1
sessionId: id-1

View file

@ -40,7 +40,9 @@ instances:
ignoreUnknownInstances: true
Name: Workspace
Parent: id-2
Properties: {}
Properties:
NeedsPivotMigration:
Bool: false
id-4:
Children: []
ClassName: ObjectValue
@ -104,6 +106,8 @@ instances:
Attributes:
Rojo_Target_PrimaryPart:
String: model target
NeedsPivotMigration:
Bool: false
PrimaryPart:
Ref: id-7
id-9:
@ -118,4 +122,3 @@ instances:
Properties: {}
messageCursor: 0
sessionId: id-1

View file

@ -10,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -40,7 +40,9 @@ instances:
ignoreUnknownInstances: true
Name: Workspace
Parent: id-2
Properties: {}
Properties:
NeedsPivotMigration:
Bool: false
id-4:
Children: []
ClassName: ObjectValue
@ -104,8 +106,12 @@ instances:
Attributes:
Rojo_Target_PrimaryPart:
String: model target
NeedsPivotMigration:
Bool: false
PrimaryPart:
Ref: id-7
Scale:
Float32: 1
id-9:
Children:
- id-10
@ -116,5 +122,5 @@ instances:
Name: ProjectTarget
Parent: id-3
Properties: {}
messageCursor: 0
messageCursor: 1
sessionId: id-1

View file

@ -40,7 +40,9 @@ instances:
ignoreUnknownInstances: true
Name: Workspace
Parent: id-2
Properties: {}
Properties:
NeedsPivotMigration:
Bool: false
id-4:
Children: []
ClassName: ObjectValue
@ -104,6 +106,8 @@ instances:
Attributes:
Rojo_Target_PrimaryPart:
String: model target
NeedsPivotMigration:
Bool: false
PrimaryPart:
Ref: id-7
id-9:

View file

@ -10,3 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -0,0 +1,17 @@
---
source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
---
messageCursor: 1
messages:
- added: {}
removed: []
updated:
- changedClassName: ~
changedMetadata: ~
changedName: ~
changedProperties:
Scale:
Float32: 1
id: id-8
sessionId: id-1

View file

@ -10,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -18,6 +18,8 @@ messages:
Attributes:
Rojo_Target_PrimaryPart:
String: project target
NeedsPivotMigration:
Bool: false
PrimaryPart:
Ref: id-9
removed: []
@ -43,4 +45,3 @@ messages:
PrimaryPart: ~
id: id-8
sessionId: id-1

View file

@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,6 +1,5 @@
---
source: tests/tests/serve.rs
assertion_line: 265
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,6 +1,5 @@
---
source: tests/tests/serve.rs
assertion_line: 281
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,6 +1,5 @@
---
source: tests/tests/serve.rs
assertion_line: 297
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
@ -11,4 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -1,6 +1,5 @@
---
source: tests/tests/serve.rs
assertion_line: 383
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
@ -11,3 +10,4 @@ protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1
unexpectedPlaceIds: ~

View file

@ -0,0 +1,3 @@
{
"className": "Tool"
}

View file

@ -0,0 +1,14 @@
{
"name": "pivot_migration",
"tree": {
"$className": "DataModel",
"Workspace": {
"Model": {
"$className": "Model"
},
"Tool": {
"$path": "Tool.model.json"
}
}
}
}

View file

@ -8,7 +8,7 @@ use clap::Parser;
use fs_err::File;
use memofs::Vfs;
use rayon::prelude::*;
use rbx_dom_weak::types::Ref;
use rbx_dom_weak::{types::Ref, Ustr};
use serde::Serialize;
use tokio::runtime::Runtime;
@ -26,7 +26,7 @@ const PATH_STRIP_FAILED_ERR: &str = "Failed to create relative paths for project
#[serde(rename_all = "camelCase")]
struct SourcemapNode<'a> {
name: &'a str,
class_name: &'a str,
class_name: Ustr,
#[serde(skip_serializing_if = "Vec::is_empty")]
file_paths: Vec<PathBuf>,
@ -113,7 +113,7 @@ fn filter_nothing(_instance: &InstanceWithMeta) -> bool {
fn filter_non_scripts(instance: &InstanceWithMeta) -> bool {
matches!(
instance.class_name(),
instance.class_name().as_str(),
"Script" | "LocalScript" | "ModuleScript"
)
}

View file

@ -7,6 +7,7 @@ use std::{
};
use memofs::Vfs;
use rbx_dom_weak::{Ustr, UstrMap};
use serde::{Deserialize, Serialize};
use thiserror::Error;
@ -78,6 +79,14 @@ pub struct Project {
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_place_ids: Option<HashSet<u64>>,
/// If specified, contains a set of place IDs that this project is
/// not compatible with when doing live sync.
///
/// This setting is intended to help prevent syncing a Rojo project into the
/// wrong Roblox place.
#[serde(skip_serializing_if = "Option::is_none")]
pub blocked_place_ids: Option<HashSet<u64>>,
/// If specified, sets the current place's place ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
@ -310,7 +319,7 @@ pub struct ProjectNode {
/// `$className` CANNOT be set if `$path` is set and the instance described
/// by that path has a ClassName other than Folder.
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
pub class_name: Option<Ustr>,
/// If set, defines an ID for the described Instance that can be used
/// to refer to it for the purpose of referent properties.
@ -329,7 +338,7 @@ pub struct ProjectNode {
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub properties: HashMap<String, UnresolvedValue>,
pub properties: UstrMap<UnresolvedValue>,
#[serde(
rename = "$attributes",

View file

@ -2,8 +2,8 @@ use std::borrow::Borrow;
use anyhow::{bail, format_err};
use rbx_dom_weak::types::{
Attributes, CFrame, Color3, Content, Enum, Font, MaterialColors, Matrix3, Tags, Variant,
VariantType, Vector2, Vector3,
Attributes, CFrame, Color3, Content, ContentId, Enum, Font, MaterialColors, Matrix3, Tags,
Variant, VariantType, Vector2, Vector3,
};
use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize};
@ -116,6 +116,9 @@ impl AmbiguousValue {
(VariantType::Content, AmbiguousValue::String(value)) => {
Ok(Content::from(value).into())
}
(VariantType::ContentId, AmbiguousValue::String(value)) => {
Ok(ContentId::from(value).into())
}
(VariantType::Vector2, AmbiguousValue::Array2(value)) => {
Ok(Vector2::new(value[0] as f32, value[1] as f32).into())
@ -275,10 +278,20 @@ mod test {
Variant::String("Hello!".into()),
);
// String literals can also turn into Content
// String literals can also turn into ContentId
assert_eq!(
resolve("Sky", "MoonTextureId", "\"rbxassetid://12345\""),
Variant::Content("rbxassetid://12345".into()),
Variant::ContentId("rbxassetid://12345".into()),
);
// String literals can turn into Content!
assert_eq!(
resolve(
"MeshPart",
"MeshContent",
"\"rbxasset://totally-a-real-uri.tiff\""
),
Variant::Content("rbxasset://totally-a-real-uri.tiff".into())
);
// What about BinaryString values? For forward-compatibility reasons, we

View file

@ -208,6 +208,10 @@ impl ServeSession {
self.root_project.serve_place_ids.as_ref()
}
pub fn blocked_place_ids(&self) -> Option<&HashSet<u64>> {
self.root_project.blocked_place_ids.as_ref()
}
pub fn serve_address(&self) -> Option<IpAddr> {
self.root_project.serve_address
}

View file

@ -1,10 +1,10 @@
//! Defines the structure of an instance snapshot.
use std::{borrow::Cow, collections::HashMap};
use std::borrow::Cow;
use rbx_dom_weak::{
types::{Ref, Variant},
Instance, WeakDom,
ustr, AHashMap, HashMapExt as _, Instance, Ustr, UstrMap, WeakDom,
};
use serde::{Deserialize, Serialize};
@ -27,10 +27,10 @@ pub struct InstanceSnapshot {
pub name: Cow<'static, str>,
/// Corresponds to the ClassName property of the instance.
pub class_name: Cow<'static, str>,
pub class_name: Ustr,
/// All other properties of the instance, weakly-typed.
pub properties: HashMap<String, Variant>,
pub properties: UstrMap<Variant>,
/// The children of the instance represented as more snapshots.
///
@ -44,8 +44,8 @@ impl InstanceSnapshot {
snapshot_id: Ref::none(),
metadata: InstanceMetadata::default(),
name: Cow::Borrowed("DEFAULT"),
class_name: Cow::Borrowed("DEFAULT"),
properties: HashMap::new(),
class_name: ustr("DEFAULT"),
properties: UstrMap::new(),
children: Vec::new(),
}
}
@ -57,23 +57,23 @@ impl InstanceSnapshot {
}
}
pub fn class_name(self, class_name: impl Into<String>) -> Self {
pub fn class_name<S: Into<Ustr>>(self, class_name: S) -> Self {
Self {
class_name: Cow::Owned(class_name.into()),
class_name: class_name.into(),
..self
}
}
pub fn property<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
K: Into<Ustr>,
V: Into<Variant>,
{
self.properties.insert(key.into(), value.into());
self
}
pub fn properties(self, properties: impl Into<HashMap<String, Variant>>) -> Self {
pub fn properties(self, properties: impl Into<UstrMap<Variant>>) -> Self {
Self {
properties: properties.into(),
..self
@ -107,7 +107,7 @@ impl InstanceSnapshot {
Self::from_raw_tree(&mut raw_tree, id)
}
fn from_raw_tree(raw_tree: &mut HashMap<Ref, Instance>, id: Ref) -> Self {
fn from_raw_tree(raw_tree: &mut AHashMap<Ref, Instance>, id: Ref) -> Self {
let instance = raw_tree
.remove(&id)
.expect("instance did not exist in tree");
@ -122,7 +122,7 @@ impl InstanceSnapshot {
snapshot_id: id,
metadata: InstanceMetadata::default(),
name: Cow::Owned(instance.name),
class_name: Cow::Owned(instance.class),
class_name: instance.class,
properties: instance.properties,
children,
}

View file

@ -1,8 +1,9 @@
//! Defines the data structures used for describing instance patches.
use std::collections::HashMap;
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{
types::{Ref, Variant},
HashMapExt as _, Ustr, UstrMap,
};
use serde::{Deserialize, Serialize};
use super::{InstanceMetadata, InstanceSnapshot};
@ -41,11 +42,11 @@ pub struct PatchAdd {
pub struct PatchUpdate {
pub id: Ref,
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
pub changed_class_name: Option<Ustr>,
/// Contains all changed properties. If a property is assigned to `None`,
/// then that property has been removed.
pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_properties: UstrMap<Option<Variant>>,
/// Changed Rojo-specific metadata, if any of it changed.
pub changed_metadata: Option<InstanceMetadata>,
@ -88,8 +89,8 @@ pub struct AppliedPatchUpdate {
// TODO: Store previous values in order to detect application conflicts
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_class_name: Option<Ustr>,
pub changed_properties: UstrMap<Option<Variant>>,
pub changed_metadata: Option<InstanceMetadata>,
}
@ -99,7 +100,7 @@ impl AppliedPatchUpdate {
id,
changed_name: None,
changed_class_name: None,
changed_properties: HashMap::new(),
changed_properties: UstrMap::new(),
changed_metadata: None,
}
}

View file

@ -5,7 +5,10 @@ use std::{
mem::take,
};
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{
types::{Ref, Variant},
ustr, Ustr,
};
use super::{
patch::{AppliedPatchSet, AppliedPatchUpdate, PatchSet, PatchUpdate},
@ -76,7 +79,7 @@ struct PatchApplyContext {
/// Tracks all ref properties that were specified using attributes. This has
/// to be handled after everything else is done just like normal referent
/// properties.
attribute_refs_to_rewrite: MultiMap<Ref, (String, String)>,
attribute_refs_to_rewrite: MultiMap<Ref, (Ustr, String)>,
/// The current applied patch result, describing changes made to the tree.
applied_patch_set: AppliedPatchSet,
@ -193,7 +196,7 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
}
if let Some(class_name) = patch.changed_class_name {
*instance.class_name_mut() = class_name.clone();
instance.set_class_name(class_name);
applied_patch.changed_class_name = Some(class_name);
}
@ -219,10 +222,10 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
instance
.properties_mut()
.insert(key.clone(), Variant::Ref(new_referent));
.insert(key, Variant::Ref(new_referent));
}
Some(ref value) => {
instance.properties_mut().insert(key.clone(), value.clone());
instance.properties_mut().insert(key, value.clone());
}
None => {
instance.properties_mut().remove(&key);
@ -247,7 +250,7 @@ fn defer_ref_properties(tree: &mut RojoTree, id: Ref, context: &mut PatchApplyCo
let instance = tree
.get_instance(id)
.expect("Instances should exist when calculating deferred refs");
let attributes = match instance.properties().get("Attributes") {
let attributes = match instance.properties().get(&ustr("Attributes")) {
Some(Variant::Attributes(attrs)) => attrs,
_ => return,
};
@ -275,12 +278,12 @@ fn defer_ref_properties(tree: &mut RojoTree, id: Ref, context: &mut PatchApplyCo
if let Variant::String(prop_value) = attr_value {
context
.attribute_refs_to_rewrite
.insert(id, (prop_name.to_owned(), prop_value.clone()));
.insert(id, (ustr(prop_name), prop_value.clone()));
} else if let Variant::BinaryString(prop_value) = attr_value {
if let Ok(str) = std::str::from_utf8(prop_value.as_ref()) {
context
.attribute_refs_to_rewrite
.insert(id, (prop_name.to_owned(), str.to_string()));
.insert(id, (ustr(prop_name), str.to_string()));
} else {
log::error!("IDs specified by referent property attributes must be valid UTF-8 strings.")
}
@ -304,7 +307,7 @@ mod test {
use std::borrow::Cow;
use rbx_dom_weak::types::Variant;
use rbx_dom_weak::{types::Variant, UstrMap};
use super::super::PatchAdd;
@ -320,8 +323,8 @@ mod test {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("Foo"),
class_name: Cow::Borrowed("Bar"),
properties: [("Baz".to_owned(), Variant::Int32(5))].into(),
class_name: ustr("Bar"),
properties: UstrMap::from_iter([(ustr("Baz"), Variant::Int32(5))]),
children: Vec::new(),
};
@ -340,7 +343,7 @@ mod test {
let child_instance = tree.get_instance(child_id).unwrap();
assert_eq!(child_instance.name(), &snapshot.name);
assert_eq!(child_instance.class_name(), &snapshot.class_name);
assert_eq!(child_instance.class_name(), snapshot.class_name);
assert_eq!(child_instance.properties(), &snapshot.properties);
assert!(child_instance.children().is_empty());
}
@ -363,16 +366,15 @@ mod test {
let patch = PatchUpdate {
id: root_id,
changed_name: Some("Foo".to_owned()),
changed_class_name: Some("NewClassName".to_owned()),
changed_properties: [
changed_class_name: Some(ustr("NewClassName")),
changed_properties: UstrMap::from_iter([
// The value of Foo has changed
("Foo".to_owned(), Some(Variant::Int32(8))),
(ustr("Foo"), Some(Variant::Int32(8))),
// Bar has been deleted
("Bar".to_owned(), None),
(ustr("Bar"), None),
// Baz has been added
("Baz".to_owned(), Some(Variant::Int32(10))),
]
.into(),
(ustr("Baz"), Some(Variant::Int32(10))),
]),
changed_metadata: None,
};
@ -383,12 +385,11 @@ mod test {
apply_patch_set(&mut tree, patch_set);
let expected_properties = [
("Foo".to_owned(), Variant::Int32(8)),
("Baz".to_owned(), Variant::Int32(10)),
("Unchanged".to_owned(), Variant::Int32(-5)),
]
.into();
let expected_properties = UstrMap::from_iter([
(ustr("Foo"), Variant::Int32(8)),
(ustr("Baz"), Variant::Int32(10)),
(ustr("Unchanged"), Variant::Int32(-5)),
]);
let root_instance = tree.get_instance(root_id).unwrap();
assert_eq!(root_instance.name(), "Foo");

View file

@ -1,12 +1,12 @@
//! Defines the algorithm for computing a roughly-minimal patch set given an
//! existing instance tree and an instance snapshot.
use std::{
collections::{HashMap, HashSet},
mem::take,
};
use std::{collections::HashMap, mem::take};
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{
types::{Ref, Variant},
ustr, HashMapExt as _, UstrMap, UstrSet,
};
use crate::{RojoRef, REF_POINTER_ATTRIBUTE_PREFIX};
@ -99,8 +99,8 @@ fn compute_property_patches(
patch_set: &mut PatchSet,
tree: &RojoTree,
) {
let mut visited_properties = HashSet::new();
let mut changed_properties = HashMap::new();
let mut visited_properties = UstrSet::default();
let mut changed_properties = UstrMap::new();
let attribute_ref_properties = compute_ref_properties(snapshot, tree);
@ -113,7 +113,7 @@ fn compute_property_patches(
let changed_class_name = if snapshot.class_name == instance.class_name() {
None
} else {
Some(take(&mut snapshot.class_name).into_owned())
Some(take(&mut snapshot.class_name))
};
let changed_metadata = if &snapshot.metadata == instance.metadata() {
@ -123,7 +123,7 @@ fn compute_property_patches(
};
for (name, snapshot_value) in take(&mut snapshot.properties) {
visited_properties.insert(name.clone());
visited_properties.insert(name);
match instance.properties().get(&name) {
Some(instance_value) => {
@ -138,11 +138,11 @@ fn compute_property_patches(
}
for name in instance.properties().keys() {
if visited_properties.contains(name.as_str()) {
if visited_properties.contains(name) {
continue;
}
changed_properties.insert(name.clone(), None);
changed_properties.insert(*name, None);
}
for (name, ref_value) in attribute_ref_properties {
@ -163,6 +163,34 @@ fn compute_property_patches(
}
}
// !!!!!!!!!! UGLY HACK !!!!!!!!!!
//
// See RojoTree::insert_instance. Adjust that code also if you are touching this.
let actual_class = changed_class_name.unwrap_or(instance.class_name());
match actual_class.as_str() {
"Model" | "Actor" | "Tool" | "HopperBin" | "Flag" | "WorldModel" | "Workspace"
| "Status" => {
let migration_prop = ustr("NeedsPivotMigration");
// We want to just ignore this if it's being removed by a patch.
// Normally this would not matter because serving != building but
// if we start syncing models using SerializationService
// (or GetObjects) it will affect how Studio deserializes things.
if !instance.properties().contains_key(&migration_prop) {
changed_properties.insert(migration_prop, Some(Variant::Bool(false)));
}
match changed_properties.get(&migration_prop) {
Some(Some(Variant::Bool(_))) => {}
Some(None) => {
changed_properties.remove(&migration_prop);
}
_ => {
changed_properties.insert(migration_prop, Some(Variant::Bool(false)));
}
}
}
_ => {}
};
if changed_properties.is_empty()
&& changed_name.is_none()
&& changed_class_name.is_none()
@ -250,9 +278,9 @@ fn compute_children_patches(
fn compute_ref_properties(
snapshot: &InstanceSnapshot,
tree: &RojoTree,
) -> HashMap<String, Option<Variant>> {
let mut map = HashMap::new();
let attributes = match snapshot.properties.get("Attributes") {
) -> UstrMap<Option<Variant>> {
let mut map = UstrMap::new();
let attributes = match snapshot.properties.get(&ustr("Attributes")) {
Some(Variant::Attributes(attrs)) => attrs,
_ => return map,
};
@ -284,9 +312,9 @@ fn compute_ref_properties(
}
};
if let Some(target_id) = tree.get_specified_id(&rojo_ref) {
map.insert(prop_name.to_string(), Some(Variant::Ref(target_id)));
map.insert(ustr(prop_name), Some(Variant::Ref(target_id)));
} else {
map.insert(prop_name.to_string(), None);
map.insert(ustr(prop_name), None);
}
}
@ -314,11 +342,11 @@ mod test {
let snapshot_id = Ref::new();
let snapshot = InstanceSnapshot {
snapshot_id,
properties: [("Self".to_owned(), Variant::Ref(snapshot_id))].into(),
properties: UstrMap::from_iter([(ustr("Self"), Variant::Ref(snapshot_id))]),
metadata: Default::default(),
name: Cow::Borrowed("foo"),
class_name: Cow::Borrowed("foo"),
class_name: ustr("foo"),
children: Vec::new(),
};
@ -329,7 +357,10 @@ mod test {
id: root_id,
changed_name: None,
changed_class_name: None,
changed_properties: [("Self".to_owned(), Some(Variant::Ref(root_id)))].into(),
changed_properties: UstrMap::from_iter([(
ustr("Self"),
Some(Variant::Ref(root_id)),
)]),
changed_metadata: None,
}],
added_instances: Vec::new(),
@ -353,19 +384,19 @@ mod test {
let snapshot = InstanceSnapshot {
snapshot_id,
children: vec![InstanceSnapshot {
properties: [("Self".to_owned(), Variant::Ref(snapshot_id))].into(),
properties: UstrMap::from_iter([(ustr("Self"), Variant::Ref(snapshot_id))]),
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("child"),
class_name: Cow::Borrowed("child"),
class_name: ustr("child"),
children: Vec::new(),
}],
metadata: Default::default(),
properties: HashMap::new(),
properties: UstrMap::new(),
name: Cow::Borrowed("foo"),
class_name: Cow::Borrowed("foo"),
class_name: ustr("foo"),
};
let patch_set = compute_patch_set(Some(snapshot), &tree, root_id);
@ -376,9 +407,9 @@ mod test {
instance: InstanceSnapshot {
snapshot_id: Ref::none(),
metadata: Default::default(),
properties: [("Self".to_owned(), Variant::Ref(root_id))].into(),
properties: UstrMap::from_iter([(ustr("Self"), Variant::Ref(root_id))]),
name: Cow::Borrowed("child"),
class_name: Cow::Borrowed("child"),
class_name: ustr("child"),
children: Vec::new(),
},
}],

View file

@ -1,5 +1,6 @@
use insta::assert_yaml_snapshot;
use rbx_dom_weak::{ustr, UstrMap};
use rojo_insta_ext::RedactionMap;
use crate::{
@ -18,7 +19,7 @@ fn set_name_and_class_name() {
updated_instances: vec![PatchUpdate {
id: tree.get_root_id(),
changed_name: Some("Hello, world!".to_owned()),
changed_class_name: Some("Folder".to_owned()),
changed_class_name: Some(ustr("Folder")),
changed_properties: Default::default(),
changed_metadata: None,
}],
@ -46,7 +47,7 @@ fn add_property() {
id: tree.get_root_id(),
changed_name: None,
changed_class_name: None,
changed_properties: [("Foo".to_owned(), Some("Value of Foo".into()))].into(),
changed_properties: UstrMap::from_iter([(ustr("Foo"), Some("Value of Foo".into()))]),
changed_metadata: None,
}],
..Default::default()
@ -74,7 +75,7 @@ fn remove_property() {
root_instance
.properties_mut()
.insert("Foo".to_owned(), "Should be removed".into());
.insert(ustr("Foo"), "Should be removed".into());
}
let tree_view = view_tree(&tree, &mut redactions);
@ -85,7 +86,7 @@ fn remove_property() {
id: tree.get_root_id(),
changed_name: None,
changed_class_name: None,
changed_properties: [("Foo".to_owned(), None)].into(),
changed_properties: UstrMap::from_iter([(ustr("Foo"), None)]),
changed_metadata: None,
}],
..Default::default()

View file

@ -2,7 +2,7 @@ use std::borrow::Cow;
use insta::assert_yaml_snapshot;
use rbx_dom_weak::types::Ref;
use rbx_dom_weak::{types::Ref, ustr, UstrMap};
use rojo_insta_ext::RedactionMap;
use crate::snapshot::{compute_patch_set, InstanceSnapshot, RojoTree};
@ -18,7 +18,7 @@ fn set_name_and_class_name() {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("Some Folder"),
class_name: Cow::Borrowed("Folder"),
class_name: ustr("Folder"),
properties: Default::default(),
children: Vec::new(),
};
@ -40,8 +40,8 @@ fn set_property() {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
properties: [("PropertyName".into(), "Hello, world!".into())].into(),
class_name: ustr("ROOT"),
properties: UstrMap::from_iter([(ustr("PropertyName"), "Hello, world!".into())]),
children: Vec::new(),
};
@ -61,17 +61,16 @@ fn remove_property() {
{
let root_id = tree.get_root_id();
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
root_instance.properties_mut().insert(
"Foo".to_owned(),
"This should be removed by the patch.".into(),
);
root_instance
.properties_mut()
.insert(ustr("Foo"), "This should be removed by the patch.".into());
}
let snapshot = InstanceSnapshot {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
class_name: ustr("ROOT"),
properties: Default::default(),
children: Vec::new(),
};
@ -93,13 +92,13 @@ fn add_child() {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
class_name: ustr("ROOT"),
properties: Default::default(),
children: vec![InstanceSnapshot {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("New"),
class_name: Cow::Borrowed("Folder"),
class_name: ustr("Folder"),
properties: Default::default(),
children: Vec::new(),
}],
@ -132,7 +131,7 @@ fn remove_child() {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
class_name: ustr("ROOT"),
properties: Default::default(),
children: Vec::new(),
};

View file

@ -5,7 +5,7 @@ use std::{
use rbx_dom_weak::{
types::{Ref, Variant},
Instance, InstanceBuilder, WeakDom,
ustr, Instance, InstanceBuilder, Ustr, UstrMap, WeakDom,
};
use crate::{multimap::MultiMap, RojoRef};
@ -94,9 +94,33 @@ impl RojoTree {
}
pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
// !!!!!!!!!! UGLY HACK !!!!!!!!!!
// ! If you are going to change this, go change it in patch_compute/compute_property_patches too
//
// This is a set of special cases working around a more general problem upstream
// in rbx-dom that causes pivots to not build to file correctly, described in
// github.com/rojo-rbx/rojo/issues/628 and github.com/rojo-rbx/rbx-dom/issues/385
//
// We need to insert the NeedsPivotMigration property with a value of false on
// every instance that inherits from Model for pivots to build correctly.
let hack_needs_pivot_migration = match snapshot.class_name.as_ref() {
// This is not a future proof way to do this but the last time a
// descendant of Model was added was in 2020 so it's probably fine.
"Model" | "Actor" | "Tool" | "HopperBin" | "Flag" | "WorldModel" | "Workspace"
| "Status"
if !snapshot
.properties
.contains_key(&ustr("NeedsPivotMigration")) =>
{
vec![("NeedsPivotMigration", Variant::Bool(false))]
}
_ => Vec::new(),
};
let builder = InstanceBuilder::empty()
.with_class(snapshot.class_name.into_owned())
.with_class(snapshot.class_name)
.with_name(snapshot.name.into_owned())
.with_properties(hack_needs_pivot_migration)
.with_properties(snapshot.properties);
let referent = self.inner.insert(parent_ref, builder);
@ -283,11 +307,11 @@ impl<'a> InstanceWithMeta<'a> {
&self.instance.name
}
pub fn class_name(&self) -> &'a str {
&self.instance.class
pub fn class_name(&self) -> Ustr {
self.instance.class
}
pub fn properties(&self) -> &'a HashMap<String, Variant> {
pub fn properties(&self) -> &'a UstrMap<Variant> {
&self.instance.properties
}
@ -328,15 +352,15 @@ impl InstanceWithMetaMut<'_> {
&self.instance.class
}
pub fn class_name_mut(&mut self) -> &mut String {
&mut self.instance.class
pub fn set_class_name<'a, S: Into<&'a str>>(&mut self, new_class: S) {
self.instance.class = ustr(new_class.into());
}
pub fn properties(&self) -> &HashMap<String, Variant> {
pub fn properties(&self) -> &UstrMap<Variant> {
&self.instance.properties
}
pub fn properties_mut(&mut self) -> &mut HashMap<String, Variant> {
pub fn properties_mut(&mut self) -> &mut UstrMap<Variant> {
&mut self.instance.properties
}

View file

@ -2,6 +2,7 @@ use std::{collections::BTreeMap, path::Path};
use anyhow::Context;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::ustr;
use serde::Serialize;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@ -30,7 +31,7 @@ pub fn snapshot_csv(
let mut snapshot = InstanceSnapshot::new()
.name(name)
.class_name("LocalizationTable")
.properties([("Contents".to_owned(), table_contents.into())])
.property(ustr("Contents"), table_contents)
.metadata(
InstanceMetadata::new()
.instigating_source(path)

View file

@ -2,6 +2,7 @@ use std::path::Path;
use anyhow::Context;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::ustr;
use crate::{
lua_ast::{Expression, Statement},
@ -23,14 +24,12 @@ pub fn snapshot_json(
let as_lua = json_to_lua(value).to_string();
let properties = [("Source".to_owned(), as_lua.into())];
let meta_path = path.with_file_name(format!("{}.meta.json", name));
let mut snapshot = InstanceSnapshot::new()
.name(name)
.class_name("ModuleScript")
.properties(properties)
.property(ustr("Source"), as_lua)
.metadata(
InstanceMetadata::new()
.instigating_source(path)

View file

@ -2,7 +2,10 @@ use std::{borrow::Cow, collections::HashMap, path::Path, str};
use anyhow::Context;
use memofs::Vfs;
use rbx_dom_weak::types::{Attributes, Ref};
use rbx_dom_weak::{
types::{Attributes, Ref},
HashMapExt as _, Ustr, UstrMap,
};
use serde::Deserialize;
use crate::{
@ -68,7 +71,7 @@ struct JsonModel {
name: Option<String>,
#[serde(alias = "ClassName")]
class_name: String,
class_name: Ustr,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
@ -82,10 +85,10 @@ struct JsonModel {
#[serde(
alias = "Properties",
default = "HashMap::new",
default = "UstrMap::new",
skip_serializing_if = "HashMap::is_empty"
)]
properties: HashMap<String, UnresolvedValue>,
properties: UstrMap<UnresolvedValue>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
attributes: HashMap<String, UnresolvedValue>,
@ -93,7 +96,7 @@ struct JsonModel {
impl JsonModel {
fn into_snapshot(self) -> anyhow::Result<InstanceSnapshot> {
let name = self.name.unwrap_or_else(|| self.class_name.clone());
let name = self.name.unwrap_or_else(|| self.class_name.to_owned());
let class_name = self.class_name;
let mut children = Vec::with_capacity(self.children.len());
@ -101,7 +104,7 @@ impl JsonModel {
children.push(child.into_snapshot()?);
}
let mut properties = HashMap::with_capacity(self.properties.len());
let mut properties = UstrMap::with_capacity(self.properties.len());
for (key, unresolved) in self.properties {
let value = unresolved.resolve(&class_name, &key)?;
properties.insert(key, value);
@ -122,7 +125,7 @@ impl JsonModel {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Owned(name),
class_name: Cow::Owned(class_name),
class_name,
properties,
children,
})

View file

@ -1,7 +1,7 @@
use std::{collections::HashMap, path::Path, str};
use std::{path::Path, str};
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::types::Enum;
use rbx_dom_weak::{types::Enum, ustr, HashMapExt as _, UstrMap};
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@ -15,6 +15,11 @@ pub enum ScriptType {
Server,
Client,
Module,
Plugin,
LegacyServer,
LegacyClient,
RunContextServer,
RunContextClient,
}
/// Core routine for turning Lua files into snapshots.
@ -31,23 +36,38 @@ pub fn snapshot_lua(
.expect("Unable to get RunContext enums!")
.items;
let (class_name, run_context) = match (context.emit_legacy_scripts, script_type) {
(false, ScriptType::Server) => ("Script", run_context_enums.get("Server")),
(false, ScriptType::Client) => ("Script", run_context_enums.get("Client")),
(true, ScriptType::Server) => ("Script", run_context_enums.get("Legacy")),
(true, ScriptType::Client) => ("LocalScript", None),
(_, ScriptType::Module) => ("ModuleScript", None),
let (class_name, run_context) = match script_type {
ScriptType::Server => {
if context.emit_legacy_scripts {
("Script", run_context_enums.get("Legacy"))
} else {
("Script", run_context_enums.get("Server"))
}
}
ScriptType::Client => {
if context.emit_legacy_scripts {
("LocalScript", None)
} else {
("Script", run_context_enums.get("Client"))
}
}
ScriptType::Module => ("ModuleScript", None),
ScriptType::Plugin => ("Script", run_context_enums.get("Plugin")),
ScriptType::LegacyServer => ("Script", run_context_enums.get("Legacy")),
ScriptType::LegacyClient => ("LocalScript", None),
ScriptType::RunContextServer => ("Script", run_context_enums.get("Server")),
ScriptType::RunContextClient => ("Script", run_context_enums.get("Client")),
};
let contents = vfs.read_to_string_lf_normalized(path)?;
let contents_str = contents.as_str();
let mut properties = HashMap::with_capacity(2);
properties.insert("Source".to_owned(), contents_str.into());
let mut properties = UstrMap::with_capacity(2);
properties.insert(ustr("Source"), contents_str.into());
if let Some(run_context) = run_context {
properties.insert(
"RunContext".to_owned(),
ustr("RunContext"),
Enum::from_u32(run_context.to_owned()).into(),
);
}
@ -164,6 +184,29 @@ mod test {
});
}
#[test]
fn plugin_module_from_vfs() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot("/foo.plugin.lua", VfsSnapshot::file("Hello there!"))
.unwrap();
let vfs = Vfs::new(imfs);
let instance_snapshot = snapshot_lua(
&InstanceContext::with_emit_legacy_scripts(Some(false)),
&vfs,
Path::new("/foo.plugin.lua"),
"foo",
ScriptType::Plugin,
)
.unwrap()
.unwrap();
insta::with_settings!({ sort_maps => true }, {
insta::assert_yaml_snapshot!(instance_snapshot);
});
}
#[test]
fn class_server_from_vfs() {
let mut imfs = InMemoryFs::new();

View file

@ -1,7 +1,7 @@
use std::{borrow::Cow, collections::HashMap, path::PathBuf};
use std::{collections::HashMap, path::PathBuf};
use anyhow::{format_err, Context};
use rbx_dom_weak::types::Attributes;
use rbx_dom_weak::{types::Attributes, Ustr, UstrMap};
use serde::{Deserialize, Serialize};
use crate::{resolution::UnresolvedValue, snapshot::InstanceSnapshot, RojoRef};
@ -23,7 +23,7 @@ pub struct AdjacentMetadata {
pub ignore_unknown_instances: Option<bool>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub properties: HashMap<String, UnresolvedValue>,
pub properties: UstrMap<UnresolvedValue>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub attributes: HashMap<String, UnresolvedValue>,
@ -117,13 +117,13 @@ pub struct DirectoryMetadata {
pub ignore_unknown_instances: Option<bool>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub properties: HashMap<String, UnresolvedValue>,
pub properties: UstrMap<UnresolvedValue>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub attributes: HashMap<String, UnresolvedValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
pub class_name: Option<Ustr>,
#[serde(skip)]
pub path: PathBuf,
@ -161,7 +161,7 @@ impl DirectoryMetadata {
));
}
snapshot.class_name = Cow::Owned(class_name);
snapshot.class_name = class_name;
}
Ok(())

View file

@ -202,6 +202,11 @@ pub enum Middleware {
ServerScript,
ClientScript,
ModuleScript,
PluginScript,
LegacyClientScript,
LegacyServerScript,
RunContextServerScript,
RunContextClientScript,
Project,
Rbxm,
Rbxmx,
@ -227,6 +232,19 @@ impl Middleware {
Self::ServerScript => snapshot_lua(context, vfs, path, name, ScriptType::Server),
Self::ClientScript => snapshot_lua(context, vfs, path, name, ScriptType::Client),
Self::ModuleScript => snapshot_lua(context, vfs, path, name, ScriptType::Module),
Self::PluginScript => snapshot_lua(context, vfs, path, name, ScriptType::Plugin),
Self::LegacyClientScript => {
snapshot_lua(context, vfs, path, name, ScriptType::LegacyClient)
}
Self::LegacyServerScript => {
snapshot_lua(context, vfs, path, name, ScriptType::LegacyServer)
}
Self::RunContextClientScript => {
snapshot_lua(context, vfs, path, name, ScriptType::RunContextClient)
}
Self::RunContextServerScript => {
snapshot_lua(context, vfs, path, name, ScriptType::RunContextServer)
}
Self::Project => snapshot_project(context, vfs, path, name),
Self::Rbxm => snapshot_rbxm(context, vfs, path, name),
Self::Rbxmx => snapshot_rbxmx(context, vfs, path, name),
@ -286,6 +304,8 @@ pub fn default_sync_rules() -> &'static [SyncRule] {
sync_rule!("*.server.luau", ServerScript, ".server.luau"),
sync_rule!("*.client.lua", ClientScript, ".client.lua"),
sync_rule!("*.client.luau", ClientScript, ".client.luau"),
sync_rule!("*.plugin.lua", PluginScript, ".plugin.lua"),
sync_rule!("*.plugin.luau", PluginScript, ".plugin.luau"),
sync_rule!("*.{lua,luau}", ModuleScript),
sync_rule!("*.project.json", Project, ".project.json"),
sync_rule!("*.model.json", JsonModel, ".model.json"),

View file

@ -1,8 +1,11 @@
use std::{borrow::Cow, collections::HashMap, path::Path};
use std::{borrow::Cow, path::Path};
use anyhow::{bail, Context};
use memofs::Vfs;
use rbx_dom_weak::types::{Attributes, Ref};
use rbx_dom_weak::{
types::{Attributes, Ref},
ustr, HashMapExt as _, Ustr, UstrMap,
};
use rbx_reflection::ClassTag;
use crate::{
@ -88,14 +91,10 @@ pub fn snapshot_project_node(
) -> anyhow::Result<Option<InstanceSnapshot>> {
let project_folder = project_path.parent().unwrap();
let class_name_from_project = node
.class_name
.as_ref()
.map(|name| Cow::Owned(name.clone()));
let mut class_name_from_path = None;
let name = Cow::Owned(instance_name.to_owned());
let mut properties = HashMap::new();
let mut properties = UstrMap::new();
let mut children = Vec::new();
let mut metadata = InstanceMetadata::new().context(context);
@ -136,7 +135,7 @@ pub fn snapshot_project_node(
let class_name_from_inference = infer_class_name(&name, parent_class);
let class_name = match (
class_name_from_project,
node.class_name,
class_name_from_path,
class_name_from_inference,
&node.path,
@ -249,7 +248,7 @@ pub fn snapshot_project_node(
_ => {}
}
properties.insert(key.clone(), value);
properties.insert(*key, value);
}
if !node.attributes.is_empty() {
@ -304,7 +303,7 @@ pub fn snapshot_project_node(
}))
}
fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<Cow<'static, str>> {
fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<Ustr> {
// If className wasn't defined from another source, we may be able
// to infer one.
@ -317,18 +316,18 @@ fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<Cow<'stati
let descriptor = rbx_reflection_database::get().classes.get(name)?;
if descriptor.tags.contains(&ClassTag::Service) {
return Some(Cow::Owned(name.to_owned()));
return Some(ustr(name));
}
} else if parent_class == "StarterPlayer" {
// StarterPlayer has two special members with their own classes.
if name == "StarterPlayerScripts" || name == "StarterCharacterScripts" {
return Some(Cow::Owned(name.to_owned()));
return Some(ustr(name));
}
} else if parent_class == "Workspace" {
// Workspace has a special Terrain class inside it
if name == "Terrain" {
return Some(Cow::Owned(name.to_owned()));
return Some(ustr(name));
}
}

View file

@ -0,0 +1,23 @@
---
source: src/snapshot_middleware/lua.rs
expression: instance_snapshot
---
snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
Path: /foo.plugin.lua
relevant_paths:
- /foo.plugin.lua
- /foo.meta.json
context:
emit_legacy_scripts: false
specified_id: ~
name: foo
class_name: Script
properties:
RunContext:
Enum: 3
Source:
String: Hello there!
children: []

View file

@ -2,6 +2,7 @@ use std::path::Path;
use anyhow::Context;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::ustr;
use crate::{
lua_ast::{Expression, Statement},
@ -23,14 +24,12 @@ pub fn snapshot_toml(
let as_lua = toml_to_lua(value).to_string();
let properties = [("Source".to_owned(), as_lua.into())];
let meta_path = path.with_file_name(format!("{}.meta.json", name));
let mut snapshot = InstanceSnapshot::new()
.name(name)
.class_name("ModuleScript")
.properties(properties)
.property(ustr("Source"), as_lua)
.metadata(
InstanceMetadata::new()
.instigating_source(path)

View file

@ -1,6 +1,7 @@
use std::{path::Path, str};
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::ustr;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@ -15,14 +16,12 @@ pub fn snapshot_txt(
let contents = vfs.read_to_string(path)?;
let contents_str = contents.as_str();
let properties = [("Value".to_owned(), contents_str.into())];
let meta_path = path.with_file_name(format!("{}.meta.json", name));
let mut snapshot = InstanceSnapshot::new()
.name(name)
.class_name("StringValue")
.properties(properties)
.property(ustr("Value"), contents_str)
.metadata(
InstanceMetadata::new()
.instigating_source(path)

View file

@ -1,9 +1,10 @@
//! Utiilty that helps redact nondeterministic information from trees so that
//! they can be part of snapshot tests.
use std::collections::HashMap;
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{
types::{Ref, Variant},
Ustr, UstrMap,
};
use rojo_insta_ext::RedactionMap;
use serde::Serialize;
@ -34,8 +35,8 @@ pub fn intern_tree(tree: &RojoTree, redactions: &mut RedactionMap) {
struct InstanceView {
id: Ref,
name: String,
class_name: String,
properties: HashMap<String, Variant>,
class_name: Ustr,
properties: UstrMap<Variant>,
metadata: InstanceMetadata,
children: Vec<InstanceView>,
}
@ -46,7 +47,7 @@ fn extract_instance_view(tree: &RojoTree, id: Ref) -> InstanceView {
InstanceView {
id: instance.id(),
name: instance.name().to_owned(),
class_name: instance.class_name().to_owned(),
class_name: instance.class_name(),
properties: instance.properties().clone(),
metadata: instance.metadata().clone(),
children: instance

View file

@ -64,6 +64,7 @@ impl ApiService {
session_id: self.serve_session.session_id(),
project_name: self.serve_session.project_name().to_owned(),
expected_place_ids: self.serve_session.serve_place_ids().cloned(),
unexpected_place_ids: self.serve_session.blocked_place_ids().cloned(),
place_id: self.serve_session.place_id(),
game_id: self.serve_session.game_id(),
root_instance_id,
@ -280,7 +281,7 @@ impl ApiService {
/// If this instance is represented by a script, try to find the correct .lua or .luau
/// file to open to edit it.
fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
match instance.class_name() {
match instance.class_name().as_str() {
"Script" | "LocalScript" | "ModuleScript" => {}
_ => return None,
}

View file

@ -7,7 +7,10 @@ use std::{
collections::{HashMap, HashSet},
};
use rbx_dom_weak::types::{Ref, Variant, VariantType};
use rbx_dom_weak::{
types::{Ref, Variant, VariantType},
Ustr, UstrMap,
};
use serde::{Deserialize, Serialize};
use crate::{
@ -84,12 +87,12 @@ impl<'a> SubscribeMessage<'a> {
pub struct InstanceUpdate {
pub id: Ref,
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
pub changed_class_name: Option<Ustr>,
// TODO: Transform from HashMap<String, Option<_>> to something else, since
// TODO: Transform from UstrMap<String, Option<_>> to something else, since
// null will get lost when decoding from JSON in some languages.
#[serde(default)]
pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_properties: UstrMap<Option<Variant>>,
pub changed_metadata: Option<InstanceMetadata>,
}
@ -113,8 +116,8 @@ pub struct Instance<'a> {
pub id: Ref,
pub parent: Ref,
pub name: Cow<'a, str>,
pub class_name: Cow<'a, str>,
pub properties: HashMap<String, Cow<'a, Variant>>,
pub class_name: Ustr,
pub properties: UstrMap<Cow<'a, Variant>>,
pub children: Cow<'a, [Ref]>,
pub metadata: Option<InstanceMetadata>,
}
@ -125,14 +128,14 @@ impl Instance<'_> {
.properties()
.iter()
.filter(|(_key, value)| property_filter(Some(value)))
.map(|(key, value)| (key.clone(), Cow::Borrowed(value)))
.map(|(key, value)| (*key, Cow::Borrowed(value)))
.collect();
Instance {
id: source.id(),
parent: source.parent(),
name: Cow::Borrowed(source.name()),
class_name: Cow::Borrowed(source.class_name()),
class_name: source.class_name(),
properties,
children: Cow::Borrowed(source.children()),
metadata: Some(InstanceMetadata::from_rojo_metadata(source.metadata())),
@ -157,6 +160,7 @@ pub struct ServerInfoResponse {
pub protocol_version: u64,
pub project_name: String,
pub expected_place_ids: Option<HashSet<u64>>,
pub unexpected_place_ids: Option<HashSet<u64>>,
pub game_id: Option<u64>,
pub place_id: Option<u64>,
pub root_instance_id: Ref,

View file

@ -119,7 +119,7 @@ impl UiService {
.map(|(key, value)| {
html! {
<div class="instance-property" title={ Self::display_value(value) }>
{ key.clone() } ": " { format!("{:?}", value.ty()) }
{ key.as_str() } ": " { format!("{:?}", value.ty()) }
</div>
}
})

View file

@ -502,12 +502,35 @@ fn ref_properties_patch_update() {
read_response.intern_and_redact(&mut redactions, root_id)
);
let project_path = session.path().join("default.project.json");
let mut project_content = fs::read(&project_path).unwrap();
// Adding a newline to force the change processor to pick up a change.
project_content.push(b'\n');
let target_path = session.path().join("ModelTarget.model.json");
fs::write(project_path, project_content).unwrap();
// Inserting scale just to force the change processor to run
fs::write(
target_path,
r#"{
"id": "model target",
"className": "Folder",
"children": [
{
"name": "ModelPointer",
"className": "Model",
"attributes": {
"Rojo_Target_PrimaryPart": "model target"
},
"properties": {
"Scale": 1
}
}
]
}"#,
)
.unwrap();
let subscribe_response = session.get_api_subscribe(0).unwrap();
assert_yaml_snapshot!(
"ref_properties_patch_update_subscribe",
subscribe_response.intern_and_redact(&mut redactions, ())
);
let read_response = session.get_api_read(root_id).unwrap();
assert_yaml_snapshot!(
@ -516,3 +539,55 @@ fn ref_properties_patch_update() {
);
});
}
#[test]
fn model_pivot_migration() {
run_serve_test("pivot_migration", |session, mut redactions| {
let info = session.get_api_rojo().unwrap();
let root_id = info.root_instance_id;
assert_yaml_snapshot!("pivot_migration_info", redactions.redacted_yaml(info));
let read_response = session.get_api_read(root_id).unwrap();
assert_yaml_snapshot!(
"pivot_migration_all",
read_response.intern_and_redact(&mut redactions, root_id)
);
let project_path = session.path().join("default.project.json");
fs::write(
project_path,
r#"{
"name": "pivot_migration",
"tree": {
"$className": "DataModel",
"Workspace": {
"Model": {
"$className": "Model"
},
"Tool": {
"$path": "Tool.model.json"
},
"Actor": {
"$className": "Actor"
}
}
}
}"#,
)
.unwrap();
let subscribe_response = session.get_api_subscribe(0).unwrap();
assert_yaml_snapshot!(
"model_pivot_migration_all",
subscribe_response.intern_and_redact(&mut redactions, ())
);
let read_response = session.get_api_read(root_id).unwrap();
assert_yaml_snapshot!(
"model_pivot_migration_all-2",
read_response.intern_and_redact(&mut redactions, root_id)
);
});
}