SociableWeaver

Swift 与 GraphQL 的结合体,轻量级且易于使用的框架。SociableWeaver 采用声明式的编程风格,使 GraphQL 查询在 Swift 代码中看起来非常自然。通过使用 Swift 5.1 的函数构建器和 CodingKeys,SociableWeaver 在创建对象和字段时消除了对不安全字符串和字典的需求。

要求

Xcode 11.x 或带有 Swift Package Manager 的 Swift 5.1x 工具链。

安装

Swift Package Manager

对于使用 .xcodeproj 的项目,最好的方法是导航到 File > Swift Packages > Add Package Dependency...。 从那里只需输入 https://github.com/NicholasBellucci/SociableWeaver 作为包仓库的 url,并使用 master 分支或最新的版本。 Master 将始终与最新版本保持一致。 另一种方法是简单地将 .package(url: "https://github.com/NicholasBellucci/SociableWeaver.git", from: "0.1.0") 添加到你的 Package.swift 文件的 dependencies 中。

Carthage

将以下条目添加到你的 Cartfile 并运行 $ carthage update SociableWeaver

github "NicholasBellucci/SociableWeaver"

目录

用法

SociableWeaver 支持 GraphQL 提供的所有功能。 为了充分利用此框架,只需确保使用的任何 Codable 模型都包含 CodingKeys。 例如

public struct Post: Codable {
    public enum CodingKeys: String, CodingKey, CaseIterable {
        case id, title, content
    }

    public let id: String
    public let title: String
    public let content: String

    public init(id: String, title: String, content: String) {
        self.id = id
        self.title = title
        self.content = content
    }
}

如果无法使用 CodingKeys,SociableWeaver 确实支持字符串。 强烈建议将其作为最后的手段使用,因为它会使查询更难以管理。

对象和字段

GraphQL 字段

GraphQL 的核心是查询对象上的特定字段,并仅返回所需的内容。 使用 SociableWeaver 构建带有字段的对象非常容易。

Swift
Weave(.query) {
    Object(Post.self) {
        Field(Post.CodingKeys.id)
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)
    }
}
GraphQL 查询
query {
    post {
        id
        title
        content
    }
}

参数

GraphQL 参数

参数是 GraphQL 的关键部分,允许进行更精确的查询。 SociableWeaver 支持对象和字段上的参数。

唯一的要求是参数的值符合 ArgumentValueRepresentable。 诸如 StringIntBool 等核心类型已经符合。 枚举将需要符合 EnumValueRepresentable 协议。

Swift
Weave(.query) {
    Object(Post.self) {
        Field(Post.CodingKeys.title)

        Object(Post.CodingKeys.author) {
            Field(Author.CodingKeys.id)
            Field(Author.CodingKeys.name)
                .argument(key: "lastName", value: "Doe")
        }

        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.id)
            Field(Comment.CodingKeys.content)
        }
        .argument(key: "filter", value: CommentFilter.recent)
    }
}
GraphQL 查询
query {
    post {
        title
        author {
            id
            name(lastName: "Doe")
        }
        comments(filter: RECENT) {
            id
            content
        }
    }
}

可选项

支持可选项,并且可以包含在查询中。 如果应包含可选项并且该值为 nil,则生成的 GraphQL 值为 null

为了包含可选项,请确保在不包含 ? 的情况下获取属性的参数值。 这将导致查询参数为 age: null

public struct Author: Codable {
    public enum CodingKeys: String, CodingKey, CaseIterable {
        case id, name, age, birthplace
    }

    ...
    public let age: Int?
    ...
}

extension Author: ArgumentValueRepresentable {
    public var argumentValue: String {
        var params: [String: String?] = [:]

        ...
        params["age"] = age.argumentValue
        ...

        let paramStrings: [String] = params.compactMap { argument in
            guard let value = argument.value else {
                return nil
            }

            return "\(argument.key): \(value)"
        }

        return "{ \(paramStrings.joined(separator: ",")) }"
    }
}'

别名

GraphQL 别名

在同一请求中多次查询单个对象时,别名是关键。

Swift
Weave(.query) {
    Object(Post.self) {
        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.id)
            Field(Comment.CodingKeys.content)
        }
        .argument(key: "filter", value: CommentFilter.recent)
        .alias("newComments")
        
        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.id)
            Field(Comment.CodingKeys.content)
        }
        .argument(key: "filter", value: CommentFilter.old)
        .alias("oldComments")
    }
}
GraphQL 查询
query {
    post {
        newComments: comments(filter: RECENT) {
            id
            content
        }
        oldComments: comments(filter: OLD) {
            id
            content
        }
    }
}

片段

GraphQL 片段

GraphQL 片段可以在构建复杂的查询时提供帮助。 SociableWeaver 使它们非常简单,并允许将正确的引用放置在查询中的相应位置。 在 FragmentBuilder 的帮助下,可以将 FragmentReference 添加到需要字段的对象,并将 Fragment 添加到操作本身。

