Git Product home page Git Product logo

php-graphql-oqm's Introduction

PHP GraphQL OQM

Build Status Build Status Codacy Badge Total Downloads Latest Stable Version License

This package utilizes the introspection feature of GraphQL APIs to generate a set of classes that map to the structure of the API schema. The generated classes can then be used in a very simple and intuitive way to query the API server.

Interacting with GraphQL API's using PHP has never been easier!

Installation

Run the following command to install the package using composer:

composer require gmostafa/php-graphql-oqm

Generating The Schema Objects

After installing the package, the first step is to generate the schema objects. This can be easily achieved by executing the following command:

php vendor/bin/generate_schema_objects

This script will retrieve the API schema types using the introspection feature in GraphQL, then generate the schema objects from the types, and save them in the schema_object directory in the root directory of the package. You can override the default write directory by providing the "Custom classes writing dir" value when running the command.

You can also specify all options via command line options:

php vendor/bin/generate_schema_objects \
    -u "https://graphql-pokemon.vercel.app/" \
    -h "Authorization" \
    -v "Bearer 123" \
    -d "customClassesWritingDirectory" \
    -n "Vendor\Custom\Namespace"

or if you prefer long arguments

php vendor/bin/generate_schema_objects \
    --url "https://graphql-pokemon.vercel.app/" \
    --authorization-header-name "Authorization" \
    --authorization-header-value "Bearer 123" \
    --directory "customClassesWritingDirectory" \
    --namespace "Vendor\Custom\Namespace"

Usage

In all the examples below I'm going to use the super cool public Pokemon GraphQL API as an illustration.

Check out the API at: https://graphql-pokemon.now.sh/

And Github Repo: https://github.com/lucasbento/graphql-pokemon

After generating the schema objects for the public Pokemon API, we can easily query the API by using the RootQueryObject. Here's an example:

$rootObject = new RootQueryObject();
$rootObject
    ->selectPokemons((new RootPokemonsArgumentsObject())->setFirst(5))
        ->selectName()
        ->selectId()
        ->selectFleeRate()
        ->selectAttacks()
            ->selectFast()
                ->selectName();

What this query does is that it selects the first 5 pokemons returning their names, ids, flee rates, fast attacks with their names. Easy right!?

All what remains is that we actually run the query to obtain results:

$results = $client->runQuery($rootObject->getQuery());

For more on how to use the client class refer to:

Notes

A couple of notes about schema objects to make your life easier when using the generating classes:

Dealing With Object Selectors

Whilst scalar field setters return an instance of the current query object, object field selectors return objects of the nested query object. This means that setting the $rootObject reference to the result returned by an object selector means that the root query object reference is gone.

Don't:

$rootObject = (new RootQueryObject())->selectAttacks()->selectSpecial()->selectName();

This way you end up with reference to the PokemonAttackQueryObject, and the reference to the RootQueryObject is gone.

Do:

$rootObjet = new RootQueryObject();
$rootObject->selectAttacks()->selectSpecial()->selectName();

This way you can keep track of the RootQueryObject reference and develop your query safely.

Dealing With Multiple Object Selectors

Suppose we want to get the pokemon "Charmander", retrieve his evolutions, evolution requirements, and evolution requirements of his evolutions, how can we do that?

We can't do this:

$rootObject = new RootQueryObject();
$rootObject->selectPokemon(
    (new RootPokemonArgumentsObject())->setName('charmander')
)
    ->selectEvolutions()
        ->selectName()
        ->selectNumber()
        ->selectEvolutionRequirements()
            ->selectName()
            ->selectAmount()
    ->selectEvolutionRequirements()
        ->selectName()
        ->selectAmount();

This is because the reference is now pointing to the evolution requirements of the evolutions of charmander and not charmander himself.

The best way to do this is by structuring the query like this:

$rootObject = new RootQueryObject();
$charmander = $rootObject->selectPokemon(
    (new RootPokemonArgumentsObject())->setName('charmander')
);
$charmander->selectEvolutions()
    ->selectName()
    ->selectNumber()
    ->selectEvolutionRequirements()
        ->selectName()
        ->selectAmount();
$charmander->selectEvolutionRequirements()
    ->selectName()
    ->selectAmount();

This way we have kept the reference to charmander safe and constructed our query in an intuitive way.

