GithubHelp home page GithubHelp logo

nerdsupremacist / graphaello Goto Github PK

View Code? Open in Web Editor NEW
493.0 4.0 18.0 23.68 MB

A Tool for Writing Declarative, Type-Safe and Data-Driven Applications in SwiftUI using GraphQL

Home Page: https://graphaello.dev

License: MIT License

Makefile 0.25% Swift 99.75%
swiftui graphql command-line-tool type-safety codegen swift declarative data-driven

graphaello's Introduction

Low Effort Graphaello Logo

Twitter: @nerdsupremacist Twitter: @nerdsupremacist

Graphaello

Use GraphQL directly from your SwiftUI Views. Graphaello is a Code Generation Command Line Tool that allows you to use property wrappers in your SwiftUI Views, to use data from GraphQL.

The main features of Graphaello are:

  • ๐Ÿ•“/โ—๏ธ Loading and Error State handling
  • ๐Ÿ“ Declarative
  • ๐Ÿ” Type-Safety
  • ๐Ÿงฐ Reusable Components
  • ๐Ÿ“– Paging Support
  • ๐Ÿฆ Only write Swift (your Swift code is your GraphQL code)

If you're looking for something like this, but for other platforms, Graphaello is heavily inspired by Relay.

Examples

Code snippets are cool, but how does this look in a real project? Here are some Example Apps that you can take a look at.

Countries Music CovidUI
Simple Hello World App that displays information about a lot of different countries More complex App that uses Paging and a lot of reusable components Integrationg Test, displaying data from my own GraphQL API: CovidQL
Uses Countries API Uses GraphBrains Uses CovidQL
Repo Repo Repo

Tutorial

This Readme is intended to document everything about Graphaello, from the CLI and API. However, for starting out that's not the best resource. I wrote a tutorial where I go into the benefits of Graphaello and how to build a simple App to browse movies with it. This post is intended even for people who are not familiar GraphQL at all. So if you're interested please do check it out here.

TLDR?

Let's cut the chase and go directly to our first example of a View using Graphaello:

// Define a Cell
struct CharacterCell: View {
    // Use the GraphQL Property Wrapper
    @GraphQL(StarWars.Person.name)
    var name: String?

    @GraphQL(StarWars.Person.homeworld.name)
    var home: String?

    var body: some View {
        HStack {
            name.map { Text($0).bold() }
            Spacer()
            home.map { Text($0) }
        }
    }
}

This code tells Graphaello to:

  • Generate a GraphQL Fragment: A reusable definition of the Person type, made especially for your View:
fragment CharacterCell_Person on Person {
    name
    homeworld {
        name
    }
}
  • Create an initializer for your View using this type:
let person: CharacterCell.Person = ...
let view = CharacterCell(person: person)

And did I mention it's all type safe?!?!

@GraphQL(StarWars.Person.name)
var name: String? // works

@GraphQL(StarWars.Person.name)
var name: Bool // doesn't work

AAAAaaaaand: if it's a scalar then you don't even need to specify the type!!

@GraphQL(StarWars.Person.name)
var name // Swift knows it's a String?

Installation

Please remember that Graphaello is in its early stages and is therefore not production ready. Use at your own caution.

Via Homebrew

Graphaello can be installed via Homebrew:

brew tap nerdsupremacist/tap
brew install graphaello

From Source

Or if you are one of those, you can install it directly from the source code. You do you!

git clone https://github.com/nerdsupremacist/Graphaello.git
cd Graphaello
sudo make install 

Usage

We will cover how to use Graphaello from two sides.

  • What can you do in your code,
  • and how to you use the Command Line Tool:

Code

Almost all examples will refer to the Star Wars API: https://swapi-graphql.netlify.com

Views

You very easily use information from a GraphQL API directly from your SwiftUI View:

For example this CharacterCell displays a single Cell with a Person's Name and Home World

struct CharacterCell: View {
    @GraphQL(StarWars.Person.name)
    var name: String?

    @GraphQL(StarWars.Person.homeworld.name)
    var home: String?

    var body: some View {
        HStack {
            name.map { Text($0).bold() }
            Spacer()
            home.map { Text($0) }
        }
    }
}

// Initializer is automatically created by Graphaello
let view = CharacterCell(person: person)

Composing Views

If your view has a sub view with it's own data, your view doesn't need to know the specifics of it, but only the fact that it needs to populate it:

struct CharacterDetail: View {
    @GraphQL(StarWars.Person._fragment)
    var headerCell: CharacterCell.Person
    
    @GraphQL(StarWars.Person.eyes)
    var eyes: String?

    var body: some View {
        VStack {
           CharacterCell(person: headerCell)
           eyes.map { Text($0) }
        }
    }
}

let view = CharacterDetail(person: person)

Using Queries

You can access any query fields of the API directly:

struct FilmView {
  // .film refers to a field in the query
  @GraphQL(StarWars.film.title)
  var title: String?
  
  var body: String {
    title.map { Text($0) }
  }
}

let client = ApolloClient(url: ...)
let api = StarWars(client: client)

let view = api.filmView(id: ...)

Using Mutations

All mutations can directly be used from {API_NAME}.Mutation. For this example we're using a TODO app since the Star Wars API doesn't support mutations:

struct TodoCell: View {
    // _nonNull() is equivalent to !
    @GraphQL(Todos.Todo.id._nonNull())
    var id: String

    // _withDefault(FOO) is equivalent to ?? FOO
    @GraphQL(Todos.Todo.title._withDefault(""))
    var title: String
    
    @GraphQL(Todos.Todo.completed._withDefault(false))
    var completed: Bool

    @GraphQL(Todos.Mutation.toggle.completed._withDefault(false))
    var toggle: Toggle // Define a type name for your mutation

    var body: some View {
        HStack {
            Text(title)

            Spacer()
            
            Button(completed ? "Mark as not done" : "Mark as done") {
              toggle.commit(id: self.id) { completed in 
                self.completed = completed
              } 
            }

            ActivityIndicator().animated(toggle.isLoading)
        }
    }
}

Using Paging

If your API suppors Connections you can include paging in your App out of the box:

struct CharacterList: View {
    @GraphQL(StarWars.allPeople._nonNull())
    var characters: Paging<CharacterCell.Person>

    var body: some View {
        List {
            ForEach(characters.values) { character in 
                CharacterCell(person: character)
            }
            
            characters.hasMore ? Button("Load More") {
              self.characters.loadMore()
            }.disabled(characters.isLoading) : nil
        }
    }
}

Or you can even use the Shipped PagingView and items will automatically load when you get near the end of the list:

struct CharacterList: View {
    @GraphQL(StarWars.allPeople._nonNull())
    var characters: Paging<CharacterCell.Person>

    var body: some View {
        List {
            PagingView(characters) { character in
                CharacterCell(person: character)
            }
        }
    }
}

Handling arguments

Whevener you use fields with arguments, those arguments are propagated to whoever uses your view. But you can also prefill them from the @GraphQL annotation. You can :

  • Use the default from the API (default behavior)
  • Force them to be filled by the caller
  • Hard code them
  • Override the default value

Default

struct FilmView {
  @GraphQL(StarWars.film.title)
  var title: String?
  
  var body: String {
    title.map { Text($0) }
  }
}

...
let first = api.filmView(id: ...)
let second = api.filmView() // uses the default from the API

Force them

struct FilmView {
  @GraphQL(StarWars.film(id: .argument).title)
  var title: String?
  
  var body: String {
    title.map { Text($0) }
  }
}

...
let view = api.filmView(id: ...) // id is required

Hardcode them

struct MyFavoriteFilmView {
  @GraphQL(StarWars.film(id: .value("...")).title)
  var title: String?
  
  var body: String {
    title.map { Text($0) }
  }
}

...
let view = api.filmView() // id is not available as an argument

Override the default

struct FilmView {
  @GraphQL(StarWars.film(id: .argument(default: "...")).title)
  var title: String?
  
  var body: String {
    title.map { Text($0) }
  }
}

...
let first = api.filmView(id: ...)
let second = api.filmView() // uses the default set by the View

Other operations

