common-parsers

swift-parser-printer 启发的通用解析器-打印器

用法

PartialIso(部分同构)

PartialIso 是一个值类型,它表示一个部分同构,即从 AB (apply) 和从 BA (unapply) 的一对可能失败的转换。有一些内置的 PartialIso 用于常见类型,例如从 String 转换为 Int

extension PartialIso where A == String, B == Int {
    public static var int: PartialIso {
        return PartialIso(
            apply: { Int.init($0) },
            unapply: String.init(describing:)
        )
    }
}

Parser(解析器)

Parser 是一个值类型,负责解析顺序数据(不一定是字符串数据),以及“打印”它,这是一种将解析的数据转换为解析器期望的输入类型并可选择地为输入数据创建模板的操作。

例如,我们可以定义一个解析器,它负责解析 URL 路径中的字符串字面量

public func path(_ str: String) -> Parser<URLComponents, Prelude.Unit> {
    return Parser<URLComponents, Prelude.Unit>.init(
        parse: { components in
            let pathComponents = components.path.components(separatedBy: "/")
            return components.pathComponents.head().flatMap { (p, ps) in
                return p == str
                    ? (components.with { $0.path = ps.joined(separator: "/") }, unit)
                    : nil
            }
        },
        print: { _ in URLComponents().with { $0.path = str } },
        template: { _ in URLComponents().with { $0.path = str } }
    )
}

Parser 的输入类型应该实现 Prelude.Monoid 协议

import Prelude

extension URLComponents: Monoid {
    public static var empty: URLComponents { return URLComponents() }
    public static func <>(lhs: URLComponents, rhs: URLComponents) -> URLComponents {
        // create URLComponents by composing lhs and rhs components
    }
}

FormatType(格式类型)

FormatType 是一个协议,你的类型可以实现它,以便它可以利用 Parser 解析输入。 当符合此协议时,你将免费获得 matchprinttemplate 函数,但如果需要,你可以覆盖它们。它还附带一些运算符,例如 <¢> (map) 和 <|>,你将使用它们将简单的格式组合成更复杂的格式。

例如,使用 <|>reduce 函数,你可以将多个格式组合成一个可以解析这些格式中任何一个的格式

public struct URLFormat<A>: FormatType { ... }

let formats: URLFormat = 
    scheme("https") </> host("me.com") </> [
        // matches URLs with "/echo" path
        path("echo"),
        // matches URLs with "/hello" path
        path("hello")
    ].reduce(.empty, <|>)
    
formats.match(URLComponents(string: "https://me.com/echo")!) // Prelude.unit
formats.match(URLComponents(string: "https://me.com/hello")!) // Prelude.unit
formats.match(URLComponents(string: "https://me.com/echo/hello")!) // nil

注意:你可以从 FormatType 元素的数组字面量创建相同的 FormatType

当所有格式都具有相同的类型时,这效果很好,例如 URLFormat<Prelude.Unit>URLFormat<String>,但无法将 URLFormat<Prelude.Unit>URLFormat<String> 组合。 为此,你可以为这样的格式化程序定义一个和类型(在 Swift 中是一个 enum),并使用 <¢> 运算符。

enum Routes {
  case echo
  case hello(String)
}

let formats: URLFormat = 
    scheme("https") </> host("me.com") </> [
        // matches URLs with "/echo" path
        iso(Routes.echo) <¢> path("echo"),
        // matches URLs with "/hello/:string" path
        iso(Routes.hello) <¢> path("hello") </> path(.string)
    ].reduce(.empty, <|>)

formats.match(URLComponents(string: "https://me.com/echo")!) // Routes.echo
formats.match(URLComponents(string: "https://me.com/hello/world")!) // Routes.hello("world")

这里的 iso 是一个辅助函数,可以在实现 Matchable 协议的类型上使用。 或者,你可以像这样为你的和类型定义部分同构

extension Routes {
    enum iso {
        static let echo = PartialIso(
            apply: { (_: Prelude.Unit) -> Routes? in
                return .echo
            },
            unapply: { (r: Routes) -> Prelude.Unit? in
                return Prelude.unit
        })

        static let hello = PartialIso(
            apply: { (s: String) -> Routes? in
                return .hello(s)
            },
            unapply: { (r: Routes) -> String? in
                guard case let .hello(str) = r else { return nil }
                return str
        })
    }
}

let formats: URLFormat =
    scheme("https") </> host("me.com") </> [
        // matches URLs with "/echo" path
        Routes.iso.echo <¢> path("echo"),
        // matches URLs with "/hello/:string" path
        Routes.iso.hello <¢> path("hello") </> path(.string)
    ].reduce(.empty, <|>)

Matchable(可匹配的)

Matchable 协议是一个辅助协议,可简化为你的和类型定义部分同构。 无需为每个案例实现部分同构,你可以实现一个单一的方法,在该方法中你可以使用简单的模式匹配。 然后,你可以将 iso 函数与枚举案例构造器一起使用,为这些枚举案例创建部分同构。

extension Routes: Matchable {
    func match<A>(_ constructor: (A) -> Routes) -> A? {
        switch self {
        case let .hello(values):
            guard let a = values as? A, self == constructor(a) else { return nil }
            return a
        case .echo:
            guard let a = unit as? A, self == constructor(a) else { return nil }
            return a
        default: return nil
        }
    }
}

let formats: URLFormat =
    scheme("https") </> host("me.com") </> [
        // matches URLs with "echo/" path
        iso(Routes.echo) <¢> path("echo"),
        // matches URLs with "hello/:string" path
        iso(Routes.hello) <¢> path("hello") </> path(.string)
    ].reduce(.empty, <|>)

注意:你可以使用 Sourcery 或 SwiftSyntax 为 Matchable 实现或为每个案例的单独 iso 生成代码。

printtemplate

Parser 不仅可以将任意输入解析为任意输出类型,还可以用于基于输出生成输入。 例如,使用 URLFormat,我们可以打印任意路由的 URL

try formats.print(.echo)!.render() // "https://me.com/echo"
try formats.print(.hello("world"))!.render() // "https://me.com/hello/world"

当你需要提供获得特定输出所需的输入时,这会很有帮助,即在 URL 的情况下,你可以使用它来渲染指向特定内容的 URL。

你也可以使用 template 函数来获取类似模板的输入

try formats.template(.echo)!.render() // "https://me.com/echo"
try formats.template(.hello("world"))!.render() // "https://me.com/hello/:string"

这对于诸如文档、日志或错误消息之类的东西很有用。

安装

import PackageDescription

let package = Package(
    dependencies: [
        .package(url: "https://github.com/ilyapuchka/common-parsers.git", .branch("master")),
    ]
)