{"id":19319157,"url":"https://github.com/ericrabil/corespeck","last_synced_at":"2025-11-09T10:03:38.914Z","repository":{"id":125575016,"uuid":"430278862","full_name":"EricRabil/CoreSpeck","owner":"EricRabil","description":null,"archived":false,"fork":false,"pushed_at":"2021-11-22T17:17:23.000Z","size":97,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-06T04:42:02.563Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/EricRabil.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-11-21T05:09:44.000Z","updated_at":"2021-11-22T17:17:26.000Z","dependencies_parsed_at":null,"dependency_job_id":"8bfdac2f-ce2e-44bc-8c4f-78f855f85281","html_url":"https://github.com/EricRabil/CoreSpeck","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EricRabil%2FCoreSpeck","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EricRabil%2FCoreSpeck/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EricRabil%2FCoreSpeck/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EricRabil%2FCoreSpeck/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/EricRabil","download_url":"https://codeload.github.com/EricRabil/CoreSpeck/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240420983,"owners_count":19798502,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-10T01:22:23.036Z","updated_at":"2025-11-09T10:03:33.877Z","avatar_url":"https://github.com/EricRabil.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CoreSpeck\n\nCoreSpeck is the framework driving Speck, a language-agnostic type management system.\n\nSpeck has four components:\n\n- [Importing](#Importing)\n- [Customizations](#Customizations)\n- [Type mashing \u0026 annotation processing](#Type Mashing)\n- [Transpilation](#Transpilation)\n\n## Importing\n\nSpeck provides APIs that make it easy to ingest arbitrary data, allowing you to build up your models from live data. This is especially useful for scenarios in which you do not have access to the original documentation, and wish to reconstruct an API model from responses. For your convenience, there is a reference implementation of an importer utilizing these APIs at [XMLImporter](Sources/CoreSpeck/Importers/XMLImporter.swift).\n\nYou are encouraged to import as many pieces of data as needed to get good coverage of the API you are documenting. This allows you to have representative types. There are annotations that exist which allow you to specify nullability, as well as make grouped nullability (via synthesized aggregates), in the event you have uneven responses.\n\nAn example of why you can never over-import is the iMessage protocol – it uses a nearly entirely flat structure, with groups of keys that are either always or never present, many keys that are optionally present, many keys that may be present or missing if a key is present, otherwise always missing, and keys that are simply always present. This creates a very complex serialized format, and by importing a large set of data you are able to perform complex customizations that restructure these payloads into comprehensible, nested types.\n\n### SpecBuilder\n\nThe import system is powered by [SpecBuilder](Sources/CoreSpeck/Importers/Builder/SpecBuilder.swift), an interface that allows you to construct a tree of data as you process the corresponding raw data.\n\n```swift\n/// SpecBuilders are an abstraction for scaffolding out a specification while parsing arbitrary data. There is a reference implementation of an XML importer that demonstrates how this can be used.\npublic protocol SpecBuilder {\n    var specType: SpecType { get }\n    \n    /// Descend into an array builder, optionally keyed depending on your current context\n    func pushArray(withKey key: String?) throws -\u003e SpecBuilder\n    /// Descend into a dictionary builder, optionally keyed depending on your current context\n    func pushDictionary(withKey key: String?) throws -\u003e SpecBuilder\n    /// Returns the parent, or self if this is the top\n    func moveOut() throws -\u003e SpecBuilder\n    /// Descend into a primitive builder, which you should immediately move back out of.\n    func pushPrimitive(withType type: SpecPrimitive, key: String?) throws -\u003e SpecBuilder\n}\n\npublic enum SpecBuilderError: Error {\n    /// Thrown when a key is not passed but the builder is a dictionary builder\n    case keyInconsistencyError\n    /// Thrown when a key is passed but the builder is an array builder\n    case arrayInconsistencyError\n    /// Thrown when attempting to write to a primitive builder. You should move back out of it, it is a terminal point.\n    case primitiveAbuseError\n}\n```\n\n## Customizations\n\nCustomizations allow you to have granular control over how your types are documented, transpiled, and handled in later processing stages. Their main, but not only, purpose is to add annotations to properties in a type to adjust how they are mashed. For example,\n\n```yml\nkind: Object\nname: 624B3339-B52C-4D10-9451-3893669A2FC3.plist\nmetadata:\n  hash: bf8d7b4d9eeb9bcebe94a3d2ccdbdef5\n  annotations:\n    ericrabil.com/xml-import-source: file:///Users/ericrabil/Documents/iMessagePayloads/624B3339-B52C-4D10-9451-3893669A2FC3.plist\nchildren:\n  v:\n    kind: Primitive\n    type: String\n  amrlc:\n    kind: Primitive\n    type: Integer\n  amt:\n    kind: Primitive\n    type: Integer\n  gid:\n    kind: Primitive\n    type: String\n  msi:\n    kind: Primitive\n    type: Data\n  amk:\n    kind: Primitive\n    type: String\n  gv:\n    kind: Primitive\n    type: String\n  p:\n    kind: Array\n    element:\n      kind: Primitive\n      type: String\n  amrln:\n    kind: Primitive\n    type: Integer\n  r:\n    kind: Primitive\n    type: String\n  gpru:\n    kind: Primitive\n    type: Integer\n  n:\n    kind: Primitive\n    type: String\n  pv:\n    kind: Primitive\n    type: Integer\n  t:\n    kind: Primitive\n    type: String\n```\n\nThe above spec represents an iMessage payload in a group chat, in which someone has sent a message acknowledgment. Here's what we know about this payload:\n\n`amrlc`, `amt`, `amk`, and `amrln` will either all be present, or all be absent. They are the bits that represent the message acknowledgment. Through this, we can write the following customization which will instruct the type masher to lift those four properties out to their own structure:\n\n```yaml\nkind: Customization\nname: IMAssociatedMessageWireAnnotations\ntarget:\n  children: |\n    amk:\n      kind: Primitive\n      type: String\npatches:\n  - op: add\n    path: /children/amk/metadata\n    value:\n      annotations:\n        ericrabil.com/type-group: IMMessageAssociation\n        ericrabil.com/readable-name: associatedMessageGUID\n      description: The GUID of the message this message is associated with.\n  - op: add\n    path: /children/amt/metadata\n    value:\n      annotations:\n        ericrabil.com/type-group: IMMessageAssociation\n        ericrabil.com/readable-name: associatedMessageType\n      description: The type of associated message being sent.\n  - op: add\n    path: /children/amrlc/metadata\n    value:\n      annotations:\n        ericrabil.com/type-group: IMMessageAssociation\n        ericrabil.com/readable-name: associatedMessageLowerBound\n      description: In a rich message, the first character (as in an attributed string) that is part of the association.\n  - op: add\n    path: /children/amrln/metadata\n    value:\n      annotations:\n        ericrabil.com/type-group: IMMessageAssociation\n        ericrabil.com/readable-name: associatedMessageUpperBound\n      description: In a rich message, the last character (as in an attributed string) that is part of the association.\n  - op: add\n    path: /children/messageAssociation\n    value:\n      kind: Reference\n      name: messageAssociation\n      aliasedName: IMMessageAssociation\n      aliasedKind: Object\n      metadata:\n        annotations:\n          ericrabil.com/type-group: IMMessage\n          ericrabil.com/synthesized-aggregate: IMMessageAssociation\n          ericrabil.com/value-nullable: true\n        description: If present, identifies the message this message should be correlated to.\n```\n\nIn this customization, we are targeting all payloads which contain `amk`, in which case we assert that `amt`, `amrlc`, and `amrln` will also be present. Each of these properties receive the following annotations:\n\n- `ericrabil.com/type-group`: This instructs the type masher to store the associated property within a type named `IMMessageAssociation`. If we wanted it to be in the root message, we could've put `IMMessage` instead, but for clarity we will lift them out to their own association object.\n- `ericrabil.com/readable-name`: This instructs code generators to use the associated text (i.e. `associatedMessageGUID` rather than `amk`) when defining the type, but to still use `amk` for de/serialization.\n\nAt the bottom of the customization, we add a new child to the acknowldegment message called `messageAssociation`. This object is known as a \"synthesized aggregate\", which means \"a type that is a logical collection of values from a pre-existing type\". However, because this collection of values is actually from another type, synthesized aggregates are de/serialized inline with their parent, rather than being nested. This allows you to maintain serialization compatibility, while creating order from chaos.\n\nHere's an example of Swift-generated code with and without a synthesized aggregate\n\n```swift\nstruct IMMessage: IMBaseMessage { \n    /// The unique identifier of this conversation.\n    var groupID: String\n    /// The name of this group chat, if set. If it missing, the group is no longer named.\n    var groupName: String?\n    /// The incrementing number correlated to the chat properties revision history.\n    var propertiesVersion: Int\n    /// The group protocol version, which should always be 8.\n    var groupVersion: String\n    /// When present, this message is in reply to another message.\n    var threadIdentifier: String?\n    /// The participants this message has been sent to.\n    var participants: [String]\n    /// The GUID of the message this message is associated with.\n    var associatedMessageGUID: String?\n    /// Human-readable message text, can be used as a fallback text or the default if no other message formats are provided.\n    var textContent: String\n    /// The type of associated message being sent.\n    var associatedMessageType: IMMessageAssociationType?\n    /// In a rich message, the first character (as in an attributed string) that is part of the association.\n    var associatedMessageLowerBound: Int?\n    /// If present, provides the plugin payload.\n    var pluginMessage: IMPluginMessage?\n    /// The base protocol version, which should always be 1.\n    var protocolVersion: String\n    /// In a rich message, the last character (as in an attributed string) that is part of the association.\n    var associatedMessageUpperBound: Int?\n    /// The GUID of the message prior to this one\n    var replyToGUID: String\n    /// HTML-based rich text for this message\n    var richContent: String?\n    /// A binary property list with additional information about this message.\n    var messageSummaryInfo: Data?\n}\n\nextension IMMessage: Codable {\n    public init(from decoder: Decoder) throws {\n        let container = try decoder.container(keyedBy: StringLiteralCodingKey.self)\n        \n        // IMBaseMessage\n        groupID = try container.decode(String.self, forKey: \"gid\")\n        groupName = try container.decodeIfPresent(String.self, forKey: \"n\")\n        propertiesVersion = try container.decode(Int.self, forKey: \"pv\")\n        groupVersion = try container.decode(String.self, forKey: \"gv\")\n        \n        // IMMessage\n        threadIdentifier = try container.decodeIfPresent(String.self, forKey: \"tg\")\n        participants = try container.decode([String].self, forKey: \"p\")\n        associatedMessageGUID = try container.decodeIfPresent(String.self, forKey: \"amk\")\n        textContent = try container.decode(String.self, forKey: \"t\")\n        associatedMessageType = try container.decodeIfPresent(IMMessageAssociationType.self, forKey: \"amt\")\n        associatedMessageLowerBound = try container.decodeIfPresent(Int.self, forKey: \"amrlc\")\n        pluginMessage = try? IMPluginMessage(from: decoder)\n        protocolVersion = try container.decode(String.self, forKey: \"v\")\n        associatedMessageUpperBound = try container.decodeIfPresent(Int.self, forKey: \"amrln\")\n        replyToGUID = try container.decode(String.self, forKey: \"r\")\n        richContent = try container.decodeIfPresent(String.self, forKey: \"x\")\n        messageSummaryInfo = try container.decodeIfPresent(Data.self, forKey: \"msi\")\n    }\n\n    public func encode(to encoder: Encoder) throws {\n        var container = encoder.container(keyedBy: StringLiteralCodingKey.self)\n        \n        // IMBaseMessage\n        try container.encode(groupID, forKey: \"gid\")\n        try container.encodeIfPresent(groupName, forKey: \"n\")\n        try container.encode(propertiesVersion, forKey: \"pv\")\n        try container.encode(groupVersion, forKey: \"gv\")\n        \n        // IMMessage\n        try container.encodeIfPresent(threadIdentifier, forKey: \"tg\")\n        try container.encode(participants, forKey: \"p\")\n        try container.encodeIfPresent(associatedMessageGUID, forKey: \"amk\")\n        try container.encode(textContent, forKey: \"t\")\n        try container.encodeIfPresent(associatedMessageType, forKey: \"amt\")\n        try container.encodeIfPresent(associatedMessageLowerBound, forKey: \"amrlc\")\n        try pluginMessage?.encode(to: encoder)\n        try container.encode(protocolVersion, forKey: \"v\")\n        try container.encodeIfPresent(associatedMessageUpperBound, forKey: \"amrln\")\n        try container.encode(replyToGUID, forKey: \"r\")\n        try container.encodeIfPresent(richContent, forKey: \"x\")\n        try container.encodeIfPresent(messageSummaryInfo, forKey: \"msi\")\n    }\n}\n```\n\n`amk`, `amt`, `amrlc`, and `amrln` are all optionally encoded and decoded, and despite their co-existing properties, one's presence will not guarantee the others in the eyes of Swifts type system. On the other hand, with type aggregates, you'll get code like this:\n\n```swift\nstruct IMMessage: IMBaseMessage { \n    /// The incrementing number correlated to the chat properties revision history.\n    var propertiesVersion: Int\n    /// The name of this group chat, if set. If it missing, the group is no longer named.\n    var groupName: String?\n    /// The unique identifier of this conversation.\n    var groupID: String\n    /// The group protocol version, which should always be 8.\n    var groupVersion: String\n    /// If present, provides the plugin payload.\n    var pluginMessage: IMPluginMessage?\n    /// If present, identifies the message this message should be correlated to.\n    var messageAssociation: IMMessageAssociation?\n    /// The base protocol version, which should always be 1.\n    var protocolVersion: String\n    /// When present, this message is in reply to another message.\n    var threadIdentifier: String?\n    /// Human-readable message text, can be used as a fallback text or the default if no other message formats are provided.\n    var textContent: String\n    /// The participants this message has been sent to.\n    var participants: [String]\n    /// HTML-based rich text for this message\n    var richContent: String?\n    /// A binary property list with additional information about this message.\n    var messageSummaryInfo: Data?\n    /// The GUID of the message prior to this one\n    var replyToGUID: String\n}\n\nstruct IMMessageAssociation { \n    /// The GUID of the message this message is associated with.\n    var associatedMessageGUID: String\n    /// In a rich message, the last character (as in an attributed string) that is part of the association.\n    var associatedMessageUpperBound: Int\n    /// The type of associated message being sent.\n    var associatedMessageType: Int\n    /// In a rich message, the first character (as in an attributed string) that is part of the association.\n    var associatedMessageLowerBound: Int\n}\n\nextension IMMessage: Codable {\n    public init(from decoder: Decoder) throws {\n        let container = try decoder.container(keyedBy: StringLiteralCodingKey.self)\n        \n        // IMBaseMessage\n        propertiesVersion = try container.decode(Int.self, forKey: \"pv\")\n        groupName = try container.decodeIfPresent(String.self, forKey: \"n\")\n        groupID = try container.decode(String.self, forKey: \"gid\")\n        groupVersion = try container.decode(String.self, forKey: \"gv\")\n        \n        // IMMessage\n        pluginMessage = try? IMPluginMessage(from: decoder)\n        messageAssociation = try? IMMessageAssociation(from: decoder)\n        protocolVersion = try container.decode(String.self, forKey: \"v\")\n        threadIdentifier = try container.decodeIfPresent(String.self, forKey: \"tg\")\n        textContent = try container.decode(String.self, forKey: \"t\")\n        participants = try container.decode([String].self, forKey: \"p\")\n        richContent = try container.decodeIfPresent(String.self, forKey: \"x\")\n        messageSummaryInfo = try container.decodeIfPresent(Data.self, forKey: \"msi\")\n        replyToGUID = try container.decode(String.self, forKey: \"r\")\n    }\n\n    public func encode(to encoder: Encoder) throws {\n        var container = encoder.container(keyedBy: StringLiteralCodingKey.self)\n        \n        // IMBaseMessage\n        try container.encode(propertiesVersion, forKey: \"pv\")\n        try container.encodeIfPresent(groupName, forKey: \"n\")\n        try container.encode(groupID, forKey: \"gid\")\n        try container.encode(groupVersion, forKey: \"gv\")\n        \n        // IMMessage\n        try pluginMessage?.encode(to: encoder)\n        try messageAssociation?.encode(to: encoder)\n        try container.encode(protocolVersion, forKey: \"v\")\n        try container.encodeIfPresent(threadIdentifier, forKey: \"tg\")\n        try container.encode(textContent, forKey: \"t\")\n        try container.encode(participants, forKey: \"p\")\n        try container.encodeIfPresent(richContent, forKey: \"x\")\n        try container.encodeIfPresent(messageSummaryInfo, forKey: \"msi\")\n        try container.encode(replyToGUID, forKey: \"r\")\n    }\n}\n\nextension IMMessageAssociation: Codable {\n    public init(from decoder: Decoder) throws {\n        let container = try decoder.container(keyedBy: StringLiteralCodingKey.self)\n        \n        // IMMessageAssociation\n        associatedMessageGUID = try container.decode(String.self, forKey: \"amk\")\n        associatedMessageUpperBound = try container.decode(Int.self, forKey: \"amrln\")\n        associatedMessageType = try container.decode(Int.self, forKey: \"amt\")\n        associatedMessageLowerBound = try container.decode(Int.self, forKey: \"amrlc\")\n    }\n\n    public func encode(to encoder: Encoder) throws {\n        var container = encoder.container(keyedBy: StringLiteralCodingKey.self)\n        \n        // IMMessageAssociation\n        try container.encode(associatedMessageGUID, forKey: \"amk\")\n        try container.encode(associatedMessageUpperBound, forKey: \"amrln\")\n        try container.encode(associatedMessageType, forKey: \"amt\")\n        try container.encode(associatedMessageLowerBound, forKey: \"amrlc\")\n    }\n}\n```\n\nType aggregates are an essential part of Speck, and allow you to craft logical type representations from minified payloads.\n\n## Type Mashing\n\nType mashing is the process of deduplicating and deep-merging your imported data, creating supertypes that represent your data.\n\nThis is how type mashing works:\n\n1. You pass along an array of types, either parsed from the filesystem or retrieved from an importer.\n2. These types are then \"pushed\" through the pipeline, where the annotation processor recursively searches nodes for annotations and applies all processors that claim said annotations. During processing, types are searched for type groups and are aggregated into a collection of occurrances for all properties.\n3. Annotation processors may generate new types and replace existing types, which then need to be pushed as well. These are recursively processed until the annotation processors no longer generate new types, as they will strip the annotations from the types as they are processed.\n4. Data aggregated for type groups is flattened, deep-merged, and stored into a type that is stored as a finished product.\n5. The type masher finishes, with all of the generated types stored in `types`. These types may encompass enumerations, objects, protocols/interfaces, and typealiases.\n\n### A note on annotations\n\nYou can, and are encouraged to, add your own annotations to Speck that suit your projects needs. Annotations allow you to create powerful macros and common behavior among types, and create code that is more personalized to your project without writing extensive amounts of YAML. Simply define a class that conforms to `AnnotationProcessor`, then register it in the `AnnotationRegistry`. Types that yield your annotations will automatically flow through your processor.\n\nThere are currently three annotatino processors that are built in, and the rest of the annotations are used during codegen.\n\n- `EnumSynthesisParser` - this detects the usage of `ericrabil.com/open-enumeration` and `ericrabil.com/closed-enumeration`, and generates a SpecEnumeration from it. The type declaring this annotation will be replaced with a SpecAlias pointing to the generated enumeration. This processor is registered by default.\n\n```yaml\nannotations:\n    ericrabil.com/closed-enumeration: |\n      kind: String\n      name: IMGroupMessageUpdateType\n      cases:\n        nameChange: n\n        participantChange: p\n        photoChange: v\n```\n\nThe declaring type will be overwritten with an alias to `IMGroupMessageUpdateType`, and a closed enum will be pushed into the global type namespace.\n\n- `TypeLiftingProcessor` – this detects the usage of `ericrabil.com/extracted-type-name`, and rips out the type and all of its descendents into a root type whose name corresponds to the annotation value. The declaring type will be overwritten with an alias pointing to the generated root type. This processor is registered by default.\n\n- `_TypeGroupProcessor` – underscored, used as part of type group collection. All types who declare this are captured and processed at the end of the recursion, where they are deep-merged from left to right and then assembled into a root type. This is an internal processor that is always injected.\n\n## Transpilation\n\nTranspilation is wholly implementation-specific, as is any programming language. There is a reference generator for Swift which utilizes the SwiftSyntax library. Generally, you can subclass the `SpecMasher` class, pass a set of types to it, and then transpile the `types` value to code as you wish.\n\n---\n\n## CoreSpeck API\n\nCoreSpeck comes with a Standard Specification, or `stdspec`, which offers types for representing objects, array, dictionaries, enumerations, primitives, and type aliases. This is separate from the Specification definition, which is merely a set of protocols and lightweight classes for the runtime. You can technically create your own standard spec, though you'd sacrifice a lot of first-class annotations in the process, as they specialize to certain spec types.\n\nAll SpecTypes provide at least two values: their kind, and their metadata. These are guaranteed to be both present and consistent, as they are required for successfully performing type processing. SpecTypes can encompass multiple kinds, but multiple SpecTypes cannot encompass the same kinds. **There can be only one.**\n\nThe following specs are provided in the stdspec:\n\n### SpecPrimitive\n\nThe SpecPrimitive represents a primitive, terminal point in your types. The following are supported by the stdspec:\n\n- String\n- Integer\n- Double\n- Boolean\n- Never (tombstone type)\n- Date\n- Data\n\nAs primitives are SpecTypes, they too may yield metadata.\n\n```swift\n/// Lowest-level representation a spec can yield\npublic enum SpecPrimitive {\n    public enum Kind: String, Codable {\n        case string = \"String\"\n        case integer = \"Integer\"\n        case double = \"Double\"\n        case bool = \"Boolean\"\n        case never = \"Never\"\n        case date = \"Date\"\n        case data = \"Data\"\n    }\n\n    case string(SpecMetadata)\n    case integer(SpecMetadata)\n    case double(SpecMetadata)\n    case bool(SpecMetadata)\n    case never(SpecMetadata)\n    case date(SpecMetadata)\n    case data(SpecMetadata)\n    \n    public var primitiveKind: Kind { get set }\n    public var metadata: SpecMetadata { get set }\n}\n\nvar primitive: SpecPrimitive = ...\n\nprimitive.primitiveKind = .double\nprimitive.metadata.description = \"A double!!?\"\n```\n\nThe above primitive would have the following YAML representation:\n\n```yaml\nkind: Primitive\ntype: Double\nmetadata:\n    description: \"A double!!?\"\n```\n\nSpecTypes with empty metadata will not have a metadata entry, so if we didn't assign a description, we'd get this instead:\n\n```yaml\nkind: Primitive\ntype: Double\n```\n\n### SpecAlias\n\nA SpecAlias allows you to reference another named type, rather than declaring your own structure. They carry around the alias name, the aliasee name, and the aliasee kind. In code generation it is expected that the aliasee name be used as the type, rather than the underlying type.\n\n```swift\nlet alias = SpecAlias(name: \"MyAlias\", aliasedName: \"SomeOtherType\", aliasedKind: .object)\n```\n\n```yaml\nkind: Reference\nname: MyAlias\naliasedName: SomeOtherType\naliasedKind: Object\n```\n\nIf you were to generate this to swift, you'd see this:\n\n```\nvar property: SomeOtherType\n```\n\n### SpecCluster\n\nA cluster is a group of same-typed values, either in the form of an array or a dictionary. It is your responsibility to ensure that, in dictionaries, the key value is actually able to be a key. This is not a type checking system, it is a generation system. It does what you tell it to.\n\n```swift\npublic enum SpecCluster {\n    case array(element: SpecType, metadata: SpecMetadata)\n    case dictionary(key: SpecType, element: SpecType, metadata: SpecMetadata)\n    \n    var element: SpecType { get set }\n    var key: SpecType? { get set } // moving from nil to non-nil converts to a dictionary, non-nil to nil to an array\n    \n    var isArray: Bool { get }\n    var isDictionary: Bool { get }\n    \n    var metadata: SpecMetadata { get set }\n    \n    var kind: SpecKind { get } // either .array or .dictionary\n}\n```\n\n```yaml\nkind: Array\nelement:\n    kind: Primitive\n    type: String\n```\n\n```yaml\nkind: Dictionary\nkey:\n    kind: Primitive\n    type: String\nelement:\n    kind: Primitive\n    type: String\n```\n\n### SpecNode\n\nA SpecNode is an object of mixed key-value content. It is most commonly used to represent classes, structs, and protocols.\n\n```swift\npublic class SpecNode {\n    public var name: String\n    public var children: [String: SpecType]\n    public var metadata: SpecMetadata\n}\n```\n\n```yaml\nkind: Object\nname: IMMessage\nmetadata:\n    description: \"Represents a singular message\"\nchildren:\n    guid:\n        kind: Primitive\n        type: String\n    participants:\n        kind: Array\n        element:\n            kind: Primitive\n            type: String\n```\n\n```swift\n/// Represents a singular message\nstruct IMMessage {\n    var guid: String\n    var participants: [String]\n}\n```\n\n### SpecEnumeration\n\nA SpecEnumeration corresponds to an enum type, and is constricted to primitive types. Support for associated-value enums is not present at this time, though could easily be achieved through a new spec for documenting an associatied value.\n\n```swift\npublic class SpecEnumeration {\n    /// The name of this enumeration\n    public var name: String\n    \n    /// Whether there are additional, unknown potential values\n    public var extensible: Bool\n    \n    public var metadata: SpecMetadata\n    public var enumerationKind: SpecPrimitive.Kind\n    \n    /// Though these are String:String, generators will render their case values differently depending on their primitive kind. I.e. strings will get quotes, numbers will be unwrapped, etc.\n    public var cases: [String: String]\n}\n```\n\n```yaml\nkind: Enumeration\nenumerationKind: Integer\nname: IMMessageAssociationType\ncases:\n    unspecified: 0\n    edit: 1\n    unconsumed: 2\n    consumed: 4\n    sticker: 1000\n    heart: 2000\n    thumbsUp: 2001\n    thumbsDown: 2002\n    ha: 2003\n    exclamation: 2004\n    questionMark: 2005\n    deselectedHeart: 3000\n    deselectedThumbsUp: 3001\n    deselectedThumbsDown: 3002\n    deselectedHa: 3003\n    deselectedExclamation: 3004\n    deselectedQuestionMark: 3005\n```\n\n```swift\nenum IMMessageAssociationType: Int {\n    case consumed = 4\n    case ha = 2003\n    case thumbsUp = 2001\n    case sticker = 1000\n    case exclamation = 2004\n    case thumbsDown = 2002\n    case deselectedThumbsUp = 3001\n    case deselectedExclamation = 3004\n    case edit = 1\n    case questionMark = 2005\n    case deselectedHa = 3003\n    case deselectedHeart = 3000\n    case heart = 2000\n    case unspecified = 0\n    case unconsumed = 2\n    case deselectedThumbsDown = 3002\n    case deselectedQuestionMark = 3005\n}\n```\n\n### TypeGroup\nA TypeGroup is a specialized type which does not get converted to code. Rather, it serves as a sidecar to provide additional metadata for existing type groups, in order to control code generation.\n\n```swift\npublic class TypeGroup {\n    public enum GenerationStyle: String, Codable {\n        case concrete = \"Concrete\"\n        case abstract = \"Abstract\"\n    }\n    \n    public struct Settings: Codable {\n        /// Whether to generate as an interface (protocol) or concrete type (struct/class)\n        public var generationStyle: GenerationStyle\n        /// Similar to implementing an interface, will include all of the defined entries in property and de/serialization synthesis.\n        public var explicitlyExtends: [String]\n    }\n    \n    public var name: String\n    public var settings: Settings\n    public var metadata: SpecMetadata\n}\n}\n```\n\nThe name of the TypeGroup is used to find a SpecNode with the same name. If one is present, its generation is modulated by the settings defined in the TypeGroup.\n\n```yaml\nkind: TypeGroup\nname: IMBaseMessage\nsettings:\n  generationStyle: Abstract\n---\nkind: TypeGroup\nname: IMGroupMessage\nsettings:\n  explicitlyExtends:\n    - IMBaseMessage\n---\nkind: TypeGroup\nname: IMMessage\nsettings:\n  explicitlyExtends:\n    - IMBaseMessage\n```\n\nThis set of type groups declares a common ancestor IMBaseMessage between IMGroupMessage and IMMessage. Because IMBaseMessage is abstract, it will be generated as a protocol/interface type. IMGroupMessage and IMMessage will include IMBaseMessage's properties and serialization as if it was declared within IMGroupMessage and IMMessage.\n\n### SpecCustomization\n\nCustomizations are heavily inspired by Kustomize, so you should take a look there for additional clarifications. They support the following operations:\n\n- add\n- replace\n- remove\n- append\n\nIn the event a patch target does not have the value you are trying to patch, you can specify to skip it via missingBehavior. This is useful for when a value is optionally defined, but has no other suitable or convenient target.\n\n```swift\npublic struct CustomizationPatch: Equatable {\n    public enum PatchType: String, Codable, Hashable, Equatable {\n        case add, replace, remove, append\n    }\n    \n    public enum MissingBehavior: String, Codable, Hashable, Equatable {\n        case skip, `throw`\n    }\n    \n    public var operation: PatchType\n    public var path: String\n    public var missingBehavior: MissingBehavior\n    \n    public var metadata: SpecMetadata\n    public var value: Node?\n}\n\npublic struct CustomizationTarget: Equatable {\n    public var kind: String?\n    public var name: String?\n    public var metadata: SpecMetadata?\n    public var hashes: [String]? // a target can specify multiple hashes to apply patch to multiple models\n    public var children: [Node.Mapping]?\n}\n\npublic struct SpecCustomization: Codable, Equatable {\n    public var target: CustomizationTarget\n    public var patches: [CustomizationPatch]\n    public var name: String\n}\n```\n\nThis customization targets payloads who have either a value `type` whose type is a string, or a value `p` whose type is [String]. It then applies a series of patches which add documentation and annotations to the grouped properties. Note that these properties are being assigned type groups, which will automatically generate a type group for them. This allows you to write a customization that groups together common values from a set of imported data.\n\n```yaml\nkind: Customization\nname: IMMessageBaseAnnotations\ntarget:\n  children:\n    - |\n      type:\n        kind: Primitive\n        type: String\n    - |\n      p:\n        kind: Array\n        element:\n          kind: Primitive\n          type: String\npatches:\n  - op: add\n    path: /children/n/metadata\n    missing-behavior: skip\n    value:\n      annotations:\n        ericrabil.com/readable-name: groupName\n        ericrabil.com/type-group: IMBaseMessage\n        ericrabil.com/value-nullable: true\n      description: The name of this group chat, if set. If it missing, the group is no longer named.\n  - op: add\n    path: /children/gid/metadata\n    value:\n      annotations:\n        ericrabil.com/readable-name: groupID\n        ericrabil.com/type-group: IMBaseMessage\n      description: The unique identifier of this conversation.\n  - op: add\n    path: /children/pv/metadata\n    value:\n      annotations:\n        ericrabil.com/readable-name: propertiesVersion\n        ericrabil.com/type-group: IMBaseMessage\n      description: The incrementing number correlated to the chat properties revision history.\n  - op: add\n    path: /children/gv/metadata\n    value:\n      annotations:\n        ericrabil.com/readable-name: groupVersion\n        ericrabil.com/require-constant: \"8\"\n        ericrabil.com/type-group: IMBaseMessage\n      description: The group protocol version, which should always be 8.\n```\n\n---\n\n## Example\n\nYou can see Speck in action in the [Example](Example) directory. Here's a breakdown of its contents:\n\n- [iMessageSpeck](Example/iMessageSpeck) contains a set of raw types generated by the XMLImporter. I dumped XML-encoded plists of my messages for 48 hours, and then deleted all duplicate types.\n- [iMessageCustomizations](Example/iMessageCustomizations) contains a set of customizations to apply that will bring cohesiveness to these types.\n- [iMessageGenerations](Example/iMessageGenerations) contains a final set of types that were mashed together following annotation processing. These specs can then be passed to a transpiler to create types in any language.\n- [swiftgen.swift](Example/swiftgen.swift) contains the generated swift code from the iMessageGenerations.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericrabil%2Fcorespeck","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fericrabil%2Fcorespeck","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericrabil%2Fcorespeck/lists"}