Swift
let authorFragment = FragmentBuilder(name: "authorFields", type: Author.self)
let query = Weave(.query) {
    Object(Post.self) {
        Object(Post.CodingKeys.author) {
            FragmentReference(for: authorFragment)
        }

        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.content)
            
            Object(Comment.CodingKeys.author) {
                FragmentReference(for: authorFragment)
            }
        }
    }

    Fragment(authorFragment) {
        Field(Author.CodingKeys.id)
        Field(Author.CodingKeys.name)
    }
}
GraphQL 查询
query {
  post {
    author {
      ...authorFields
    }
    comments {
      content
      author {
        ...authorFields
      }
    }
  }
}

fragment authorFields on Author {
  id
  name
}

操作名称

GraphQL 操作名称

操作名称不是必需的,但可以使查询更具唯一性。

Weave(.query) {
    Object(Post.self) {
        Field(Post.CodingKeys.id)
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)
    }
}
.name("GetPostAndContent")
GraphQL 查询
query GetPost {
  post {
    id
    title
    content
  }
}

变量

GraphQL 变量

由于在使用 SociableWeaver 进行查询时不需要直接的 JSON,因此可以在方法中定义变量,并作为参数传递到查询中。

Swift
queryPost(id: 1)

func queryPost(id: Int) {
    Weave(.query) {
        Object(Post.self) {
            Field(Post.CodingKeys.title)
            Field(Post.CodingKeys.content)
            
            Object(Post.CodingKeys.author) {
                Field(Author.CodingKeys.id)
                Field(Author.CodingKeys.name)
            }
        }
        .argument(key: "id", value: id)
    }
}
GraphQL 查询
query {
  post(id: 1) {
    title
    content
    author {
      id
      name
    }
  }
}

指令

GraphQL 指令

GraphQL 中的指令允许服务器影响查询的执行。 两个指令是 @include@skip,它们都可以添加到字段或包含的片段中。 该示例定义了 true 或 false,但在实际查询中,这些值将是布尔变量。

请注意,Skip 始终优先于 include。 此外,任何最终没有字段的对象/片段都将从查询中删除。

let query = Weave(.query) {
    Object(Post.self) {
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)
            .include(if: true)

        Object(Post.CodingKeys.author) {
            Field(Author.CodingKeys.name)
        }
        .include(if: false)

        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.content)
                .include(if: true)
                .skip(if: true)
                
            Object(Comment.CodingKeys.author) {
                Field(Author.CodingKeys.name)
                    .skip(if: true)
            }
        }
    }
}
GraphQL 查询
query { 
    post { 
        title 
        content 
    } 
}

突变

GraphQL 突变

突变的工作方式与简单查询相同,应在应写入数据时使用。 Object.schemaName 将替换初始化程序中包含的对象或键的名称。

Swift
Weave(.mutation) {
    Object(Post.self) {
        Field(Post.CodingKeys.id)
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)
    }
    .schemaName("createPost")
    .argument(key: "title", value: "TestPost")
    .argument(key: "content", value: "This is a test post.")
    .argument(key: "author", value: "John Doe")
}
GraphQL 突变
mutation {
  createPost(title: "TestPost", content: "This is a test post.", author: "John Doe") {
    id
    title
    content
  }
}

内联片段

GraphQL 内联片段

在接口或联合类型上进行查询时,内联片段非常有用,因为它们允许返回底层类型。

Swift
Weave(.query) {
    Object(Post.self) {
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)

        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.content)
        
            Object(Comment.CodingKeys.author) {
                InlineFragment("AnonymousUser") {
                    Field(Author.CodingKeys.id)
                }

                InlineFragment("RegisteredUser") {
                    Field(Author.CodingKeys.id)
                    Field(Author.CodingKeys.name)
                }
            }
        }
    }
}
GraphQL 查询
query {
  post {
    title
    content
    comments {
      content
      author {
        ... on AnonymousUser {
          id
        }
        ... on RegisteredUser {
          id
          name
        }
      }
    }
  }
}

元字段

GraphQL 元字段

可以自定义 GraphQL 元字段,并且可以识别为具有两个前导下划线。 __typename 元字段是 GraphQL 默认值,可用于返回查询结果中的对象类型。

可以使用 MetaFieldType.custom 定义自定义元字段。 此枚举采用一个关联的 String,该 String 不需要包含名称前的双下划线。 例如:.custom("schema") 结果为 __schema

Swift
Weave(.query) {
    Object(Post.self){
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)

        Object(Post.CodingKeys.author) {
            MetaField(.typename)
            Field(Author.CodingKeys.name)
        }
    }
}
GraphQL 查询
query {
  post {
    title
    content
    author {
      __typename
      name
    }
  }
}

分页

GraphQL 分页

