├── 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 | ![Yams](https://raw.githubusercontent.com/jpsim/Yams/main/yams.jpg) 4 | 5 | A sweet and swifty [YAML](http://yaml.org/) parser built on 6 | [LibYAML](https://github.com/yaml/libyaml). 7 | 8 | [![SwiftPM](https://github.com/jpsim/Yams/workflows/SwiftPM/badge.svg)](https://github.com/jpsim/Yams/actions?query=workflow%3ASwiftPM) 9 | [![xcodebuild](https://github.com/jpsim/Yams/workflows/xcodebuild/badge.svg)](https://github.com/jpsim/Yams/actions?query=workflow%3Axcodebuild) 10 | [![pod lib lint](https://github.com/jpsim/Yams/workflows/pod%20lib%20lint/badge.svg)](https://github.com/jpsim/Yams/actions?query=workflow%3A%22pod+lib+lint%22) 11 | [![Nightly](https://github.com/jpsim/Yams/workflows/Nightly/badge.svg)](https://github.com/jpsim/Yams/actions?query=workflow%3ANightly) 12 | [![codecov](https://codecov.io/gh/jpsim/Yams/branch/main/graph/badge.svg)](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 | --------------------------------------------------------------------------------