├── WORKSPACE
├── .bazelversion
├── .codecov.yml
├── Sources
├── CYaml
│ ├── include
│ │ ├── CYaml.h
│ │ └── module.modulemap
│ ├── CMakeLists.txt
│ └── src
│ │ └── writer.c
├── CMakeLists.txt
└── Yams
│ ├── Yams.h
│ ├── YamlTagProviding.swift
│ ├── YamlAnchorProviding.swift
│ ├── CMakeLists.txt
│ ├── Mark.swift
│ ├── Node.Alias.swift
│ ├── Anchor.swift
│ ├── AliasDereferencingStrategy.swift
│ ├── String+Yams.swift
│ ├── Node.Scalar.swift
│ ├── Node.Sequence.swift
│ ├── Resolver.swift
│ ├── RedundancyAliasingStrategy.swift
│ ├── Tag.swift
│ ├── Node.Mapping.swift
│ └── YamlError.swift
├── yams.jpg
├── Gemfile
├── .spi.yml
├── cmake
└── modules
│ ├── YamsConfig.cmake.in
│ ├── CMakeLists.txt
│ └── SwiftSupport.cmake
├── .bazelrc
├── .bcr
├── source.template.json
├── presubmit.yml
└── metadata.template.json
├── Yams.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcshareddata
│ └── xcschemes
│ │ ├── xcschememanagement.plist
│ │ └── Yams.xcscheme
├── Yams_Info.plist
├── YamsTests_Info.plist
└── Yams.xctestplan
├── Tests
├── CMakeLists.txt
├── LinuxMain.swift
├── YamsTests
│ ├── CMakeLists.txt
│ ├── NodeInternalHelpersTests.swift
│ ├── AliasingStrategyTests.swift
│ ├── TopLevelDecoderTests.swift
│ ├── StringTests.swift
│ ├── NodeDecoderTests.swift
│ ├── MarkTests.swift
│ ├── DecodableWithConfigurationTests.swift
│ ├── RepresenterTests.swift
│ ├── ResolverTests.swift
│ ├── TestHelper.swift
│ ├── AnchorTolerancesTests.swift
│ ├── EmitterTests.swift
│ ├── TagTolerancesTests.swift
│ ├── YamlErrorTests.swift
│ ├── NodeTests.swift
│ └── PerformanceTests.swift
└── BUILD
├── .jazzy.yaml
├── .swiftlint.yml
├── MODULE.bazel
├── .github
└── workflows
│ ├── swiftlint.yml
│ ├── nightly.yml
│ ├── pod_lib_lint.yml
│ ├── swiftlint_analyze.yml
│ ├── bazel.yml
│ ├── jazzy.yml
│ ├── cmake.yml
│ ├── swiftpm.yml
│ └── xcodebuild.yml
├── BUILD
├── Package.swift
├── Package@swift-6.0.swift
├── Yams.podspec
├── CONTRIBUTING.md
├── LICENSE
├── CMakeLists.txt
├── .gitignore
├── Gemfile.lock
├── Docs.md
└── README.md
/WORKSPACE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.bazelversion:
--------------------------------------------------------------------------------
1 | 7.1.1
2 |
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | paths:
2 | - Sources/Yams
3 |
--------------------------------------------------------------------------------
/Sources/CYaml/include/CYaml.h:
--------------------------------------------------------------------------------
1 | #include "yaml.h"
2 |
--------------------------------------------------------------------------------
/yams.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jpsim/Yams/HEAD/yams.jpg
--------------------------------------------------------------------------------
/Sources/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | add_subdirectory(CYaml)
2 | add_subdirectory(Yams)
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'cocoapods'
4 | gem "jazzy"
5 |
--------------------------------------------------------------------------------
/Sources/CYaml/include/module.modulemap:
--------------------------------------------------------------------------------
1 | module CYaml {
2 | header "yaml.h"
3 | }
4 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [Yams]
5 |
--------------------------------------------------------------------------------
/cmake/modules/YamsConfig.cmake.in:
--------------------------------------------------------------------------------
1 | if(NOT TARGET Yams)
2 | include(@YAMS_EXPORTS_FILE@)
3 | endif()
4 |
--------------------------------------------------------------------------------
/.bazelrc:
--------------------------------------------------------------------------------
1 | common --enable_bzlmod
2 |
3 | build --macos_minimum_os=10.13
4 | build --host_macos_minimum_os=10.15
5 | build --repo_env=CC=clang
6 | test --test_output=errors
7 |
--------------------------------------------------------------------------------
/.bcr/source.template.json:
--------------------------------------------------------------------------------
1 | {
2 | "url": "https://github.com/jpsim/Yams/archive/refs/tags/{TAG}.tar.gz",
3 | "integrity": "",
4 | "strip_prefix": "Yams-{TAG}"
5 | }
6 |
--------------------------------------------------------------------------------
/Yams.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.bcr/presubmit.yml:
--------------------------------------------------------------------------------
1 | tasks:
2 | verify_targets:
3 | name: Verify build targets
4 | platform: macos
5 | bazel: 7.x
6 | build_targets:
7 | - '@yams//:Yams'
8 | build_flags:
9 | - "--repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1"
10 |
--------------------------------------------------------------------------------
/Tests/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | find_package(XCTest CONFIG QUIET)
2 |
3 | add_subdirectory(YamsTests)
4 |
5 | add_executable(YamsTestRunner
6 | LinuxMain.swift)
7 | target_link_libraries(YamsTestRunner PRIVATE
8 | YamsTests
9 | XCTest)
10 |
11 | add_test(NAME YamsTests
12 | COMMAND YamsTestRunner)
13 |
--------------------------------------------------------------------------------
/Yams.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/cmake/modules/CMakeLists.txt:
--------------------------------------------------------------------------------
1 |
2 | set(YAMS_EXPORTS_FILE ${CMAKE_CURRENT_BINARY_DIR}/YamsExports.cmake)
3 |
4 | configure_file(YamsConfig.cmake.in
5 | ${CMAKE_CURRENT_BINARY_DIR}/YamsConfig.cmake)
6 |
7 | get_property(YAMS_EXPORTS GLOBAL PROPERTY YAMS_EXPORTS)
8 | export(TARGETS ${YAMS_EXPORTS}
9 | FILE ${YAMS_EXPORTS_FILE})
10 |
--------------------------------------------------------------------------------
/.jazzy.yaml:
--------------------------------------------------------------------------------
1 | module: Yams
2 | author: JP Simard, Norio Nomura
3 | author_url: https://jpsim.com
4 | root_url: https://jpsim.com/Yams/
5 | github_url: https://github.com/jpsim/Yams
6 | github_file_prefix: https://github.com/jpsim/Yams/tree/main
7 | theme: fullwidth
8 | clean: true
9 | copyright: '© 2018 [JP Simard](https://jpsim.com) under MIT.'
10 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | included:
2 | - Sources
3 | - Tests
4 | disabled_rules:
5 | - blanket_disable_command
6 | - todo
7 | analyzer_rules:
8 | - unused_import
9 | - unused_declaration
10 | line_length: 120
11 | identifier_name:
12 | excluded:
13 | - cr
14 | - ln
15 | - no
16 | nesting:
17 | type_level: 2
18 | large_tuple: 3
19 |
--------------------------------------------------------------------------------
/Yams.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SchemeUserState
5 |
6 | Yams.xcscheme
7 |
8 |
9 | SuppressBuildableAutocreation
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.bcr/metadata.template.json:
--------------------------------------------------------------------------------
1 | {
2 | "homepage": "https://github.com/jpsim/Yams",
3 | "maintainers": [
4 | {
5 | "email": "jp@jpsim.com",
6 | "github": "jpsim",
7 | "name": "JP Simard"
8 | }
9 | ],
10 | "repository": [
11 | "github:jpsim/Yams"
12 | ],
13 | "versions": [],
14 | "yanked_versions": {}
15 | }
16 |
--------------------------------------------------------------------------------
/MODULE.bazel:
--------------------------------------------------------------------------------
1 | module(
2 | name = "yams",
3 | version = "6.0.1",
4 | compatibility_level = 1,
5 | )
6 |
7 | bazel_dep(name = "apple_support", version = "1.15.1")
8 | bazel_dep(name = "rules_swift", version = "1.18.0", repo_name = "build_bazel_rules_swift", max_compatibility_level = 3)
9 |
10 | bazel_dep(name = "platforms", version = "0.0.9", dev_dependency = True)
11 | bazel_dep(name = "rules_apple", version = "3.5.1", dev_dependency = True)
12 |
--------------------------------------------------------------------------------
/.github/workflows/swiftlint.yml:
--------------------------------------------------------------------------------
1 | name: SwiftLint
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - '.github/workflows/swiftlint.yml'
7 | - '.swiftlint.yml'
8 | - '**/*.swift'
9 |
10 | concurrency:
11 | group: swiftlint-${{ github.ref }}
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | SwiftLint:
16 | runs-on: ubuntu-latest
17 | container:
18 | image: ghcr.io/realm/swiftlint:0.54.0
19 | steps:
20 | - uses: actions/checkout@v4
21 | - name: SwiftLint
22 | run: swiftlint lint --strict
23 |
--------------------------------------------------------------------------------
/Sources/Yams/Yams.h:
--------------------------------------------------------------------------------
1 | //
2 | // Yams.h
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 1/25/17.
6 | // Copyright (c) 2017 Yams. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for Yams.
12 | FOUNDATION_EXPORT double YamsVersionNumber;
13 |
14 | //! Project version string for Yams.
15 | FOUNDATION_EXPORT const unsigned char YamsVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 | #import
20 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import YamsTests
4 |
5 | XCTMain([
6 | testCase(ConstructorTests.allTests),
7 | testCase(EmitterTests.allTests),
8 | testCase(EncoderTests.allTests),
9 | testCase(MarkTests.allTests),
10 | testCase(NodeInternalHelpersTests.allTests),
11 | testCase(NodeTests.allTests),
12 | testCase(PerformanceTests.allTests),
13 | testCase(RepresenterTests.allTests),
14 | testCase(ResolverTests.allTests),
15 | testCase(SpecTests.allTests),
16 | testCase(StringTests.allTests),
17 | testCase(YamlErrorTests.allTests)
18 | ])
19 |
--------------------------------------------------------------------------------
/Sources/CYaml/CMakeLists.txt:
--------------------------------------------------------------------------------
1 |
2 | add_library(CYaml STATIC
3 | src/api.c
4 | src/emitter.c
5 | src/parser.c
6 | src/reader.c
7 | src/scanner.c
8 | src/writer.c)
9 | target_compile_definitions(CYaml PUBLIC
10 | $<$:YAML_DECLARE_STATIC>)
11 | target_include_directories(CYaml PUBLIC
12 | $<$:${CMAKE_CURRENT_SOURCE_DIR}/include>)
13 | target_compile_options(CYaml PUBLIC
14 | "$<$:SHELL:-Xcc -DYAML_DECLARE_STATIC>"
15 | "$<$:SHELL:-Xcc -I${CMAKE_CURRENT_SOURCE_DIR}/include>")
16 | set_property(TARGET CYaml PROPERTY POSITION_INDEPENDENT_CODE ON)
17 |
18 | set_property(GLOBAL APPEND PROPERTY YAMS_EXPORTS CYaml)
19 |
--------------------------------------------------------------------------------
/Yams.xcodeproj/Yams_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 6.0.1
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Yams.xcodeproj/YamsTests_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | BNDL
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/BUILD:
--------------------------------------------------------------------------------
1 | load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
2 |
3 | cc_library(
4 | name = "CYaml",
5 | srcs = glob([
6 | "Sources/CYaml/src/*.c",
7 | "Sources/CYaml/src/*.h",
8 | ]),
9 | hdrs = ["Sources/CYaml/include/yaml.h"],
10 | copts = [
11 | # Required because of https://github.com/bazelbuild/bazel/pull/10143 otherwise host transition builds fail.
12 | "-fPIC",
13 | "-DYAML_DECLARE_STATIC"
14 | ],
15 | includes = ["Sources/CYaml/include"],
16 | linkstatic = True,
17 | tags = ["swift_module"],
18 | visibility = ["//Tests:__subpackages__"],
19 | )
20 |
21 | swift_library(
22 | name = "Yams",
23 | srcs = glob(["Sources/Yams/*.swift"]),
24 | copts = ["-DSWIFT_PACKAGE"],
25 | module_name = "Yams",
26 | visibility = ["//visibility:public"],
27 | deps = ["//:CYaml"],
28 | )
29 |
--------------------------------------------------------------------------------
/Tests/YamsTests/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | add_library(YamsTests
2 | AliasingStrategyTests.swift
3 | AnchorCodingTests.swift
4 | AnchorTolerancesTests.swift
5 | ClassReferenceDecodingTests.swift
6 | ConstructorTests.swift
7 | EmitterTests.swift
8 | EncoderTests.swift
9 | MarkTests.swift
10 | NodeInternalHelpersTests.swift
11 | NodeTests.swift
12 | PerformanceTests.swift
13 | RepresenterTests.swift
14 | ResolverTests.swift
15 | SpecTests.swift
16 | StringTests.swift
17 | TagCodingTests.swift
18 | TagTolerancesTests.swift
19 | TestHelper.swift
20 | TopLevelDecoderTests.swift
21 | YamlErrorTests.swift)
22 | set_target_properties(YamsTests PROPERTIES
23 | INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
24 | target_link_libraries(YamsTests PUBLIC
25 | Foundation
26 | XCTest
27 | Yams)
28 | target_compile_options(YamsTests PRIVATE
29 | -enable-testing)
30 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.7
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Yams",
6 | products: [
7 | .library(name: "Yams", targets: ["Yams"])
8 | ],
9 | dependencies: [],
10 | targets: [
11 | .target(
12 | name: "CYaml",
13 | exclude: ["CMakeLists.txt"],
14 | cSettings: [.define("YAML_DECLARE_STATIC")]
15 | ),
16 | .target(
17 | name: "Yams",
18 | dependencies: ["CYaml"],
19 | exclude: ["CMakeLists.txt"],
20 | cSettings: [.define("YAML_DECLARE_STATIC")]
21 | ),
22 | .testTarget(
23 | name: "YamsTests",
24 | dependencies: ["Yams"],
25 | exclude: ["CMakeLists.txt"],
26 | resources: [
27 | .copy("Fixtures/SourceKitten#289/debug.yaml"),
28 | ]
29 | )
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/Package@swift-6.0.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:6.0
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Yams",
6 | products: [
7 | .library(name: "Yams", targets: ["Yams"])
8 | ],
9 | dependencies: [],
10 | targets: [
11 | .target(
12 | name: "CYaml",
13 | exclude: ["CMakeLists.txt"],
14 | cSettings: [.define("YAML_DECLARE_STATIC")]
15 | ),
16 | .target(
17 | name: "Yams",
18 | dependencies: ["CYaml"],
19 | exclude: ["CMakeLists.txt"],
20 | cSettings: [.define("YAML_DECLARE_STATIC")]
21 | ),
22 | .testTarget(
23 | name: "YamsTests",
24 | dependencies: ["Yams"],
25 | exclude: ["CMakeLists.txt"],
26 | resources: [
27 | .copy("Fixtures/SourceKitten#289/debug.yaml"),
28 | ]
29 | )
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/Yams.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'Yams'
3 | s.version = '6.0.1'
4 | s.summary = 'A sweet and swifty YAML parser.'
5 | s.homepage = 'https://github.com/jpsim/Yams'
6 | s.source = { :git => s.homepage + '.git', :tag => s.version }
7 | s.license = { :type => 'MIT', :file => 'LICENSE' }
8 | s.authors = { 'JP Simard' => 'jp@jpsim.com',
9 | 'Norio Nomura' => 'norio.nomura@gmail.com' }
10 | s.source_files = 'Sources/**/*.{h,c,swift}'
11 | s.swift_versions = ['5.7', '5.8', '5.9', '5.10']
12 | s.pod_target_xcconfig = { 'APPLICATION_EXTENSION_API_ONLY' => 'YES' }
13 | s.ios.deployment_target = '11.0'
14 | s.osx.deployment_target = '10.13'
15 | s.tvos.deployment_target = '11.0'
16 | s.visionos.deployment_target = '1.0'
17 | end
18 |
--------------------------------------------------------------------------------
/Sources/Yams/YamlTagProviding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // YamlTagProviding.swift
3 | // Yams
4 | //
5 | // Created by Adora Lynch on 9/5/24.
6 | // Copyright (c) 2024 Yams. All rights reserved.
7 | //
8 |
9 | /// Types that conform to YamlTagProviding and Encodable can optionally dictate the name of
10 | /// a yaml tag when they are encoded with YAMLEncoder
11 | public protocol YamlTagProviding {
12 | /// the Tag to encode with this node or nil
13 | var yamlTag: Tag? { get }
14 | }
15 |
16 | /// YamlTagCoding refines YamlTagProviding.
17 | /// Types that conform to YamlTagCoding and Decodable can decode yaml tags
18 | /// from source documents into `Tag` values for reference or modification in memory.
19 | public protocol YamlTagCoding: YamlTagProviding {
20 | /// the Tag coded with this node or nil if none is present
21 | var yamlTag: Tag? { get set }
22 | }
23 |
24 | internal extension Node {
25 | static var tagKeyNode: Self { .scalar(.init("yamlTag")) }
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/YamsTests/NodeInternalHelpersTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeTests.swift
3 | // Yams
4 | //
5 | // Created by Adora Lynch on 6/23/25.
6 | // Copyright (c) 2024 Yams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | @testable import Yams
12 |
13 | final class NodeInternalHelpersTests: XCTestCase, @unchecked Sendable {
14 | // swiftlint:disable force_try
15 | func testIsScalar() {
16 | var node = Node("1") // a scalar
17 | XCTAssertEqual(node.isScalar, true)
18 | node = try! Node(["key": "1"]) // a mapping
19 | XCTAssertEqual(node.isScalar, false)
20 | node = try! Node(["one", "1"]) // a sequnce
21 | XCTAssertEqual(node.isScalar, false)
22 | }
23 | // swiftlint:enable force_try
24 | }
25 |
26 | extension NodeInternalHelpersTests {
27 | static var allTests: [(String, (NodeInternalHelpersTests) -> () throws -> Void)] {
28 | return [
29 | ("testIsScalar", testIsScalar)
30 | ]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Yams.xcodeproj/Yams.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "31BD53C3-DF26-457B-B8F0-68B6819351E5",
5 | "name" : "UTF16",
6 | "options" : {
7 | "environmentVariableEntries" : [
8 | {
9 | "key" : "YAMS_DEFAULT_ENCODING",
10 | "value" : "UTF16"
11 | }
12 | ]
13 | }
14 | },
15 | {
16 | "id" : "D9E2FAEF-259D-447B-B5D6-88979EA2AEC6",
17 | "name" : "UTF8",
18 | "options" : {
19 | "environmentVariableEntries" : [
20 | {
21 | "key" : "YAMS_DEFAULT_ENCODING",
22 | "value" : "UTF8"
23 | }
24 | ]
25 | }
26 | }
27 | ],
28 | "defaultOptions" : {
29 |
30 | },
31 | "testTargets" : [
32 | {
33 | "parallelizable" : true,
34 | "target" : {
35 | "containerPath" : "container:Yams.xcodeproj",
36 | "identifier" : "OBJ_54",
37 | "name" : "YamsTests"
38 | }
39 | }
40 | ],
41 | "version" : 1
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/Yams/YamlAnchorProviding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // YamlAnchorProviding.swift
3 | // Yams
4 | //
5 | // Created by Adora Lynch on 8/15/24.
6 | // Copyright (c) 2024 Yams. All rights reserved.
7 | //
8 |
9 | /// Types that conform to YamlAnchorProviding and Encodable can optionally dictate the name of
10 | /// a yaml anchor when they are encoded with YAMLEncoder
11 | public protocol YamlAnchorProviding {
12 | /// the Anchor to encode with this node or nil
13 | var yamlAnchor: Anchor? { get }
14 | }
15 |
16 | /// YamlAnchorCoding refines YamlAnchorProviding.
17 | /// Types that conform to YamlAnchorCoding and Decodable can decode yaml anchors
18 | /// from source documents into `Anchor` values for reference or modification in memory.
19 | public protocol YamlAnchorCoding: YamlAnchorProviding {
20 | /// the Anchor coded with this node or nil if none is present
21 | var yamlAnchor: Anchor? { get set }
22 | }
23 |
24 | internal extension Node {
25 | static var anchorKeyNode: Self { .scalar(.init("yamlAnchor")) }
26 | }
27 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Tracking Changes
2 |
3 | All changes should be made via pull requests on GitHub.
4 |
5 | When issuing a pull request, please add a summary of your changes to the
6 | `CHANGELOG.md` file.
7 |
8 | We follow the same syntax as [CocoaPods' `CHANGELOG.md`](https://github.com/CocoaPods/CocoaPods/blob/master/CHANGELOG.md):
9 |
10 | 1. One Markdown unnumbered list item describing the change.
11 | 2. 2 trailing spaces on the last line describing the change.
12 | 3. A list of Markdown hyperlinks to the change's contributors. One entry
13 | per line. Usually just one.
14 | 4. A list of Markdown hyperlinks to the issues the change addresses. One entry
15 | per line. Usually just one.
16 | 5. All `CHANGELOG.md` content is hard-wrapped at 80 characters.
17 |
18 | ## Updating CI Jobs
19 |
20 | CI jobs for the latest official Swift and Xcode releases should be kept
21 | up to date based on the available Xcode versions that can be found
22 | in the [actions/virtual-environments](https://github.com/actions/runner-images?tab=readme-ov-file#available-images)
23 | repo.
24 |
--------------------------------------------------------------------------------
/.github/workflows/nightly.yml:
--------------------------------------------------------------------------------
1 | name: Nightly
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - '.github/workflows/nightly.yml'
8 | - 'Package*'
9 | - 'Sources/**/*.[ch]'
10 | - 'Sources/**/*.swift'
11 | - 'Sources/**/module.modulemap'
12 | - 'Tests/**/*.swift'
13 | - 'Tests/**/*.ya?ml'
14 | pull_request:
15 | paths:
16 | - '.github/workflows/nightly.yml'
17 | - 'Package*'
18 | - 'Sources/**/*.[ch]'
19 | - 'Sources/**/*.swift'
20 | - 'Sources/**/module.modulemap'
21 | - 'Tests/**/*.swift'
22 | - 'Tests/**/*.ya?ml'
23 | schedule:
24 | - cron: '0 4 * * *'
25 |
26 | concurrency:
27 | group: nightly-${{ github.ref }}
28 | cancel-in-progress: true
29 |
30 | jobs:
31 | Nightly:
32 | runs-on: ubuntu-latest
33 | container:
34 | image: swiftlang/swift:nightly
35 | steps:
36 | - uses: actions/checkout@v4
37 | - run: swift --version
38 | - run: YAMS_DEFAULT_ENCODING=UTF16 swift test --parallel
39 | - run: YAMS_DEFAULT_ENCODING=UTF8 swift test --parallel
40 |
--------------------------------------------------------------------------------
/Sources/Yams/CMakeLists.txt:
--------------------------------------------------------------------------------
1 |
2 | add_library(Yams
3 | AliasDereferencingStrategy.swift
4 | Anchor.swift
5 | Constructor.swift
6 | Decoder.swift
7 | Emitter.swift
8 | Encoder.swift
9 | Mark.swift
10 | Node.Alias.swift
11 | Node.Mapping.swift
12 | Node.Scalar.swift
13 | Node.Sequence.swift
14 | Node.swift
15 | Parser.swift
16 | RedundancyAliasingStrategy.swift
17 | Representer.swift
18 | Resolver.swift
19 | String+Yams.swift
20 | Tag.swift
21 | YamlAnchorProviding.swift
22 | YamlError.swift
23 | YamlTagProviding.swift)
24 | target_compile_definitions(Yams PRIVATE
25 | SWIFT_PACKAGE)
26 | target_compile_options(Yams PRIVATE
27 | $<$:-enable-testing>)
28 |
29 | target_link_libraries(Yams PRIVATE
30 | CYaml
31 | $<$>:dispatch>
32 | $<$>:Foundation>)
33 | set_target_properties(Yams PROPERTIES
34 | INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
35 |
36 | set_property(GLOBAL APPEND PROPERTY YAMS_EXPORTS Yams)
37 | swift_install(TARGETS Yams
38 | EXPORT YamsExports)
39 |
--------------------------------------------------------------------------------
/.github/workflows/pod_lib_lint.yml:
--------------------------------------------------------------------------------
1 | name: pod lib lint
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - '.github/workflows/pod_lib_lint.yml'
8 | - '*.podspec'
9 | - 'Gemfile*'
10 | - 'Sources/**/*.[ch]'
11 | - 'Sources/**/*.swift'
12 | pull_request:
13 | paths:
14 | - '.github/workflows/pod_lib_lint.yml'
15 | - '*.podspec'
16 | - 'Gemfile*'
17 | - 'Sources/**/*.[ch]'
18 | - 'Sources/**/*.swift'
19 |
20 | concurrency:
21 | group: pod-lib-lint-${{ github.ref }}
22 | cancel-in-progress: true
23 |
24 | jobs:
25 | pod_lib_lint:
26 | name: pod lib lint
27 | runs-on: macos-14
28 | env:
29 | DEVELOPER_DIR: /Applications/Xcode_15.4.app
30 | strategy:
31 | matrix:
32 | platform: [macOS, iOS, tvOS, visionOS]
33 | steps:
34 | - uses: actions/checkout@v4
35 | - run: bundle install --path vendor/bundle
36 | - if: matrix.platform == 'visionOS'
37 | run: xcodebuild -downloadPlatform visionOS
38 | - run: bundle exec pod lib lint --platforms=${{ matrix.platform }} --verbose
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 JP Simard.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 |
2 | cmake_minimum_required(VERSION 3.15.1)
3 |
4 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
5 |
6 | project(Yams
7 | LANGUAGES C Swift)
8 |
9 | option(BUILD_SHARED_LIBS "build shared libraries" ON)
10 | if(CMAKE_SYSTEM_NAME STREQUAL Darwin)
11 | option(BUILD_TESTING "build tests by default" NO)
12 | else()
13 | option(BUILD_TESTING "build tests by default" YES)
14 | endif()
15 |
16 | find_package(dispatch CONFIG QUIET)
17 | find_package(Foundation CONFIG QUIET)
18 |
19 | if(CMAKE_VERSION VERSION_LESS 3.16 AND CMAKE_SYSTEM_NAME STREQUAL Windows)
20 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
21 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
22 | else()
23 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
24 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
25 | endif()
26 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
27 | set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/swift)
28 |
29 | include(SwiftSupport)
30 | include(CTest)
31 |
32 | add_subdirectory(Sources)
33 | if(BUILD_TESTING)
34 | add_subdirectory(Tests)
35 | endif()
36 | add_subdirectory(cmake/modules)
37 |
--------------------------------------------------------------------------------
/.github/workflows/swiftlint_analyze.yml:
--------------------------------------------------------------------------------
1 | name: SwiftLint Analyze
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - '.github/workflows/swiftlint_analyze.yml'
8 | - 'Yams.xcodeproj/**'
9 | - 'Sources/**/*.[ch]'
10 | - 'Sources/**/*.swift'
11 | - 'Tests/**/*.swift'
12 | - '!Tests/LinuxMain.swift'
13 | pull_request:
14 | paths:
15 | - '.github/workflows/swiftlint_analyze.yml'
16 | - 'Yams.xcodeproj/**'
17 | - 'Sources/**/*.[ch]'
18 | - 'Sources/**/*.swift'
19 | - 'Tests/**/*.swift'
20 | - '!Tests/LinuxMain.swift'
21 |
22 | concurrency:
23 | group: swiftlint-analyze-${{ github.ref }}
24 | cancel-in-progress: true
25 |
26 | jobs:
27 | Analyze:
28 | runs-on: macos-14
29 | env:
30 | DEVELOPER_DIR: /Applications/Xcode_15.4.app
31 | steps:
32 | - uses: actions/checkout@v4
33 | - name: Generate xcodebuild.log
34 | run: xcodebuild -sdk macosx -scheme Yams -project Yams.xcodeproj clean build-for-testing > xcodebuild.log
35 | shell: bash
36 | - name: Install SwiftLint
37 | run: brew install swiftlint || brew upgrade swiftlint
38 | - name: Run SwiftLint Analyze
39 | run: swiftlint analyze --strict --compiler-log-path xcodebuild.log --reporter github-actions-logging
40 |
--------------------------------------------------------------------------------
/.github/workflows/bazel.yml:
--------------------------------------------------------------------------------
1 | name: Bazel
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - '.github/workflows/bazel.yml'
8 | - 'Sources/**/*.[ch]'
9 | - 'Sources/**/*.swift'
10 | - 'Tests/**/*.swift'
11 | - 'Tests/**/*.ya?ml'
12 | - '**/BUILD'
13 | - 'MODULE.bazel'
14 | - 'WORKSPACE'
15 | - '.bazelrc'
16 | - '.bazelversion'
17 | pull_request:
18 | paths:
19 | - '.github/workflows/bazel.yml'
20 | - 'Sources/**/*.[ch]'
21 | - 'Sources/**/*.swift'
22 | - 'Tests/**/*.swift'
23 | - 'Tests/**/*.ya?ml'
24 | - '**/BUILD'
25 | - 'MODULE.bazel'
26 | - 'WORKSPACE'
27 | - '.bazelrc'
28 | - '.bazelversion'
29 |
30 | concurrency:
31 | group: bazel-${{ github.ref }}
32 | cancel-in-progress: true
33 |
34 | jobs:
35 | macOS:
36 | runs-on: macos-14
37 | steps:
38 | - uses: actions/checkout@v4
39 | - name: Apple tests
40 | run: bazelisk test //Tests/...
41 | Linux:
42 | strategy:
43 | matrix:
44 | tag: ['5.7', '5.8', '5.9', '5.10']
45 | runs-on: ubuntu-latest
46 | container:
47 | image: swift:${{ matrix.tag }}-focal
48 | steps:
49 | - uses: actions/checkout@v4
50 | - uses: bazelbuild/setup-bazelisk@v3
51 | - name: Yams tests
52 | run: bazel test --test_output=all //Tests:UnitTests
53 |
--------------------------------------------------------------------------------
/Sources/Yams/Mark.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Mark.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 4/11/17.
6 | // Copyright (c) 2017 Yams. All rights reserved.
7 | //
8 |
9 | /// The pointer position.
10 | public struct Mark: Sendable {
11 | /// Line number starting from 1.
12 | public let line: Int
13 | /// Column number starting from 1. libYAML counts columns in `UnicodeScalar`.
14 | public let column: Int
15 | }
16 |
17 | // MARK: - CustomStringConvertible Conformance
18 |
19 | extension Mark: CustomStringConvertible {
20 | /// A textual representation of this instance.
21 | public var description: String { return "\(line):\(column)" }
22 | }
23 |
24 | // MARK: Snippet
25 |
26 | extension Mark {
27 | /// Returns snippet string pointed by Mark instance from YAML String.
28 | public func snippet(from yaml: String) -> String {
29 | let contents = yaml.substring(at: line - 1)
30 | let columnIndex = contents.unicodeScalars
31 | .index(contents.unicodeScalars.startIndex,
32 | offsetBy: column - 1,
33 | limitedBy: contents.unicodeScalars.endIndex)?
34 | .samePosition(in: contents.utf16) ?? contents.utf16.endIndex
35 | let columnInUTF16 = contents.utf16.distance(from: contents.utf16.startIndex, to: columnIndex)
36 | return contents.endingWithNewLine +
37 | String(repeating: " ", count: columnInUTF16) + "^"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/Yams/Node.Alias.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Node.Alias.swift
3 | // Yams
4 | //
5 | // Created by Adora Lynch on 8/19/24.
6 | // Copyright (c) 2024 Yams. All rights reserved.
7 | //
8 |
9 | // MARK: Node+Alias
10 |
11 | extension Node {
12 | /// Scalar node.
13 | public struct Alias {
14 | /// The anchor for this alias.
15 | public var anchor: Anchor
16 | /// This node's tag (its type).
17 | public var tag: Tag
18 | /// The location for this node.
19 | public var mark: Mark?
20 |
21 | /// Create a `Node.Alias` using the specified parameters.
22 | ///
23 | /// - parameter tag: This scalar's `Tag`.
24 | /// - parameter mark: This scalar's `Mark`.
25 | public init(_ anchor: Anchor, _ tag: Tag = .implicit, _ mark: Mark? = nil) {
26 | self.anchor = anchor
27 | self.tag = tag
28 | self.mark = mark
29 | }
30 | }
31 | }
32 |
33 | extension Node.Alias: Comparable {
34 | /// :nodoc:
35 | public static func < (lhs: Node.Alias, rhs: Node.Alias) -> Bool {
36 | lhs.anchor.rawValue < rhs.anchor.rawValue
37 | }
38 | }
39 |
40 | extension Node.Alias: Equatable {
41 | /// :nodoc:
42 | public static func == (lhs: Node.Alias, rhs: Node.Alias) -> Bool {
43 | lhs.anchor == rhs.anchor
44 | }
45 | }
46 |
47 | extension Node.Alias: Hashable {
48 | /// :nodoc:
49 | public func hash(into hasher: inout Hasher) {
50 | hasher.combine(anchor)
51 | }
52 | }
53 |
54 | extension Node.Alias: TagResolvable {
55 | static let defaultTagName = Tag.Name.implicit
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/Yams/Anchor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Anchor.swift
3 | // Yams
4 | //
5 | // Created by Adora Lynch on 8/9/24.
6 | // Copyright (c) 2024 Yams. All rights reserved.
7 |
8 | import Foundation
9 |
10 | /// A representation of a YAML tag see: https://yaml.org/spec/1.2.2/
11 | /// Types interested in Encoding and Decoding Anchors should
12 | /// conform to YamlAnchorProviding and YamlAnchorCoding respectively.
13 | public final class Anchor: RawRepresentable, ExpressibleByStringLiteral, Codable, Hashable {
14 |
15 | /// A CharacterSet containing only characters which are permitted by the underlying cyaml implementation
16 | public static let permittedCharacters = CharacterSet.lowercaseLetters
17 | .union(.uppercaseLetters)
18 | .union(.decimalDigits)
19 | .union(.init(charactersIn: "-_"))
20 |
21 | /// Returns true if and only if `string` contains only characters which are also in `permittedCharacters`
22 | public static func is_cyamlAlpha(_ string: String) -> Bool {
23 | Anchor.permittedCharacters.isSuperset(of: .init(charactersIn: string))
24 | }
25 |
26 | public let rawValue: String
27 |
28 | public init(rawValue: String) {
29 | self.rawValue = rawValue
30 | }
31 |
32 | public init(stringLiteral value: String) {
33 | rawValue = value
34 | }
35 | }
36 |
37 | /// Conformance of Anchor to CustomStringConvertible returns `rawValue` as `description`
38 | extension Anchor: CustomStringConvertible {
39 | public var description: String { rawValue }
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/YamsTests/AliasingStrategyTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AliasingStrategyTests.swift
3 | // Yams
4 | //
5 | // Created by Adora Lynch on 2/23/25.
6 | // Copyright (c) 2024 Yams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Yams
11 |
12 | class AliasingStrategyTests: XCTestCase {
13 |
14 | func testRemitAnchor_HashableAliasingStrategy() throws {
15 | try _testRemitAnchor(strategy: HashableAliasingStrategy())
16 | }
17 |
18 | func testRemitAnchor_StrictCodableAliasingStrategy() throws {
19 | try _testRemitAnchor(strategy: StrictEncodableAliasingStrategy())
20 | }
21 |
22 | private func _testRemitAnchor(strategy: any RedundancyAliasingStrategy) throws {
23 | let subject = "subject"
24 |
25 | let response1 = try strategy.alias(for: subject)
26 | guard case let .anchor(anchor1) = response1 else {
27 | XCTFail("should be anchor: \(response1)")
28 | return
29 | }
30 | // _ = consume response1
31 |
32 | let response2 = try strategy.alias(for: subject)
33 | guard case let .alias(anchor2) = response2 else {
34 | XCTFail("should be alias: \(response2)")
35 | return
36 | }
37 | // _ = consume response2
38 |
39 | XCTAssertEqual(anchor1, anchor2)
40 |
41 | try strategy.remit(anchor: anchor2)
42 |
43 | let response3 = try strategy.alias(for: subject)
44 | guard case let .anchor(anchor3) = response3 else {
45 | XCTFail("should be anchor: \(response1)")
46 | return
47 | }
48 | // _ = consume response3
49 |
50 | XCTAssertNotEqual(anchor1, anchor3)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | # vim swap files
6 | .*.sw[nop]
7 |
8 | ## Build generated
9 | build/
10 | DerivedData
11 |
12 | ## Various settings
13 | *.pbxuser
14 | !default.pbxuser
15 | *.mode1v3
16 | !default.mode1v3
17 | *.mode2v3
18 | !default.mode2v3
19 | *.perspectivev3
20 | !default.perspectivev3
21 | xcuserdata
22 |
23 | ## Other
24 | *.xccheckout
25 | *.moved-aside
26 | *.xcuserstate
27 | *.xcscmblueprint
28 |
29 | ## Obj-C/Swift specific
30 | *.hmap
31 | *.ipa
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | Packages/
41 | .build/
42 | .swiftpm/
43 |
44 | # CocoaPods
45 | #
46 | # We recommend against adding the Pods directory to your .gitignore. However
47 | # you should judge for yourself, the pros and cons are mentioned at:
48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
49 | #
50 | # Pods/
51 |
52 | # Carthage
53 | #
54 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
55 | # Carthage/Checkouts
56 |
57 | Carthage/Build
58 |
59 | # fastlane
60 | #
61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
62 | # screenshots whenever they are needed.
63 | # For more information about the recommended setup visit:
64 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
65 |
66 | fastlane/report.xml
67 | fastlane/screenshots
68 |
69 | # Docs
70 | docs
71 | .swiftpm
72 |
73 | # Bazel
74 | bazel-*
75 | MODULE.bazel.lock
76 |
--------------------------------------------------------------------------------
/Tests/BUILD:
--------------------------------------------------------------------------------
1 | load("@rules_apple//apple:macos.bzl", "macos_build_test")
2 | load("@rules_apple//apple:watchos.bzl", "watchos_build_test")
3 | load("@rules_apple//apple:tvos.bzl", "tvos_build_test")
4 | load("@rules_apple//apple:ios.bzl", "ios_build_test")
5 | load("@build_bazel_rules_swift//swift:swift.bzl", "swift_test")
6 |
7 | config_setting(
8 | name = "linux",
9 | constraint_values = ["@platforms//os:linux"],
10 | )
11 |
12 | genrule(
13 | name = "LinuxMain",
14 | srcs = ["LinuxMain.swift"],
15 | outs = ["main.swift"],
16 | cmd = "cp $< $@",
17 | )
18 |
19 | swift_test(
20 | name = "UnitTests",
21 | srcs = glob(["YamsTests/*.swift"]) + select({
22 | ":linux": ["main.swift"],
23 | "//conditions:default": [],
24 | }),
25 | data = glob(["YamsTests/Fixtures/**/*.*"]),
26 | module_name = "YamsTests",
27 | deps = [
28 | "//:Yams",
29 | ],
30 | )
31 |
32 | macos_build_test(
33 | name = "macOSBuildTest",
34 | minimum_os_version = "10.13",
35 | target_compatible_with = ["@platforms//os:macos"],
36 | targets = [
37 | "//:Yams",
38 | "//:CYaml",
39 | ],
40 | )
41 |
42 | watchos_build_test(
43 | name = "watchOSBuildTest",
44 | minimum_os_version = "2.0",
45 | target_compatible_with = ["@platforms//os:macos"],
46 | targets = [
47 | "//:Yams",
48 | "//:CYaml",
49 | ],
50 | )
51 |
52 | tvos_build_test(
53 | name = "tvOSBuildTest",
54 | minimum_os_version = "9.0",
55 | target_compatible_with = ["@platforms//os:macos"],
56 | targets = [
57 | "//:Yams",
58 | "//:CYaml",
59 | ],
60 | )
61 |
62 | ios_build_test(
63 | name = "iOSBuildTest",
64 | minimum_os_version = "8.0",
65 | target_compatible_with = ["@platforms//os:macos"],
66 | targets = [
67 | "//:Yams",
68 | "//:CYaml",
69 | ],
70 | )
71 |
--------------------------------------------------------------------------------
/Tests/YamsTests/TopLevelDecoderTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopLevelDecoderTests.swift
3 | // Yams
4 | //
5 | // Created by JP Simard on 2020-07-05.
6 | // Copyright (c) 2020 Yams. All rights reserved.
7 | //
8 |
9 | #if canImport(Combine)
10 | import Combine
11 | import XCTest
12 | @testable import Yams
13 |
14 | @available(iOS 13.0, macOS 10.15.0, tvOS 13.0, watchOS 6.0, *)
15 | class TopLevelDecoderTests: XCTestCase {
16 | func testDecodeFromYAMLDecoder() throws {
17 | let yaml = """
18 | name: Bird
19 | """
20 | let data = try XCTUnwrap(yaml.data(using: Parser.Encoding.default.swiftStringEncoding))
21 |
22 | struct Foo: Decodable {
23 | var name: String
24 | }
25 |
26 | var foo: Foo?
27 | _ = Just(data)
28 | .decode(type: Foo.self, decoder: YAMLDecoder())
29 | .sink(
30 | receiveCompletion: { _ in },
31 | receiveValue: { foo = $0 }
32 | )
33 | XCTAssertEqual(foo?.name, "Bird")
34 | }
35 |
36 | func testDecodeOptionalTypes() throws {
37 | let yaml = """
38 | AAA: ''
39 | BBB:
40 | CCC: null
41 | DDD: ~
42 | EEE: ""
43 | json: {
44 | "FFF": "",
45 | "GGG": "null"
46 | }
47 | array:
48 | - one
49 | - ''
50 | - null
51 | - 'null'
52 | - '~'
53 | """
54 |
55 | struct Container: Codable, Equatable {
56 | struct JSON: Codable, Equatable {
57 | var FFF: String?
58 | var GGG: String?
59 | }
60 |
61 | var AAA: String?
62 | var BBB: String?
63 | var CCC: Int?
64 | var DDD: String?
65 | var EEE: String?
66 | var json: JSON
67 | var array: [String?]
68 | }
69 |
70 | let container = try YAMLDecoder().decode(Container.self, from: yaml)
71 |
72 | XCTAssertEqual(container.AAA, "")
73 | XCTAssertEqual(container.BBB, nil)
74 | XCTAssertEqual(container.CCC, nil)
75 | XCTAssertEqual(container.DDD, nil)
76 | XCTAssertEqual(container.EEE, "")
77 | XCTAssertEqual(container.json.FFF, "")
78 | XCTAssertEqual(container.json.GGG, "null")
79 | XCTAssertEqual(container.array, ["one", "", nil, "null", "~"])
80 | }
81 | }
82 | #endif
83 |
--------------------------------------------------------------------------------
/.github/workflows/jazzy.yml:
--------------------------------------------------------------------------------
1 | name: Jazzy
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - '.github/workflows/jazzy.yml'
8 | - '.jazzy.yaml'
9 | - '**/*.md'
10 | - '**/*.jpg'
11 | - 'Gemfile*'
12 | - 'Package*'
13 | - 'Sources/**/*.swift'
14 | pull_request:
15 | paths:
16 | - '.github/workflows/jazzy.yml'
17 | - '.jazzy.yaml'
18 | - '**/*.md'
19 | - '**/*.jpg'
20 | - 'Gemfile*'
21 | - 'Package*'
22 | - 'Sources/**/*.swift'
23 |
24 | concurrency:
25 | group: jazzy-${{ github.ref }}
26 | cancel-in-progress: true
27 |
28 | jobs:
29 | Jazzy:
30 | runs-on: macos-14
31 | env:
32 | DEVELOPER_DIR: /Applications/Xcode_15.4.app
33 | steps:
34 | - uses: actions/checkout@v4
35 | - name: Install SourceKitten
36 | run: brew install sourcekitten
37 | - run: swift build
38 | - name: Generate documentation json
39 | run: sourcekitten doc --spm --module-name Yams > yams.json
40 | - uses: ruby/setup-ruby@v1
41 | with:
42 | ruby-version: 3.3.3
43 | bundler-cache: true
44 | - name: Run jazzy
45 | run: bundle exec jazzy --clean --sourcekitten-sourcefile yams.json
46 | - name: Validate documentation coverage
47 | run: |
48 | if ruby -rjson -e "j = JSON.parse(File.read('docs/undocumented.json')); exit j['warnings'].length != 0"; then
49 | echo "Undocumented declarations:"
50 | cat docs/undocumented.json
51 | exit 1
52 | fi
53 | - name: Upload Artifact
54 | uses: actions/upload-artifact@v4
55 | with:
56 | name: API Docs
57 | path: docs
58 | - name: Push to gh-pages
59 | if: github.event_name == 'push'
60 | run: |
61 | git config --global user.email "${GITHUB_ACTOR}"
62 | git config --global user.name "${GITHUB_ACTOR}@users.noreply.github.com"
63 | git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" out
64 |
65 | cd out
66 | git checkout gh-pages
67 | git rm -rf .
68 | cd ..
69 |
70 | cp -a docs/. out/.
71 | cd out
72 |
73 | git add -A
74 | git commit -m "Automated deployment to GitHub Pages: ${GITHUB_SHA}" --allow-empty
75 |
76 | git push origin gh-pages
77 | env:
78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
79 |
--------------------------------------------------------------------------------
/Sources/Yams/AliasDereferencingStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AliasDereferencingStrategy.swift
3 | // Yams
4 | //
5 | // Created by Adora Lynch on 8/9/24.
6 | // Copyright (c) 2024 Yams. All rights reserved.
7 | //
8 |
9 | /// A class-bound protocol which implements a strategy for dereferencing aliases (or dealiasing) values during
10 | /// YAML document decoding. YAML documents which do not contain anchors will not benefit from the use of
11 | /// an AliasDereferencingStrategy in any way. The main use-case for dereferencing aliases in a YML document
12 | /// is when decoding into class types. If the yaml document is large and contains many references
13 | /// (perhaps it is a representation of a dense graph) then, decoding into structs will require the of large amounts
14 | /// of system memory to represent highly redundant (duplicated) data structures.
15 | /// However, if the same document is decoded into class types and the decoding uses
16 | /// an `AliasDereferencingStrategy` such as `BasicAliasDereferencingStrategy` then the emitted value will have its
17 | /// class references coalesced. No duplicate objects will be initialized (unless identical objects have multiple
18 | /// distinct anchors in the YAML document). In some scenarios this may significantly reduce the memory footprint of
19 | /// the decoded type.
20 | public protocol AliasDereferencingStrategy: AnyObject {
21 | /// The stored exestential type of all AliasDereferencingStrategys
22 | typealias Value = (any Decodable)
23 | /// get and set cached references, keyed bo an Anchor
24 | subscript(_ key: Anchor) -> Value? { get set }
25 | }
26 |
27 | /// A AliasDereferencingStrategy which caches all values (even value-type values) in a Dictionary,
28 | /// keyed by their Anchor.
29 | /// For reference types, this strategy achieves reference coalescing
30 | /// For value types, this strategy achieves short-cutting the decoding process when dereferencing aliases.
31 | /// if the aliased structure is large, this may result in a time savings
32 | public class BasicAliasDereferencingStrategy: AliasDereferencingStrategy {
33 | /// Create a new BasicAliasDereferencingStrategy
34 | public init() {}
35 |
36 | private var map: [Anchor: Value] = .init()
37 |
38 | /// get and set cached references, keyed bo an Anchor
39 | public subscript(_ key: Anchor) -> Value? {
40 | get { map[key] }
41 | set { map[key] = newValue }
42 | }
43 | }
44 |
45 | extension CodingUserInfoKey {
46 | internal static let aliasDereferencingStrategy = Self(rawValue: "aliasDereferencingStrategy")!
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/YamsTests/StringTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringTests.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 12/7/16.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Yams
11 |
12 | final class StringTests: XCTestCase, @unchecked Sendable {
13 | // column 1 2 3 4 5 6 7 8 9 10 11
14 | // line 1 L I N E 1 _ 6 7 あ \n
15 | // line 2 L I N E 2 _ 7 8 9 0 \n
16 | // line 3 L I N E 3 _ 8 9 0 1 \n
17 | let string = "LINE1_67あ\nLINE2_7890\nLINE3_8901\n"
18 |
19 | // Confirm behavior of Standard Library API
20 | func testConfirmBehaviorOfStandardLibraryAPI() {
21 | let rangeOfFirstLine = string.lineRange(for: string.startIndex.. () throws -> Void)] {
61 | return [
62 | ("testConfirmBehaviorOfStandardLibraryAPI", testConfirmBehaviorOfStandardLibraryAPI),
63 | ("testLineNumberColumnAndContentsAtOffset", testLineNumberColumnAndContentsAtOffset),
64 | ("testSubstringAtLine", testSubstringAtLine)
65 | ]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/YamsTests/NodeDecoderTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeDecoderTests.swift
3 | //
4 | //
5 | // Created by Rob Napier on 6/3/23.
6 | //
7 |
8 | import XCTest
9 | import Yams
10 |
11 | final class NodeDecoderTests: XCTestCase {
12 | func testMultiLevelPartialDecode() throws {
13 | let yaml = """
14 | ---
15 | topLevel:
16 | secondLevel:
17 | desired:
18 | name: My Name
19 | age: 123
20 | """
21 |
22 | struct Desired: Decodable {
23 | var name: String
24 | var age: Int
25 | }
26 |
27 | let node = try Yams.compose(yaml: yaml)!
28 |
29 | let desiredNode = try XCTUnwrap(node["topLevel"]?["secondLevel"]?["desired"])
30 |
31 | let desired = try YAMLDecoder().decode(Desired.self, from: desiredNode)
32 |
33 | XCTAssertEqual(desired.name, "My Name")
34 | XCTAssertEqual(desired.age, 123)
35 | }
36 |
37 | func testDecodeBools() throws {
38 | let yaml = """
39 | ---
40 | topLevel:
41 | unquotedBool: true
42 | explicitBool: !!bool true
43 | explicitStringNotBool: !!str true
44 | singleQuotedStringNotBool: 'true'
45 | doubleQuotedStringNotBool: "true"
46 | """
47 |
48 | struct TopLevel: Decodable {
49 | var unquotedBool: BoolOrString
50 | var explicitBool: BoolOrString
51 | var explicitStringNotBool: BoolOrString
52 | var singleQuotedStringNotBool: BoolOrString
53 | var doubleQuotedStringNotBool: BoolOrString
54 | }
55 |
56 | let node = try Yams.compose(yaml: yaml)!
57 |
58 | let desiredNode = try XCTUnwrap(node["topLevel"])
59 |
60 | let desired = try YAMLDecoder().decode(TopLevel.self, from: desiredNode)
61 |
62 | XCTAssertEqual(desired.unquotedBool, .bool(true))
63 | XCTAssertEqual(desired.explicitBool, .bool(true))
64 | XCTAssertEqual(desired.explicitStringNotBool, .string("true"))
65 | XCTAssertEqual(desired.singleQuotedStringNotBool, .string("true"))
66 | XCTAssertEqual(desired.doubleQuotedStringNotBool, .string("true"))
67 | }
68 | }
69 |
70 | enum BoolOrString: Equatable {
71 | case bool(Bool)
72 | case string(String)
73 | }
74 |
75 | extension BoolOrString: Decodable {
76 | init(from decoder: any Decoder) throws {
77 | let container = try decoder.singleValueContainer()
78 | if let bool = try? container.decode(Bool.self) {
79 | self = .bool(bool)
80 | } else {
81 | self = .string(try container.decode(String.self))
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/.github/workflows/cmake.yml:
--------------------------------------------------------------------------------
1 | name: CMake
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - '.github/workflows/cmake.yml'
8 | - '**/CMakeLists.txt'
9 | - '**/*.cmake'
10 | - '**/*.cmake.in'
11 | - 'Sources/**/*.[ch]'
12 | - 'Sources/**/*.swift'
13 | - 'Sources/**/module.modulemap'
14 | pull_request:
15 | paths:
16 | - '.github/workflows/cmake.yml'
17 | - '**/CMakeLists.txt'
18 | - '**/*.cmake'
19 | - '**/*.cmake.in'
20 | - 'Sources/**/*.[ch]'
21 | - 'Sources/**/*.swift'
22 | - 'Sources/**/module.modulemap'
23 |
24 | concurrency:
25 | group: cmake-${{ github.ref }}
26 | cancel-in-progress: true
27 |
28 | jobs:
29 | CMake:
30 | name: macOS with Xcode ${{ matrix.xcode_version }}
31 | strategy:
32 | matrix:
33 | xcode_version: ['15.0', '15.4']
34 | runs-on: macos-14
35 | env:
36 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app
37 | steps:
38 | - uses: actions/checkout@v4
39 | - run: sudo chown runner:admin /usr/local
40 | - run: |
41 | # macOS GHA runners have an issue with the version of cmake installed by defaut from the pinned tap
42 | # https://github.com/actions/runner-images/issues/12912
43 | # Quick fix is to update the brew tap and install cmake only if it's not already installed
44 | # https://github.com/actions/runner-images/issues/12912#issuecomment-3240845829
45 |
46 | echo "Updating brew and installing cmake if needed..."
47 | brew update && (brew list cmake || brew install cmake)
48 |
49 | echo "Installing ninja..."
50 | brew install ninja
51 | name: Install Homebrew dependencies
52 |
53 | - run: swift -version
54 | - run: rm -rf build
55 | - run: cmake -B build -G Ninja -S . -DCMAKE_BUILD_TYPE=Release
56 | - run: cmake --build build
57 | - run: cmake --build build --target install
58 | - run: file /usr/local/lib/swift/macosx/libYams.dylib | grep "Mach-O 64-bit dynamically linked shared library arm64"
59 |
60 | CMake_Linux:
61 | name: Linux with Swift ${{ matrix.tag }}
62 | strategy:
63 | matrix:
64 | tag: ['5.7', '5.8', '5.9', '5.10']
65 | runs-on: ubuntu-latest
66 | container:
67 | image: swift:${{ matrix.tag }}
68 | steps:
69 | - uses: actions/checkout@v4
70 | - run: apt-get update && apt-get install -y ninja-build curl
71 | - run: curl -L https://github.com/Kitware/CMake/releases/download/v3.23.1/cmake-3.23.1-linux-x86_64.tar.gz | tar xz --strip-components=1 -C /usr
72 | - run: swift -version
73 | - run: rm -rf build
74 | - run: cmake -B build -G Ninja -S . -DCMAKE_BUILD_TYPE=Release
75 | - run: cmake --build build
76 | - run: cmake --build build --target install
77 |
--------------------------------------------------------------------------------
/Yams.xcodeproj/xcshareddata/xcschemes/Yams.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
35 |
36 |
37 |
38 |
40 |
46 |
47 |
48 |
49 |
50 |
60 |
61 |
67 |
68 |
70 |
71 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/Tests/YamsTests/MarkTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkTests.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 4/11/17.
6 | // Copyright (c) 2017 Yams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Yams
11 |
12 | final class MarkTests: XCTestCase, @unchecked Sendable {
13 | func testLocatableDeprecationMessageForSwiftLint() throws {
14 | let deprecatedRulesIdentifiers = [("variable_name", "identifier_name")].map { (Node($0.0), $0.1) }
15 | func deprecatedMessage(from rule: Node) -> String? {
16 | guard let index = deprecatedRulesIdentifiers.firstIndex(where: { $0.0 == rule }) else {
17 | return nil
18 | }
19 | let changed = deprecatedRulesIdentifiers[index].1
20 | return "\(rule.mark?.description ?? ""): warning: '\(rule.string ?? "")' has been renamed to " +
21 | "'\(changed)' and will be completely removed in a future release."
22 | }
23 |
24 | let yaml = """
25 | disabled_rules:
26 | - variable_name
27 | - line_length
28 | variable_name:
29 | min_length: 2
30 | """
31 | let configuration = try Yams.compose(yaml: yaml)
32 | let disabledRules = configuration?.mapping?["disabled_rules"]?.array() ?? []
33 | let configuredRules = configuration?.mapping?.keys.filter({ $0 != "disabled_rules" }) ?? []
34 | let deprecatedMessages = (disabledRules + configuredRules).compactMap(deprecatedMessage(from:))
35 | XCTAssertEqual(deprecatedMessages, [
36 | "2:5: warning: 'variable_name' has been renamed to " +
37 | "'identifier_name' and will be completely removed in a future release.",
38 | "4:1: warning: 'variable_name' has been renamed to " +
39 | "'identifier_name' and will be completely removed in a future release."
40 | ])
41 | }
42 |
43 | func testMappingMarkIsCorrect() throws {
44 | let yaml = """
45 | values:
46 | sequence:
47 | - Hello
48 | - World
49 | """
50 | let root = try Yams.compose(yaml: yaml)
51 | let values = root?.mapping?["values"]
52 | let sequence = values?.mapping?["sequence"]
53 | let firstElement = sequence?.sequence?[0]
54 |
55 | XCTAssertEqual(root?.mark?.description, "1:1")
56 | XCTAssertEqual(values?.mark?.description, "2:3")
57 | XCTAssertEqual(sequence?.mark?.description, "3:5")
58 | XCTAssertEqual(firstElement?.mark?.description, "3:7")
59 | }
60 | }
61 |
62 | extension MarkTests {
63 | static var allTests: [(String, (MarkTests) -> () throws -> Void)] {
64 | return [
65 | ("testLocatableDeprecationMessageForSwiftLint", testLocatableDeprecationMessageForSwiftLint),
66 | ("testMappingMarkIsCorrect", testMappingMarkIsCorrect)
67 | ]
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/Yams/String+Yams.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Yams.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 12/7/16.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension String {
12 | typealias LineNumberColumnAndContents = (lineNumber: Int, column: Int, contents: String)
13 |
14 | /// line number, column and contents at offset.
15 | ///
16 | /// - parameter offset: Int
17 | ///
18 | /// - returns: lineNumber: line number start from 0,
19 | /// column: utf16 column start from 0,
20 | /// contents: substring of line
21 | func lineNumberColumnAndContents(at offset: Int) -> LineNumberColumnAndContents? {
22 | return index(startIndex, offsetBy: offset, limitedBy: endIndex).flatMap(lineNumberColumnAndContents)
23 | }
24 |
25 | /// line number, column and contents at Index.
26 | ///
27 | /// - parameter index: String.Index
28 | ///
29 | /// - returns: lineNumber: line number start from 0,
30 | /// column: utf16 column start from 0,
31 | /// contents: substring of line
32 | func lineNumberColumnAndContents(at index: Index) -> LineNumberColumnAndContents {
33 | assert((startIndex.. String {
59 | var number = 0
60 | var outStartIndex = startIndex, outEndIndex = startIndex, outContentsEndIndex = startIndex
61 | getLineStart(&outStartIndex, end: &outEndIndex, contentsEnd: &outContentsEndIndex,
62 | for: startIndex.. 1.0, >= 1.0.2)
12 | connection_pool (>= 2.2.5)
13 | drb
14 | i18n (>= 1.6, < 2)
15 | minitest (>= 5.1)
16 | mutex_m
17 | tzinfo (~> 2.0)
18 | addressable (2.8.7)
19 | public_suffix (>= 2.0.2, < 7.0)
20 | algoliasearch (1.27.5)
21 | httpclient (~> 2.8, >= 2.8.3)
22 | json (>= 1.5.1)
23 | atomos (0.1.3)
24 | base64 (0.2.0)
25 | bigdecimal (3.1.8)
26 | claide (1.1.0)
27 | cocoapods (1.15.2)
28 | addressable (~> 2.8)
29 | claide (>= 1.0.2, < 2.0)
30 | cocoapods-core (= 1.15.2)
31 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
32 | cocoapods-downloader (>= 2.1, < 3.0)
33 | cocoapods-plugins (>= 1.0.0, < 2.0)
34 | cocoapods-search (>= 1.0.0, < 2.0)
35 | cocoapods-trunk (>= 1.6.0, < 2.0)
36 | cocoapods-try (>= 1.1.0, < 2.0)
37 | colored2 (~> 3.1)
38 | escape (~> 0.0.4)
39 | fourflusher (>= 2.3.0, < 3.0)
40 | gh_inspector (~> 1.0)
41 | molinillo (~> 0.8.0)
42 | nap (~> 1.0)
43 | ruby-macho (>= 2.3.0, < 3.0)
44 | xcodeproj (>= 1.23.0, < 2.0)
45 | cocoapods-core (1.15.2)
46 | activesupport (>= 5.0, < 8)
47 | addressable (~> 2.8)
48 | algoliasearch (~> 1.0)
49 | concurrent-ruby (~> 1.1)
50 | fuzzy_match (~> 2.0.4)
51 | nap (~> 1.0)
52 | netrc (~> 0.11)
53 | public_suffix (~> 4.0)
54 | typhoeus (~> 1.0)
55 | cocoapods-deintegrate (1.0.5)
56 | cocoapods-downloader (2.1)
57 | cocoapods-plugins (1.0.0)
58 | nap
59 | cocoapods-search (1.0.1)
60 | cocoapods-trunk (1.6.0)
61 | nap (>= 0.8, < 2.0)
62 | netrc (~> 0.11)
63 | cocoapods-try (1.2.0)
64 | colored2 (3.1.2)
65 | concurrent-ruby (1.3.3)
66 | connection_pool (2.4.1)
67 | drb (2.2.1)
68 | escape (0.0.4)
69 | ethon (0.16.0)
70 | ffi (>= 1.15.0)
71 | ffi (1.17.0)
72 | fourflusher (2.3.1)
73 | fuzzy_match (2.0.4)
74 | gh_inspector (1.1.3)
75 | httpclient (2.8.3)
76 | i18n (1.14.5)
77 | concurrent-ruby (~> 1.0)
78 | jazzy (0.15.1)
79 | cocoapods (~> 1.5)
80 | mustache (~> 1.1)
81 | open4 (~> 1.3)
82 | redcarpet (~> 3.4)
83 | rexml (>= 3.2.7, < 4.0)
84 | rouge (>= 2.0.6, < 5.0)
85 | sassc (~> 2.1)
86 | sqlite3 (~> 1.3)
87 | xcinvoke (~> 0.3.0)
88 | json (2.7.2)
89 | liferaft (0.0.6)
90 | mini_portile2 (2.8.7)
91 | minitest (5.24.1)
92 | molinillo (0.8.0)
93 | mustache (1.1.1)
94 | mutex_m (0.2.0)
95 | nanaimo (0.3.0)
96 | nap (1.1.0)
97 | netrc (0.11.0)
98 | nkf (0.2.0)
99 | open4 (1.3.4)
100 | public_suffix (4.0.7)
101 | redcarpet (3.6.0)
102 | rexml (3.4.2)
103 | rouge (4.3.0)
104 | ruby-macho (2.5.1)
105 | sassc (2.4.0)
106 | ffi (~> 1.9)
107 | sqlite3 (1.7.3)
108 | mini_portile2 (~> 2.8.0)
109 | typhoeus (1.4.1)
110 | ethon (>= 0.9.0)
111 | tzinfo (2.0.6)
112 | concurrent-ruby (~> 1.0)
113 | xcinvoke (0.3.0)
114 | liferaft (~> 0.0.6)
115 | xcodeproj (1.25.1)
116 | CFPropertyList (>= 2.3.3, < 4.0)
117 | atomos (~> 0.1.3)
118 | claide (>= 1.0.2, < 2.0)
119 | colored2 (~> 3.1)
120 | nanaimo (~> 0.3.0)
121 | rexml (>= 3.3.6, < 4.0)
122 |
123 | PLATFORMS
124 | ruby
125 |
126 | DEPENDENCIES
127 | cocoapods
128 | jazzy
129 |
130 | BUNDLED WITH
131 | 2.5.11
132 |
--------------------------------------------------------------------------------
/Sources/Yams/Node.Scalar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Node.Scalar.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 2/24/17.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | // MARK: Node+Scalar
10 |
11 | extension Node {
12 | /// Scalar node.
13 | public struct Scalar {
14 | /// This node's string value.
15 | public var string: String {
16 | didSet {
17 | tag = .implicit
18 | }
19 | }
20 | /// This node's tag (its type).
21 | public var tag: Tag
22 | /// The style to be used when emitting this node.
23 | public var style: Style
24 | /// The location for this node.
25 | public var mark: Mark?
26 | /// The anchor for this node.
27 | public weak var anchor: Anchor?
28 |
29 | /// The style to use when emitting a `Scalar`.
30 | public enum Style: UInt32 {
31 | /// Let the emitter choose the style.
32 | case any = 0
33 | /// The plain scalar style.
34 | case plain
35 |
36 | /// The single-quoted scalar style.
37 | case singleQuoted
38 | /// The double-quoted scalar style.
39 | case doubleQuoted
40 |
41 | /// The literal scalar style.
42 | case literal
43 | /// The folded scalar style.
44 | case folded
45 | }
46 |
47 | /// Create a `Node.Scalar` using the specified parameters.
48 | ///
49 | /// - parameter string: The string to generate this scalar.
50 | /// - parameter tag: This scalar's `Tag`.
51 | /// - parameter style: The style to use when emitting this `Scalar`.
52 | /// - parameter mark: This scalar's `Mark`.
53 | public init(_ string: String,
54 | _ tag: Tag = .implicit,
55 | _ style: Style = .any,
56 | _ mark: Mark? = nil,
57 | _ anchor: Anchor? = nil) {
58 | self.string = string
59 | self.tag = tag
60 | self.style = style
61 | self.mark = mark
62 | self.anchor = anchor
63 | }
64 | }
65 |
66 | /// Get or set the `Node.Scalar` value if this node is a `Node.scalar`.
67 | public var scalar: Scalar? {
68 | get {
69 | if case let .scalar(scalar) = self {
70 | return scalar
71 | }
72 | return nil
73 | }
74 | set {
75 | if let newValue = newValue {
76 | self = .scalar(newValue)
77 | }
78 | }
79 | }
80 |
81 | /// Get or set the `Node.Alias` value if this node is a `Node.alias`.
82 | public var alias: Alias? {
83 | get {
84 | if case let .alias(alias) = self {
85 | return alias
86 | }
87 | return nil
88 | }
89 | set {
90 | if let newValue = newValue {
91 | self = .alias(newValue)
92 | }
93 | }
94 | }
95 | }
96 |
97 | extension Node.Scalar: Comparable {
98 | /// :nodoc:
99 | public static func < (lhs: Node.Scalar, rhs: Node.Scalar) -> Bool {
100 | return lhs.string < rhs.string
101 | }
102 | }
103 |
104 | extension Node.Scalar: Equatable {
105 | /// :nodoc:
106 | public static func == (lhs: Node.Scalar, rhs: Node.Scalar) -> Bool {
107 | return lhs.string == rhs.string && lhs.resolvedTag == rhs.resolvedTag
108 | }
109 | }
110 |
111 | extension Node.Scalar: Hashable {
112 | /// :nodoc:
113 | public func hash(into hasher: inout Hasher) {
114 | hasher.combine(string)
115 | hasher.combine(resolvedTag)
116 | }
117 | }
118 |
119 | extension Node.Scalar: TagResolvable {
120 | static let defaultTagName = Tag.Name.str
121 | func resolveTag(using resolver: Resolver) -> Tag.Name {
122 | return tag.name == .implicit ? resolver.resolveTag(from: string) : tag.name
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/cmake/modules/SwiftSupport.cmake:
--------------------------------------------------------------------------------
1 |
2 | include(CMakeParseArguments)
3 |
4 | # Returns the architecture name in a variable
5 | #
6 | # Usage:
7 | # swift_get_host_arch(result_var_name)
8 | #
9 | # Sets ${result_var_name} with the converted architecture name derived from
10 | # CMAKE_SYSTEM_PROCESSOR.
11 | function(swift_get_host_arch result_var_name)
12 | if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64")
13 | set("${result_var_name}" "x86_64" PARENT_SCOPE)
14 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "aarch64")
15 | set("${result_var_name}" "aarch64" PARENT_SCOPE)
16 | elseif("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "arm64|ARM64")
17 | set("${result_var_name}" "aarch64" PARENT_SCOPE)
18 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64")
19 | set("${result_var_name}" "powerpc64" PARENT_SCOPE)
20 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64le")
21 | set("${result_var_name}" "powerpc64le" PARENT_SCOPE)
22 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "s390x")
23 | set("${result_var_name}" "s390x" PARENT_SCOPE)
24 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv6l")
25 | set("${result_var_name}" "armv6" PARENT_SCOPE)
26 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7l")
27 | set("${result_var_name}" "armv7" PARENT_SCOPE)
28 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7-a")
29 | set("${result_var_name}" "armv7" PARENT_SCOPE)
30 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64")
31 | set("${result_var_name}" "x86_64" PARENT_SCOPE)
32 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "IA64")
33 | set("${result_var_name}" "itanium" PARENT_SCOPE)
34 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86")
35 | set("${result_var_name}" "i686" PARENT_SCOPE)
36 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "i686")
37 | set("${result_var_name}" "i686" PARENT_SCOPE)
38 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "riscv64")
39 | set("${result_var_name}" "riscv64" PARENT_SCOPE)
40 | else()
41 | message(FATAL_ERROR "Unrecognized architecture on host system: ${CMAKE_SYSTEM_PROCESSOR}")
42 | endif()
43 | endfunction()
44 |
45 | # Returns the os name in a variable
46 | #
47 | # Usage:
48 | # get_swift_host_os(result_var_name)
49 | #
50 | #
51 | # Sets ${result_var_name} with the converted OS name derived from
52 | # CMAKE_SYSTEM_NAME.
53 | function(get_swift_host_os result_var_name)
54 | if(CMAKE_SYSTEM_NAME STREQUAL Darwin)
55 | set(${result_var_name} macosx PARENT_SCOPE)
56 | else()
57 | string(TOLOWER ${CMAKE_SYSTEM_NAME} cmake_system_name_lc)
58 | set(${result_var_name} ${cmake_system_name_lc} PARENT_SCOPE)
59 | endif()
60 | endfunction()
61 |
62 | function(swift_install)
63 | set(options)
64 | set(single_parameter_options EXPORT)
65 | set(multiple_parameter_options TARGETS)
66 |
67 | cmake_parse_arguments(SI
68 | "${options}"
69 | "${single_parameter_options}"
70 | "${multiple_parameter_options}"
71 | ${ARGN})
72 |
73 | list(LENGTH ${SI_TARGETS} si_num_targets)
74 | if(si_num_targets GREATER 1)
75 | message(SEND_ERROR "swift_install only supports a single target at a time")
76 | endif()
77 |
78 | get_swift_host_os(swift_os)
79 | get_target_property(type ${SI_TARGETS} TYPE)
80 |
81 | if(type STREQUAL STATIC_LIBRARY)
82 | set(swift_dir swift_static)
83 | else()
84 | set(swift_dir swift)
85 | endif()
86 |
87 | install(TARGETS ${SI_TARGETS}
88 | EXPORT ${SI_EXPORT}
89 | ARCHIVE DESTINATION lib/${swift_dir}/${swift_os}
90 | LIBRARY DESTINATION lib/${swift_dir}/${swift_os}
91 | RUNTIME DESTINATION bin)
92 | if(type STREQUAL EXECUTABLE)
93 | return()
94 | endif()
95 |
96 | swift_get_host_arch(swift_arch)
97 | get_target_property(module_name ${SI_TARGETS} Swift_MODULE_NAME)
98 | if(NOT module_name)
99 | set(module_name ${SI_TARGETS})
100 | endif()
101 |
102 | if(CMAKE_SYSTEM_NAME STREQUAL Darwin)
103 | install(FILES
104 | $/${module_name}.swiftdoc
105 | DESTINATION lib/${swift_dir}/${swift_os}/${module_name}.swiftmodule
106 | RENAME ${swift_arch}.swiftdoc)
107 | install(FILES
108 | $/${module_name}.swiftmodule
109 | DESTINATION lib/${swift_dir}/${swift_os}/${module_name}.swiftmodule
110 | RENAME ${swift_arch}.swiftmodule)
111 | else()
112 | install(FILES
113 | $/${module_name}.swiftdoc
114 | $/${module_name}.swiftmodule
115 | DESTINATION lib/${swift_dir}/${swift_os}/${swift_arch})
116 | endif()
117 | endfunction()
118 |
--------------------------------------------------------------------------------
/Sources/CYaml/src/writer.c:
--------------------------------------------------------------------------------
1 |
2 | #include "yaml_private.h"
3 |
4 | /*
5 | * Declarations.
6 | */
7 |
8 | static int
9 | yaml_emitter_set_writer_error(yaml_emitter_t *emitter, const char *problem);
10 |
11 | YAML_DECLARE(int)
12 | yaml_emitter_flush(yaml_emitter_t *emitter);
13 |
14 | /*
15 | * Set the writer error and return 0.
16 | */
17 |
18 | static int
19 | yaml_emitter_set_writer_error(yaml_emitter_t *emitter, const char *problem)
20 | {
21 | emitter->error = YAML_WRITER_ERROR;
22 | emitter->problem = problem;
23 |
24 | return 0;
25 | }
26 |
27 | /*
28 | * Flush the output buffer.
29 | */
30 |
31 | YAML_DECLARE(int)
32 | yaml_emitter_flush(yaml_emitter_t *emitter)
33 | {
34 | int low, high;
35 |
36 | assert(emitter); /* Non-NULL emitter object is expected. */
37 | assert(emitter->write_handler); /* Write handler must be set. */
38 | assert(emitter->encoding); /* Output encoding must be set. */
39 |
40 | emitter->buffer.last = emitter->buffer.pointer;
41 | emitter->buffer.pointer = emitter->buffer.start;
42 |
43 | /* Check if the buffer is empty. */
44 |
45 | if (emitter->buffer.start == emitter->buffer.last) {
46 | return 1;
47 | }
48 |
49 | /* If the output encoding is UTF-8, we don't need to recode the buffer. */
50 |
51 | if (emitter->encoding == YAML_UTF8_ENCODING)
52 | {
53 | if (emitter->write_handler(emitter->write_handler_data,
54 | emitter->buffer.start,
55 | emitter->buffer.last - emitter->buffer.start)) {
56 | emitter->buffer.last = emitter->buffer.start;
57 | emitter->buffer.pointer = emitter->buffer.start;
58 | return 1;
59 | }
60 | else {
61 | return yaml_emitter_set_writer_error(emitter, "write error");
62 | }
63 | }
64 |
65 | /* Recode the buffer into the raw buffer. */
66 |
67 | low = (emitter->encoding == YAML_UTF16LE_ENCODING ? 0 : 1);
68 | high = (emitter->encoding == YAML_UTF16LE_ENCODING ? 1 : 0);
69 |
70 | while (emitter->buffer.pointer != emitter->buffer.last)
71 | {
72 | unsigned char octet;
73 | unsigned int width;
74 | unsigned int value;
75 | size_t k;
76 |
77 | /*
78 | * See the "reader.c" code for more details on UTF-8 encoding. Note
79 | * that we assume that the buffer contains a valid UTF-8 sequence.
80 | */
81 |
82 | /* Read the next UTF-8 character. */
83 |
84 | octet = emitter->buffer.pointer[0];
85 |
86 | width = (octet & 0x80) == 0x00 ? 1 :
87 | (octet & 0xE0) == 0xC0 ? 2 :
88 | (octet & 0xF0) == 0xE0 ? 3 :
89 | (octet & 0xF8) == 0xF0 ? 4 : 0;
90 |
91 | value = (octet & 0x80) == 0x00 ? octet & 0x7F :
92 | (octet & 0xE0) == 0xC0 ? octet & 0x1F :
93 | (octet & 0xF0) == 0xE0 ? octet & 0x0F :
94 | (octet & 0xF8) == 0xF0 ? octet & 0x07 : 0;
95 |
96 | for (k = 1; k < width; k ++) {
97 | octet = emitter->buffer.pointer[k];
98 | value = (value << 6) + (octet & 0x3F);
99 | }
100 |
101 | emitter->buffer.pointer += width;
102 |
103 | /* Write the character. */
104 |
105 | if (value < 0x10000)
106 | {
107 | emitter->raw_buffer.last[high] = value >> 8;
108 | emitter->raw_buffer.last[low] = value & 0xFF;
109 |
110 | emitter->raw_buffer.last += 2;
111 | }
112 | else
113 | {
114 | /* Write the character using a surrogate pair (check "reader.c"). */
115 |
116 | value -= 0x10000;
117 | emitter->raw_buffer.last[high] = 0xD8 + (value >> 18);
118 | emitter->raw_buffer.last[low] = (value >> 10) & 0xFF;
119 | emitter->raw_buffer.last[high+2] = 0xDC + ((value >> 8) & 0xFF);
120 | emitter->raw_buffer.last[low+2] = value & 0xFF;
121 |
122 | emitter->raw_buffer.last += 4;
123 | }
124 | }
125 |
126 | /* Write the raw buffer. */
127 |
128 | if (emitter->write_handler(emitter->write_handler_data,
129 | emitter->raw_buffer.start,
130 | emitter->raw_buffer.last - emitter->raw_buffer.start)) {
131 | emitter->buffer.last = emitter->buffer.start;
132 | emitter->buffer.pointer = emitter->buffer.start;
133 | emitter->raw_buffer.last = emitter->raw_buffer.start;
134 | emitter->raw_buffer.pointer = emitter->raw_buffer.start;
135 | return 1;
136 | }
137 | else {
138 | return yaml_emitter_set_writer_error(emitter, "write error");
139 | }
140 | }
141 |
142 |
--------------------------------------------------------------------------------
/.github/workflows/swiftpm.yml:
--------------------------------------------------------------------------------
1 | name: SwiftPM
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - '.codecov.yml'
8 | - '.github/workflows/swiftpm.yml'
9 | - 'Package*'
10 | - 'Sources/**/*.[ch]'
11 | - 'Sources/**/*.swift'
12 | - 'Sources/**/module.modulemap'
13 | - 'Tests/**/*.swift'
14 | - 'Tests/**/*.ya?ml'
15 | pull_request:
16 | paths:
17 | - '.codecov.yml'
18 | - '.github/workflows/swiftpm.yml'
19 | - 'Package*'
20 | - 'Sources/**/*.[ch]'
21 | - 'Sources/**/*.swift'
22 | - 'Sources/**/module.modulemap'
23 | - 'Tests/**/*.swift'
24 | - 'Tests/**/*.ya?ml'
25 |
26 | concurrency:
27 | group: swiftpm-${{ github.ref }}
28 | cancel-in-progress: true
29 |
30 | jobs:
31 | Xcode_Ventura:
32 | name: macOS 13 with Xcode ${{ matrix.xcode_version }}
33 | strategy:
34 | matrix:
35 | xcode_version: ['14.3', '15.0']
36 | runs-on: macos-13
37 | env:
38 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app
39 | steps:
40 | - uses: actions/checkout@v4
41 | - run: swift -version
42 | - run: YAMS_DEFAULT_ENCODING=UTF16 swift test --parallel
43 | - run: YAMS_DEFAULT_ENCODING=UTF8 swift test --parallel
44 |
45 | Xcode_Sonoma:
46 | name: macOS 14 with Xcode ${{ matrix.xcode_version }}
47 | strategy:
48 | matrix:
49 | xcode_version: ['15.4']
50 | runs-on: macos-14
51 | env:
52 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app
53 | steps:
54 | - uses: actions/checkout@v4
55 | - run: swift -version
56 | - run: YAMS_DEFAULT_ENCODING=UTF16 swift test --parallel
57 | - run: YAMS_DEFAULT_ENCODING=UTF8 swift test --parallel
58 | - name: Code Coverage
59 | if: matrix.xcode_version == '15.4'
60 | run: |
61 | swift test --enable-code-coverage
62 | xcrun llvm-cov export -format="lcov" .build/debug/YamsPackageTests.xctest/Contents/MacOS/YamsPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage.lcov
63 | if [[ -n "${CODECOV_TOKEN}" ]]; then
64 | bash <(curl -s https://codecov.io/bash) -f coverage.lcov
65 | fi
66 | env: { 'CODECOV_TOKEN': '${{ secrets.CODECOV_TOKEN }}' }
67 |
68 | Xcode_Sequoia:
69 | name: macOS 15 with Xcode ${{ matrix.xcode_version }}
70 | strategy:
71 | matrix:
72 | xcode_version: ['16.0', '16.2', '16.3']
73 | runs-on: macos-15
74 | env:
75 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app
76 | steps:
77 | - uses: actions/checkout@v4
78 | - run: swift -version
79 | - run: YAMS_DEFAULT_ENCODING=UTF16 swift test --parallel
80 | - run: YAMS_DEFAULT_ENCODING=UTF8 swift test --parallel
81 | - name: Code Coverage
82 | if: matrix.xcode_version == '16.0'
83 | run: |
84 | swift test --enable-code-coverage
85 | xcrun llvm-cov export -format="lcov" .build/debug/YamsPackageTests.xctest/Contents/MacOS/YamsPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage.lcov
86 | if [[ -n "${CODECOV_TOKEN}" ]]; then
87 | bash <(curl -s https://codecov.io/bash) -f coverage.lcov
88 | fi
89 | env: { 'CODECOV_TOKEN': '${{ secrets.CODECOV_TOKEN }}' }
90 |
91 | Linux:
92 | name: Linux with Swift ${{ matrix.tag }}
93 | strategy:
94 | matrix:
95 | tag: ['5.7', '5.8', '5.9', '5.10', '6.0', '6.1']
96 | runs-on: ubuntu-latest
97 | container:
98 | image: swift:${{ matrix.tag }}
99 | steps:
100 | - uses: actions/checkout@v4
101 | - run: YAMS_DEFAULT_ENCODING=UTF16 swift test --parallel
102 | - run: YAMS_DEFAULT_ENCODING=UTF8 swift test --parallel
103 |
104 | Windows:
105 | name: Windows with Swift ${{ matrix.swift_version }}
106 | runs-on: windows-latest
107 | strategy:
108 | matrix:
109 | swift_version: ['6.1', '6.2']
110 | steps:
111 | - uses: actions/checkout@v4
112 | - uses: SwiftyLab/setup-swift@4bbb093f8c68d1dee1caa8b67c681a3f8fe70a91
113 | with:
114 | swift-version: ${{ matrix.swift_version }}
115 | - name: Build
116 | run: swift build -v
117 | - name: Test
118 | run: swift test -v
119 | Android:
120 | name: Android with Swift ${{ matrix.tag }}
121 | strategy:
122 | matrix:
123 | tag: ['6.0.3']
124 | runs-on: ubuntu-latest
125 | steps:
126 | - uses: actions/checkout@v4
127 | - uses: skiptools/swift-android-action@v2
128 | with:
129 | swift-version: ${{ matrix.tag }}
130 | # needed for tests that use fixturesDirectory
131 | copy-files: Tests
132 | test-env: TEST_WORKSPACE=1
133 |
134 |
--------------------------------------------------------------------------------
/Sources/Yams/Node.Sequence.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Node.Sequence.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 2/24/17.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | // MARK: Node+Sequence
10 |
11 | extension Node {
12 | /// Sequence node.
13 | public struct Sequence {
14 | private var nodes: [Node]
15 | /// This node's tag (its type).
16 | public var tag: Tag
17 | /// The style to be used when emitting this node.
18 | public var style: Style
19 | /// The location for this node.
20 | public var mark: Mark?
21 | /// The anchor for this node.
22 | public weak var anchor: Anchor?
23 |
24 | /// The style to use when emitting a `Sequence`.
25 | public enum Style: UInt32 {
26 | /// Let the emitter choose the style.
27 | case any
28 | /// The block sequence style.
29 | case block
30 | /// The flow sequence style.
31 | case flow
32 | }
33 |
34 | /// Create a `Node.Sequence` using the specified parameters.
35 | ///
36 | /// - parameter nodes: The array of nodes to generate this sequence.
37 | /// - parameter tag: This sequence's `Tag`.
38 | /// - parameter style: The style to use when emitting this `Sequence`.
39 | /// - parameter mark: This sequence's `Mark`.
40 | public init(_ nodes: [Node],
41 | _ tag: Tag = .implicit,
42 | _ style: Style = .any,
43 | _ mark: Mark? = nil,
44 | _ anchor: Anchor? = nil) {
45 | self.nodes = nodes
46 | self.tag = tag
47 | self.style = style
48 | self.mark = mark
49 | self.anchor = anchor
50 | }
51 | }
52 |
53 | /// Get or set the `Node.Sequence` value if this node is a `Node.sequence`.
54 | public var sequence: Sequence? {
55 | get {
56 | if case let .sequence(sequence) = self {
57 | return sequence
58 | }
59 | return nil
60 | }
61 | set {
62 | if let newValue = newValue {
63 | self = .sequence(newValue)
64 | }
65 | }
66 | }
67 | }
68 |
69 | // MARK: - Node.Sequence
70 |
71 | extension Node.Sequence: Comparable {
72 | /// :nodoc:
73 | public static func < (lhs: Node.Sequence, rhs: Node.Sequence) -> Bool {
74 | return lhs.nodes < rhs.nodes
75 | }
76 | }
77 |
78 | extension Node.Sequence: Equatable {
79 | /// :nodoc:
80 | public static func == (lhs: Node.Sequence, rhs: Node.Sequence) -> Bool {
81 | return lhs.nodes == rhs.nodes && lhs.resolvedTag == rhs.resolvedTag
82 | }
83 | }
84 |
85 | extension Node.Sequence: Hashable {
86 | /// :nodoc:
87 | public func hash(into hasher: inout Hasher) {
88 | hasher.combine(nodes)
89 | hasher.combine(resolvedTag)
90 | }
91 | }
92 |
93 | extension Node.Sequence: ExpressibleByArrayLiteral {
94 | /// :nodoc:
95 | public init(arrayLiteral elements: Node...) {
96 | self.init(elements)
97 | }
98 | }
99 |
100 | extension Node.Sequence: MutableCollection {
101 | // Sequence
102 | /// :nodoc:
103 | public func makeIterator() -> Array.Iterator {
104 | return nodes.makeIterator()
105 | }
106 |
107 | // Collection
108 | /// :nodoc:
109 | public typealias Index = Array.Index
110 |
111 | /// :nodoc:
112 | public var startIndex: Index {
113 | return nodes.startIndex
114 | }
115 |
116 | /// :nodoc:
117 | public var endIndex: Index {
118 | return nodes.endIndex
119 | }
120 |
121 | /// :nodoc:
122 | public func index(after index: Index) -> Index {
123 | return nodes.index(after: index)
124 | }
125 |
126 | /// :nodoc:
127 | public subscript(index: Index) -> Node {
128 | get {
129 | return nodes[index]
130 | }
131 | // MutableCollection
132 | set {
133 | nodes[index] = newValue
134 | }
135 | }
136 |
137 | /// :nodoc:
138 | public subscript(bounds: Range) -> Array.SubSequence {
139 | get {
140 | return nodes[bounds]
141 | }
142 | // MutableCollection
143 | set {
144 | nodes[bounds] = newValue
145 | }
146 | }
147 |
148 | /// :nodoc:
149 | public var indices: Array.Indices {
150 | return nodes.indices
151 | }
152 | }
153 |
154 | extension Node.Sequence: RandomAccessCollection {
155 | // BidirectionalCollection
156 | /// :nodoc:
157 | public func index(before index: Index) -> Index {
158 | return nodes.index(before: index)
159 | }
160 |
161 | // RandomAccessCollection
162 | /// :nodoc:
163 | public func index(_ index: Index, offsetBy num: Int) -> Index {
164 | return nodes.index(index, offsetBy: num)
165 | }
166 |
167 | /// :nodoc:
168 | public func distance(from start: Index, to end: Int) -> Index {
169 | return nodes.distance(from: start, to: end)
170 | }
171 | }
172 |
173 | extension Node.Sequence: RangeReplaceableCollection {
174 | /// :nodoc:
175 | public init() {
176 | self.init([])
177 | }
178 |
179 | /// :nodoc:
180 | public mutating func replaceSubrange(_ subrange: Range, with newElements: C)
181 | where C: Collection, C.Iterator.Element == Node {
182 | nodes.replaceSubrange(subrange, with: newElements)
183 | }
184 | }
185 |
186 | extension Node.Sequence: TagResolvable {
187 | static let defaultTagName = Tag.Name.seq
188 | }
189 |
--------------------------------------------------------------------------------
/Docs.md:
--------------------------------------------------------------------------------
1 | # Yams Documentation
2 |
3 | For installation instructions, see [README.md](README.md).
4 |
5 | API documentation available at [jpsim.com/Yams](https://jpsim.com/Yams).
6 |
7 | ## Usage
8 |
9 | ### Consume YAML
10 |
11 | Here's a simple example parsing a YAML array of strings:
12 |
13 | ```swift
14 | import Yams
15 |
16 | let yamlString = """
17 | - a
18 | - b
19 | - c
20 | """
21 | do {
22 | let yamlNode = try Yams.load(yaml: yamlString)
23 | if let yamlArray = yamlNode as? [String] {
24 | print(yamlArray)
25 | }
26 | } catch {
27 | print("handle error: \(error)")
28 | }
29 |
30 | // Prints:
31 | // ["a", "b", "c"]
32 | ```
33 |
34 | ### Emit YAML
35 |
36 | Here's a simple example emitting YAML string from a Swift `Array`:
37 |
38 | ```swift
39 | import Yams
40 |
41 | do {
42 | let yamlString = try Yams.serialize(node: ["a", "b", "c"])
43 | print(yamlString)
44 | } catch {
45 | print("handle error: \(error)")
46 | }
47 |
48 | // Prints:
49 | // - a
50 | // - b
51 | // - c
52 | ```
53 |
54 | You can even customize the style:
55 |
56 | ```swift
57 | import Yams
58 |
59 | var node: Node = ["a", "b", "c"]
60 | node.sequence?.style = .flow
61 |
62 | do {
63 | let yamlString = try Yams.serialize(node: node)
64 | print(yamlString)
65 | } catch {
66 | print("handle error: \(error)")
67 | }
68 |
69 | // Prints:
70 | // [a, b, c]
71 | ```
72 |
73 | ### Customize Parsing
74 |
75 | For example, say you only want the literals `true` and `false` to represent booleans, unlike the
76 | YAML spec compliant boolean which also includes `on`/`off` and many others.
77 |
78 | You can customize Yams' Constructor map:
79 |
80 | ```swift
81 | import Yams
82 |
83 | extension Constructor {
84 | public static func withBoolAsTrueFalse() -> Constructor {
85 | var map = defaultScalarMap
86 | map[.bool] = Bool.constructUsingOnlyTrueAndFalse
87 | return Constructor(map)
88 | }
89 | }
90 |
91 | private extension Bool {
92 | static func constructUsingOnlyTrueAndFalse(from scalar: Node.Scalar) -> Bool? {
93 | switch scalar.string.lowercased() {
94 | case "true":
95 | return true
96 | case "false":
97 | return false
98 | default:
99 | return nil
100 | }
101 | }
102 | }
103 |
104 | // Usage:
105 |
106 | let yamlString = """
107 | - true
108 | - on
109 | - off
110 | - false
111 | """
112 | if let array = try? Yams.load(yaml: yamlString, .default, .withBoolAsTrueFalse()) as? [Any] {
113 | print(array)
114 | }
115 |
116 | // Prints:
117 | // [true, "on", "off", false]
118 | ```
119 |
120 | ### Expanding Environment Variables
121 |
122 | For example:
123 |
124 | ```swift
125 | import Yams
126 |
127 | extension Constructor {
128 | public static func withEnv(_ env: [String: String]) -> Constructor {
129 | var map = defaultScalarMap
130 | map[.str] = String.constructExpandingEnvVars(env: env)
131 | return Constructor(map)
132 | }
133 | }
134 |
135 | private extension String {
136 | static func constructExpandingEnvVars(env: [String: String]) -> (_ scalar: Node.Scalar) -> String? {
137 | return { (scalar: Node.Scalar) -> String? in
138 | return node.string.expandingEnvVars(env: env)
139 | }
140 | }
141 |
142 | func expandingEnvVars(env: [String: String]) -> String {
143 | var result = self
144 | for (key, value) in env {
145 | result = result.replacingOccurrences(of: "${\(key)}", with: value)
146 | }
147 |
148 | return result
149 | }
150 | }
151 |
152 | // Usage:
153 |
154 | let yamlString = """
155 | - first
156 | - ${SECOND}
157 | - SECOND
158 | """
159 | let env = ["SECOND": "2"]
160 | if let array = try? Yams.load(yaml: yamlString, .default, .withEnv(env)) as? [String] {
161 | print(array)
162 | }
163 |
164 | // Prints:
165 | // ["first", "2", "SECOND"]
166 | ```
167 |
168 | ### Converting Between Formats
169 |
170 | Because Yams conforms to Swift 4's Codable protocol and provides a YAML Encoder and Decoder,
171 | you can easily convert between YAML and other formats that also provide Swift 4 Encoders and
172 | Decoders, such as JSON and Plist.
173 |
174 | ### Error Handling
175 |
176 | Failable operations in Yams throw Swift errors.
177 |
178 | ### Types
179 |
180 | | Name | Yams Tag | YAML Tag | Swift Types |
181 | |----------------|---------------|-------------------------------|--------------------------------|
182 | | ... | `implicit` | `` | ... |
183 | | ... | `nonSpecific` | `!` | ... |
184 | | String | `str` | `tag:yaml.org,2002:str` | `String` |
185 | | Sequence | `seq` | `tag:yaml.org,2002:seq` | `Array` |
186 | | Map | `map` | `tag:yaml.org,2002:map` | `Dictionary` |
187 | | Boolean | `bool` | `tag:yaml.org,2002:bool` | `Bool` |
188 | | Floating Point | `float` | `tag:yaml.org,2002:float` | ... |
189 | | Null | `null` | `tag:yaml.org,2002:null` | `Void` |
190 | | Integer | `int` | `tag:yaml.org,2002:int` | `FixedWidthInteger` |
191 | | ... | `binary` | `tag:yaml.org,2002:binary` | `Data` |
192 | | ... | `merge` | `tag:yaml.org,2002:merge` | ... |
193 | | ... | `omap` | `tag:yaml.org,2002:omap` | ... |
194 | | ... | `pairs` | `tag:yaml.org,2002:pairs` | ... |
195 | | Set | `set` | `tag:yaml.org,2002:set` | `Set` |
196 | | Timestamp | `timestamp` | `tag:yaml.org,2002:timestamp` | `Date` |
197 | | ... | `value` | `tag:yaml.org,2002:value` | ... |
198 | | YAML | `yaml` | `tag:yaml.org,2002:yaml` | Unsupported |
199 |
--------------------------------------------------------------------------------
/.github/workflows/xcodebuild.yml:
--------------------------------------------------------------------------------
1 | name: xcodebuild
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - '.github/workflows/xcodebuild.yml'
8 | - 'Yams.xcodeproj/**'
9 | - 'Sources/**/*.[ch]'
10 | - 'Sources/**/*.swift'
11 | - 'Tests/**/*.swift'
12 | - 'Tests/**/*.ya?ml'
13 | - '!Tests/LinuxMain.swift'
14 | pull_request:
15 | paths:
16 | - '.github/workflows/xcodebuild.yml'
17 | - 'Yams.xcodeproj/**'
18 | - 'Sources/**/*.[ch]'
19 | - 'Sources/**/*.swift'
20 | - 'Tests/**/*.swift'
21 | - 'Tests/**/*.ya?ml'
22 | - '!Tests/LinuxMain.swift'
23 |
24 | concurrency:
25 | group: xcodebuild-${{ github.ref }}
26 | cancel-in-progress: true
27 |
28 | jobs:
29 | xcodebuild_Ventura:
30 | name: macOS 13 with Xcode ${{ matrix.xcode_version }}
31 | strategy:
32 | matrix:
33 | xcode_version: ['14.3', '15.0']
34 | xcode_flags: ['-scheme Yams -project Yams.xcodeproj']
35 | runs-on: macos-13
36 | env:
37 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app
38 | steps:
39 | - uses: actions/checkout@v4
40 | - run: xcodebuild -version
41 | - name: macOS with UTF16
42 | if: always()
43 | run: set -o pipefail && YAMS_DEFAULT_ENCODING=UTF16 xcodebuild ${{ matrix.xcode_flags }} test | xcbeautify --renderer github-actions
44 | shell: bash
45 | - name: macOS with UTF8
46 | if: always()
47 | run: set -o pipefail && YAMS_DEFAULT_ENCODING=UTF8 xcodebuild ${{ matrix.xcode_flags }} test | xcbeautify --renderer github-actions
48 | shell: bash
49 | - name: iPhone Simulator
50 | if: always()
51 | run: set -o pipefail && xcodebuild ${{ matrix.xcode_flags }} test -sdk iphonesimulator -destination "name=iPhone 8" | xcbeautify --renderer github-actions
52 | shell: bash
53 | - name: Apple TV Simulator
54 | if: always()
55 | run: set -o pipefail && xcodebuild ${{ matrix.xcode_flags }} test -sdk appletvsimulator -destination "name=Apple TV 4K (2nd generation)" | xcbeautify --renderer github-actions
56 | shell: bash
57 | - name: watchOS Simulator
58 | if: always()
59 | run: set -o pipefail && xcodebuild ${{ matrix.xcode_flags }} build -sdk watchsimulator | xcbeautify --renderer github-actions
60 | shell: bash
61 |
62 | xcodebuild_Sonoma:
63 | name: macOS 14 with Xcode ${{ matrix.xcode_version }}
64 | strategy:
65 | matrix:
66 | xcode_version: ['15.4']
67 | xcode_flags: ['-scheme Yams -project Yams.xcodeproj']
68 | runs-on: macos-14
69 | env:
70 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app
71 | steps:
72 | - uses: actions/checkout@v4
73 | - run: xcodebuild -version
74 | - name: macOS with UTF16
75 | if: always()
76 | run: set -o pipefail && YAMS_DEFAULT_ENCODING=UTF16 xcodebuild ${{ matrix.xcode_flags }} test | xcbeautify --renderer github-actions
77 | shell: bash
78 | - name: macOS with UTF8
79 | if: always()
80 | run: set -o pipefail && YAMS_DEFAULT_ENCODING=UTF8 xcodebuild ${{ matrix.xcode_flags }} test | xcbeautify --renderer github-actions
81 | shell: bash
82 | - name: iPhone Simulator
83 | if: always()
84 | run: set -o pipefail && xcodebuild ${{ matrix.xcode_flags }} test -sdk iphonesimulator -destination "name=iPhone 8" | xcbeautify --renderer github-actions
85 | shell: bash
86 | - name: Apple TV Simulator
87 | if: always()
88 | run: set -o pipefail && xcodebuild ${{ matrix.xcode_flags }} test -sdk appletvsimulator -destination "name=Apple TV 4K (2nd generation)" | xcbeautify --renderer github-actions
89 | shell: bash
90 | - name: watchOS Simulator
91 | if: always()
92 | run: set -o pipefail && xcodebuild ${{ matrix.xcode_flags }} build -sdk watchsimulator | xcbeautify --renderer github-actions
93 | shell: bash
94 | - name: visionOS Simulator
95 | if: always()
96 | run: set -o pipefail && xcodebuild ${{ matrix.xcode_flags }} build -sdk xrsimulator | xcbeautify --renderer github-actions
97 | shell: bash
98 | xcodebuild_Sequoia:
99 | name: macOS 15 with Xcode ${{ matrix.xcode_version }}
100 | strategy:
101 | matrix:
102 | xcode_version: ['16.0', '16.2', '16.3']
103 | xcode_flags: ['-scheme Yams -project Yams.xcodeproj']
104 | runs-on: macos-15
105 | env:
106 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app
107 | steps:
108 | - uses: actions/checkout@v4
109 | - run: xcodebuild -version
110 | - name: macOS with UTF16
111 | if: always()
112 | run: set -o pipefail && YAMS_DEFAULT_ENCODING=UTF16 xcodebuild ${{ matrix.xcode_flags }} test | xcbeautify --renderer github-actions
113 | shell: bash
114 | - name: macOS with UTF8
115 | if: always()
116 | run: set -o pipefail && YAMS_DEFAULT_ENCODING=UTF8 xcodebuild ${{ matrix.xcode_flags }} test | xcbeautify --renderer github-actions
117 | shell: bash
118 | - name: iPhone Simulator
119 | if: always()
120 | run: |
121 | set -o pipefail
122 | xcodebuild -downloadPlatform iOS
123 | xcodebuild ${{ matrix.xcode_flags }} test -sdk iphonesimulator -destination "name=iPhone 16" | xcbeautify --renderer github-actions
124 | shell: bash
125 | - name: Apple TV Simulator
126 | if: always()
127 | run: |
128 | set -o pipefail
129 | xcodebuild -downloadPlatform tvOS
130 | xcodebuild ${{ matrix.xcode_flags }} test -sdk appletvsimulator -destination "name=Apple TV 4K (3rd generation)" | xcbeautify --renderer github-actions
131 | shell: bash
132 | - name: watchOS Simulator
133 | if: always()
134 | run: set -o pipefail && xcodebuild ${{ matrix.xcode_flags }} build -sdk watchsimulator | xcbeautify --renderer github-actions
135 | shell: bash
136 | - name: visionOS Simulator
137 | if: always()
138 | run: set -o pipefail && xcodebuild ${{ matrix.xcode_flags }} build -sdk xrsimulator | xcbeautify --renderer github-actions
139 | shell: bash
140 |
--------------------------------------------------------------------------------
/Sources/Yams/Resolver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Resolver.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 12/15/16.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Class used to resolve nodes to tags based on customizable rules.
12 | public final class Resolver: Sendable {
13 | /// Rule describing how to resolve tags from regex patterns.
14 | public struct Rule: Sendable {
15 | /// The tag name this rule applies to.
16 | public let tag: Tag.Name
17 | fileprivate let regexp: NSRegularExpression
18 | /// The regex pattern used to resolve this rule.
19 | public var pattern: String { return regexp.pattern }
20 |
21 | /// Create a rule with the specified tag name and regex pattern.
22 | ///
23 | /// - parameter tag: The tag name this rule should apply to.
24 | /// - parameter pattern: The regex pattern used to resolve this rule.
25 | ///
26 | /// - throws: Throws an error if the regular expression pattern is invalid.
27 | public init(_ tag: Tag.Name, _ pattern: String) throws {
28 | self.tag = tag
29 | self.regexp = try .init(pattern: pattern, options: [])
30 | }
31 | }
32 |
33 | /// The rules used by this resolver to resolve nodes to tags.
34 | public let rules: [Rule]
35 |
36 | internal init(_ rules: [Rule] = []) { self.rules = rules }
37 |
38 | /// Resolve a tag name from a given node.
39 | ///
40 | /// - parameter node: Node whose tag should be resolved.
41 | ///
42 | /// - returns: The resolved tag name.
43 | public func resolveTag(of node: Node) -> Tag.Name {
44 | switch node {
45 | case let .scalar(scalar):
46 | return resolveTag(of: scalar)
47 | case let .mapping(mapping):
48 | return resolveTag(of: mapping)
49 | case let .sequence(sequence):
50 | return resolveTag(of: sequence)
51 | case let .alias(alias):
52 | return resolveTag(of: alias)
53 | }
54 | }
55 |
56 | /// Returns a Resolver constructed by appending rule.
57 | public func appending(_ rule: Rule) -> Resolver {
58 | return .init(rules + [rule])
59 | }
60 |
61 | /// Returns a Resolver constructed by appending pattern for tag.
62 | public func appending(_ tag: Tag.Name, _ pattern: String) throws -> Resolver {
63 | return appending(try Rule(tag, pattern))
64 | }
65 |
66 | /// Returns a Resolver constructed by replacing rule.
67 | public func replacing(_ rule: Rule) -> Resolver {
68 | return .init(rules.map { $0.tag == rule.tag ? rule : $0 })
69 | }
70 |
71 | /// Returns a Resolver constructed by replacing pattern for tag.
72 | public func replacing(_ tag: Tag.Name, with pattern: String) throws -> Resolver {
73 | return .init(try rules.map { $0.tag == tag ? try Rule($0.tag, pattern) : $0 })
74 | }
75 |
76 | /// Returns a Resolver constructed by removing pattern for tag.
77 | public func removing(_ tag: Tag.Name) -> Resolver {
78 | return .init(rules.filter({ $0.tag != tag }))
79 | }
80 |
81 | // MARK: - internal
82 |
83 | func resolveTag(of value: T) -> Tag.Name where T: TagResolvable {
84 | return value.resolveTag(using: self)
85 | }
86 |
87 | func resolveTag(from string: String) -> Tag.Name {
88 | for rule in rules where rule.regexp.matches(in: string) {
89 | return rule.tag
90 | }
91 | return .str
92 | }
93 | }
94 |
95 | // MARK: Defaults
96 |
97 | extension Resolver {
98 | /// Resolver with no rules.
99 | public static let basic = Resolver()
100 | /// Resolver with a default set of rules.
101 | public static let `default` = Resolver([.bool, .int, .float, .merge, .null, .timestamp, .value])
102 | }
103 |
104 | // MARK: Default Resolver Rules
105 |
106 | extension Resolver.Rule {
107 | // swiftlint:disable force_try
108 |
109 | /// Default bool resolver rule.
110 | public static let bool = try! Resolver.Rule(.bool, """
111 | ^(?:yes|Yes|YES|no|No|NO\
112 | |true|True|TRUE|false|False|FALSE\
113 | |on|On|ON|off|Off|OFF)$
114 | """)
115 |
116 | /// Default int resolver rule.
117 | public static let int = try! Resolver.Rule(.int, """
118 | ^(?:[-+]?0b[0-1_]+\
119 | |[-+]?0o?[0-7_]+\
120 | |[-+]?(?:0|[1-9][0-9_]*)\
121 | |[-+]?0x[0-9a-fA-F_]+\
122 | |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$
123 | """)
124 |
125 | /// Default float resolver rule.
126 | public static let float = try! Resolver.Rule(.float, """
127 | ^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?\
128 | |\\.[0-9_]+(?:[eE][-+][0-9]+)?\
129 | |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*\
130 | |[-+]?\\.(?:inf|Inf|INF)\
131 | |\\.(?:nan|NaN|NAN))$
132 | """)
133 |
134 | /// Default merge resolver rule.
135 | public static let merge = try! Resolver.Rule(.merge, "^(?:<<)$")
136 |
137 | /// Default null resolver rule.
138 | public static let null = try! Resolver.Rule(.null, """
139 | ^(?:~\
140 | |null|Null|NULL\
141 | |)$
142 | """)
143 |
144 | /// Default timestamp resolver rule.
145 | public static let timestamp = try! Resolver.Rule(.timestamp, """
146 | ^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]\
147 | |[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?\
148 | (?:[Tt]|[ \\t]+)[0-9][0-9]?\
149 | :[0-9][0-9]:[0-9][0-9](?:\\.[0-9]*)?\
150 | (?:[ \\t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$
151 | """)
152 |
153 | /// Default value resolver rule.
154 | public static let value = try! Resolver.Rule(.value, "^(?:=)$")
155 |
156 | // swiftlint:enable force_try
157 | }
158 |
159 | func pattern(_ string: String) -> NSRegularExpression {
160 | do {
161 | return try .init(pattern: string, options: [])
162 | } catch {
163 | fatalError("unreachable")
164 | }
165 | }
166 |
167 | private extension NSRegularExpression {
168 | func matches(in string: String) -> Bool {
169 | let range = NSRange(location: 0, length: string.utf16.count)
170 | if let match = firstMatch(in: string, options: [], range: range) {
171 | return match.range.location != NSNotFound
172 | }
173 | return false
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/Sources/Yams/RedundancyAliasingStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RedundancyAliasingStrategy.swift
3 | // Yams
4 | //
5 | // Created by Adora Lynch on 8/15/24.
6 | // Copyright (c) 2024 Yams. All rights reserved.
7 | //
8 |
9 | /// An enum indicating the outcome of a `RedundancyAliasingStrategy`
10 | public enum RedundancyAliasingOutcome {
11 | /// encoder will encode an Anchor
12 | case anchor(Anchor)
13 | /// encoder will encode an alias to an anchor which should already have been specified.
14 | case alias(Anchor)
15 | /// encoder will encode without an anchor or an alias
16 | case none
17 | }
18 |
19 | /// A class-bound protocol which implements a strategy for detecting aliasable values in a YAML document.
20 | /// Implementations should return RedundancyAliasingOutcome.anchor(...) for the first occurrence of a value.
21 | /// Subsequent occurrences of the same value (where same-ness is defined by the implementation) should
22 | /// return RedundancyAliasingOutcome.alias(...) where the contained Anchor has the same value as the previously
23 | /// returned RedundancyAliasingOutcome.anchor(...). Its the identity of the Anchor values returned that ultimately
24 | /// informs the YAML encoder when to use aliases.
25 | /// N,B. It is essential that implementations release all references to Anchors which are created by this type
26 | /// when releaseAnchorReferences() is called by the Encoder. After this call the implementation will no longer be
27 | /// referenced by the Encoder and will itself be released.
28 | public protocol RedundancyAliasingStrategy: AnyObject {
29 |
30 | /// Implementations should return RedundancyAliasingOutcome.anchor(...) for the first occurrence of a value.
31 | /// Subsequent occurrences of the same value (where same-ness is defined by the implementation) should
32 | /// return RedundancyAliasingOutcome.alias(...) where the contained Anchor has the same value as the previously
33 | /// returned RedundancyAliasingOutcome.anchor(...). Its the identity of the Anchor values returned that ultimately
34 | /// informs the YAML encoder when to use aliases.
35 | func alias(for encodable: any Encodable) throws -> RedundancyAliasingOutcome
36 |
37 | /// It is essential that implementations release all references to Anchors which are created by this type
38 | /// when releaseAnchorReferences() is called by the Encoder. After this call, the implementation will no longer be
39 | /// referenced by the Encoder and will itself be released.
40 |
41 | func releaseAnchorReferences() throws
42 |
43 | /// Implementations must remove all reference to the supplied anchor, permitting it to be deallocated.
44 | func remit(anchor: Anchor) throws
45 | }
46 |
47 | /// An implementation of RedundancyAliasingStrategy that defines alias-ability by Hashable-Equality.
48 | /// i.e. if two values are Hashable-Equal, they will be aliased in the resultant YML document.
49 | public class HashableAliasingStrategy: RedundancyAliasingStrategy {
50 | private var hashesToAliases: [AnyHashable: Anchor] = [:]
51 |
52 | let uniqueAliasProvider = UniqueAliasProvider()
53 |
54 | /// Initialize a new HashableAliasingStrategy
55 | public init() {}
56 |
57 | public func alias(for encodable: any Encodable) throws -> RedundancyAliasingOutcome {
58 | guard let hashable = encodable as? any Hashable & Encodable else {
59 | return .none
60 | }
61 | return try alias(for: hashable)
62 | }
63 |
64 | private func alias(for hashable: any Hashable & Encodable) throws -> RedundancyAliasingOutcome {
65 | let anyHashable = AnyHashable(hashable)
66 | if let existing = hashesToAliases[anyHashable] {
67 | return .alias(existing)
68 | } else {
69 | let newAlias = uniqueAliasProvider.uniqueAlias(for: hashable)
70 | hashesToAliases[anyHashable] = newAlias
71 | return .anchor(newAlias)
72 | }
73 | }
74 |
75 | public func releaseAnchorReferences() throws {
76 | hashesToAliases.removeAll()
77 | }
78 |
79 | public func remit(anchor: Anchor) throws {
80 | hashesToAliases.remove(keysForValue: anchor)
81 | }
82 | }
83 |
84 | /// An implementation of RedundancyAliasingStrategy that defines alias-ability by the coded representation
85 | /// of the values. i.e. if two values encode to exactly the same, they will be aliased in the resultant YML
86 | /// document even if the values themselves are of different types
87 | public class StrictEncodableAliasingStrategy: RedundancyAliasingStrategy {
88 | private var codedToAliases: [String: Anchor] = [:]
89 |
90 | let uniqueAliasProvider = UniqueAliasProvider()
91 |
92 | /// Initialize a new StrictEncodableAliasingStrategy
93 | public init() {}
94 |
95 | private let encoder = YAMLEncoder()
96 |
97 | public func alias(for encodable: any Encodable) throws -> RedundancyAliasingOutcome {
98 | let coded = try encoder.encode(encodable)
99 | if let existing = codedToAliases[coded] {
100 | return .alias(existing)
101 | } else {
102 | let newAlias = uniqueAliasProvider.uniqueAlias(for: encodable)
103 | codedToAliases[coded] = newAlias
104 | return .anchor(newAlias)
105 | }
106 | }
107 |
108 | public func releaseAnchorReferences() throws {
109 | codedToAliases.removeAll()
110 | }
111 |
112 | public func remit(anchor: Anchor) throws {
113 | codedToAliases.remove(keysForValue: anchor)
114 | }
115 | }
116 |
117 | class UniqueAliasProvider {
118 | private var counter = 0
119 |
120 | func uniqueAlias(for encodable: any Encodable) -> Anchor {
121 | if let anchorProviding = encodable as? YamlAnchorProviding,
122 | let anchor = anchorProviding.yamlAnchor {
123 | return anchor
124 | } else {
125 | counter += 1
126 | return Anchor(rawValue: String(counter))
127 | }
128 | }
129 | }
130 |
131 | extension CodingUserInfoKey {
132 | internal static let redundancyAliasingStrategyKey = Self(rawValue: "redundancyAliasingStrategy")!
133 | }
134 |
135 | fileprivate extension Dictionary {
136 |
137 | func removing(keysForValue: Value) -> Self where Value: Equatable {
138 | var mutable = Self(minimumCapacity: self.count)
139 | for (key, value) in self where value != keysForValue {
140 | mutable[key] = value
141 | }
142 | return mutable
143 | }
144 |
145 | mutating func remove(keysForValue value: Value) where Value: Equatable {
146 | self = self.removing(keysForValue: value)
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Tests/YamsTests/RepresenterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepresenterTests.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 1/14/17.
6 | // Copyright (c) 2017 Yams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Yams
12 |
13 | final class RepresenterTests: XCTestCase, @unchecked Sendable {
14 | func testBool() throws {
15 | XCTAssertEqual(try Node(true), "true")
16 | XCTAssertEqual(try Node(false), "false")
17 | }
18 |
19 | func testData() throws {
20 | let base64EncodedString = """
21 | R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\
22 | OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\
23 | +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\
24 | AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=
25 | """
26 | let data = Data(base64Encoded: base64EncodedString, options: .ignoreUnknownCharacters)!
27 | XCTAssertEqual(try Node(data), Node(base64EncodedString, Tag(.binary)))
28 | }
29 |
30 | func testDate() throws {
31 | do {
32 | let date = timestamp( 0, 2001, 12, 15, 02, 59, 43)
33 | XCTAssertEqual(try Node(date), "2001-12-15T02:59:43Z")
34 | }
35 | do { // fractional seconds
36 | let date = timestamp( 0, 2001, 12, 15, 02, 59, 43, 0.1)
37 | XCTAssertEqual(try Node(date), "2001-12-15T02:59:43.1Z")
38 | }
39 | }
40 |
41 | func testDouble() throws {
42 | XCTAssertEqual(try Node(Double.infinity), ".inf")
43 | XCTAssertEqual(try Node(-Double.infinity), "-.inf")
44 | XCTAssertEqual(try Node(Double.nan), ".nan")
45 | XCTAssertEqual(try Node(Double(6.8523015e+5)), "6.8523015e+5")
46 | XCTAssertEqual(try Node(Double(6.8523015e-5)), "6.8523015e-5")
47 | XCTAssertEqual(try Node(Double.greatestFiniteMagnitude), "1.79769313486232e+308")
48 | XCTAssertEqual(try Node(Double.leastNormalMagnitude), "2.2250738585072e-308")
49 | }
50 |
51 | func testFloat() throws {
52 | XCTAssertEqual(try Node(Float.infinity), ".inf")
53 | XCTAssertEqual(try Node(-Float.infinity), "-.inf")
54 | XCTAssertEqual(try Node(Float.nan), ".nan")
55 | XCTAssertEqual(try Node(Float(6.852301e+5)), "6.852301e+5")
56 | XCTAssertEqual(try Node(Float(6.852301e-5)), "6.852301e-5")
57 | XCTAssertEqual(try Node(Float.greatestFiniteMagnitude), "3.402823e+38")
58 | XCTAssertEqual(try Node(Float.leastNormalMagnitude), "1.175494e-38")
59 | }
60 |
61 | func testInteger() throws {
62 | if MemoryLayout.size == 4 {
63 | XCTAssertEqual(try Node(Int.max), "2147483647")
64 | XCTAssertEqual(try Node(Int.min), "-2147483648")
65 | XCTAssertEqual(try Node(UInt.max), "4294967295")
66 | } else if MemoryLayout.size == 8 {
67 | XCTAssertEqual(try Node(Int.max), "9223372036854775807")
68 | XCTAssertEqual(try Node(Int.min), "-9223372036854775808")
69 | XCTAssertEqual(try Node(UInt.max), "18446744073709551615")
70 | }
71 | XCTAssertEqual(try Node(Int(0)), "0")
72 | XCTAssertEqual(try Node(UInt(0)), "0")
73 |
74 | XCTAssertEqual(try Node(Int16.max), "32767")
75 | XCTAssertEqual(try Node(Int16(0)), "0")
76 | XCTAssertEqual(try Node(Int16.min), "-32768")
77 | XCTAssertEqual(try Node(Int32.max), "2147483647")
78 | XCTAssertEqual(try Node(Int32(0)), "0")
79 | XCTAssertEqual(try Node(Int32.min), "-2147483648")
80 | XCTAssertEqual(try Node(Int64.max), "9223372036854775807")
81 | XCTAssertEqual(try Node(Int64(0)), "0")
82 | XCTAssertEqual(try Node(Int64.min), "-9223372036854775808")
83 | XCTAssertEqual(try Node(Int8.max), "127")
84 | XCTAssertEqual(try Node(Int8(0)), "0")
85 | XCTAssertEqual(try Node(Int8.min), "-128")
86 |
87 | XCTAssertEqual(try Node(UInt16.max), "65535")
88 | XCTAssertEqual(try Node(UInt16(0)), "0")
89 | XCTAssertEqual(try Node(UInt32.max), "4294967295")
90 | XCTAssertEqual(try Node(UInt32(0)), "0")
91 | XCTAssertEqual(try Node(UInt64.max), "18446744073709551615")
92 | XCTAssertEqual(try Node(UInt64(0)), "0")
93 | XCTAssertEqual(try Node(UInt8.max), "255")
94 | XCTAssertEqual(try Node(UInt8(0)), "0")
95 | }
96 |
97 | func testString() throws {
98 | XCTAssertEqual(Node("test"), "test")
99 | }
100 |
101 | func testUUID() throws {
102 | try XCTAssertEqual(
103 | Node(UUID(uuidString: "B5C6C790-BC0A-4781-9AFF-F9896E0C030C")),
104 | "B5C6C790-BC0A-4781-9AFF-F9896E0C030C"
105 | )
106 | }
107 |
108 | func testOptional() throws {
109 | // Optional.none
110 | XCTAssertEqual(try Node(Int?.none), "null")
111 |
112 | // Optional.some(.none)
113 | XCTAssertEqual(try Node(Int??.some(nil)), "null")
114 |
115 | // Keyed null - https://github.com/jpsim/Yams/issues/232
116 | let keyedNullString = """
117 | A: null
118 |
119 | """
120 | let keyedNullObject = try Yams.load(yaml: keyedNullString)
121 | let keyedNullDump = try Yams.dump(object: keyedNullObject)
122 | XCTAssertEqual(keyedNullDump, keyedNullString)
123 | }
124 |
125 | func testArray() throws {
126 | let ints = [1, 2, 3]
127 | XCTAssertEqual(try Node(ints), [1, 2, 3])
128 | }
129 |
130 | func testDictionary() throws {
131 | let stringToString = ["key": "value"]
132 | XCTAssertEqual(try Node(stringToString), ["key": "value"])
133 |
134 | let intToInt = [1: 2]
135 | XCTAssertEqual(try Node(intToInt), [1: 2])
136 | }
137 |
138 | func testEmptyDictionary() throws {
139 | XCTAssertEqual(try Yams.dump(object: NSDictionary()), "{}\n")
140 | }
141 | }
142 |
143 | extension RepresenterTests {
144 | static var allTests: [(String, (RepresenterTests) -> () throws -> Void)] {
145 | return [
146 | ("testBool", testBool),
147 | ("testData", testData),
148 | ("testDate", testDate),
149 | ("testDouble", testDouble),
150 | ("testFloat", testFloat),
151 | ("testInteger", testInteger),
152 | ("testString", testString),
153 | ("testUUID", testUUID),
154 | ("testOptional", testOptional),
155 | ("testArray", testArray),
156 | ("testDictionary", testDictionary),
157 | ("testEmptyDictionary", testEmptyDictionary)
158 | ]
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/Sources/Yams/Tag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tag.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 12/15/16.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | /// Tags describe the the _type_ of a Node.
10 | public final class Tag {
11 | /// Tag name.
12 | public struct Name: RawRepresentable, Hashable, Sendable {
13 | /// This `Tag.Name`'s raw string value.
14 | public let rawValue: String
15 | /// Create a `Tag.Name` with a raw string value.
16 | public init(rawValue: String) {
17 | self.rawValue = rawValue
18 | }
19 | }
20 |
21 | /// Shorthand accessor for `Tag(.implicit)`.
22 | public static var implicit: Tag {
23 | return Tag(.implicit)
24 | }
25 |
26 | /// Create a `Tag` with the specified name, resolver and constructor.
27 | ///
28 | /// - parameter name: Tag name.
29 | /// - parameter resolver: `Resolver` this tag should use, `.default` if omitted.
30 | /// - parameter constructor: `Constructor` this tag should use, `.default` if omitted.
31 | public init(_ name: Name,
32 | _ resolver: Resolver = .default,
33 | _ constructor: Constructor = .default) {
34 | self.resolver = resolver
35 | self.constructor = constructor
36 | self.name = name
37 | }
38 |
39 | /// Lens returning a copy of the current `Tag` with the specified overridden changes.
40 | ///
41 | /// - note: Omitting or passing nil for a parameter will preserve the current `Tag`'s value in the copy.
42 | ///
43 | /// - parameter name: Overridden tag name.
44 | /// - parameter resolver: Overridden resolver.
45 | /// - parameter constructor: Overridden constructor.
46 | ///
47 | /// - returns: A copy of the current `Tag` with the specified overridden changes.
48 | public func copy(with name: Name? = nil, resolver: Resolver? = nil, constructor: Constructor? = nil) -> Tag {
49 | return .init(name ?? self.name, resolver ?? self.resolver, constructor ?? self.constructor)
50 | }
51 |
52 | // internal
53 | let constructor: Constructor
54 | var name: Name
55 |
56 | fileprivate func resolved(with value: T) -> Tag where T: TagResolvable {
57 | if name == .implicit {
58 | name = resolver.resolveTag(of: value)
59 | } else if name == .nonSpecific {
60 | name = T.defaultTagName
61 | }
62 | return self
63 | }
64 |
65 | // private
66 | private let resolver: Resolver
67 | }
68 |
69 | extension Tag: CustomStringConvertible {
70 | /// A textual representation of this tag.
71 | public var description: String {
72 | return name.rawValue
73 | }
74 | }
75 |
76 | extension Tag: Hashable {
77 | /// :nodoc:
78 | public func hash(into hasher: inout Hasher) {
79 | hasher.combine(name)
80 | }
81 |
82 | /// :nodoc:
83 | public static func == (lhs: Tag, rhs: Tag) -> Bool {
84 | return lhs.name == rhs.name
85 | }
86 | }
87 |
88 | extension Tag: RawRepresentable {
89 | public convenience init?(rawValue: String) {
90 | self.init(stringLiteral: rawValue)
91 | }
92 | public var rawValue: String {
93 | name.rawValue
94 | }
95 | }
96 |
97 | extension Tag: Codable {}
98 |
99 | extension Tag: ExpressibleByStringLiteral {
100 | /// :nodoc:
101 | public convenience init(stringLiteral value: String) {
102 | self.init(.init(rawValue: value))
103 | }
104 | }
105 |
106 | extension Tag.Name: ExpressibleByStringLiteral {
107 | /// :nodoc:
108 | public init(stringLiteral value: String) {
109 | self.rawValue = value
110 | }
111 | }
112 |
113 | extension Tag.Name: Codable {}
114 |
115 | // http://www.yaml.org/spec/1.2/spec.html#Schema
116 | extension Tag.Name {
117 | // Special
118 | /// Tag should be resolved by value.
119 | public static let implicit: Tag.Name = ""
120 | /// Tag should not be resolved by value, and be resolved as .str, .seq or .map.
121 | public static let nonSpecific: Tag.Name = "!"
122 |
123 | // Failsafe Schema
124 | /// "tag:yaml.org,2002:str"
125 | public static let str: Tag.Name = "tag:yaml.org,2002:str"
126 | /// "tag:yaml.org,2002:seq"
127 | public static let seq: Tag.Name = "tag:yaml.org,2002:seq"
128 | /// "tag:yaml.org,2002:map"
129 | public static let map: Tag.Name = "tag:yaml.org,2002:map"
130 | // JSON Schema
131 | /// "tag:yaml.org,2002:bool"
132 | public static let bool: Tag.Name = "tag:yaml.org,2002:bool"
133 | /// "tag:yaml.org,2002:float"
134 | public static let float: Tag.Name = "tag:yaml.org,2002:float"
135 | /// "tag:yaml.org,2002:null"
136 | public static let null: Tag.Name = "tag:yaml.org,2002:null"
137 | /// "tag:yaml.org,2002:int"
138 | public static let int: Tag.Name = "tag:yaml.org,2002:int"
139 | // http://yaml.org/type/index.html
140 | /// "tag:yaml.org,2002:binary"
141 | public static let binary: Tag.Name = "tag:yaml.org,2002:binary"
142 | /// "tag:yaml.org,2002:merge"
143 | public static let merge: Tag.Name = "tag:yaml.org,2002:merge"
144 | /// "tag:yaml.org,2002:omap"
145 | public static let omap: Tag.Name = "tag:yaml.org,2002:omap"
146 | /// "tag:yaml.org,2002:pairs"
147 | public static let pairs: Tag.Name = "tag:yaml.org,2002:pairs"
148 | /// "tag:yaml.org,2002:set".
149 | public static let set: Tag.Name = "tag:yaml.org,2002:set"
150 | /// "tag:yaml.org,2002:timestamp"
151 | public static let timestamp: Tag.Name = "tag:yaml.org,2002:timestamp"
152 | /// "tag:yaml.org,2002:value"
153 | public static let value: Tag.Name = "tag:yaml.org,2002:value"
154 | /// "tag:yaml.org,2002:yaml" We don't support this.
155 | public static let yaml: Tag.Name = "tag:yaml.org,2002:yaml"
156 | }
157 |
158 | protocol TagResolvable {
159 | var tag: Tag { get }
160 | static var defaultTagName: Tag.Name { get }
161 | func resolveTag(using resolver: Resolver) -> Tag.Name
162 | }
163 |
164 | extension TagResolvable {
165 | var resolvedTag: Tag {
166 | return tag.resolved(with: self)
167 | }
168 |
169 | func resolveTag(using resolver: Resolver) -> Tag.Name {
170 | return tag.name == .implicit ? Self.defaultTagName : tag.name
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/Tests/YamsTests/ResolverTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResolverTests.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 12/15/16.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Yams
11 |
12 | final class ResolverTests: XCTestCase, @unchecked Sendable {
13 |
14 | func testBasic() {
15 | let resolver = Resolver.basic
16 | XCTAssertEqual(resolver.resolveTag(of: "null"), .str)
17 | XCTAssertEqual(resolver.resolveTag(of: "Null"), .str)
18 | XCTAssertEqual(resolver.resolveTag(of: "NULL"), .str)
19 | XCTAssertEqual(resolver.resolveTag(of: "~"), .str)
20 | XCTAssertEqual(resolver.resolveTag(of: ""), .str)
21 |
22 | XCTAssertEqual(resolver.resolveTag(of: "true"), .str)
23 | XCTAssertEqual(resolver.resolveTag(of: "True"), .str)
24 | XCTAssertEqual(resolver.resolveTag(of: "TRUE"), .str)
25 | XCTAssertEqual(resolver.resolveTag(of: "false"), .str)
26 | XCTAssertEqual(resolver.resolveTag(of: "False"), .str)
27 | XCTAssertEqual(resolver.resolveTag(of: "FALSE"), .str)
28 |
29 | XCTAssertEqual(resolver.resolveTag(of: "0"), .str)
30 | XCTAssertEqual(resolver.resolveTag(of: "+1"), .str)
31 | XCTAssertEqual(resolver.resolveTag(of: "0o7"), .str)
32 | XCTAssertEqual(resolver.resolveTag(of: "0x3A"), .str)
33 | XCTAssertEqual(resolver.resolveTag(of: "-19"), .str)
34 |
35 | XCTAssertEqual(resolver.resolveTag(of: "0."), .str)
36 | XCTAssertEqual(resolver.resolveTag(of: "-0.0"), .str)
37 | XCTAssertEqual(resolver.resolveTag(of: ".5"), .str)
38 | XCTAssertEqual(resolver.resolveTag(of: "+12e03"), .str)
39 | XCTAssertEqual(resolver.resolveTag(of: "-2E+05"), .str)
40 | XCTAssertEqual(resolver.resolveTag(of: ".inf"), .str)
41 | XCTAssertEqual(resolver.resolveTag(of: ".Inf"), .str)
42 | XCTAssertEqual(resolver.resolveTag(of: ".INF"), .str)
43 | XCTAssertEqual(resolver.resolveTag(of: "+.inf"), .str)
44 | XCTAssertEqual(resolver.resolveTag(of: "+.Inf"), .str)
45 | XCTAssertEqual(resolver.resolveTag(of: "+.INF"), .str)
46 | XCTAssertEqual(resolver.resolveTag(of: "-.inf"), .str)
47 | XCTAssertEqual(resolver.resolveTag(of: "-.Inf"), .str)
48 | XCTAssertEqual(resolver.resolveTag(of: "-.INF"), .str)
49 | XCTAssertEqual(resolver.resolveTag(of: ".nan"), .str)
50 | XCTAssertEqual(resolver.resolveTag(of: ".NaN"), .str)
51 | XCTAssertEqual(resolver.resolveTag(of: ".NAN"), .str)
52 | }
53 |
54 | func testDefault() {
55 | let resolver = Resolver.default
56 |
57 | XCTAssertEqual(resolver.resolveTag(of: "null"), .null)
58 | XCTAssertEqual(resolver.resolveTag(of: "Null"), .null)
59 | XCTAssertEqual(resolver.resolveTag(of: "NULL"), .null)
60 | XCTAssertEqual(resolver.resolveTag(of: "~"), .null)
61 | XCTAssertEqual(resolver.resolveTag(of: ""), .null)
62 |
63 | XCTAssertEqual(resolver.resolveTag(of: "true"), .bool)
64 | XCTAssertEqual(resolver.resolveTag(of: "True"), .bool)
65 | XCTAssertEqual(resolver.resolveTag(of: "TRUE"), .bool)
66 | XCTAssertEqual(resolver.resolveTag(of: "false"), .bool)
67 | XCTAssertEqual(resolver.resolveTag(of: "False"), .bool)
68 | XCTAssertEqual(resolver.resolveTag(of: "FALSE"), .bool)
69 |
70 | XCTAssertEqual(resolver.resolveTag(of: "0"), .int)
71 | XCTAssertEqual(resolver.resolveTag(of: "+1"), .int)
72 | XCTAssertEqual(resolver.resolveTag(of: "0o7"), .int)
73 | XCTAssertEqual(resolver.resolveTag(of: "0x3A"), .int)
74 | XCTAssertEqual(resolver.resolveTag(of: "-19"), .int)
75 |
76 | XCTAssertEqual(resolver.resolveTag(of: "0."), .float)
77 | XCTAssertEqual(resolver.resolveTag(of: "-0.0"), .float)
78 | XCTAssertEqual(resolver.resolveTag(of: ".5"), .float)
79 | XCTAssertEqual(resolver.resolveTag(of: "+12e03"), .float)
80 | XCTAssertEqual(resolver.resolveTag(of: "-2E+05"), .float)
81 | XCTAssertEqual(resolver.resolveTag(of: ".inf"), .float)
82 | XCTAssertEqual(resolver.resolveTag(of: ".Inf"), .float)
83 | XCTAssertEqual(resolver.resolveTag(of: ".INF"), .float)
84 | XCTAssertEqual(resolver.resolveTag(of: "+.inf"), .float)
85 | XCTAssertEqual(resolver.resolveTag(of: "+.Inf"), .float)
86 | XCTAssertEqual(resolver.resolveTag(of: "+.INF"), .float)
87 | XCTAssertEqual(resolver.resolveTag(of: "-.inf"), .float)
88 | XCTAssertEqual(resolver.resolveTag(of: "-.Inf"), .float)
89 | XCTAssertEqual(resolver.resolveTag(of: "-.INF"), .float)
90 | XCTAssertEqual(resolver.resolveTag(of: ".nan"), .float)
91 | XCTAssertEqual(resolver.resolveTag(of: ".NaN"), .float)
92 | XCTAssertEqual(resolver.resolveTag(of: ".NAN"), .float)
93 | }
94 |
95 | func testCustomize() throws {
96 | // appending rule
97 | XCTAssertEqual(Resolver.basic.rules.count, 0)
98 | let basicAppendingBool = Resolver.basic.appending(.bool)
99 | XCTAssertEqual(basicAppendingBool.rules.count, 1)
100 | XCTAssertEqual(basicAppendingBool.resolveTag(of: "true"), .bool)
101 |
102 | // replacing rule with pattern string
103 | XCTAssertEqual(Resolver.default.resolveTag(of: "はい"), .str)
104 | let resolverRecognizingJapaneseYesNo = try Resolver.default.replacing(.bool, with: "(はい|いいえ)")
105 | XCTAssertEqual(resolverRecognizingJapaneseYesNo.resolveTag(of: "はい"), .bool)
106 |
107 | // replacing rule with Rule
108 | let ruleForOuiNon = try Resolver.Rule(.bool, "(Oui|Non)")
109 | XCTAssertEqual(resolverRecognizingJapaneseYesNo.replacing(ruleForOuiNon).resolveTag(of: "Oui"), .bool)
110 |
111 | // removing
112 | XCTAssertEqual(Resolver.default.rules.count, 7)
113 | let defaultRemovingBool = Resolver.default.removing(.bool)
114 | XCTAssertEqual(defaultRemovingBool.rules.count, 6)
115 | XCTAssertEqual(defaultRemovingBool.resolveTag(of: "true"), .str)
116 |
117 | // custom tag and rule
118 | let xml: Tag.Name = "XML"
119 | let defaultAppendingXML = try Resolver.default.appending(xml, "<[^>]*>")
120 | XCTAssertEqual(defaultAppendingXML.resolveTag(of: ""), xml)
121 |
122 | // Use customized Resolver on Yams.load()
123 | let bool = try Yams.load(yaml: "true")
124 | XCTAssertTrue(bool is Bool)
125 | XCTAssertFalse(bool is String)
126 | let string = try Yams.load(yaml: "true", Resolver.default.removing(.bool))
127 | XCTAssertFalse(string is Bool)
128 | XCTAssertTrue(string is String)
129 | }
130 | }
131 |
132 | extension ResolverTests {
133 | static var allTests: [(String, (ResolverTests) -> () throws -> Void)] {
134 | return [
135 | ("testBasic", testBasic),
136 | ("testDefault", testDefault),
137 | ("testCustomize", testCustomize)
138 | ]
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Tests/YamsTests/TestHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestHelper.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 12/22/16.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | private let gregorianCalendar = Calendar(identifier: .gregorian)
13 |
14 | func timestamp(_ timeZoneHour: Int = 0,
15 | _ year: Int? = nil,
16 | _ month: Int? = nil,
17 | _ day: Int? = nil,
18 | _ hour: Int? = nil,
19 | _ minute: Int? = nil,
20 | _ second: Int? = nil,
21 | _ fraction: Double? = nil ) -> Date {
22 | let timeZone = TimeZone(secondsFromGMT: timeZoneHour * 60 * 60)
23 | let dateComponents = DateComponents(calendar: gregorianCalendar, timeZone: timeZone,
24 | year: year, month: month, day: day,
25 | hour: hour, minute: minute, second: second)
26 | guard let date = dateComponents.date else { fatalError("Tests shouldn't create timestamps for invalid dates") }
27 | return fraction.map(date.addingTimeInterval) ?? date
28 | }
29 |
30 | /// AssertEqual for Any
31 | ///
32 | /// - parameter lhs: Any
33 | /// - parameter rhs: Any
34 | /// - parameter context: Closure generating String that used on generating assertion
35 | /// - parameter file: file path string
36 | /// - parameter line: line number
37 | ///
38 | /// - returns: true if lhs is equal to rhs
39 | @discardableResult
40 | func YamsAssertEqual(_ lhs: Any?, _ rhs: Any?,
41 | // swiftlint:disable:previous identifier_name
42 | _ context: @autoclosure @escaping () -> String = "",
43 | file: StaticString = #file, line: UInt = #line) -> Bool {
44 | // use inner function for capturing `file` and `line`
45 | // swiftlint:disable:next cyclomatic_complexity
46 | @discardableResult func equal(_ lhs: Any?, _ rhs: Any?,
47 | _ context: @autoclosure @escaping () -> String = "") -> Bool {
48 | switch (lhs, rhs) {
49 | case (nil, nil):
50 | return true
51 | case let (lhs as [Any], rhs as [Any]):
52 | equal(lhs.count, rhs.count, joined("comparing count of \(dumped(lhs)) to \(dumped(rhs))", context()))
53 | for (index, (lhsElement, rhsElement)) in zip(lhs, rhs).enumerated() where !equal(
54 | lhsElement, rhsElement,
55 | joined("elements at \(index) from \(dumped(lhs)) and \(dumped(rhs))", context())) {
56 | return false
57 | }
58 | return true
59 | case let (lhs as [String: Any], rhs as [String: Any]):
60 | let message1 = { "comparing count of \(dumped(lhs)) to \(dumped(rhs))" }
61 | equal(lhs.count, rhs.count, joined(message1(), context()))
62 | let keys = Set(lhs.keys).union(rhs.keys)
63 | for key in keys where !equal(
64 | lhs[key], rhs[key],
65 | joined("values for key(\"\(key)\") in \(dumped(lhs)) and \(dumped(rhs))", context())) {
66 | return false
67 | }
68 | return true
69 | case let (lhs?, nil):
70 | let message = { "(\"\(type(of: lhs))(\(dumped(lhs)))\") is not equal to (\"nil\")" }
71 | XCTFail(joined(message(), context()), file: (file), line: line)
72 | return false
73 | case let (nil, rhs?):
74 | let message = { "(\"nil\") is not equal to (\"\(type(of: rhs))(\(dumped(rhs)))\")" }
75 | XCTFail(joined(message(), context()), file: (file), line: line)
76 | return false
77 | case let (lhs as Double, rhs as Double):
78 | if lhs.isNaN && rhs.isNaN { return true } // NaN is not equal to any value, including NaN
79 | XCTAssertEqual(lhs, rhs, context(), file: (file), line: line)
80 | return lhs == rhs
81 | case let (lhs as AnyHashable, rhs as AnyHashable):
82 | XCTAssertEqual(lhs, rhs, context(), file: (file), line: line)
83 | return lhs == rhs
84 | case let (lhs as (Any, Any), rhs as (Any, Any)):
85 | return equal(lhs.0, rhs.0) && equal(lhs.1, rhs.1)
86 | case let (lhs as Set, rhs as Set):
87 | return lhs == rhs
88 | default:
89 | let message = { "Can't compare \(type(of: lhs))(\(dumped(lhs))) to \(type(of: rhs))(\(dumped(rhs)))" }
90 | XCTFail(joined(message(), context()), file: (file), line: line)
91 | return false
92 | }
93 | }
94 | return equal(lhs, rhs, context())
95 | }
96 |
97 | func YamsAssertEqualType(_ actual: Any?, _ expected: Any?, path: [String] = []) {
98 | // swiftlint:disable:previous identifier_name
99 | let pathString = path.joined(separator: ".")
100 |
101 | guard let actual = actual else {
102 | XCTFail("Actual is nil at '\(pathString)'")
103 | return
104 | }
105 | guard let expected = expected else {
106 | XCTFail("Expected is nil at '\(pathString)'")
107 | return
108 | }
109 |
110 | let actualType = type(of: actual)
111 | let expectedType = type(of: expected)
112 |
113 | switch (actual, expected) {
114 | case let (aDict as [String: Any], eDict as [String: Any]):
115 | XCTAssertEqual(aDict.count, eDict.count, "Dictionary count mismatch at '\(pathString)'")
116 | for key in eDict.keys {
117 | guard let aValue = aDict[key], let eValue = eDict[key] else {
118 | XCTFail("Missing key '\(key)' at '\(pathString)'")
119 | continue
120 | }
121 | YamsAssertEqualType(aValue, eValue, path: path + [key])
122 | }
123 |
124 | case let (aArray as [Any], eArray as [Any]):
125 | XCTAssertEqual(aArray.count, eArray.count, "Array count mismatch at '\(pathString)'")
126 | for index in 0..(_ value: T) -> String {
138 | var output = ""
139 | dump(value, to: &output)
140 | let outputs = output.components(separatedBy: .newlines)
141 | let firstLine = outputs.first ?? ""
142 | let count = outputs.count
143 | if count == 1 {
144 | // remove `- ` prefix if
145 | let index = firstLine.index(firstLine.startIndex, offsetBy: 2)
146 | return String(firstLine[index...])
147 | } else {
148 | return "[\n" + output + "]"
149 | }
150 | }
151 |
152 | private func joined(_ lhs: String, _ rhs: String) -> String {
153 | return lhs.isEmpty ? rhs : rhs.isEmpty ? lhs : lhs + " " + rhs
154 | }
155 |
--------------------------------------------------------------------------------
/Tests/YamsTests/AnchorTolerancesTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnchorTolerancesTests.swift
3 | // Yams
4 | //
5 | // Created by Adora Lynch on 9/18/24.
6 | // Copyright (c) 2024 Yams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Yams
11 |
12 | class AnchorTolerancesTests: XCTestCase {
13 |
14 | struct Example: Codable, Hashable {
15 | var myCustomAnchorDeclaration: Anchor
16 | var extraneousValue: Int
17 | }
18 |
19 | /// Any type that is Encodable and contains an `Anchor`value but with a coding key different from
20 | /// YamlAnchorProviding will not encode to a yaml anchor
21 | /// This may be unexpected
22 | func testAnchorEncoding_undeclaredBehavior() throws {
23 | let expectedYAML = """
24 | myCustomAnchorDeclaration: I-did-it-myyyyy-way
25 | extraneousValue: 3
26 |
27 | """
28 |
29 | let value = Example(myCustomAnchorDeclaration: "I-did-it-myyyyy-way",
30 | extraneousValue: 3)
31 |
32 | let encoder = YAMLEncoder()
33 | let producedYAML = try encoder.encode(value)
34 | XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.")
35 | }
36 |
37 | /// Any type that is Encodable and contains an `Anchor`value with the same coding key as
38 | /// YamlAnchorProviding will encode to a yaml anchor even though the type does not conform to
39 | /// YamlAnchorProviding
40 | /// This may be unexpected
41 | func testAnchorEncoding_undeclaredBehavior_7() throws {
42 | struct Example: Codable, Hashable {
43 | var yamlAnchor: Anchor
44 | var extraneousValue: Int
45 | }
46 |
47 | let expectedYAML = """
48 | &I-did-it-myyyyy-way
49 | extraneousValue: 3
50 |
51 | """
52 |
53 | let value = Example(yamlAnchor: "I-did-it-myyyyy-way",
54 | extraneousValue: 3)
55 |
56 | let encoder = YAMLEncoder()
57 | let producedYAML = try encoder.encode(value)
58 | XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.")
59 | }
60 |
61 | /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from
62 | /// YamlAnchorProviding will not decode an anchor from the text representation.
63 | /// In this case a key not found error will be thrown during decoding
64 | /// This may be unexpected
65 | func testAnchorDecoding_undeclaredBehavior_1() throws {
66 | let sourceYAML = """
67 | &a-different-tag
68 | extraneousValue: 3
69 | """
70 | let decoder = YAMLDecoder()
71 | XCTAssertThrowsError(try decoder.decode(Example.self, from: sourceYAML))
72 | // error is ^^ key not found, "myCustomAnchorDeclaration"
73 | }
74 |
75 | /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from
76 | /// YamlAnchorProviding will not decode an anchor from the text representation.
77 | /// In this case the decoding is successful and the anchor is respected by the parser.
78 | /// This may be unexpected
79 | func testAnchorDecoding_undeclaredBehavior_6() throws {
80 | struct Example: Codable, Hashable {
81 | var myCustomAnchorDeclaration: Anchor?
82 | var extraneousValue: Int
83 | }
84 | let sourceYAML = """
85 | &a-different-tag
86 | extraneousValue: 3
87 |
88 | """
89 |
90 | let expectedValue = Example(myCustomAnchorDeclaration: nil,
91 | extraneousValue: 3)
92 |
93 | let decoder = YAMLDecoder()
94 | let decodedValue = try decoder.decode(Example.self, from: sourceYAML)
95 | XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.")
96 | }
97 |
98 | /// Any type that is Decodable and contains an `Anchor` value with the same coding key as
99 | /// YamlAnchorProviding will decode an anchor from the text representation even though the type does
100 | /// not conform to YamlAnchorCoding
101 | /// This may be unexpected
102 | func testAnchorDecoding_undeclaredBehavior_8() throws {
103 | struct Example: Codable, Hashable {
104 | var yamlAnchor: Anchor?
105 | var extraneousValue: Int
106 | }
107 | let sourceYAML = """
108 | &a-different-tag
109 | extraneousValue: 3
110 |
111 | """
112 |
113 | let expectedValue = Example(yamlAnchor: "a-different-tag",
114 | extraneousValue: 3)
115 |
116 | let decoder = YAMLDecoder()
117 | let decodedValue = try decoder.decode(Example.self, from: sourceYAML)
118 | XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.")
119 | }
120 |
121 | /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from
122 | /// YamlAnchorProviding will not decode an anchor from the text representation.
123 | /// In this case the decoding is successful and the anchor is respected by the parser.
124 | /// This is expected behavior, but in a strange situation.
125 | func testAnchorDecoding_undeclaredBehavior_3() throws {
126 | let sourceYAML = """
127 | &a-different-tag
128 | extraneousValue: 3
129 | myCustomAnchorDeclaration: deliver-us-from-evil
130 |
131 | """
132 | let expectedValue = Example(myCustomAnchorDeclaration: "deliver-us-from-evil",
133 | extraneousValue: 3)
134 |
135 | let decoder = YAMLDecoder()
136 | let decodedValue = try decoder.decode(Example.self, from: sourceYAML)
137 | XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.")
138 |
139 | }
140 |
141 | /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from
142 | /// YamlAnchorProviding will not decode an anchor from the text representation.
143 | /// In this case the decoding is successful even though and the `Anchor` was initialized with
144 | /// unsupported characters. The anchor is respected by the parser.
145 | /// This is expected behavior, but in a strange situation.
146 | func testAnchorDecoding_undeclaredBehavior_2() throws {
147 | let sourceYAML = """
148 | &a-different-tag
149 | extraneousValue: 3
150 | myCustomAnchorDeclaration: "deliver us from |()evil"
151 |
152 | """
153 |
154 | let expectedValue = Example(myCustomAnchorDeclaration: "deliver us from |()evil",
155 | extraneousValue: 3)
156 |
157 | let decoder = YAMLDecoder()
158 | let decodedValue = try decoder.decode(Example.self, from: sourceYAML)
159 | XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.")
160 |
161 | }
162 |
163 | }
164 |
--------------------------------------------------------------------------------
/Tests/YamsTests/EmitterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmitterTests.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 12/29/16.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Yams
11 |
12 | final class EmitterTests: XCTestCase, @unchecked Sendable {
13 |
14 | func testScalar() throws {
15 | var node: Node = "key"
16 |
17 | let expectedAnyAndPlain = "key\n"
18 | node.scalar?.style = .any
19 | XCTAssertEqual(try Yams.serialize(node: node), expectedAnyAndPlain)
20 | node.scalar?.style = .plain
21 | XCTAssertEqual(try Yams.serialize(node: node), expectedAnyAndPlain)
22 | node.scalar?.style = .singleQuoted
23 | XCTAssertEqual(try Yams.serialize(node: node), "'key'\n")
24 |
25 | node.scalar?.style = .doubleQuoted
26 | XCTAssertEqual(try Yams.serialize(node: node), "\"key\"\n")
27 | node.scalar?.style = .literal
28 | XCTAssertEqual(try Yams.serialize(node: node), "|-\n key\n")
29 | node.scalar?.style = .folded
30 | XCTAssertEqual(try Yams.serialize(node: node), ">-\n key\n")
31 | }
32 |
33 | func testSequence() throws {
34 | var node: Node = ["a", "b", "c"]
35 |
36 | let expectedAnyIsBlock = """
37 | - a
38 | - b
39 | - c
40 |
41 | """
42 | node.sequence?.style = .any
43 | XCTAssertEqual(try Yams.serialize(node: node), expectedAnyIsBlock)
44 | node.sequence?.style = .block
45 | XCTAssertEqual(try Yams.serialize(node: node), expectedAnyIsBlock)
46 |
47 | node.sequence?.style = .flow
48 | XCTAssertEqual(try Yams.serialize(node: node), "[a, b, c]\n")
49 | }
50 |
51 | func testIndentation() throws {
52 | // Arrays are not indented
53 | let node: Node = ["key1": ["key2": ["a", "b"]]]
54 | func expected(_ indentationCount: Int) -> String {
55 | let indentation = Array(repeating: " ", count: indentationCount).joined()
56 | return """
57 | key1:
58 | \(indentation)key2:
59 | \(indentation)- a
60 | \(indentation)- b
61 |
62 | """
63 | }
64 | XCTAssertEqual(try Yams.serialize(node: node), expected(2))
65 | XCTAssertEqual(try Yams.serialize(node: node, indent: -2), expected(2))
66 | XCTAssertEqual(try Yams.serialize(node: node, indent: -1), expected(2))
67 | XCTAssertEqual(try Yams.serialize(node: node, indent: 0), expected(2))
68 | XCTAssertEqual(try Yams.serialize(node: node, indent: 4), expected(4))
69 | XCTAssertEqual(try Yams.serialize(node: node, indent: 9), expected(9))
70 | XCTAssertEqual(try Yams.serialize(node: node, indent: 10), expected(2))
71 | }
72 |
73 | func testMapping() throws {
74 | var node: Node = ["key1": "value1", "key2": "value2"]
75 |
76 | let expectedAnyIsBlock = """
77 | key1: value1
78 | key2: value2
79 |
80 | """
81 | node.mapping?.style = .any
82 | XCTAssertEqual(try Yams.serialize(node: node), expectedAnyIsBlock)
83 | node.mapping?.style = .block
84 | XCTAssertEqual(try Yams.serialize(node: node), expectedAnyIsBlock)
85 |
86 | node.mapping?.style = .flow
87 | XCTAssertEqual(try Yams.serialize(node: node), "{key1: value1, key2: value2}\n")
88 | }
89 |
90 | func testLineBreaks() throws {
91 | let node: Node = "key"
92 | let expected = [
93 | "key",
94 | ""
95 | ]
96 | XCTAssertEqual(try Yams.serialize(node: node, lineBreak: .ln),
97 | expected.joined(separator: "\n"))
98 | XCTAssertEqual(try Yams.serialize(node: node, lineBreak: .cr),
99 | expected.joined(separator: "\r"))
100 | XCTAssertEqual(try Yams.serialize(node: node, lineBreak: .crln),
101 | expected.joined(separator: "\r\n"))
102 | }
103 |
104 | func testAllowUnicode() throws {
105 | do {
106 | let node: Node = "あ"
107 | do {
108 | let yaml = try Yams.serialize(node: node)
109 | let expected = "\"\\u3042\"\n"
110 | XCTAssertEqual(yaml, expected)
111 | }
112 | do {
113 | let yaml = try Yams.serialize(node: node, allowUnicode: true)
114 | let expected = "あ\n"
115 | XCTAssertEqual(yaml, expected)
116 | }
117 | }
118 | do {
119 | // Emoji will be escaped whether `allowUnicode` is true or not
120 | let node: Node = "😀"
121 | do {
122 | let yaml = try Yams.serialize(node: node)
123 | let expected = "\"\\U0001F600\"\n"
124 | XCTAssertEqual(yaml, expected)
125 | }
126 | do {
127 | let yaml = try Yams.serialize(node: node, allowUnicode: true)
128 | let expected = "\"\\U0001F600\"\n"
129 | XCTAssertEqual(yaml, expected)
130 | }
131 | }
132 | }
133 |
134 | func testSortKeys() throws {
135 | let node: Node = [
136 | "key3": "value3",
137 | "key2": "value2",
138 | "key1": "value1"
139 | ]
140 | let yaml = try Yams.serialize(node: node)
141 | let expected = "key3: value3\nkey2: value2\nkey1: value1\n"
142 | XCTAssertEqual(yaml, expected)
143 | let yamlSorted = try Yams.serialize(node: node, sortKeys: true)
144 | let expectedSorted = "key1: value1\nkey2: value2\nkey3: value3\n"
145 | XCTAssertEqual(yamlSorted, expectedSorted)
146 | }
147 |
148 | func testSmartQuotedString() throws {
149 | struct Sample {
150 | let string: String
151 | let tag: Tag.Name
152 | let expected: String
153 | let line: UInt
154 | }
155 | let samples = [
156 | Sample(string: "string", tag: .str, expected: "string", line: #line),
157 | Sample(string: "true", tag: .bool, expected: "'true'", line: #line),
158 | Sample(string: "1", tag: .int, expected: "'1'", line: #line),
159 | Sample(string: "1.0", tag: .float, expected: "'1.0'", line: #line),
160 | Sample(string: "null", tag: .null, expected: "'null'", line: #line),
161 | Sample(string: "2019-07-06", tag: .timestamp, expected: "'2019-07-06'", line: #line)
162 | ]
163 | let resolver = Resolver.default
164 | for sample in samples {
165 | let resolvedTag = resolver.resolveTag(of: Node(sample.string))
166 | XCTAssertEqual(resolvedTag, sample.tag, "Resolver resolves unexpected tag", line: sample.line)
167 | let yaml = try Yams.dump(object: sample.string)
168 | XCTAssertEqual(yaml, "\(sample.expected)\n", line: sample.line)
169 | }
170 | }
171 | }
172 |
173 | extension EmitterTests {
174 | static var allTests: [(String, (EmitterTests) -> () throws -> Void)] {
175 | return [
176 | ("testScalar", testScalar),
177 | ("testSequence", testSequence),
178 | ("testMapping", testMapping),
179 | ("testLineBreaks", testLineBreaks),
180 | ("testAllowUnicode", testAllowUnicode),
181 | ("testSortKeys", testSortKeys),
182 | ("testSmartQuotedString", testSmartQuotedString)
183 | ]
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/Tests/YamsTests/TagTolerancesTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TagTolerancesTests.swift
3 | // Yams
4 | //
5 | // Created by Adora Lynch on 9/18/24.
6 | // Copyright (c) 2024 Yams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Yams
11 |
12 | class TagTolerancesTests: XCTestCase {
13 |
14 | struct Example: Codable, Hashable {
15 | var myCustomTagDeclaration: Tag
16 | var extraneousValue: Int
17 | }
18 |
19 | /// Any type that is Encodable and contains an `Tag`value but with a coding key different from
20 | /// YamlTagProviding will not encode to a yaml tag
21 | /// This may be unexpected
22 | func testTagEncoding_undeclaredBehavior() throws {
23 | let expectedYAML = """
24 | myCustomTagDeclaration: I-did-it-myyyyy-way
25 | extraneousValue: 3
26 |
27 | """
28 |
29 | let value = Example(myCustomTagDeclaration: "I-did-it-myyyyy-way",
30 | extraneousValue: 3)
31 |
32 | let encoder = YAMLEncoder()
33 | let producedYAML = try encoder.encode(value)
34 | XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.")
35 | }
36 |
37 | /// Any type that is Encodable and contains an `Tag`value with the same coding key as
38 | /// YamlTagProviding will encode to a yaml tag even though the type does not conform to
39 | /// YamlTagProviding
40 | /// This may be unexpected
41 | func testTagEncoding_undeclaredBehavior_7() throws {
42 | struct Example: Codable, Hashable {
43 | var yamlTag: Tag
44 | var extraneousValue: Int
45 | }
46 | let expectedYAML = """
47 | !
48 | extraneousValue: 3
49 |
50 | """
51 |
52 | let value = Example(yamlTag: "I-did-it-myyyyy-way",
53 | extraneousValue: 3)
54 |
55 | let encoder = YAMLEncoder()
56 | let producedYAML = try encoder.encode(value)
57 | XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.")
58 | }
59 |
60 | /// Tags are oddly permissive, but some characters do get escaped
61 | /// This may be unexpected
62 | func testTagEncoding_undeclaredBehavior_4() throws {
63 | struct Example: Codable, Hashable, YamlTagProviding {
64 | var yamlTag: Tag?
65 | var extraneousValue: Int
66 | }
67 |
68 | let expectedYAML = """
69 | !
70 | extraneousValue: 3
71 |
72 | """
73 |
74 | let value = Example(yamlTag: "I-did-it-[]-*-|-!-()way",
75 | extraneousValue: 3)
76 |
77 | let encoder = YAMLEncoder()
78 | let producedYAML = try encoder.encode(value)
79 | XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.")
80 | }
81 |
82 | /// Any type that is Decodable and contains an `Tag` value but with a coding key different from
83 | /// YamlTagProviding will not decode an tag from the text representation.
84 | /// In this case a key not found error will be thrown during decoding
85 | /// This may be unexpected
86 | func testTagDecoding_undeclaredBehavior_1() throws {
87 | let sourceYAML = """
88 | !
89 | extraneousValue: 3
90 |
91 | """
92 | let decoder = YAMLDecoder()
93 | XCTAssertThrowsError(try decoder.decode(Example.self, from: sourceYAML))
94 | // error is ^^ key not found, "myCustomTagDeclaration"
95 | }
96 |
97 | /// Any type that is Decodable and contains an `Tag` value but with a coding key different from
98 | /// YamlTagProviding will not decode an tag from the text representation.
99 | /// This may be unexpected
100 | func testTagDecoding_undeclaredBehavior_6() throws {
101 | struct Example: Codable, Hashable {
102 | var myCustomTagDeclaration: Tag?
103 | var extraneousValue: Int
104 | }
105 | let sourceYAML = """
106 | !
107 | extraneousValue: 3
108 |
109 | """
110 |
111 | let expectedValue = Example(myCustomTagDeclaration: nil,
112 | extraneousValue: 3)
113 |
114 | let decoder = YAMLDecoder()
115 | let decodedValue = try decoder.decode(Example.self, from: sourceYAML)
116 | XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.")
117 | }
118 |
119 | /// Any type that is Decodable and contains an `Tag` value with the same coding key as YamlTagProviding
120 | /// will decode an tag from the text representatio even though the type does not conform to YamlTagCoding.
121 | /// This may be unexpected
122 | func testTagDecoding_undeclaredBehavior_8() throws {
123 | struct Example: Codable, Hashable {
124 | var yamlTag: Tag?
125 | var extraneousValue: Int
126 | }
127 | let sourceYAML = """
128 | !
129 | extraneousValue: 3
130 |
131 | """
132 |
133 | let expectedValue = Example(yamlTag: "a-different-tag",
134 | extraneousValue: 3)
135 |
136 | let decoder = YAMLDecoder()
137 | let decodedValue = try decoder.decode(Example.self, from: sourceYAML)
138 | XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.")
139 | }
140 |
141 | /// Any type that is Decodable and contains an `Tag` value but with a coding key different from YamlTagProviding
142 | /// will not decode an tag from the text representation.
143 | /// This is expected behavior, but in a strange situation.
144 | func testTagDecoding_undeclaredBehavior_3() throws {
145 | let sourceYAML = """
146 | !
147 | extraneousValue: 3
148 | myCustomTagDeclaration: deliver-us-from-evil
149 |
150 | """
151 | let expectedValue = Example(myCustomTagDeclaration: "deliver-us-from-evil",
152 | extraneousValue: 3)
153 |
154 | let decoder = YAMLDecoder()
155 | let decodedValue = try decoder.decode(Example.self, from: sourceYAML)
156 | XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.")
157 |
158 | }
159 |
160 | /// Any type that is Decodable and contains an `Tag` value but with a coding key different from YamlTagProviding
161 | /// will not decode an tag from the text representation.
162 | /// This is expected behavior, but in a strange situation.
163 | func testTagDecoding_undeclaredBehavior_2() throws {
164 | let sourceYAML = """
165 | !
166 | extraneousValue: 3
167 | myCustomTagDeclaration: "deliver us from |()evil"
168 |
169 | """
170 |
171 | let expectedValue = Example(myCustomTagDeclaration: "deliver us from |()evil",
172 | extraneousValue: 3)
173 |
174 | let decoder = YAMLDecoder()
175 | let decodedValue = try decoder.decode(Example.self, from: sourceYAML)
176 | XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.")
177 |
178 | }
179 |
180 | }
181 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Yams
2 |
3 | 
4 |
5 | A sweet and swifty [YAML](http://yaml.org/) parser built on
6 | [LibYAML](https://github.com/yaml/libyaml).
7 |
8 | [](https://github.com/jpsim/Yams/actions?query=workflow%3ASwiftPM)
9 | [](https://github.com/jpsim/Yams/actions?query=workflow%3Axcodebuild)
10 | [](https://github.com/jpsim/Yams/actions?query=workflow%3A%22pod+lib+lint%22)
11 | [](https://github.com/jpsim/Yams/actions?query=workflow%3ANightly)
12 | [](https://codecov.io/gh/jpsim/Yams)
13 |
14 | ## Installation
15 |
16 | Building Yams requires Xcode 14.0+ or a Swift 5.7+ toolchain with the
17 | Swift Package Manager or CMake and Ninja.
18 |
19 | ### CMake
20 |
21 | CMake 3.17.2 or newer is required, along with Ninja 1.9.0 or newer.
22 |
23 | When building for non-Apple platforms:
24 |
25 | ```
26 | cmake -B /path/to/build -G Ninja -S /path/to/yams -DCMAKE_BUILD_TYPE=Release -DFoundation_DIR=/path/to/foundation/build/cmake/modules
27 | cmake --build /path/to/build
28 | ```
29 |
30 | To build for Apple platforms (macOS, iOS, tvOS, watchOS), there is no
31 | need to separately build Foundation because it is included as part of
32 | the SDK:
33 |
34 | ```
35 | cmake -B /path/to/build -G Ninja -S /path/to/yams -DCMAKE_BUILD_TYPE=Release
36 | cmake --build /path/to/build
37 | ```
38 |
39 | ### Swift Package Manager
40 |
41 | Add `.package(url: "https://github.com/jpsim/Yams.git", from: "6.0.1")` to your
42 | `Package.swift` file's `dependencies`.
43 |
44 | ### CocoaPods
45 |
46 | Add `pod 'Yams'` to your `Podfile`.
47 |
48 | ### Carthage
49 |
50 | Add `github "jpsim/Yams"` to your `Cartfile`.
51 |
52 | ### Bazel
53 |
54 | In your WORKSPACE file
55 |
56 | ```WORKSPACE
57 | YAMS_GIT_SHA = "SOME_SHA"
58 | http_archive(
59 | name = "com_github_jpsim_yams",
60 | urls = [
61 | "https://github.com/jpsim/Yams/archive/%s.zip" % YAMS_GIT_SHA,
62 | ],
63 | strip_prefix = "Yams-%s" % YAMS_GIT_SHA,
64 | )
65 | ```
66 |
67 | ## Usage
68 |
69 | Yams has three groups of conversion APIs:
70 | one for use with [`Codable` types](#codable-types),
71 | another for [Swift Standard Library types](#swift-standard-library-types),
72 | and a third one for a [Yams-native](#yamsnode) representation.
73 |
74 | #### `Codable` types
75 |
76 | - Codable is an [encoding & decoding strategy introduced in Swift 4][Codable]
77 | enabling easy conversion between YAML and other Encoders like
78 | [JSONEncoder][JSONEncoder] and [PropertyListEncoder][PropertyListEncoder].
79 | - Lowest computational overhead, equivalent to `Yams.Node`.
80 | - **Encoding: `YAMLEncoder.encode(_:)`**
81 | Produces a YAML `String` from an instance of type conforming to `Encodable`.
82 | - **Decoding: `YAMLDecoder.decode(_:from:)`**
83 | Decodes an instance of type conforming to `Decodable` from YAML `String` or
84 | `Data`.
85 |
86 | ```swift
87 | import Foundation
88 | import Yams
89 |
90 | struct S: Codable {
91 | var p: String
92 | }
93 |
94 | let s = S(p: "test")
95 | let encoder = YAMLEncoder()
96 | let encodedYAML = try encoder.encode(s)
97 | encodedYAML == """
98 | p: test
99 |
100 | """
101 | let decoder = YAMLDecoder()
102 | let decoded = try decoder.decode(S.self, from: encodedYAML)
103 | s.p == decoded.p
104 | ```
105 |
106 | #### Swift Standard Library types
107 |
108 | - The type of Swift Standard Library is inferred from the contents of the
109 | internal `Yams.Node` representation by matching regular expressions.
110 | - This method has the largest computational overhead When decoding YAML, because
111 | the type inference of all objects is done up-front.
112 | - It may be easier to use in such a way as to handle objects created from
113 | `JSONSerialization` or if the input is already standard library types
114 | (`Any`, `Dictionary`, `Array`, etc.).
115 | - **Encoding: `Yams.dump(object:)`**
116 | Produces a YAML `String` from an instance of Swift Standard Library types.
117 | - **Decoding: `Yams.load(yaml:)`**
118 | Produces an instance of Swift Standard Library types as `Any` from YAML
119 | `String`.
120 |
121 | ```swift
122 | // [String: Any]
123 | let dictionary: [String: Any] = ["key": "value"]
124 | let mapYAML: String = try Yams.dump(object: dictionary)
125 | mapYAML == """
126 | key: value
127 |
128 | """
129 | let loadedDictionary = try Yams.load(yaml: mapYAML) as? [String: Any]
130 |
131 | // [Any]
132 | let array: [Int] = [1, 2, 3]
133 | let sequenceYAML: String = try Yams.dump(object: array)
134 | sequenceYAML == """
135 | - 1
136 | - 2
137 | - 3
138 |
139 | """
140 | let loadedArray: [Int]? = try Yams.load(yaml: sequenceYAML) as? [Int]
141 |
142 | // Any
143 | let string = "string"
144 | let scalarYAML: String = try Yams.dump(object: string)
145 | scalarYAML == """
146 | string
147 |
148 | """
149 | let loadedString: String? = try Yams.load(yaml: scalarYAML) as? String
150 | ```
151 |
152 | #### `Yams.Node`
153 |
154 | - Yams' native model representing [Nodes of YAML][Nodes Spec] which provides all
155 | functions such as detection and customization of the YAML format.
156 | - Depending on how it is used, computational overhead can be minimized.
157 | - **Encoding: `Yams.serialize(node:)`**
158 | Produces a YAML `String` from an instance of `Node`.
159 | - **Decoding `Yams.compose(yaml:)`**
160 | Produces an instance of `Node` from YAML `String`.
161 |
162 | ```swift
163 | var map: Yams.Node = [
164 | "array": [
165 | 1, 2, 3
166 | ]
167 | ]
168 | map.mapping?.style = .flow
169 | map["array"]?.sequence?.style = .flow
170 | let yaml = try Yams.serialize(node: map)
171 | yaml == """
172 | {array: [1, 2, 3]}
173 |
174 | """
175 | let node = try Yams.compose(yaml: yaml)
176 | map == node
177 | ```
178 |
179 | #### NSMutable* compatibility
180 |
181 | Yams also supports deep conversion of YAML into `NSMutableDictionary` and `NSMutableArray`,
182 | which is useful when working with mutable Cocoa-style collections, such as NSMutableDictionary and NSMutableArray.
183 |
184 | To produce `NSMutable*` results, use a custom constructor:
185 |
186 | ```swift
187 | let yaml = """
188 | names:
189 | - Alice
190 | - Bob
191 | """
192 |
193 | let constructor = Constructor(Constructor.defaultScalarMap,
194 | Constructor.nsMutableMappingMap,
195 | Constructor.nsMutableSequenceMap)
196 |
197 | let result = try Yams.load(yaml: yaml, .default, constructor) as? NSMutableDictionary
198 | let names = result?["names"] as? NSMutableArray
199 | print(names ?? "No data") // -> (Alice, Bob)
200 | ```
201 |
202 | #### Integrating with [Combine](https://developer.apple.com/documentation/combine)
203 |
204 | When Apple's Combine framework is available, `YAMLDecoder` conforms to the
205 | `TopLevelDecoder` protocol, which allows it to be used with the
206 | `decode(type:decoder:)` operator:
207 |
208 | ```swift
209 | import Combine
210 | import Foundation
211 | import Yams
212 |
213 | func fetchBook(from url: URL) -> AnyPublisher {
214 | URLSession.shared.dataTaskPublisher(for: url)
215 | .map(\.data)
216 | .decode(type: Book.self, decoder: YAMLDecoder())
217 | .eraseToAnyPublisher()
218 | }
219 | ```
220 |
221 | ## License
222 |
223 | Both Yams and libYAML are MIT licensed.
224 |
225 | [Codable]: https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
226 | [JSONEncoder]: https://developer.apple.com/documentation/foundation/jsonencoder
227 | [PropertyListEncoder]: https://developer.apple.com/documentation/foundation/propertylistencoder
228 | [Nodes Spec]: http://www.yaml.org/spec/1.2/spec.html#id2764044
229 |
--------------------------------------------------------------------------------
/Sources/Yams/Node.Mapping.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Node.Mapping.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 2/24/17.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | extension Node {
10 | /// A mapping is the YAML equivalent of a `Dictionary`.
11 | public struct Mapping {
12 | private var pairs: [Pair]
13 | /// This mapping's `Tag`.
14 | public var tag: Tag
15 | /// The style to use when emitting this `Mapping`.
16 | public var style: Style
17 | /// This mapping's `Mark`.
18 | public var mark: Mark?
19 | /// The anchor for this node.
20 | public weak var anchor: Anchor?
21 |
22 | /// The style to use when emitting a `Mapping`.
23 | public enum Style: UInt32 {
24 | /// Let the emitter choose the style.
25 | case any
26 | /// The block mapping style.
27 | case block
28 | /// The flow mapping style.
29 | case flow
30 | }
31 |
32 | /// Create a `Node.Mapping` using the specified parameters.
33 | ///
34 | /// - parameter pairs: The array of `(Node, Node)` tuples to generate this mapping.
35 | /// - parameter tag: This mapping's `Tag`.
36 | /// - parameter style: The style to use when emitting this `Mapping`.
37 | /// - parameter mark: This mapping's `Mark`.
38 | public init(_ pairs: [(Node, Node)],
39 | _ tag: Tag = .implicit,
40 | _ style: Style = .any,
41 | _ mark: Mark? = nil,
42 | _ anchor: Anchor? = nil) {
43 | self.pairs = pairs.map { Pair($0.0, $0.1) }
44 | self.tag = tag
45 | self.style = style
46 | self.mark = mark
47 | self.anchor = anchor
48 | }
49 | }
50 |
51 | /// Get or set the `Node.Mapping` value if this node is a `Node.mapping`.
52 | public var mapping: Mapping? {
53 | get {
54 | if case let .mapping(mapping) = self {
55 | return mapping
56 | }
57 | return nil
58 | }
59 | set {
60 | if let newValue = newValue {
61 | self = .mapping(newValue)
62 | }
63 | }
64 | }
65 | }
66 |
67 | extension Node.Mapping: Comparable {
68 | /// :nodoc:
69 | public static func < (lhs: Node.Mapping, rhs: Node.Mapping) -> Bool {
70 | return lhs.pairs < rhs.pairs
71 | }
72 | }
73 |
74 | extension Node.Mapping: Equatable {
75 | /// :nodoc:
76 | public static func == (lhs: Node.Mapping, rhs: Node.Mapping) -> Bool {
77 | return lhs.pairs == rhs.pairs && lhs.resolvedTag == rhs.resolvedTag
78 | }
79 | }
80 |
81 | extension Node.Mapping: Hashable {
82 | /// :nodoc:
83 | public func hash(into hasher: inout Hasher) {
84 | hasher.combine(pairs)
85 | hasher.combine(resolvedTag)
86 | }
87 | }
88 |
89 | extension Node.Mapping: ExpressibleByDictionaryLiteral {
90 | /// :nodoc:
91 | public init(dictionaryLiteral elements: (Node, Node)...) {
92 | self.init(elements)
93 | }
94 | }
95 |
96 | // MARK: - MutableCollection Conformance
97 |
98 | extension Node.Mapping: MutableCollection {
99 | /// :nodoc:
100 | public typealias Element = (key: Node, value: Node)
101 |
102 | // MARK: Sequence
103 |
104 | /// :nodoc:
105 | public func makeIterator() -> Array.Iterator {
106 | return pairs.map(Pair.toTuple).makeIterator()
107 | }
108 |
109 | // MARK: Collection
110 |
111 | /// The index type for this mapping.
112 | public typealias Index = Array.Index
113 |
114 | /// :nodoc:
115 | public var startIndex: Index {
116 | return pairs.startIndex
117 | }
118 |
119 | /// :nodoc:
120 | public var endIndex: Index {
121 | return pairs.endIndex
122 | }
123 |
124 | /// :nodoc:
125 | public func index(after index: Index) -> Index {
126 | return pairs.index(after: index)
127 | }
128 |
129 | /// :nodoc:
130 | public subscript(index: Index) -> Element {
131 | get {
132 | return (key: pairs[index].key, value: pairs[index].value)
133 | }
134 | // MutableCollection
135 | set {
136 | pairs[index] = Pair(newValue.key, newValue.value)
137 | }
138 | }
139 | }
140 |
141 | extension Node.Mapping: TagResolvable {
142 | static let defaultTagName = Tag.Name.map
143 | }
144 |
145 | // MARK: - Merge support
146 |
147 | extension Node.Mapping {
148 | func flatten() -> Node.Mapping {
149 | var pairs = Array(self)
150 | var merge = [(key: Node, value: Node)]()
151 | var index = pairs.startIndex
152 | while index < pairs.count {
153 | let pair = pairs[index]
154 | if pair.key.tag.name == .merge {
155 | pairs.remove(at: index)
156 | switch pair.value {
157 | case .mapping(let mapping):
158 | merge.append(contentsOf: mapping.flatten())
159 | case let .sequence(sequence):
160 | let submerge = sequence
161 | .compactMap { $0.mapping.map { $0.flatten() } }
162 | .reversed()
163 | submerge.forEach {
164 | merge.append(contentsOf: $0)
165 | }
166 | default:
167 | break // TODO: Should raise error on other than mapping or sequence
168 | }
169 | } else if pair.key.tag.name == .value {
170 | pair.key.tag.name = .str
171 | index += 1
172 | } else {
173 | index += 1
174 | }
175 | }
176 | return Node.Mapping(merge + pairs, tag, style, nil, anchor)
177 | }
178 | }
179 |
180 | // MARK: - Dictionary-like APIs
181 |
182 | extension Node.Mapping {
183 | /// This mapping's keys. Similar to `Dictionary.keys`.
184 | public var keys: LazyMapCollection {
185 | return lazy.map { $0.key }
186 | }
187 |
188 | /// This mapping's values. Similar to `Dictionary.values`.
189 | public var values: LazyMapCollection {
190 | return lazy.map { $0.value }
191 | }
192 |
193 | /// Set or get the `Node` for the specified string's `Node` representation.
194 | public subscript(string: String) -> Node? {
195 | get {
196 | return self[Node(string, tag.copy(with: .implicit))]
197 | }
198 | set {
199 | self[Node(string, tag.copy(with: .implicit))] = newValue
200 | }
201 | }
202 |
203 | /// Set or get the specified `Node`.
204 | public subscript(node: Node) -> Node? {
205 | get {
206 | return pairs.reversed().first(where: { $0.key == node })?.value
207 | }
208 | set {
209 | if let newValue = newValue {
210 | if let index = index(forKey: node) {
211 | pairs[index] = Pair(pairs[index].key, newValue)
212 | } else {
213 | pairs.append(Pair(node, newValue))
214 | }
215 | } else {
216 | if let index = index(forKey: node) {
217 | pairs.remove(at: index)
218 | }
219 | }
220 | }
221 | }
222 |
223 | /// Get the index of the specified `Node`, if it exists in the mapping.
224 | public func index(forKey key: Node) -> Index? {
225 | return pairs.reversed().firstIndex(where: { $0.key == key }).map({ pairs.index(before: $0.base) })
226 | }
227 | }
228 |
229 | private struct Pair: Comparable, Equatable {
230 | let key: Value
231 | let value: Value
232 |
233 | init(_ key: Value, _ value: Value) {
234 | self.key = key
235 | self.value = value
236 | }
237 |
238 | static func < (lhs: Pair, rhs: Pair) -> Bool {
239 | return lhs.key < rhs.key
240 | }
241 |
242 | static func toTuple(pair: Pair) -> (key: Value, value: Value) {
243 | return (key: pair.key, value: pair.value)
244 | }
245 | }
246 |
247 | extension Pair: Hashable where Value: Hashable {}
248 |
--------------------------------------------------------------------------------
/Tests/YamsTests/YamlErrorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // YamlErrorTests.swift
3 | // Yams
4 | //
5 | // Created by JP Simard on 2016-11-19.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Yams
11 |
12 | final class YamlErrorTests: XCTestCase, @unchecked Sendable {
13 | func testYamlErrorEmitter() throws {
14 | XCTAssertThrowsError(try Yams.serialize(node: "test", version: (1, 3))) { error in
15 | XCTAssertTrue(error is YamlError)
16 | XCTAssertEqual("\(error)", "incompatible %YAML directive")
17 | }
18 | }
19 |
20 | func testYamlErrorReader() throws {
21 | // reader
22 | let yaml = "test: 'テスト\u{12}'"
23 | XCTAssertThrowsError(_ = try Parser(yaml: yaml).nextRoot()) { error in
24 | XCTAssertTrue(error is YamlError)
25 | XCTAssertEqual("\(error)", """
26 | 1:11: error: reader: control characters are not allowed:
27 | test: 'テスト\u{12}'
28 | ^
29 | """
30 | )
31 | }
32 | }
33 |
34 | func testYamlErrorScanner() throws {
35 | let yaml = "test: 'テスト"
36 | XCTAssertThrowsError(_ = try Parser(yaml: yaml).nextRoot()) { error in
37 | XCTAssertTrue(error is YamlError)
38 | XCTAssertEqual("\(error)", """
39 | 1:11: error: scanner: while scanning a quoted scalar in line 1, column 7
40 | found unexpected end of stream:
41 | test: 'テスト
42 | ^
43 | """
44 | )
45 | }
46 | }
47 |
48 | func testYamlErrorParser() throws {
49 | let yaml = "- [キー1: 値1]\n- [key1: value1, key2: ,"
50 | XCTAssertThrowsError(_ = try Parser(yaml: yaml).nextRoot()) { error in
51 | XCTAssertTrue(error is YamlError)
52 | XCTAssertEqual("\(error)", """
53 | 3:1: error: parser: while parsing a flow node in line 3, column 1
54 | did not find expected node content:
55 | - [key1: value1, key2: ,
56 | ^
57 | """
58 | )
59 | }
60 | }
61 |
62 | func testNextRootThrowsOnInvalidYaml() throws {
63 | let invalidYAML = "|\na"
64 |
65 | let parser = try Parser(yaml: invalidYAML)
66 | // first iteration returns scalar
67 | XCTAssertEqual(try parser.nextRoot(), Node("", Tag(.str), .literal))
68 | // second iteration throws error
69 | XCTAssertThrowsError(try parser.nextRoot()) { error in
70 | XCTAssertTrue(error is YamlError)
71 | XCTAssertEqual("\(error)", """
72 | 2:1: error: parser: did not find expected :
73 | a
74 | ^
75 | """
76 | )
77 | }
78 | }
79 |
80 | func testSingleRootThrowsOnInvalidYaml() throws {
81 | let invalidYAML = "|\na"
82 |
83 | let parser = try Parser(yaml: invalidYAML)
84 | XCTAssertThrowsError(try parser.singleRoot()) { error in
85 | XCTAssertTrue(error is YamlError)
86 | XCTAssertEqual("\(error)", """
87 | 2:1: error: parser: did not find expected :
88 | a
89 | ^
90 | """
91 | )
92 | }
93 | }
94 |
95 | func testSingleRootThrowsOnMultipleDocuments() throws {
96 | let multipleDocuments = "document 1\n---\ndocument 2\n"
97 | let parser = try Parser(yaml: multipleDocuments)
98 | XCTAssertThrowsError(try parser.singleRoot()) { error in
99 | XCTAssertTrue(error is YamlError)
100 | XCTAssertEqual("\(error)", """
101 | 2:1: error: composer: expected a single document in the stream in line 1, column 1
102 | but found another document:
103 | ---
104 | ^
105 | """
106 | )
107 | }
108 | }
109 |
110 | func testUndefinedAliasCausesError() throws {
111 | let undefinedAlias = "*undefinedAlias\n"
112 | let parser = try Parser(yaml: undefinedAlias)
113 | XCTAssertThrowsError(try parser.singleRoot()) { error in
114 | XCTAssertTrue(error is YamlError)
115 | XCTAssertEqual("\(error)", """
116 | 1:1: error: composer: found undefined alias:
117 | *undefinedAlias
118 | ^
119 | """
120 | )
121 | }
122 | }
123 |
124 | func testScannerErrorMayHaveNullContext() throws {
125 | // https://github.com/realm/SwiftLint/issues/1436
126 | let swiftlint1436 = "large_tuple: warning: 3"
127 | let parser = try Parser(yaml: swiftlint1436)
128 | XCTAssertThrowsError(try parser.singleRoot()) { error in
129 | XCTAssertTrue(error is YamlError)
130 | XCTAssertEqual("\(error)", """
131 | 1:21: error: scanner: mapping values are not allowed in this context:
132 | large_tuple: warning: 3
133 | ^
134 | """
135 | )
136 | }
137 | }
138 |
139 | func testYamlErrorDataCouldNotBeDecoded() {
140 | let yamlString = """
141 | emoji: 🙃
142 | """
143 | let utf16Data = yamlString.data(using: .utf16)!
144 | XCTAssertThrowsError(try Parser(yaml: utf16Data, encoding: .utf8)) { error in
145 | XCTAssertTrue(error is YamlError)
146 | XCTAssertEqual("\(error)", """
147 | String could not be decoded from data using 'Unicode (UTF-8)' encoding
148 | """
149 | )
150 | }
151 | }
152 |
153 | func testDuplicateKeysCannotBeParsed() throws {
154 | let yamlString = """
155 | a: value
156 | a: different_value
157 | """
158 | XCTAssertThrowsError(try Parser(yaml: yamlString).singleRoot()) { error in
159 | XCTAssertTrue(error is YamlError)
160 | XCTAssertEqual("\(error)", """
161 | Parser: expected all keys to be unique but found the following duplicated key(s): 'a'.
162 | Context:
163 | a: value
164 | a: different_value in line 1, column 1
165 |
166 | """)
167 | }
168 | }
169 |
170 | func testDuplicatedKeysCannotBeParsed_MultipleDuplicates() throws {
171 | let yamlString = """
172 | a: value
173 | a: different_value
174 | b: value
175 | b: different_value
176 | b: different_different_value
177 | """
178 | XCTAssertThrowsError(try Parser(yaml: yamlString).singleRoot()) { error in
179 | XCTAssertTrue(error is YamlError)
180 | XCTAssertEqual("\(error)", """
181 | Parser: expected all keys to be unique but found the following duplicated key(s): 'a', 'b'.
182 | Context:
183 | a: value
184 | a: different_value
185 | b: value
186 | b: different_value
187 | b: different_different_value in line 1, column 1
188 |
189 | """)
190 | }
191 | }
192 | }
193 |
194 | extension YamlErrorTests {
195 | static var allTests: [(String, (YamlErrorTests) -> () throws -> Void)] {
196 | return [
197 | ("testYamlErrorReader", testYamlErrorReader),
198 | ("testYamlErrorScanner", testYamlErrorScanner),
199 | ("testYamlErrorParser", testYamlErrorParser),
200 | ("testNextRootThrowsOnInvalidYaml", testNextRootThrowsOnInvalidYaml),
201 | ("testSingleRootThrowsOnInvalidYaml", testSingleRootThrowsOnInvalidYaml),
202 | ("testSingleRootThrowsOnMultipleDocuments", testSingleRootThrowsOnMultipleDocuments),
203 | ("testUndefinedAliasCausesError", testUndefinedAliasCausesError),
204 | ("testScannerErrorMayHaveNullContext", testScannerErrorMayHaveNullContext),
205 | ("testYamlErrorDataCouldNotBeDecoded", testYamlErrorDataCouldNotBeDecoded)
206 | ]
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/Sources/Yams/YamlError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // YamlError.swift
3 | // Yams
4 | //
5 | // Created by JP Simard on 2016-11-19.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | // swiftlint:disable duplicate_imports
10 |
11 | #if SWIFT_PACKAGE
12 | #if compiler(>=6)
13 | internal import CYaml
14 | #else
15 | @_implementationOnly import CYaml
16 | #endif
17 | #endif
18 | import Foundation
19 |
20 | /// Errors thrown by Yams APIs.
21 | public enum YamlError: Error {
22 | // Used in `yaml_emitter_t` and `yaml_parser_t`
23 | /// `YAML_NO_ERROR`. No error is produced.
24 | case no
25 |
26 | /// `YAML_MEMORY_ERROR`. Cannot allocate or reallocate a block of memory.
27 | case memory
28 |
29 | // Used in `yaml_parser_t`
30 | /// `YAML_READER_ERROR`. Cannot read or decode the input stream.
31 | ///
32 | /// - parameter problem: Error description.
33 | /// - parameter offset: The offset from `yaml.startIndex` at which the problem occured.
34 | /// - parameter value: The problematic value (-1 is none).
35 | /// - parameter yaml: YAML String which the problem occured while reading.
36 | case reader(problem: String, offset: Int?, value: Int32, yaml: String)
37 |
38 | // line and column start from 1, column is counted by unicodeScalars
39 | /// `YAML_SCANNER_ERROR`. Cannot scan the input stream.
40 | ///
41 | /// - parameter context: Error context.
42 | /// - parameter problem: Error description.
43 | /// - parameter mark: Problem position.
44 | /// - parameter yaml: YAML String which the problem occured while scanning.
45 | case scanner(context: Context?, problem: String, Mark, yaml: String)
46 |
47 | /// `YAML_PARSER_ERROR`. Cannot parse the input stream.
48 | ///
49 | /// - parameter context: Error context.
50 | /// - parameter problem: Error description.
51 | /// - parameter mark: Problem position.
52 | /// - parameter yaml: YAML String which the problem occured while parsing.
53 | case parser(context: Context?, problem: String, Mark, yaml: String)
54 |
55 | /// `YAML_COMPOSER_ERROR`. Cannot compose a YAML document.
56 | ///
57 | /// - parameter context: Error context.
58 | /// - parameter problem: Error description.
59 | /// - parameter mark: Problem position.
60 | /// - parameter yaml: YAML String which the problem occured while composing.
61 | case composer(context: Context?, problem: String, Mark, yaml: String)
62 |
63 | // Used in `yaml_emitter_t`
64 | /// `YAML_WRITER_ERROR`. Cannot write to the output stream.
65 | ///
66 | /// - parameter problem: Error description.
67 | case writer(problem: String)
68 |
69 | /// `YAML_EMITTER_ERROR`. Cannot emit a YAML stream.
70 | ///
71 | /// - parameter problem: Error description.
72 | case emitter(problem: String)
73 |
74 | /// Used in `NodeRepresentable`.
75 | ///
76 | /// - parameter problem: Error description.
77 | case representer(problem: String)
78 |
79 | /// String data could not be decoded with the specified encoding.
80 | ///
81 | /// - parameter encoding: The string encoding used to decode the string data.
82 | case dataCouldNotBeDecoded(encoding: String.Encoding)
83 |
84 | /// Multiple uses of the same key detected in a mapping
85 | ///
86 | /// - parameter duplicates: A dictionary keyed by the duplicated node value, with all nodes that duplicate the value
87 | /// - parameter yaml: YAML String which the problem occured while reading.
88 | case duplicatedKeysInMapping(duplicates: [String], context: Context)
89 |
90 | /// The error context.
91 | public struct Context: CustomStringConvertible, Sendable {
92 | /// Context text.
93 | public let text: String
94 | /// Context position.
95 | public let mark: Mark
96 | /// A textual representation of this instance.
97 | public var description: String {
98 | return text + " in line \(mark.line), column \(mark.column)\n"
99 | }
100 | }
101 | }
102 |
103 | extension YamlError {
104 | init(from parser: yaml_parser_t, with yaml: String) {
105 | func context(from parser: yaml_parser_t) -> Context? {
106 | guard let context = parser.context else { return nil }
107 | return Context(
108 | text: String(cString: context),
109 | mark: Mark(line: parser.context_mark.line + 1, column: parser.context_mark.column + 1)
110 | )
111 | }
112 |
113 | func problemMark(from parser: yaml_parser_t) -> Mark {
114 | return Mark(line: parser.problem_mark.line + 1, column: parser.problem_mark.column + 1)
115 | }
116 |
117 | switch parser.error {
118 | case YAML_MEMORY_ERROR:
119 | self = .memory
120 | case YAML_READER_ERROR:
121 | let index: String.Index?
122 | if parser.encoding == YAML_UTF8_ENCODING {
123 | index = yaml.utf8
124 | .index(yaml.utf8.startIndex, offsetBy: parser.problem_offset, limitedBy: yaml.utf8.endIndex)?
125 | .samePosition(in: yaml)
126 | } else {
127 | index = yaml.utf16
128 | .index(yaml.utf16.startIndex, offsetBy: parser.problem_offset / 2, limitedBy: yaml.utf16.endIndex)?
129 | .samePosition(in: yaml)
130 | }
131 | let offset = index.map { yaml.distance(from: yaml.startIndex, to: $0) }
132 | self = .reader(problem: String(cString: parser.problem),
133 | offset: offset,
134 | value: parser.problem_value,
135 | yaml: yaml)
136 | case YAML_SCANNER_ERROR:
137 | self = .scanner(context: context(from: parser),
138 | problem: String(cString: parser.problem), problemMark(from: parser),
139 | yaml: yaml)
140 | case YAML_PARSER_ERROR:
141 | self = .parser(context: context(from: parser),
142 | problem: String(cString: parser.problem), problemMark(from: parser),
143 | yaml: yaml)
144 | case YAML_COMPOSER_ERROR:
145 | self = .composer(context: context(from: parser),
146 | problem: String(cString: parser.problem), problemMark(from: parser),
147 | yaml: yaml)
148 | default:
149 | fatalError("Parser has unknown error: \(parser.error)!")
150 | }
151 | }
152 |
153 | init(from emitter: yaml_emitter_t) {
154 | switch emitter.error {
155 | case YAML_MEMORY_ERROR:
156 | self = .memory
157 | case YAML_EMITTER_ERROR:
158 | self = .emitter(problem: String(cString: emitter.problem))
159 | default:
160 | fatalError("Emitter has unknown error: \(emitter.error)!")
161 | }
162 | }
163 | }
164 |
165 | extension YamlError: CustomStringConvertible {
166 | /// A textual representation of this instance.
167 | public var description: String {
168 | switch self {
169 | case .no:
170 | return "No error is produced"
171 | case .memory:
172 | return "Memory error"
173 | case let .reader(problem, offset, value, yaml):
174 | guard let (line, column, contents) = offset.flatMap(yaml.lineNumberColumnAndContents(at:)) else {
175 | return "\(problem) at offset: \(String(describing: offset)), value: \(value)"
176 | }
177 | let mark = Mark(line: line + 1, column: column + 1)
178 | return "\(mark): error: reader: \(problem):\n" + contents.endingWithNewLine
179 | + String(repeating: " ", count: column) + "^"
180 | case let .scanner(context, problem, mark, yaml):
181 | return "\(mark): error: scanner: \(context?.description ?? "")\(problem):\n" + mark.snippet(from: yaml)
182 | case let .parser(context, problem, mark, yaml):
183 | return "\(mark): error: parser: \(context?.description ?? "")\(problem):\n" + mark.snippet(from: yaml)
184 | case let .composer(context, problem, mark, yaml):
185 | return "\(mark): error: composer: \(context?.description ?? "")\(problem):\n" + mark.snippet(from: yaml)
186 | case let .writer(problem), let .emitter(problem), let .representer(problem):
187 | return problem
188 | case .dataCouldNotBeDecoded(encoding: let encoding):
189 | return "String could not be decoded from data using '\(encoding)' encoding"
190 | case let .duplicatedKeysInMapping(duplicates, context):
191 | let duplicateKeys = duplicates.sorted().map { "'\($0)'" }.joined(separator: ", ")
192 | return """
193 | Parser: expected all keys to be unique but found the following duplicated key(s): \(duplicateKeys).
194 | Context:
195 | \(context.description)
196 | """
197 |
198 | }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/Tests/YamsTests/NodeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeTests.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 12/25/16.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Yams
12 |
13 | final class NodeTests: XCTestCase, @unchecked Sendable {
14 |
15 | func testExpressibleByArrayLiteral() {
16 | let sequence: Node = [
17 | Node("1"),
18 | Node("2"),
19 | Node("3")
20 | ]
21 | let expected: Node = Node([
22 | Node("1"),
23 | Node("2"),
24 | Node("3")
25 | ])
26 | XCTAssertEqual(sequence, expected)
27 | }
28 |
29 | func testExpressibleByDictionaryLiteral() {
30 | let sequence: Node = [Node("key"): Node("value")]
31 | let expected: Node = Node([
32 | (Node("key"), Node("value"))
33 | ])
34 | XCTAssertEqual(sequence, expected)
35 | }
36 |
37 | func testExpressibleByFloatLiteral() {
38 | let sequence: Node = 0.0
39 | let expected: Node = Node(String(0.0))
40 | XCTAssertEqual(sequence, expected)
41 | }
42 |
43 | func testExpressibleByIntegerLiteral() {
44 | let sequence: Node = 0
45 | let expected: Node = Node(String(0))
46 | XCTAssertEqual(sequence, expected)
47 | }
48 |
49 | func testExpressibleByStringLiteral() {
50 | let sequence: Node = "string"
51 | let expected: Node = Node("string")
52 | XCTAssertEqual(sequence, expected)
53 | }
54 |
55 | func testTypedAccessorProperties() {
56 | let scalarBool: Node = "true"
57 | XCTAssertEqual(scalarBool.bool, true)
58 |
59 | let scalarFloat: Node = "1.0"
60 | XCTAssertEqual(scalarFloat.float, 1.0)
61 |
62 | let scalarNull = Node("null", .implicit, .plain)
63 | XCTAssertEqual(scalarNull.null, NSNull())
64 |
65 | let scalarInt: Node = "1"
66 | XCTAssertEqual(scalarInt.int, 1)
67 |
68 | let scalarUUID: Node = "B5C6C790-BC0A-4781-9AFF-F9896E0C030C"
69 | XCTAssertEqual(scalarUUID.uuid, UUID(uuidString: "B5C6C790-BC0A-4781-9AFF-F9896E0C030C"))
70 |
71 | let base64String = """
72 | R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\
73 | OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\
74 | +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\
75 | AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=
76 | """
77 | let scalarBinary: Node = Node(base64String)
78 | XCTAssertEqual(scalarBinary.binary, Data(base64Encoded: base64String, options: .ignoreUnknownCharacters)!)
79 |
80 | let scalarTimestamp: Node = "2001-12-15T02:59:43.1Z"
81 | XCTAssertEqual(scalarTimestamp.timestamp, timestamp( 0, 2001, 12, 15, 02, 59, 43, 0.1))
82 | }
83 |
84 | func testArray() {
85 | let base64String = """
86 | R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\
87 | OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\
88 | +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\
89 | AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=
90 | """
91 | let sequence: Node = [
92 | "true",
93 | "1.0",
94 | "1",
95 | Node(base64String)
96 | ]
97 | XCTAssertEqual(sequence.array(), ["true", "1.0", "1", Node(base64String)] as [Node])
98 | XCTAssertEqual(sequence.array(of: String.self), ["true", "1.0", "1", base64String])
99 | XCTAssertEqual(sequence.array() as [String], ["true", "1.0", "1", base64String])
100 | XCTAssertEqual(sequence.array(of: Bool.self), [true])
101 | XCTAssertEqual(sequence.array() as [Bool], [true])
102 | XCTAssertEqual(sequence.array(of: Double.self), [1.0, 1.0])
103 | XCTAssertEqual(sequence.array() as [Double], [1.0, 1.0])
104 | XCTAssertEqual(sequence.array(of: Int.self), [1])
105 | XCTAssertEqual(sequence.array() as [Int], [1])
106 |
107 | let expectedData = [
108 | Data(base64Encoded: "true", options: .ignoreUnknownCharacters)!,
109 | Data(base64Encoded: base64String, options: .ignoreUnknownCharacters)!
110 | ]
111 | XCTAssertEqual(sequence.array(of: Data.self), expectedData)
112 | XCTAssertEqual(sequence.array() as [Data], expectedData)
113 | }
114 |
115 | func testSubscriptMapping() {
116 | let mapping: Node = ["key1": "value1", "key2": "value2", "key3": "value3"]
117 | XCTAssertEqual(mapping["key1" as Node]?.string, "value1") // Node resolvable as String
118 | XCTAssertEqual(mapping["key2"]?.string, "value2") // String Literal
119 | XCTAssertEqual(mapping[String("key3")]?.string, "value3") // String
120 | }
121 |
122 | func testSubscriptSequence() {
123 | let mapping: Node = ["value1", "value2", "value3"]
124 | XCTAssertEqual(mapping["0" as Node]?.string, "value1") // Node resolvable as Integer
125 | XCTAssertEqual(mapping[1]?.string, "value2") // Integer Literal
126 | XCTAssertEqual(mapping[Int(2)]?.string, "value3") // Int
127 | }
128 |
129 | func testSubscriptWithNonDefaultResolver() {
130 | let yaml = "200: value"
131 | XCTAssertEqual(try Yams.compose(yaml: yaml)?["200"]?.string, "value")
132 | XCTAssertEqual(try Yams.compose(yaml: yaml, .basic)?["200"]?.string, "value")
133 | }
134 |
135 | func testMappingBehavesLikeADictionary() {
136 | let node: Node = ["key1": "value1", "key2": "value2"]
137 | let mapping = node.mapping!
138 | XCTAssertEqual(mapping.count, 2)
139 | XCTAssertEqual(mapping.endIndex, 2)
140 | XCTAssertTrue(mapping.first! == ("key1", "value1"))
141 | XCTAssertFalse(mapping.isEmpty)
142 | XCTAssertEqual(Set(mapping.keys), ["key1", "key2"])
143 | XCTAssertEqual(mapping.lazy.count, 2)
144 | XCTAssertEqual(mapping.startIndex, 0)
145 | XCTAssertEqual(mapping.underestimatedCount, 2)
146 | XCTAssertEqual(Set(mapping.values), ["value1", "value2"])
147 |
148 | // subscript
149 | var mutableMapping = mapping
150 | mutableMapping["key3"] = "value3"
151 | XCTAssertEqual(mutableMapping["key3"], "value3")
152 | mutableMapping["key3"] = "value3changed"
153 | XCTAssertEqual(mutableMapping["key3"], "value3changed")
154 | mutableMapping["key3"] = nil
155 | XCTAssertNil(mutableMapping["key3"])
156 |
157 | // iterator
158 | var iterator = mapping.makeIterator()
159 | XCTAssertTrue(iterator.next()! == ("key1", "value1"))
160 | XCTAssertTrue(iterator.next()! == ("key2", "value2"))
161 | XCTAssertNil(iterator.next())
162 |
163 | // ExpressibleByDictionaryLiteral
164 | XCTAssertEqual(mapping, ["key1": "value1", "key2": "value2"])
165 | }
166 |
167 | func testSequenceBehavesLikeAnArray() {
168 | let node: Node = ["value1", "value2", "value3"]
169 | let sequence = node.sequence!
170 | XCTAssertEqual(sequence.count, 3)
171 | XCTAssertEqual(sequence.endIndex, 3)
172 | XCTAssertEqual(sequence.first, "value1")
173 | XCTAssertFalse(sequence.isEmpty)
174 | XCTAssertEqual(sequence.last, "value3")
175 | XCTAssertEqual(sequence.lazy.count, 3)
176 | XCTAssertEqual(sequence.startIndex, 0)
177 | XCTAssertEqual(sequence.underestimatedCount, 3)
178 |
179 | // subscript
180 | var mutableSequence = sequence
181 | mutableSequence.append("value4")
182 | XCTAssertEqual(mutableSequence.count, 4)
183 | XCTAssertEqual(mutableSequence[3], "value4")
184 | mutableSequence.remove(at: 2)
185 | XCTAssertEqual(mutableSequence.count, 3)
186 | XCTAssertEqual(Array(mutableSequence), ["value1", "value2", "value4"])
187 |
188 | // iterator
189 | var iterator = sequence.makeIterator()
190 | XCTAssertEqual(iterator.next(), "value1")
191 | XCTAssertEqual(iterator.next(), "value2")
192 | XCTAssertEqual(iterator.next(), "value3")
193 | XCTAssertNil(iterator.next())
194 |
195 | // ExpressibleByArrayLiteral
196 | XCTAssertEqual(sequence, ["value1", "value2", "value3"])
197 | }
198 |
199 | func testScalar() {
200 | var node = Node("1")
201 | XCTAssertEqual(node.tag, Tag(.int))
202 | node.scalar?.string = "test"
203 | XCTAssertEqual(node.tag, Tag(.str))
204 | }
205 |
206 | func testAlias_read_write() {
207 | var node = Node.alias(Node.Alias(Anchor(rawValue: "test")))
208 | XCTAssertEqual(node.alias?.anchor, "test")
209 | node.alias?.anchor = "test2"
210 | XCTAssertEqual(node.alias?.anchor, "test2")
211 | }
212 |
213 | func testAlias_nil() {
214 | var node = Node("1") // a scalar
215 | XCTAssertEqual(node.alias, nil)
216 | node.alias?.anchor = "test2"
217 | XCTAssertEqual(node.alias, nil)
218 | // ^^ still nil because the alias property is nil
219 | }
220 | }
221 |
222 | extension NodeTests {
223 | static var allTests: [(String, (NodeTests) -> () throws -> Void)] {
224 | return [
225 | ("testExpressibleByArrayLiteral", testExpressibleByArrayLiteral),
226 | ("testExpressibleByDictionaryLiteral", testExpressibleByDictionaryLiteral),
227 | ("testExpressibleByFloatLiteral", testExpressibleByFloatLiteral),
228 | ("testExpressibleByIntegerLiteral", testExpressibleByIntegerLiteral),
229 | ("testExpressibleByStringLiteral", testExpressibleByStringLiteral),
230 | ("testTypedAccessorProperties", testTypedAccessorProperties),
231 | ("testArray", testArray),
232 | ("testSubscriptMapping", testSubscriptMapping),
233 | ("testSubscriptSequence", testSubscriptSequence),
234 | ("testSubscriptWithNonDefaultResolver", testSubscriptWithNonDefaultResolver),
235 | ("testMappingBehavesLikeADictionary", testMappingBehavesLikeADictionary),
236 | ("testSequenceBehavesLikeAnArray", testSequenceBehavesLikeAnArray),
237 | ("testScalar", testScalar)
238 | ]
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/Tests/YamsTests/PerformanceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PerformanceTests.swift
3 | // Yams
4 | //
5 | // Created by Norio Nomura on 12/24/16.
6 | // Copyright (c) 2016 Yams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Yams
12 |
13 | final class PerformanceTests: XCTestCase, @unchecked Sendable {
14 | private let fixturesDirectory: String = {
15 | if ProcessInfo.processInfo.environment["TEST_WORKSPACE"] != nil {
16 | return "Tests/YamsTests/Fixtures/"
17 | }
18 | let baseURL: URL
19 | #if swift(>=6.0)
20 | baseURL = URL(fileURLWithPath: #filePath)
21 | #else
22 | baseURL = URL(fileURLWithPath: #file)
23 | #endif
24 | return baseURL.deletingLastPathComponent().path + "/Fixtures/"
25 | }()
26 | let expectedImports = ["/SourceKitten/.build/debug"]
27 | let expectedOtherArguments = [
28 | "-j8", "-D", "SWIFT_PACKAGE", "-Onone", "-g", "-enable-testing",
29 | "-Xcc", "-fmodule-map-file=/SourceKitten/Packages/Clang_C-1.0.1/module.modulemap",
30 | "-Xcc", "-fmodule-map-file=/SourceKitten/Packages/SourceKit-1.0.1/module.modulemap",
31 | "-module-cache-path", "/SourceKitten/.build/debug/ModuleCache"
32 | ]
33 | let expectedSources = [
34 | "/SourceKitten/Source/SourceKittenFramework/Clang+SourceKitten.swift",
35 | "/SourceKitten/Source/SourceKittenFramework/ClangTranslationUnit.swift",
36 | "/SourceKitten/Source/SourceKittenFramework/CodeCompletionItem.swift",
37 | "/SourceKitten/Source/SourceKittenFramework/Dictionary+Merge.swift",
38 | "/SourceKitten/Source/SourceKittenFramework/Documentation.swift",
39 | "/SourceKitten/Source/SourceKittenFramework/File.swift",
40 | "/SourceKitten/Source/SourceKittenFramework/JSONOutput.swift",
41 | "/SourceKitten/Source/SourceKittenFramework/Language.swift",
42 | "/SourceKitten/Source/SourceKittenFramework/library_wrapper.swift",
43 | "/SourceKitten/Source/SourceKittenFramework/library_wrapper_CXString.swift",
44 | "/SourceKitten/Source/SourceKittenFramework/library_wrapper_Documentation.swift",
45 | "/SourceKitten/Source/SourceKittenFramework/library_wrapper_Index.swift",
46 | "/SourceKitten/Source/SourceKittenFramework/library_wrapper_sourcekitd.swift",
47 | "/SourceKitten/Source/SourceKittenFramework/LinuxCompatibility.swift",
48 | "/SourceKitten/Source/SourceKittenFramework/Module.swift",
49 | "/SourceKitten/Source/SourceKittenFramework/ObjCDeclarationKind.swift",
50 | "/SourceKitten/Source/SourceKittenFramework/OffsetMap.swift",
51 | "/SourceKitten/Source/SourceKittenFramework/Parameter.swift",
52 | "/SourceKitten/Source/SourceKittenFramework/Request.swift",
53 | "/SourceKitten/Source/SourceKittenFramework/SourceDeclaration.swift",
54 | "/SourceKitten/Source/SourceKittenFramework/SourceLocation.swift",
55 | "/SourceKitten/Source/SourceKittenFramework/StatementKind.swift",
56 | "/SourceKitten/Source/SourceKittenFramework/String+SourceKitten.swift",
57 | "/SourceKitten/Source/SourceKittenFramework/Structure.swift",
58 | "/SourceKitten/Source/SourceKittenFramework/SwiftDeclarationKind.swift",
59 | "/SourceKitten/Source/SourceKittenFramework/SwiftDocKey.swift",
60 | "/SourceKitten/Source/SourceKittenFramework/SwiftDocs.swift",
61 | "/SourceKitten/Source/SourceKittenFramework/SwiftLangSyntax.swift",
62 | "/SourceKitten/Source/SourceKittenFramework/SyntaxKind.swift",
63 | "/SourceKitten/Source/SourceKittenFramework/SyntaxMap.swift",
64 | "/SourceKitten/Source/SourceKittenFramework/SyntaxToken.swift",
65 | "/SourceKitten/Source/SourceKittenFramework/Text.swift",
66 | "/SourceKitten/Source/SourceKittenFramework/Xcode.swift"
67 | ]
68 |
69 | func loadYAML() throws -> String {
70 | let data = try Data(contentsOf: URL(fileURLWithPath: fixturesDirectory + "/SourceKitten#289/debug.yaml"))
71 | return String(data: data, encoding: .utf8)!
72 | }
73 |
74 | func parseSourceKittenIssue289UsingLoad(yaml: String, encoding: Parser.Encoding) {
75 | let spmName = "SourceKittenFramework"
76 | do {
77 | guard let object = try Yams.load(yaml: yaml, .default, .default, encoding) as? [String: Any],
78 | let commands = (object["commands"] as? [String: [String: Any]])?.values,
79 | let moduleCommand = commands.first(where: { ($0["module-name"] as? String ?? "") == spmName }),
80 | let imports = moduleCommand["import-paths"] as? [String],
81 | let otherArguments = moduleCommand["other-args"] as? [String],
82 | let sources = moduleCommand["sources"] as? [String] else {
83 | XCTFail("Invalid result form Yams.load()")
84 | return
85 | }
86 | XCTAssertEqual(imports, self.expectedImports)
87 | XCTAssertEqual(otherArguments, self.expectedOtherArguments)
88 | XCTAssertEqual(sources, self.expectedSources)
89 | } catch {
90 | XCTFail("\(error)")
91 | }
92 | }
93 |
94 | func testUsingLoadWithUTF16() throws {
95 | let yaml = try loadYAML()
96 | self.measure {
97 | parseSourceKittenIssue289UsingLoad(yaml: yaml, encoding: .utf16)
98 | }
99 | }
100 |
101 | func testUsingLoadWithUTF8() throws {
102 | let yaml = try loadYAML()
103 | self.measure {
104 | parseSourceKittenIssue289UsingLoad(yaml: yaml, encoding: .utf8)
105 | }
106 | }
107 |
108 | func parseSourceKittenIssue289UsingCompose(yaml: String, encoding: Parser.Encoding) {
109 | let spmName = "SourceKittenFramework"
110 | do {
111 | guard let node = try Yams.compose(yaml: yaml, .default, .default, encoding),
112 | let commands = node["commands"]?.mapping?.values,
113 | let moduleCommand = commands.first(where: { $0["module-name"]?.string == spmName }),
114 | let imports = moduleCommand["import-paths"]?.array(of: String.self),
115 | let otherArguments = moduleCommand["other-args"]?.array(of: String.self),
116 | let sources = moduleCommand["sources"]?.array(of: String.self) else {
117 | XCTFail("Invalid result form Yams.load()")
118 | return
119 | }
120 | XCTAssertEqual(imports, self.expectedImports)
121 | XCTAssertEqual(otherArguments, self.expectedOtherArguments)
122 | XCTAssertEqual(sources, self.expectedSources)
123 | } catch {
124 | XCTFail("\(error)")
125 | }
126 | }
127 | #if canImport(Android)
128 | func measure(_ block: () -> Void) {
129 | let start = Date.now
130 | // the hardwired max standard deviation of 10% varies too much on the Android emulator and fails the tests
131 | // this is being tracked at https://github.com/swiftlang/swift-corelibs-xctest/pull/506
132 | block()
133 | let duration = Date.now.timeIntervalSince(start)
134 | print("measured time: \(duration)")
135 | }
136 | #elseif canImport(Combine)
137 | override func measure(_ block: () -> Void) {
138 | if #available(macOS 10.15, iOS 13, tvOS 13, *) {
139 | let metrics: [XCTMetric] = [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric(), XCTStorageMetric()]
140 | super.measure(metrics: metrics, block: block)
141 | } else {
142 | super.measure(block)
143 | }
144 | }
145 | #endif
146 |
147 | func testUsingComposeWithUTF16() throws {
148 | #if os(tvOS)
149 | throw XCTSkip("Skipping this test for tvOS")
150 | #else
151 | let yaml = try loadYAML()
152 | self.measure {
153 | parseSourceKittenIssue289UsingCompose(yaml: yaml, encoding: .utf16)
154 | }
155 | #endif
156 | }
157 |
158 | func testUsingComposeWithUTF8() throws {
159 | let yaml = try loadYAML()
160 | self.measure {
161 | parseSourceKittenIssue289UsingCompose(yaml: yaml, encoding: .utf8)
162 | }
163 | }
164 |
165 | func parseSourceKittenIssue289UsingSwiftDecodable(yaml: String, encoding: Parser.Encoding) {
166 | let spmName = "SourceKittenFramework"
167 | do {
168 | guard let manifest: Manifest = try YAMLDecoder(encoding: encoding).decode(from: yaml),
169 | let command = manifest.commands.values.first(where: { $0.moduleName == spmName }),
170 | let imports = command.importPaths,
171 | let otherArguments = command.otherArguments,
172 | let sources = command.sources else {
173 | XCTFail("Invalid result form Yams.load()")
174 | return
175 | }
176 | XCTAssertEqual(imports, self.expectedImports)
177 | XCTAssertEqual(otherArguments, self.expectedOtherArguments)
178 | XCTAssertEqual(sources, self.expectedSources)
179 | } catch {
180 | XCTFail("\(error)")
181 | }
182 | }
183 |
184 | func testUsingSwiftDecodableWithUTF16() throws {
185 | let yaml = try loadYAML()
186 | self.measure {
187 | parseSourceKittenIssue289UsingSwiftDecodable(yaml: yaml, encoding: .utf16)
188 | }
189 | }
190 |
191 | func testUsingSwiftDecodableWithUTF8() throws {
192 | let yaml = try loadYAML()
193 | self.measure {
194 | parseSourceKittenIssue289UsingSwiftDecodable(yaml: yaml, encoding: .utf8)
195 | }
196 | }
197 | }
198 |
199 | extension PerformanceTests {
200 | static var allTests: [(String, (PerformanceTests) -> () throws -> Void)] {
201 | return [
202 | ("testUsingLoadWithUTF16", testUsingLoadWithUTF16),
203 | ("testUsingLoadWithUTF8", testUsingLoadWithUTF8),
204 | ("testUsingComposeWithUTF16", testUsingComposeWithUTF16),
205 | ("testUsingComposeWithUTF8", testUsingComposeWithUTF8),
206 | ("testUsingSwiftDecodableWithUTF16", testUsingSwiftDecodableWithUTF16),
207 | ("testUsingSwiftDecodableWithUTF8", testUsingSwiftDecodableWithUTF8)
208 | ]
209 | }
210 | }
211 |
212 | // Models for parsing Build File of llbuild
213 | private struct Manifest: Decodable {
214 | let commands: [String: Command]
215 | }
216 |
217 | private struct Command: Decodable {
218 | let moduleName: String?
219 | let importPaths: [String]?
220 | let otherArguments: [String]?
221 | let sources: [String]?
222 | enum CodingKeys: String, CodingKey {
223 | case moduleName = "module-name"
224 | case importPaths = "import-paths"
225 | case otherArguments = "other-args"
226 | case sources
227 | }
228 | }
229 |
--------------------------------------------------------------------------------