SociableWeaver 开箱即用地支持分页,并且可以轻松自定义。 支持的功能包括切片、边和页面信息包含。

切片

GraphQL 中的切片非常适合获取响应中指定数量的对象。 使用 SociableWeaver,可以使用 Object.slice 方法指定此值。

Swift
Weave(.query) {
    Object(Post.CodingKeys.comments) {
        Field(Comment.CodingKeys.id)
        Field(Comment.CodingKeys.author)
        Field(Comment.CodingKeys.content)
    }
    .slice(amount: 2)
}
GraphQL 查询
{
  comments(first: 2) {
    id
    author
    content
  }
}

基于游标的分页

基于游标的分页被描述为 GraphQL 提供的最强大的分页类型。 通过声明对象的分页类型来设置此分页。

Swift
Weave(.query) {
    Object(Post.CodingKeys.comments) {
        Field(Comment.CodingKeys.id)
        Field(Comment.CodingKeys.author)
        Field(Comment.CodingKeys.content)
    }
    .slice(amount: 2)
    .paginationType(.cursor)
}
GraphQL 查询
{
  comments(first: 2) {
    edges {
      cursor
      node {
        id
        author
        content
      }
    }
  }
}

分页页面信息

包含页面信息(例如是否存在下一页或结束游标)非常灵活,并支持自定义模型。

Swift
Weave(.query) {
    Object(Post.CodingKeys.comments) {
        Field(Comment.CodingKeys.id)
        Field(Comment.CodingKeys.author)
        Field(Comment.CodingKeys.content)
    }
    .slice(amount: 2)
    .paginationType(.cursor)
    .pageInfo(type: PageInfo.self,
              keys: PageInfo.CodingKeys.startCursor,
                    PageInfo.CodingKeys.endCursor,
                    PageInfo.CodingKeys.hasNextPage)
}
GraphQL 查询
{
  comments(first: 2) {
    edges {
      cursor
      node {
        id
        author
        content
      }
    }
    pageInfo {
      startCursor
      endCursor
      hasNextPage
    }
  }
}

自定义类型

SociableWeaver 提供了一些自定义类型,这些类型有助于构建更自然外观的查询。 这些类型可能已包含在示例中,也可能没有,但也会在本节中定义,以提供更清晰的说明。

ForEachWeavable

添加 ForEachWeavable 结构是为了在您可能想要在循环中添加对象或字段时使用。 有关此用例的更多讨论,请参见 此处

Swift
let authors = [
    Author(id: "1", name: "John", age: 17, birthplace: [:]),
    Author(id: "2", name: "Jane", age: 29, birthplace: [:]),
    Author(id: "3", name: "Adam", age: 41, birthplace: [:])
]

let query = Weave(.query) {
    ForEachWeavable(authors) { author in
        Object("postsForAuthor") {
            Field(Author.CodingKeys.id)
            Field(Author.CodingKeys.name)
            Field(Author.CodingKeys.age)
            Field(Author.CodingKeys.birthplace)
        }
        .argument(key: "id", value: author.id)
    }
}
GraphQL 查询
{
  postsForAuthor(id: "1") {
    id
    name
    age
    birthplace
  }
  postsForAuthor(id: "2") {
    id
    name
    age
    birthplace
  }
  postsForAuthor(id: "3") {
    id
    name
    age
    birthplace
  }
}

BuilderType

由于函数构建器当前的限制,目前不接受单个元素。 因此,每个函数构建器初始化程序都有一个对应于单个元素的初始化程序。 BuilderType.individual 已设置为指定对象或片段仅由一个元素组成时的情况。 所有初始化的 builderType 参数的默认值为 .individual。 这意味着不需要传递它,并且会产生相同的结果。

Object(Post.CodingKeys.author) {
    Field(Author.CodingKeys.name)
}

Fragment(authorFragment, .individual) {
    Field(Author.CodingKeys.name)
}

CaseStyleOption

提供此枚举是为了在初始化具有模型或编码键的对象和字段时允许进行自定义。 默认为驼峰式。

Field(Comment.CodingKeys.createdAt)
    .caseStyle(.lowercase)

public enum CaseStyleOption {
    case lowercase
    case uppercase
    case capitalized
    case camelCase
    case pascalCase
    case snakeCase
    case kebabCase
}

EnumValueRepresentable

GraphQL 枚举值表示为大小写形式的 case 名称。 因此,应作为参数值传递的 swift 中的自定义枚举可以符合 EnumValueRepresentable。 此协议符合 ArgumentValueRepresentable,并扩展为将 argumentValue 作为 case 值的大写版本提供。

enum PostCategories: EnumValueRepresentable {
    case art
    case music
    case technology
}


Object(Post.self) {
    ...
}
.argument(key: "category", value: PostCategories.technology)

/// Result: post(category: TECHNOLOGY) { ... }

许可

SociableWeaver 现在和将来都将采用 MIT 许可。 有关详细信息,请参见 LICENSE