WIP: decoding + encoding

This commit is contained in:
Christian Treffs 2020-07-22 11:37:30 +02:00
parent 4ed52db56c
commit 6812e53d7a
No known key found for this signature in database
GPG Key ID: 49A4B4B460BE3ED4
9 changed files with 350 additions and 25 deletions

View File

@ -0,0 +1,101 @@
//
// Family+Codable.swift
//
//
// Created by Christian Treffs on 22.07.20.
//
private struct FamilyMemberContainer<R> where R: FamilyRequirementsManaging {
let components: [R.Components]
}
extension FamilyMemberContainer: Decodable where R: FamilyDecoding {
init(from decoder: Decoder) throws {
var familyContainer = try decoder.unkeyedContainer()
let strategy = decoder.userInfo[.nexusCodingStrategy] as? CodingStrategy ?? DefaultCodingStrategy()
self.components = try R.decode(componentsIn: &familyContainer, using: strategy)
}
}
extension FamilyMemberContainer: Encodable where R: FamilyEncoding {
func encode(to encoder: Encoder) throws {
let strategy = encoder.userInfo[.nexusCodingStrategy] as? CodingStrategy ?? DefaultCodingStrategy()
var familyContainer = encoder.unkeyedContainer()
try R.encode(components: components, into: &familyContainer, using: strategy)
}
}
// MARK: - encoding
public protocol TopLevelEncoder {
/// The type this encoder produces.
associatedtype Output
/// Encodes an instance of the indicated type.
///
/// - Parameter value: The instance to encode.
func encode<T>(_ value: T) throws -> Self.Output where T: Encodable
/// Contextual user-provided information for use during decoding.
var userInfo: [CodingUserInfoKey: Any] { get set }
}
extension Family where R: FamilyEncoding {
/// Encode family members (entities) to data using a given encoder.
///
/// The encoded members will *NOT* be removed from the nexus and will also stay present in this family.
/// - Parameter encoder: The data encoder. Data encoder respects the coding strategy set at `nexus.codingStrategy`.
/// - Returns: The encoded data.
public func encodeMembers<Encoder>(using encoder: inout Encoder) throws -> Encoder.Output where Encoder: TopLevelEncoder {
encoder.userInfo[.nexusCodingStrategy] = nexus.codingStrategy
let components: [R.Components] = self.map { $0 }
let container = FamilyMemberContainer<R>(components: components)
return try encoder.encode(container)
}
}
// MARK: - decoding
public protocol TopLevelDecoder {
/// The type this decoder accepts.
associatedtype Input
/// Decodes an instance of the indicated type.
func decode<T>(_ type: T.Type, from: Self.Input) throws -> T where T: Decodable
/// Contextual user-provided information for use during decoding.
var userInfo: [CodingUserInfoKey: Any] { get set }
}
extension Family where R: FamilyDecoding {
/// Decode family members (entities) from given data using a decoder.
///
/// The decoded members will be added to the nexus and will be present in this family.
/// - Parameters:
/// - data: The data decoded by decoder. A unkeyed container of family members (keyed component containers) is expected.
/// - decoder: The decoder to use for decoding family member data. Decoder respects the coding strategy set at `nexus.codingStrategy`.
public func decodeMembers<Decoder>(from data: Decoder.Input, using decoder: inout Decoder) throws where Decoder: TopLevelDecoder {
decoder.userInfo[.nexusCodingStrategy] = nexus.codingStrategy
let familyMembers = try decoder.decode(FamilyMemberContainer<R>.self, from: data)
for components in familyMembers.components {
createMember(with: components)
}
}
}
extension CodingUserInfoKey {
fileprivate static let nexusCodingStrategy = CodingUserInfoKey(rawValue: "nexusCodingStrategy")!
}
// MARK: - foundation
#if canImport(Foundation)
import class Foundation.JSONEncoder
import class Foundation.JSONDecoder
import class Foundation.PropertyListEncoder
import class Foundation.PropertyListDecoder
extension JSONEncoder: TopLevelEncoder { }
extension JSONDecoder: TopLevelDecoder { }
extension PropertyListEncoder: TopLevelEncoder { }
extension PropertyListDecoder: TopLevelDecoder { }
#endif

