Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Anchor coding, Tag coding and redundancy auto-aliasing #428

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/pod_lib_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ jobs:
runs-on: macos-14
env:
DEVELOPER_DIR: /Applications/Xcode_15.4.app
strategy:
matrix:
platform: [macOS, iOS, tvOS, visionOS]
steps:
- uses: actions/checkout@v4
- run: bundle install --path vendor/bundle
- run: bundle exec pod lib lint --verbose
- if: matrix.platform == 'visionOS'
run: xcodebuild -downloadPlatform visionOS
- run: bundle exec pod lib lint --platforms=${{ matrix.platform }} --verbose
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

##### Breaking

* None.

##### Enhancements

* Yams is able to encode and decode Anchors via YamlAnchorProviding, and
YamlAnchorCoding.
[Adora Lynch](https://github.com/lynchsft)
[#125](https://github.com/jpsim/Yams/issues/125)

* Yams is able to encode and decode Tags via YamlTagProviding
and YamlTagCoding.
[Adora Lynch](https://github.com/lynchsft)
[#265](https://github.com/jpsim/Yams/issues/265)

* Yams is able to detect redundant structes and automaticaly
alias them during encoding via RedundancyAliasingStrategy
[Adora Lynch](https://github.com/lynchsft)

##### Bug Fixes

* None.

## 5.2.0

##### Breaking

* Swift 5.7 or later is now required to build Yams.
[JP Simard](https://github.com/jpsim)

Expand Down
39 changes: 39 additions & 0 deletions Sources/Yams/Anchor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// Anchor.swift
// Yams
//
// Created by Adora Lynch on 8/9/24.
// Copyright (c) 2024 Yams. All rights reserved.

import Foundation

/// A representation of a YAML tag see: https://yaml.org/spec/1.2.2/
/// Types interested in Encoding and Decoding Anchors should conform to YamlAnchorProviding and YamlAnchorCoding respectively.
public final class Anchor: RawRepresentable, ExpressibleByStringLiteral, Codable, Hashable {

/// A CharacterSet containing only characters which are permitted by the underlying cyaml implementation
public static let permittedCharacters = CharacterSet.lowercaseLetters
.union(.uppercaseLetters)
.union(.decimalDigits)
.union(.init(charactersIn: "-_"))

/// Returns true if and only if `string` contains only characters which are also in `permittedCharacters`
public static func is_cyamlAlpha(_ string: String) -> Bool {
Anchor.permittedCharacters.isSuperset(of: .init(charactersIn: string))
}

public let rawValue: String

public init(rawValue: String) {
self.rawValue = rawValue
}

public init(stringLiteral value: String) {
rawValue = value
}
}

/// Conformance of Anchor to CustomStringConvertible returns `rawValue` as `description`
extension Anchor: CustomStringConvertible {
public var description: String { rawValue }
}
7 changes: 6 additions & 1 deletion Sources/Yams/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@

add_library(Yams
Anchor.swift
Constructor.swift
Decoder.swift
Emitter.swift
Encoder.swift
Mark.swift
Node.Alias.swift
Node.Mapping.swift
Node.Scalar.swift
Node.Sequence.swift
Node.swift
Parser.swift
RedundancyAliasingStrategy.swift
Representer.swift
Resolver.swift
String+Yams.swift
Tag.swift
YamlError.swift)
YamlAnchorProviding.swift
YamlError.swift
YamlTagProviding.swift)
target_compile_definitions(Yams PRIVATE
SWIFT_PACKAGE)
target_compile_options(Yams PRIVATE
Expand Down
18 changes: 10 additions & 8 deletions Sources/Yams/Constructor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ public final class Constructor {
if let method = mappingMap[node.tag.name], let result = method(mapping) {
return result
}
return [AnyHashable: Any]._construct_mapping(from: mapping)
return [AnyHashable: Any].private_construct_mapping(from: mapping)
case .sequence(let sequence):
if let method = sequenceMap[node.tag.name], let result = method(sequence) {
return result
}
return [Any].construct_seq(from: sequence)
case .alias:
preconditionFailure("Aliases should be resolved before construction")
}
}

Expand Down Expand Up @@ -270,7 +272,7 @@ extension ScalarConstructible where Self: FloatingPoint & SexagesimalConvertible
}

private extension FixedWidthInteger where Self: SexagesimalConvertible {
static func _construct(from scalar: Node.Scalar) -> Self? {
static func private_construct(from scalar: Node.Scalar) -> Self? {
guard scalar.style == .any || scalar.style == .plain else {
return nil
}
Expand Down Expand Up @@ -315,7 +317,7 @@ extension Int: ScalarConstructible {
///
/// - returns: An instance of `Int`, if one was successfully extracted from the scalar.
public static func construct(from scalar: Node.Scalar) -> Int? {
return _construct(from: scalar)
return private_construct(from: scalar)
}
}

Expand All @@ -328,7 +330,7 @@ extension UInt: ScalarConstructible {
///
/// - returns: An instance of `UInt`, if one was successfully extracted from the scalar.
public static func construct(from scalar: Node.Scalar) -> UInt? {
return _construct(from: scalar)
return private_construct(from: scalar)
}
}

Expand All @@ -341,7 +343,7 @@ extension Int64: ScalarConstructible {
///
/// - returns: An instance of `Int64`, if one was successfully extracted from the scalar.
public static func construct(from scalar: Node.Scalar) -> Int64? {
return _construct(from: scalar)
return private_construct(from: scalar)
}
}

Expand All @@ -354,7 +356,7 @@ extension UInt64: ScalarConstructible {
///
/// - returns: An instance of `UInt64`, if one was successfully extracted from the scalar.
public static func construct(from scalar: Node.Scalar) -> UInt64? {
return _construct(from: scalar)
return private_construct(from: scalar)
}
}

Expand Down Expand Up @@ -418,12 +420,12 @@ extension Dictionary {
///
/// - returns: An instance of `[AnyHashable: Any]`, if one was successfully extracted from the mapping.
public static func construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any]? {
return _construct_mapping(from: mapping)
return private_construct_mapping(from: mapping)
}
}

private extension Dictionary {
static func _construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any] {
static func private_construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any] {
let mapping = mapping.flatten()
// TODO: YAML supports keys other than str.
return [AnyHashable: Any](
Expand Down
53 changes: 50 additions & 3 deletions Sources/Yams/Decoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,17 @@ public class YAMLDecoder {
from yaml: String,
userInfo: [CodingUserInfoKey: Any] = [:]) throws -> T where T: Swift.Decodable {
do {
let node = try Parser(yaml: yaml, resolver: Resolver([.merge]), encoding: encoding).singleRoot() ?? ""
return try self.decode(type, from: node, userInfo: userInfo)
let parser = try Parser(yaml: yaml, resolver: Resolver([.merge]), encoding: encoding)
// ^ the parser holds the references to Anchors while parsing,
return try withExtendedLifetime(parser) {
// ^ so we hold an explicit reference to the parser during decoding
let node = try parser.singleRoot() ?? ""
// ^ nodes only have weak references to Anchors (the Anchors would disappear if not held by the parser)
return try self.decode(type, from: node, userInfo: userInfo)
// ^ if the decoded type or contained types are YamlAnchorCoding,
// those types have taken ownership of Anchors.
// Otherwise the Anchors are deallocated when this function exits just like Tag and Mark
}
} catch let error as DecodingError {
throw error
} catch {
Expand Down Expand Up @@ -129,6 +138,8 @@ private struct _Decoder: Decoder {
throw _typeMismatch(at: codingPath, expectation: Node.Scalar.self, reality: mapping)
case .sequence(let sequence):
throw _typeMismatch(at: codingPath, expectation: Node.Scalar.self, reality: sequence)
case .alias(let alias):
throw _typeMismatch(at: codingPath, expectation: Node.Scalar.self, reality: alias)
}
}
}
Expand All @@ -140,7 +151,41 @@ private struct _KeyedDecodingContainer<Key: CodingKey>: KeyedDecodingContainerPr

init(decoder: _Decoder, wrapping mapping: Node.Mapping) {
self.decoder = decoder
self.mapping = mapping

let keys = mapping.keys

let decodeAnchor: Anchor?
let decodeTag: Tag?

if let anchor = mapping.anchor, keys.contains(.anchorKeyNode) == false {
decodeAnchor = anchor
} else {
decodeAnchor = nil
}

if mapping.tag.name != .implicit && keys.contains(.tagKeyNode) == false {
decodeTag = mapping.tag
} else {
decodeTag = nil
}

switch (decodeAnchor, decodeTag) {
case (nil, nil):
self.mapping = mapping
case (let anchor?, nil):
var mutableMapping = mapping
mutableMapping[.anchorKeyNode] = .scalar(.init(anchor.rawValue))
self.mapping = mutableMapping
case (nil, let tag?):
var mutableMapping = mapping
mutableMapping[.tagKeyNode] = .scalar(.init(tag.name.rawValue))
self.mapping = mutableMapping
case let (anchor?, tag?):
var mutableMapping = mapping
mutableMapping[.anchorKeyNode] = .scalar(.init(anchor.rawValue))
mutableMapping[.tagKeyNode] = .scalar(.init(tag.name.rawValue))
self.mapping = mutableMapping
}
}

// MARK: - Swift.KeyedDecodingContainerProtocol Methods
Expand Down Expand Up @@ -381,3 +426,5 @@ extension YAMLDecoder: TopLevelDecoder {
}
}
#endif

// swiftlint:disable:this file_length
Loading