Generally, whenever there's a branch off (just like in the case of getting evolutions and evolution requirements of the same object) the best way to do it is to structure the query like a tree, where the root of the tree becomes the reference to the object being branch off from. In this case, charmander is the root and evolutions and evolution requirements are 2 sub-trees branched off it.

Improving Query Objects Readability

A couple of hints on how to keep your query objects more readable:

  1. Store nodes that will be used as roots in branch offs in meaningful variables, just like the case with charmander.
  2. Write each selector on a separate line.
  3. Every time you use an object selector, add an extra indentation to the next selectors.
  4. Move construction of a new object in the middle of a query (such as an ArgumentsObject construction) to a new line.

Schema Objects Generation

After running the generation script, the SchemaInspector will run queries on the GraphQL server to retrieve the API schema. After that, the SchemaClassGenerator will traverse the schema from the root queryType recursively, creating a class for every object in the schema spec.

The SchemaClassGenerator will generate a different schema object depending on the type of object being scanned using the following mapping from GraphQL types to SchemaObject types:

  • OBJECT: QueryObject
  • INPUT_OBJECT: InputObject
  • ENUM: EnumObject

Additionally, an ArgumentsObject will be generated for the arguments on each field in every object. The arguments object naming convention is:

{CURRENT_OBJECT}{FIELD_NAME}ArgumentsObject

The QueryObject

The object generator will start traversing the schema from the root queryType, creating a class for each query object it encounters according to the following rules:

  • The RootQueryObject is generated for the type corresponding to the queryType in the schema declaration, this object is the start of all GraphQL queries.
  • For a query object of name {OBJECT_NAME}, a class with name {OBJECT_NAME}QueryObject will be created.
  • For each selection field in the selection set of the query object, a corresponding selector method will be created, according to the following rules:
    • Scalar fields will have a simple selector created for them, which will add the field name to the selection set. The simple selector will return a reference to the query object being created (this).
    • Object fields will have an object selector created for them, which will create a new query object internally and nest it inside the current query. The object selector will return instance of the new query object created.
  • For every list of arguments tied to an object field an ArgumentsObject will be created with a setter corresponding to every argument value according to the following rules:
    • Scalar arguments: will have a simple setter created for them to set the scalar argument value.
    • List arguments: will have a list setter created for them to set the argument value with an array
    • Input object arguments: will have an input object setter created for them to set the argument value with an object of type InputObject

The InputObject

For every input object the object generator encounters while traversing the schema, it will create a corresponding class according to the following rules:

  • For an input object of name {OBJECT_NAME}, a class with name {OBJECT_NAME}InputObject will be created
  • For each field in the input object declaration, a setter will be created according to the following rules:
    • Scalar fields: will have a simple setter created for them to set the scalar value.
    • List fields: will have a list setter created for them to set the value with an array
    • Input object arguments: will have an input object setter created for them to set the value with an object of type InputObject

The EnumObject

For every enum the object generator encounters while traversing the schema, it will create a corresponding ENUM class according to the following rules:

  • For an enum object of name {OBJECT_NAME}, a class with name {OBJECT_NAME}EnumObject will be created
  • For each EnumValue in the ENUM declaration, a const will be created to hold its value in the class

Live API Example

Looking at the schema of the Pokemon GraphQL API from the root queryType, that' how it looks like:

"queryType": {
  "name": "Query",
  "kind": "OBJECT",
  "description": "Query any Pokémon by number or name",
  "fields": [
    {
      "name": "query",
      "type": {
        "name": "Query",
        "kind": "OBJECT",
        "ofType": null
      },
      "args": []
    },
    {
      "name": "pokemons",
      "type": {
        "name": null,
        "kind": "LIST",
        "ofType": {
          "name": "Pokemon",
          "kind": "OBJECT"
        }
      },
      "args": [
        {
          "name": "first",
          "description": null
        }
      ]
    },
    {
      "name": "pokemon",
      "type": {
        "name": "Pokemon",
        "kind": "OBJECT",
        "ofType": null
      },
      "args": [
        {
          "name": "id",
          "description": null
        },
        {
          "name": "name",
          "description": null
        }
      ]
    }
  ]
}

What we basically have is a root query object with 2 fields:

  1. pokemons: Retrieves a list of Pokemon objects. It has one argument: first.
  2. pokemon: Retrieves one Pokemon object. It has two arguments:: id and name.

Translating this small part of the schema leads to 3 objects:

  1. RootQueryObject: Represents the entry to point to traversing the API graph
  2. RootPokemonsArgumentsObject: Represents the arguments list on the "pokemons" field in the RootQueryObject
  3. RootPokemonArgumentsObject: Represents the arguments list on the "pokemon" field in the RootQueryObject