There are other operations available on Paths for Graphaello:

  • _forEach(.{keyPath}) instead of getting an array of objects you can just get a specific value (will be transformed into an Array.map)
  • _compactMap() remove nils from an array (will be transformed into a compactMap { $0 })
  • _withDefault({y}) add a default value in case of nil (will be transformed into ?? y)
  • _nonNull() force non null values (will be transformed into a !)
  • _flatten flatten an array of arrays (will be transformed into a flatMap { $0 }

Command Line Tool

The Graphaello Tool is pretty simple and only has three commands:

  • codegen: generates all the necessary code and injects it into your project
  • init: will install all dependencies and add a custom Graphaello buildphase (so that you don't need to use codegen manually)
  • add: will add a GraphQL API to your project

Codegen

Will generate all the swift code an insert it into your project.

Arguments:

Project: points to the project. If not provided will pick the first project in your current working directory Apollo: Reference to which Apollo CLI it should use. Either "binary" (if you have installed it via npm) or "derivedData" (which will look into the build folder of your project. Only use this option from a build phase). If not provided it will default to the binary. Skip Formatting Flag: if your project is pretty large, formatting the generated code might take a lot of time. During prototyping you may want to skip formatting.

Init

Injects Graphaello into your project. This step is optional but recommended: When run it will:

  • add Apollo as a dependency (if it's not there already)
  • add a build phase to run codegen before every build (optional, not recommended for large projects)
  • run codegen (optional)

You can skip the optional steps using the flags:

  • skipBuildPhase
  • skipGencode

Add

Adds an API to your project. Simply give the url to the GraphQL Endpoint and it will be added to your project.

Arguments:

API Name: you can change what the API will be called. If not Provided it will be a UpperCamelCase version of the host name

Contributions

Contributions are welcome and encouraged!

Related Work

Graphaello works best when coupled with GraphZahl on the Server Side. GraphZahl enables you to implement your GraphQL Server Declaratively in Swift with Zero Boilerplate.

Learn

This is currenlty a research project. More details about how it works, will be published later.

License

Graphaello is available under the MIT license. See the LICENSE file for more info.

This project is being done under the supervision of the Chair for Applied Software Enginnering at the Technical University of Munich. The chair has everlasting rights to use and maintain this tool.

graphaello's People

Contributors

aleksrutins avatar eliperkins avatar flashspys avatar godcrampy avatar nerdsupremacist avatar rolandasrazma avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

graphaello's Issues

Input Objects are not Properly translated to Apollo Types

Apollo can only run the query with their types.
Despite the fact that our types are identical to theirs, we still need to convert.

Problematic areas:

We can't just run .init() as in the case with enums right now, because the Apollo type is only generated when it's being used by a query. So we can't always ship the init code

Possible Ideas:

  • Only generate the init code when the type is being used by a query
  • Maybe unsafeBitCast would do the job ยฏ\_(ใƒ„)_/ยฏ

Relevant areas:

Fix warnings from json files in target

while working on creating a sample application, using the apollographl tutorial endpoint, https://apollo-fullstack-tutorial.herokuapp.com/

I am getting errors and warnings.

Showing Recent Issues
no rule to process file '/Users/stunjiturner/Downloads/CodeReviewRepos/OutOfThisWorld-Graphaello/ApolloFullstackTutorialHerokuappCom.graphql.json' of type 'text.json' for architecture 'x86_64'

Showing Recent Issues
/Users/stunjiturner/Downloads/CodeReviewRepos/OutOfThisWorld-Graphaello/Graphaello.swift:564:23: 'ApolloFullstackTutorialHerokuappCom' is ambiguous for type lookup in this context

Mutations are not properly propagated to parents

Currently you can change values of the GraphQL property wrapper after a mutation.

However, when the mutated value is in a Fragment, the change isn't propagated to the original dictionary from where the query originated from.

This is an issue in the following scenarios:

  • The Fragment is actually in a List and the view gets recycled: then it will change back to the original value at some point.
  • The Fragment and the Parent both use the same variables. Then they run out of sync with each other

This will most likely require a rewrite of a lot of the GraphQL Property Wrapper and the generated initializer code

Handle Updating Apollo

Since our generated code accesses Apollo API it is important that before we generate the code, we make sure that the correct version of Apollo is installed.

We could either:

  1. Automatically update the version of the dependency to Apollo if out of date
  2. or warn the user that they should update Apollo

I think that 1 would lead to less frustration, but 2 is more "correct" I guess?

Instantiate structs with only one fragment directly

This example should be valid:

struct CountryMapPin {
    @GraphQL(Covid.Country.cases)
    var cases: Int

    @GraphQL(Covid.Country.info.latitude)
    var latitude: Double?

    @GraphQL(Covid.Country.info.longitude)
    var longitude: Double?
}

struct ContentView: View {
    @GraphQL(Covid.countries)
    var pins: [CountryMapPin]

    var body: some View {
         MapView(pins: pins).frame(height: 400)
    }
}

Proposal

We solve this by adding the following extension to every struct that has a single fragmentl:

extension CountryMapPin: Fragment {
    typealias UnderlyingType = Country.UnderlyingType
}

extension CountryMapPin.Country { 
    func countryMapPin() -> CountryMapPin { 
        return CountryMapPin(country: self) 
    }
}

In code generation add an extra operation to implicitly convert. Create it during resolution.

Documentation of Client API

The Generated Client API is remains largely undocumented...

It would also be awesome if we could include the documentation of the API in the Generated DSL

Graphaello Buildtime error: Keyword 'repeat' cannot be used as an identifier

The API I'm using has "repeat" as a field, which is a swift reserved keyword.

Graphaello.swift:

...
case repeat = "REPEAT"   // ".../AL/Graphaello.swift:4222:18: Keyword 'repeat' cannot be used as an identifier here"
...

Theres will always be a swift compiler error on Buildtime using that API.

A fix would be to use backticks to escape the keyword.

Explore dynamicMemberLookup and dynamicCallable to make code simpler

Something like this:

@dynamicMemberLookup
protocol Type: Target { }

extension Type {

    // make member key paths static for the type

    static subscript<T>(dynamicMember keyPath: KeyPath<GraphQLFragmentPath<Self, Self>, GraphQLFragmentPath<Self, T>>) -> GraphQLFragmentPath<Self, T> {
        return .init()
    }

    static subscript<T>(dynamicMember keyPath: KeyPath<GraphQLFragmentPath<Self, Self>, GraphQLPath<Self, T>>) -> GraphQLPath<Self, T> {
        return .init()
    }

}

@dynamicMemberLookup
struct GraphQLFragmentPath<TargetType: Target, UnderlyingType> {
    fileprivate init() {}

    // nested values inside arrays

    subscript<Value, Output>(dynamicMember _: KeyPath<GraphQLFragmentPath<TargetType, Value>, GraphQLPath<TargetType, Output>>) -> GraphQLPath<TargetType, [Output]> where UnderlyingType == [Value] {
        return .init()
    }

    subscript<Value, Output>(dynamicMember _: KeyPath<GraphQLFragmentPath<TargetType, Value>, GraphQLPath<TargetType, Output>>) -> GraphQLPath<TargetType, [Output]?> where UnderlyingType == [Value]? {
        return .init()
    }

    subscript<Value, Output>(dynamicMember _: KeyPath<GraphQLFragmentPath<TargetType, Value>, GraphQLFragmentPath<TargetType, Output>>) -> GraphQLPath<TargetType, [Output]> where UnderlyingType == [Value] {
        return .init()
    }

    subscript<Value, Output>(dynamicMember _: KeyPath<GraphQLFragmentPath<TargetType, Value>, GraphQLFragmentPath<TargetType, Output>>) -> GraphQLPath<TargetType, [Output]?> where UnderlyingType == [Value]? {
        return .init()
    }
}

and similar strategies for dealing with optionals...

The main reason this is not being used right now is the fact that autocomplete is broken. We should evaluate if there's a way to have autocomplete working and still bringing the amount of code waaaay down

Error SwiftSyntax parser library following the tutorial

โ˜•๏ธ Extracting APIs + Structs:
โ—๏ธ Error Occurred:

SwiftSyntax parser library isn't compatible

Error: Formulae found in multiple taps:
* nerdsupremacist/tap/graphaello
* nerdsupremacist/graphaello/graphaello

The initializer is not created.

Hi! Thank you for creating this great library.
As I saw int the documentation, with this code there should be autogenerated initializer similar to this CourseCell(course: PandaEntry.Course), but the initializer is missing. Can you please help me to understand what is the problem?

struct CourseCell: View {
    
    @GraphQL(PandaEntry.Course.title)
    var title: String?
    
    var body: some View {
        Text(title ?? "unknown")
    }
}

M1 Mac "graphaello is not installed on your machine"

getting M1 Mac "graphaello is not installed on your machine" even if it is

$ type "graphaello"
graphaello is /opt/homebrew/bin/graphaello

adding PATH=/opt/homebrew/bin:$PATH to run phase works so I assume it's bash/zsh difference - clean install of Mac I'm sure more people will hit this

Generated view initializers don't camelize default values for enums

My GraphQL API has an enum which is used as a default value in a field, and when my Grapheaello.swift is generated it does not camelize the enum (API.Lang.enUs) when building out the query renderer function parameters:

extension API {
  // lang should be:
  // lang: API.Lang? = API.Lang.enUs
  func contentView<Loading: View>(lang: API.Lang? = API.Lang.en_us,
                                  @ViewBuilder loading: () -> Loading) -> some View {
      return QueryRenderer(client: client,
                            query: ApolloAPIContentViewQuery(lang: lang),
                            loading: loading(),
                            error: { BasicErrorView(error: $0) }) { (data: ApolloAPI.ContentViewQuery.Data) -> ContentView in

          ContentView(data: data)
      }
  }
}

For my code to compile I have to modify the Swift to be:

extension API {
  func contentView<Loading: View>(lang: API.Lang? = API.Lang.enUs,
                                  @ViewBuilder loading: () -> Loading) -> some View {
      return QueryRenderer(client: client,
                            query: ApolloAPIContentViewQuery(lang: lang),
                            loading: loading(),
                            error: { BasicErrorView(error: $0) }) { (data: ApolloAPI.ContentViewQuery.Data) -> ContentView in

          ContentView(data: data)
      }
  }
}

Here is the enum that is generated from the GraphQL schema:

# Lang
enum Lang {
  # en-GB
  EN_GB

  # en-US
  EN_US
}
public enum ApolloAPI {
  /// Lang
  public enum Lang: RawRepresentable, Equatable, Hashable, CaseIterable, Apollo.JSONDecodable, Apollo.JSONEncodable {
    public typealias RawValue = String
    /// en-US
    case enUs
    /// en-GB
    case enGb
    /// Auto generated constant for unknown enum values
    case __unknown(RawValue)

    public init?(rawValue: RawValue) {
      switch rawValue {
        case "EN_US": self = .enUs
        case "EN_GB": self = .enGb
        default: self = .__unknown(rawValue)
      }
    }

    public var rawValue: RawValue {
      switch self {
        case .enUs: return "EN_US"
        case .enGb: return "EN_GB"
        case .__unknown(let value): return value
      }
    }

    public static func == (lhs: Lang, rhs: Lang) -> Bool {
      switch (lhs, rhs) {
        case (.enUs, .enUs): return true
        case (.enGb, .enGb): return true
        case (.__unknown(let lhsValue), .__unknown(let rhsValue)): return lhsValue == rhsValue
        default: return false
      }
    }

    public static var allCases: [Lang] {
      return [
        .enUs,
        .enGb,
      ]
    }
  }
}

I did some debugging and found that the expression passed to QueryRendererArgument isn't camelizing the enum properly when rendering the Struct.swift.stencil:

Graphaello.QueryRendererArgument(name: "lang", type: "API.Lang?", expression: Optional(SwiftSyntax.ExprSyntax))

I'm not exactly sure how to change what is returned from SwiftSyntax.ExprSyntax otherwise I'd submit a PR. Any help is appreciated!

Polling

I was looking at this, and was wondering if there currently is a way to poll a query every N seconds? (E.g. if I display a set of comments, I might want to refresh the view every few seconds such that people are always up-to-date.)

Issue with macro definition

In beta 3 there is a mistake in the #if-macro-statement. The name looks like GRAPHAELLO_SWIFT_GRAPH_QL__TARGET. There is on _ too much.

graphaello add throws Fatal error

Can anybody explain to me why my API generation throws an error? I am using graphcms.com.

Screen Shot 2021-08-10 at 8 35 39 pm

graphaello add --apiName GAPI https://api-eu-central-1.graphcms.com/v2/ckqxy4gw1m2gb01z6gj25c7nf/master

It does generate the json, but nothing in Graphaello.swift.

Thanks,
Michal

Make promotional Website

make a nice promotional website for Graphaello...

Include some nice examples (with animations) and documentation on how it all can be used

How production ready is this?

I'm wondering how production ready this library is? And if not, what other features would be required to be added in until it becomes production ready? Also is there a specific timeframe for this?

RegexTokenGenerator(word: "false") wrong value

Shouldn't the value for false be this way?

RegexTokenGenerator(word: "false").map(to: .value(.bool(true))),

should be:

RegexTokenGenerator(word: "false").map(to: .value(.bool(false))),

???

Xcode 11.4 will not build.

SwiftSyntax parser library isn't compatible
Command PhaseScriptExecution failed with a nonzero exit code

Caching

On larger projects, Code Generation takes a very long time.

Evaluate caching:

  • Fragments and Queries for structs
  • Code generated for each struct

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.