View File

@ -36,6 +36,18 @@ public struct Requires1<A>: FamilyRequirementsManaging where A: Component {
}
}
extension Requires1: FamilyDecoding where A: Decodable {
public static func decode(componentsIn container: KeyedDecodingContainer<DynamicCodingKey>, using strategy: CodingStrategy) throws -> A {
try container.decode(A.self, forKey: strategy.codingKey(for: A.self))
}
}
extension Requires1: FamilyEncoding where A: Encodable {
public static func encode(components: Components, into container: inout KeyedEncodingContainer<DynamicCodingKey>, using strategy: CodingStrategy) throws {
try container.encode(components, forKey: strategy.codingKey(for: A.self))
}
}
extension Nexus {
public func family<A>(
requires componentA: A.Type,

View File

@ -42,6 +42,21 @@ public struct Requires2<A, B>: FamilyRequirementsManaging where A: Component, B:
}
}
extension Requires2: FamilyEncoding where A: Encodable, B: Encodable {
public static func encode(components: (A, B), into container: inout KeyedEncodingContainer<DynamicCodingKey>, using strategy: CodingStrategy) throws {
try container.encode(components.0, forKey: strategy.codingKey(for: A.self))
try container.encode(components.1, forKey: strategy.codingKey(for: B.self))
}
}
extension Requires2: FamilyDecoding where A: Decodable, B: Decodable {
public static func decode(componentsIn container: KeyedDecodingContainer<DynamicCodingKey>, using strategy: CodingStrategy) throws -> (A, B) {
let compA = try container.decode(A.self, forKey: strategy.codingKey(for: A.self))
let compB = try container.decode(B.self, forKey: strategy.codingKey(for: B.self))
return Components(compA, compB)
}
}
extension Nexus {
public func family<A, B>(
requiresAll componentA: A.Type,

View File

@ -0,0 +1,69 @@
//
// FamilyRequirementsManaging.swift
//
//
// Created by Christian Treffs on 21.08.19.
//
public protocol FamilyRequirementsManaging {
associatedtype Components
associatedtype ComponentTypes
associatedtype EntityAndComponents
associatedtype RelativesDescending
init(_ types: ComponentTypes)
var componentTypes: [Component.Type] { get }
static func components(nexus: Nexus, entityId: EntityIdentifier) -> Components
static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> EntityAndComponents
static func relativesDescending(nexus: Nexus, parentId: EntityIdentifier, childId: EntityIdentifier) -> RelativesDescending
static func createMember(nexus: Nexus, components: Components) -> Entity
}
public protocol FamilyEncoding: FamilyRequirementsManaging {
static func encode(components: [Components], into container: inout UnkeyedEncodingContainer, using strategy: CodingStrategy) throws
static func encode(components: Components, into container: inout KeyedEncodingContainer<DynamicCodingKey>, using strategy: CodingStrategy) throws
}
extension FamilyEncoding {
public static func encode(components: [Components], into container: inout UnkeyedEncodingContainer, using strategy: CodingStrategy) throws {
for comps in components {
var container = container.nestedContainer(keyedBy: DynamicCodingKey.self)
try Self.encode(components: comps, into: &container, using: strategy)
}
}
}
public protocol FamilyDecoding: FamilyRequirementsManaging {
static func decode(componentsIn unkeyedContainer: inout UnkeyedDecodingContainer, using strategy: CodingStrategy) throws -> [Components]
static func decode(componentsIn container: KeyedDecodingContainer<DynamicCodingKey>, using strategy: CodingStrategy) throws -> Components
}
extension FamilyDecoding {
public static func decode(componentsIn unkeyedContainer: inout UnkeyedDecodingContainer, using strategy: CodingStrategy) throws -> [Components] {
var components = [Components]()
if let count = unkeyedContainer.count {
components.reserveCapacity(count)
}
while !unkeyedContainer.isAtEnd {
let container = try unkeyedContainer.nestedContainer(keyedBy: DynamicCodingKey.self)
let comps = try Self.decode(componentsIn: container, using: strategy)
components.append(comps)
}
return components
}
}
public protocol CodingStrategy {
func codingKey<C>(for componentType: C.Type) -> DynamicCodingKey where C: Component
}
public struct DynamicCodingKey: CodingKey {
public var intValue: Int?
public var stringValue: String
public init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
public init?(stringValue: String) { self.stringValue = stringValue }
}

View File

@ -1,23 +0,0 @@
//
// FamilyRequirementsManaging.swift
//
//
// Created by Christian Treffs on 21.08.19.
//
public protocol FamilyRequirementsManaging {
associatedtype Components
associatedtype ComponentTypes
associatedtype EntityAndComponents
associatedtype RelativesDescending
init(_ types: ComponentTypes)
var componentTypes: [Component.Type] { get }
static func components(nexus: Nexus, entityId: EntityIdentifier) -> Components
static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> EntityAndComponents
static func relativesDescending(nexus: Nexus, parentId: EntityIdentifier, childId: EntityIdentifier) -> RelativesDescending
static func createMember(nexus: Nexus, components: Components) -> Entity
}

View File

@ -31,6 +31,8 @@ public final class Nexus {
/// - Value: Tightly packed EntityIdentifiers that represent the association of an entity to the family.
@usableFromInline final var familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet<EntityIdentifier, EntityIdentifier.Id>]
public final var codingStrategy: CodingStrategy
public final weak var delegate: NexusEventDelegate?
public convenience init() {
@ -39,7 +41,8 @@ public final class Nexus {
componentsByEntity: [:],
entityIdGenerator: EntityIdentifierGenerator(),
familyMembersByTraits: [:],
childrenByParentEntity: [:])
childrenByParentEntity: [:],
codingStrategy: DefaultCodingStrategy())
}
internal init(entityStorage: UnorderedSparseSet<EntityIdentifier, EntityIdentifier.Id>,
@ -47,13 +50,15 @@ public final class Nexus {
componentsByEntity: [EntityIdentifier: Set<ComponentIdentifier>],
entityIdGenerator: EntityIdentifierGenerator,
familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet<EntityIdentifier, EntityIdentifier.Id>],
childrenByParentEntity: [EntityIdentifier: Set<EntityIdentifier>]) {
childrenByParentEntity: [EntityIdentifier: Set<EntityIdentifier>],
codingStrategy: CodingStrategy) {
self.entityStorage = entityStorage
self.componentsByType = componentsByType
self.componentIdsByEntity = componentsByEntity
self.familyMembersByTraits = familyMembersByTraits
self.childrenByParentEntity = childrenByParentEntity
self.entityIdGenerator = entityIdGenerator
self.codingStrategy = codingStrategy
}
deinit {
@ -76,3 +81,12 @@ extension Nexus: CustomDebugStringConvertible {
"<Nexus entities:\(numEntities) components:\(numComponents) families:\(numFamilies)>"
}
}
// MARK: - default coding strategy
public struct DefaultCodingStrategy: CodingStrategy {
public init() { }
public func codingKey<C>(for componentType: C.Type) -> DynamicCodingKey where C: Component {
DynamicCodingKey(stringValue: "\(C.self)")!
}
}

View File

@ -55,6 +55,28 @@ class Index: Component {
}
}
final class MyComponent: Component {
var name: String
var flag: Bool
init(name: String, flag: Bool) {
self.name = name
self.flag = flag
}
}
extension MyComponent: Decodable { }
extension MyComponent: Encodable { }
final class YourComponent: Component {
var number: Float
init(number: Float) {
self.number = number
}
}
extension YourComponent: Decodable { }
extension YourComponent: Encodable { }
final class SingleGameState: SingleComponent {
var shouldQuit: Bool = false
var playerHealth: Int = 67

View File

@ -0,0 +1,102 @@
//
// FamilyCodingTests.swift
//
//
// Created by Christian Treffs on 22.07.20.
//
import FirebladeECS
import XCTest
final class FamilyCodingTests: XCTestCase {
func testEncodingFamily1() throws {
let nexus = Nexus()
let family = nexus.family(requires: MyComponent.self)
family.createMember(with: MyComponent(name: "My Name", flag: true))
family.createMember(with: MyComponent(name: "Your Name", flag: false))
XCTAssertEqual(family.count, 2)
var jsonEncoder = JSONEncoder()
let encodedData = try family.encodeMembers(using: &jsonEncoder)
XCTAssertGreaterThanOrEqual(encodedData.count, 90)
}
func testDecodingFamily1() throws {
let jsonString = """
[
{
"MyComponent": {
"name": "My Name",
"flag": true
}
},
{
"MyComponent": {
"name": "Your Name",
"flag": false
}
}
]
"""
let jsonData = jsonString.data(using: .utf8)!
let nexus = Nexus()
let family = nexus.family(requires: MyComponent.self)
XCTAssertTrue(family.isEmpty)
var jsonDecoder = JSONDecoder()
try family.decodeMembers(from: jsonData, using: &jsonDecoder)
XCTAssertEqual(family.count, 2)
}
func testEncodeFamily2() throws {
let nexus = Nexus()
let family = nexus.family(requiresAll: MyComponent.self, YourComponent.self)
family.createMember(with: (MyComponent(name: "My Name", flag: true), YourComponent(number: 1.23)))
family.createMember(with: (MyComponent(name: "Your Name", flag: false), YourComponent(number: 3.45)))
XCTAssertEqual(family.count, 2)
var jsonEncoder = JSONEncoder()
let encodedData = try family.encodeMembers(using: &jsonEncoder)
XCTAssertGreaterThanOrEqual(encodedData.count, 190)
}
func testDecodingFamily2() throws {
let jsonString = """
[
{
"MyComponent": {
"name": "My Name",
"flag": true
},
"YourComponent": {
"number": 2.13
}
},
{
"MyComponent": {
"name": "Your Name",
"flag": false
},
"YourComponent": {
"number": 3.1415
}
}
]
"""
let jsonData = jsonString.data(using: .utf8)!
let nexus = Nexus()
let family = nexus.family(requiresAll: YourComponent.self, MyComponent.self)
XCTAssertTrue(family.isEmpty)
var jsonDecoder = JSONDecoder()
try family.decodeMembers(from: jsonData, using: &jsonDecoder)
XCTAssertEqual(family.count, 2)
}
}

View File

@ -34,6 +34,18 @@ extension EntityTests {
]
}
extension FamilyCodingTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__FamilyCodingTests = [
("testDecodingFamily1", testDecodingFamily1),
("testDecodingFamily2", testDecodingFamily2),
("testEncodeFamily2", testEncodeFamily2),
("testEncodingFamily1", testEncodingFamily1)
]
}
extension FamilyTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
@ -144,6 +156,7 @@ public func __allTests() -> [XCTestCaseEntry] {
testCase(ComponentIdentifierTests.__allTests__ComponentIdentifierTests),
testCase(ComponentTests.__allTests__ComponentTests),
testCase(EntityTests.__allTests__EntityTests),
testCase(FamilyCodingTests.__allTests__FamilyCodingTests),
testCase(FamilyTests.__allTests__FamilyTests),
testCase(FamilyTraitsTests.__allTests__FamilyTraitsTests),
testCase(HashingTests.__allTests__HashingTests),