Here are the 3 classes generated:

<?php

namespace GraphQL\SchemaObject;

class RootQueryObject extends QueryObject
{
    const OBJECT_NAME = "query";

    public function selectPokemons(RootPokemonsArgumentsObject $argsObject = null)
    {
        $object = new PokemonQueryObject("pokemons");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectPokemon(RootPokemonArgumentsObject $argsObject = null)
    {
        $object = new PokemonQueryObject("pokemon");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }
}

The RootQueryObject contains 2 selector methods, one for each field, and an optional argument containing the ArgumentsObjects required.

<?php

namespace GraphQL\SchemaObject;

class RootPokemonsArgumentsObject extends ArgumentsObject
{
    protected $first;

    public function setFirst($first)
    {
        $this->first = $first;
    
        return $this;
    }
}

The RootPokemonsArgumentsObject contains the only argument in the list for the "pokemons" field as a property with a setter for altering its value.

<?php

namespace GraphQL\SchemaObject;

class RootPokemonArgumentsObject extends ArgumentsObject
{
    protected $id;
    protected $name;

    public function setId($id)
    {
        $this->id = $id;
    
        return $this;
    }

    public function setName($name)
    {
        $this->name = $name;
    
        return $this;
    }
}

The RootPokemonArgumentsObject contains the 2 arguments in the list for the "pokemon" field as properties with setters to alter their values.

Extra

Additionally, PokemonQueryObject will be created while traversing the schema recursively. It is not needed to complete this demo, but I will add it below to make things clearer in case someone wants to see more of the generation in action:

<?php

namespace GraphQL\SchemaObject;

class PokemonQueryObject extends QueryObject
{
    const OBJECT_NAME = "Pokemon";

    public function selectId()
    {
        $this->selectField("id");
    
        return $this;
    }

    public function selectNumber()
    {
        $this->selectField("number");
    
        return $this;
    }

    public function selectName()
    {
        $this->selectField("name");
    
        return $this;
    }

    public function selectWeight(PokemonWeightArgumentsObject $argsObject = null)
    {
        $object = new PokemonDimensionQueryObject("weight");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectHeight(PokemonHeightArgumentsObject $argsObject = null)
    {
        $object = new PokemonDimensionQueryObject("height");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectClassification()
    {
        $this->selectField("classification");
    
        return $this;
    }

    public function selectTypes()
    {
        $this->selectField("types");
    
        return $this;
    }

    public function selectResistant()
    {
        $this->selectField("resistant");
    
        return $this;
    }

    public function selectAttacks(PokemonAttacksArgumentsObject $argsObject = null)
    {
        $object = new PokemonAttackQueryObject("attacks");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectWeaknesses()
    {
        $this->selectField("weaknesses");
    
        return $this;
    }

    public function selectFleeRate()
    {
        $this->selectField("fleeRate");
    
        return $this;
    }

    public function selectMaxCP()
    {
        $this->selectField("maxCP");
    
        return $this;
    }

    public function selectEvolutions(PokemonEvolutionsArgumentsObject $argsObject = null)
    {
        $object = new PokemonQueryObject("evolutions");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectEvolutionRequirements(PokemonEvolutionRequirementsArgumentsObject $argsObject = null)
    {
        $object = new PokemonEvolutionRequirementQueryObject("evolutionRequirements");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectMaxHP()
    {
        $this->selectField("maxHP");
    
        return $this;
    }

    public function selectImage()
    {
        $this->selectField("image");
    
        return $this;
    }
}

php-graphql-oqm's People

Contributors

jorrit avatar mghoneimy avatar savamarkovic 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

Watchers

 avatar

php-graphql-oqm's Issues

Enum fields are treated as objects but should be treated as scalars

In SchemaClassGenerator::generateObject() and SchemaClassGenerator::appendQueryObjectFields() it seems that enum fields are treated as object fields. This generates query bodies like these:

    public function selectEnumField(ParentEnumFieldArgumentsObject $argsObject = null)
    {
        $object = new EnumFieldQueryObject("enumField");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

However, EnumFieldQueryObject is never generated because it is only generated for object types. I think somewhere in SchemaClassGenerator::appendQueryObjectFields() a code path should be taken that looks more like scalar.

Am I missing something perhaps?

Support for generating `mutationType` objects?

Hi there. I'm curious if adding support for generating mutation objects from the schema is on the roadmap. I've been toying around and have a really quick and dirty branch, but it looks like adding this functionality would require updating to the client lib to add a AbstractMutationBuilder or something similar.

The apollo code gen does this for android and presumably for the node client. Would be real nice to have it for PHP too, but I haven't found this functionality in any other lib.

So, two questions:

  1. have you already gone down this road and decided against it?
  2. open to a PR or do you have an idea of how you'd like to see it implemented?

missing some functions with params in QueryObject classes (fix)

I use this package with my private graphql endpoint, it generates more than 130 files.
But the package generates the "TournamentQueryObject" class with some functions without params.

For example, the TournamentQueryObject->selectNameByLanguage() function should be selectNameByLanguage(string $lang)

It generates the query :

query {
  tournaments() {
    nameByLanguage
  }
}

But I need the query:

query {
  tournaments() {
    en:nameByLanguage ( lang : "en" )
  }
}

I think there is some uncompleted features in the package while parsing graphql schema, but I have an easy solution without breaking changes.

How to implement these features:

php:

  ->selectNameByLanguage()
    ->addPrefix('en')->addArguments(['lang' => 'en'])

Another example with the https://graphql-pokemon.now.sh endpoint

Can't generate a graphql query with the "new_id:id" prefix

query {
  pokemon(name: "Pikachu") {
    new_id:id 
    number
    name
  }
}

Can't generate a graphql query with the arguments

query {
  pokemon(name: "Pikachu") {
    new_id:id (argument_name: "argument_value")
    number
    name
  }
}

php:

->selectPokemon((new RootPokemonArgumentsObject())->setName('Pikachu'))
        ->selectId()
        ->selectNumber()
        ->selectName();

Easy solution:

php:

->selectPokemon((new RootPokemonArgumentsObject())->setName('Pikachu'))
        ->selectId()
        ->addPrefix('new_id')->addArguments(['argument_name' => 'argument_value'])
        ->selectNumber()
        ->selectName();

The addPrefix() function adds the prefix to the last selection element.
The addArguments() function adds the arguments to the last selection element.

I have added these functions in the php-graphql-client pull request.

ENUMs in arguments

I'm looking into using this in one of my projects, but I'm seeing that it doesn't properly support using Enums in query arguments.

for example, I have this schema snipit

queryName(OrderBy: [OrderByClause!]){
 ...
}

Input OrderByClause{
 field: String!
 order: SortOrder!
}

enum SortOrder{ 
 ASC
 DESC
}

But when that gets run through your generator, the OrderByClause's order field gets converted into an InputObject argurment, when its an enum.

I am using Laravel and Lighthouse, if it helps any

Enum as query argument not properly managed

Hi,
let's assume we have the following schema:

  enum Provider {
    GOOGLE_PLACES
    WALLET_SAVER
  }

  autocompleteAddress(
    query: String!
    provider: Provider!
    istatCode: String
    limit: Int = 10
  ): [AddressDetails]

Launching ./vendor/bin/generate_schema_objects without arguments, I'll get the class RootAutocompleteAddressArgumentsObject with the following setter method public function setProvider(ProviderInputObject $providerInputObject) but that class is missing among the generated files.
So I had to create such file as the following (not sure it's the best way though):

<?php

namespace SosTariffe\Components\GqlGenerator\SchemaObject;

use GraphQL\RawObject;
use GraphQL\SchemaObject\InputObject;

class ProviderInputObject extends InputObject
{
    private $value;

    public function __construct(string $enum)
    {
        $this->value = $enum;
    }

    public function __toString()
    {
        return $this->value;
    }

    public function toRawObject(): RawObject
    {
        return new RawObject($this->value);
    }
}

The final test produces a valid GraphQL query:

        $rootObject = new RootQueryObject();

        $args = new RootAutocompleteAddressArgumentsObject();
        $args
            ->setIstatCode("50008")
            ->setLimit(5)
            ->setProvider(new ProviderInputObject(ProviderEnumObject::WALLET_SAVER))
            ->setQuery("Via Tosc");

        $addrDetails = $rootObject
            ->selectAutocompleteAddress($args);

        $addrDetails
            ->selectAddress()
            ->selectToponym()
            ->selectTown()
                ->selectName();

        $addrDetails->selectProviders()
                ->selectWalletSaver();

        echo $rootObject->getQuery();

And the result is the following:

{
  autocompleteAddress(query: "Via Tosc" provider: WALLET_SAVER istatCode: "50008" limit: 5) {
    address {
      toponym
      town {
        name
      }
    }
    providers {
      walletSaver
    }
  }
}

CLI Arguments

The object generation script does not accept arguments, which requires an interactive prompt. In environments where vendor directory is not included in the repo, but are built for deploy, it is impossible to generate in the build process.

While a custom write directory can be created and committed, there are additional benefits to allowing CLI arguments instead of the interactive command such as sharing commands between team members or simply bypassing the interactive prompt entirely.

Support for command-line parameters instead of interactive approach

Thanks for this amazing project, which I've been using for about a year now. :)

In my use case the schema objects change quite frequently. The generate_schema_objects to regenerate them works fine, but putting in the GraphQL-Endpoint, Authorization info & output folder every time is not ideal for me.

I would prefer if I could specify them as command-line arguments (that way I could wrap the script into another one and just call it with the same parameters every time; Or I would put the values in env variables).

I think this functionality would be very beneficial. What do you think?

Problem with special characters in object names

Found a small problems with underscore.

The endpoint is providing the "_foobar" and "foobar" names of objects. The the generated code creates 2 selectFoobar() functions, hence its an error.

Not sure how to tackle this. Maybe replace the underscore with some character combination?

Unsupported object type when running generate_schema_objects

Hi there,

Just wanted to try it against eZ Platform GraphQL and got an error. Any ideas what could be the issue?

PHP Fatal error: Uncaught RuntimeException: Unsupported object type in /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php:139
Stack trace:
#0 /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php(106): GraphQL\SchemaGenerator\SchemaClassGenerator->generateObject('DomainContent', 'INTERFACE')
#1 /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php(155): GraphQL\SchemaGenerator\SchemaClassGenerator->appendQueryObjectFields(Object(GraphQL\SchemaGenerator\CodeGenerator\QueryObjectClassBuilder), 'ContentRelation', Array)
#2 /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php(133): GraphQL\SchemaGenerator\SchemaClassGenerator->generateQueryObject('ContentRelation')
#3 /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php(106): GraphQL\SchemaGenerator\ in /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php on line 139

Fatal error: Uncaught RuntimeException: Unsupported object type in /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php:139
Stack trace:
#0 /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php(106): GraphQL\SchemaGenerator\SchemaClassGenerator->generateObject('DomainContent', 'INTERFACE')
#1 /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php(155): GraphQL\SchemaGenerator\SchemaClassGenerator->appendQueryObjectFields(Object(GraphQL\SchemaGenerator\CodeGenerator\QueryObjectClassBuilder), 'ContentRelation', Array)
#2 /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php(133): GraphQL\SchemaGenerator\SchemaClassGenerator->generateQueryObject('ContentRelation')
#3 /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php(106): GraphQL\SchemaGenerator\ in /vendor/gmostafa/php-graphql-oqm/src/SchemaGenerator/SchemaClassGenerator.php on line 139

Duplicate Method Names

Thank you very much for this awesome tool. I encountered an (admittedly, rather odd) edge-case for which I would open a PR if you consider it worth it (and tell me what your desired resolution is).

The problem is the following:

Duplicate methods are generated for an API with two fields in two different "cases".

Example: an API that has a field timeZone and a field time_zone (of which one might be deprecated) (this applies e.g. to Pipefy: https://api-docs.pipefy.com/reference/objects/User/) will result in two times the function

public function selectTimeZone()

, one time selecting timeZone and one time time_zone.

PHP does not like that of course.
I can imagine various resultions: If there are multiple,

  1. just discard the deprecated one,
  2. use the case as used in the API (i.e. resulting in one selectTimeZone() and one selectTime_zone(),
  3. select both at once

Shall I open a PR with which of these possibilities? (Any hints on where to look for the deciding code?)

InputObject

Hello,

I was wondering what to do with the generated InputObject Classes ( use then in a mutation ?) when i noticed some "InputObject" classes seems to not be generated.

I understand that InputObject classes are generated with this model name : {OBJECT_NAME}InputObject

I have a table that i can query like this:

query MyQuery {
  solvant {
    id
    name
    abbr
  }
}

i cannot find "solvantInputObject" class, but "solvant_order_byInputObject" class is generated.

I can't undertand why.

Many thanks in advance.

nb: my GraphQL schema is generated with Hasura.

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.