GithubHelp home page GithubHelp logo

maslosoft / addendum Goto Github PK

View Code? Open in Web Editor NEW
7.0 4.0 0.0 1.16 MB

Powerfull and easy to use annotations with lightweight container

Home Page: https://maslosoft.com/addendum/

License: Other

PHP 99.54% Shell 0.41% Batchfile 0.05%
php annotations annotations-processor metadata meta container introspection extra

addendum's Introduction

Powerful and easy to use PHP annotations

Latest Stable Version License Scrutinizer Code Quality Code Coverage

Quick Install

composer require maslosoft/addendum

Documentation

Full Addendum Documentation

Annotations for PHP

This project provides smart annotations support for PHP language with the aim on performance and ease of use.

Annotations are in fact a kind declarative language embedded in PHP code. It does not allow and flow control structures, loops, conditions. It allows us to describe what is required or what we expect from parf of code.

Where to use annotations

While (PHP) programming language itself is very flexible and allows us to describe complex bahaviors, there are some aspects where we want to only describe (or configure) such behaviors. And we want to keep our main code as is, while adding some additional behaviors, which might include:

  • Access control
  • Way to display data
  • Whether to store data
  • Where to store data
  • Should the data be searchable

And so on, with ever growing list of aspects and behaviors. While this could be made by implementing in our classe more and more interfaces, soon those would end up in hundreds of methods.

Another way could be some kind of extra meta-data configuration, let it be in XML, JSON or YAML files. This requires us to keep those too files in sync and also separates declarations from code. But we might need extra behaviors/aspects for many libraries - resulting in code being split into two or more files.

Embed behavior into code

So the idea is to embed those extra aspects of class into code. With annotations - special comment tags placed just above class/method/property declaration.

These annotations can be interpreted independently by different parts of program. These are extensible, so that adding more annotations does not influence existing code.

Different application parts might interpret just part of annotations while ignoring unknown ones.

In below example one part will react @Label and @Description declarations, other will perform some extra operations when will encouner @I18N annotation.

Example:

class Page implements AnnotatedInterface
{
	/**
	 * Page URL
	 * @Label('Page URL')
	 * @Description('Relative URL, it should be simple lowercase string with words separated by hyphen')
	 * @SearchBoost(3.5)
	 * @I18N
	 * @SafeValidator
	 * @UrlValidator
	 * @Decorator(Action, 'update')
	 * @see Action
	 * @var string
	 */
	public $url = '';
}

All extra metadata is contained just where class property is declared. This is it. Annotations itself are not evaluated on each request. These are used to generate metadata, which is later available by using meta containers. Having any object implementing AnnotatedInterface, metadata can be obtained by passing either object or class name.

Example of getting meta data:

echo Meta::create(Page::class)->url->label;
// Will echo "Page URL"

This metadata is cached, so there is no overhead of parsing annotations.

addendum's People

Contributors

pmaselkowski avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

addendum's Issues

Refactor DocComment

Maybe this could be refactored into separate classes. Currently it's very fragile.

Namespace Cache Race condition

Namespace Cache uses addendum namespaces property. This clashes with multi-namespace usage cia meta options.

It clears cache over and over, when different meta classes use different namespaces.

see Maslosoft\Addendum\Cache\NsCache::isValid()

Cache validity must be checked against each options set separatelly.

Even more importantly, it should not include namespaces from other options classes.

Possibly should be some prefix, like nsSet added to addNamespace.

Possibly nested trait annotations are not merged properly

Use case: Class with trait. That trait has another trait. Annotations from nested trait seems to be not present on class:

namespace Maslosoft\ManganelTest\Models;

use Maslosoft\Mangan\Document;
use Maslosoft\Mangan\Sanitizers\MongoStringId;
use Maslosoft\Mangan\Traits\Model\SimpleTreeTrait;
use MongoId;

/**
 * ModelWithSimpleTree
 *
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
 */
class ModelWithSimpleTree extends Document
{

    use SimpleTreeTrait;
}
namespace Maslosoft\Mangan\Traits\Model;

use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
use Maslosoft\Mangan\EntityManager;
use Maslosoft\Mangan\Events\Event;
use Maslosoft\Mangan\Events\ModelEvent;
use Maslosoft\Mangan\Helpers\RawFinder;
use Maslosoft\Mangan\Interfaces\SimpleTreeInterface;
use Maslosoft\Mangan\Interfaces\TrashInterface;
use MongoId;

/**
 * Related Tree Trait
 * @see SimpleTreeInterface
 * @author Piotr
 */
trait SimpleTreeTrait
{

    use WithParentTrait;

    /**
     * @RelatedArray(join = {'_id' = 'parentId'}, updatable = true)
     * @RelatedOrdering('order')
     * @var AnnotatedInterface[]
     */
    public $children = [];
}
namespace Maslosoft\Mangan\Traits\Model;

use Maslosoft\Mangan\Sanitizers\MongoStringId;
use MongoId;

/**
 * This is trait for models having parent element
 *
 * @author Piotr
 */
trait WithParentTrait
{

    /**
     * @SafeValidator
     * @Sanitizer(MongoStringId, nullable = true)
     * @see MongoStringId
     * @var MongoId
     */
    public $parentId = null;

}

Test:

    $meta = ManganelMeta::create($model)->field('parentId');
    $this->assertTrue(!empty($meta->sanitizer), 'That has sanitizer explicitly set');

NOTE: $meta->sanitizer is set properly if parentId is added directly to ModelWithSimpleTree class.

Cache not cleared on child classes

This leads to confusing situations. Example:
Changed base class.
Expected some change in child classes.
But nothing happens as base class cache was cleared, while child class remained valid.

TLDR;
Invalidating base class cache, should invalidate all child classes cache too.

Create "class exists" helper

Class exists helper should check for class, interface or trait existence.
ie.:

    ....
    if (!class_exists($value) && !interface_exists($value) && !trait_exists($value))
    {
        return false;
    }
    ....

Must check for exceptions as some autloaders might throw if failed to autoload. This should add to ignore list too.

Optionaloly log not found classes.

Params Expander should expand params based on order of declared class fields

ParamsExpander should have option to call it without fields param.

This should get list of fields from annotation declaration and assign it in order or by names.
Example:

class TestAnnotation extends Annotation
{
    public $className = '';
    public $name = '';
    public $active = false;
    ....
}

Using such declared annotation:

@Test(Vendor\Package\ClassName, 'fooo')
@Test(class = Vendor\Package\ClassName, name = 'fooo')
@Test(Vendor\Package\ClassName, name = 'fooo')

Should all yield:

[
    'class' => 'Vendor\Package\ClassName',
    'name' => 'fooo'
]

Bottom line:
IMPORTANT: With this feature, one could forgot that order is important and reordering/adding/removing annotation class fields could break code using this annotation.

Perhaps it could be better to add Addendum annotation denoting param order:

class TestAnnotation extends Annotation
{
   /**
    * @ArgNum(0)
    */
    public $className = '';
   /**
    * @ArgNum(1)
    */
    public $name = '';
   /**
    * @ArgNum(2)
    */
    public $active = false;
    ....
}
  • or - drop this feature at all.

Class literal with use statements from traits are not evaluated

Use case:

class PageItem implements AnnotatedInteface
{
    use \Maslosoft\Menulis\Traits\WithUrls;
}

use Maslosoft\Ilmatar\Widgets\Decorators\Grid\CellEnumCss;
use Maslosoft\Menulis\Enum\Published;

trait WithUrls
{

    /**
    *    
     * @Decorator(CellEnumCss, Published)
     * @see CellEnumCss
     * @see Published
     * @var boolean
     */
    public $published = false;
}

Use statments from WithUrls trait are ignored, thus class names not found.

Workaround:

Use strings instead of class literals or full class names, ie:

trait WithUrls
{

    /**
    *    
     * @Decorator(\Maslosoft\Ilmatar\Widgets\Decorators\Grid\CellEnumCss, \Maslosoft\Menulis\Enum\Published)
     * @see CellEnumCss
     * @see Published
     * @var boolean
     */
    public $published = false;
}

File walker must follow symlinks.

This is desired for some projects configurations, but could create loops.

Use realpath and record all real path visited folders and files - so scan only if not visited.

Filter out fenced code out of comments

Filter out comment code blocks with triple backticks before parsing.

This will allow examples in code.

NOTE: Annotation utility possibly need this too.

Investigate bracket syntax for arrays

There should be alternative (possibly becoming main style) syntax for array elements, same as in php for easier learning:

@MyAnnotation(['foo' => 1, 'bar' => 'baz'])

This should not break backwards compatibility, but need to investigate further.

self::ConstantName expression unrecognized

Error msg:

Error: Could not find class self, when processing annotations on Maslosoft\Fc\Tracker\Models\Project, near self::ValidatorPattern)
	 * @var string
	 */ while scanning file /www/fc/src/Tracker/Models/Project.php

Relevant code:

class Project extends MongoDocument
{
	const AllowedPattern = '\p{L}0-9-';
	const MatchPattern = '[' . self::AllowedPattern . ']+';
	const FullPattern = '^' . self::MatchPattern . '$';
	const ValidatorPattern = '~' . self::FullPattern . '~';

	/**
	 * @Label('Project Name')
	 * @RequiredValidator
	 * @UniqueValidator
	 * @ImmutableValidator
	 * @MatchValidator('pattern': self::ValidatorPattern)
	 * @var string
	 */
	public $name = '';

Class name matcher should match `use`d classes

Currently class name matcher need fully qualified name, which is cumberstone.
Here is example class:

namespace Maslosoft\ManganTest\Models\Embedded;

use Maslosoft\Mangan\Document;
use Maslosoft\ManganTest\Models\ModelWithI18N;

class WithEmbeddedI18NModel extends Document
{
    /**
     * @EmbeddedArray(Maslosoft\ManganTest\Models\ModelWithI18N)
     * @var ModelWithI18N[]
     */
    public $pages = [];
}

@EmbeddedArray annotation param is class that is in use statement. Allow mathing base class name, ie:

namespace Maslosoft\ManganTest\Models\Embedded;

use Maslosoft\Mangan\Document;
use Maslosoft\ManganTest\Models\ModelWithI18N;

class WithEmbeddedI18NModel extends Document
{
    /**
     * @EmbeddedArray(ModelWithI18N)
     * @var ModelWithI18N[]
     */
    public $pages = [];
}

NOTE: This require major changes in matchers, as matcher does know nothing about currently parsed file.
NOTE 2: This should be implemented as matcher plugin. This could allow further improvements without changing base code.
NOTE 3: There is no php API to get use statements. Raw file parsing is needed.

[Insight] Object parameters should be type hinted - in src/Builder/DocComment.php, line 113

in src/Builder/DocComment.php, line 113

The parameter reflection, which is an object, should be typehinted.

        $class = $reflection->getDeclaringClass()->getName();
        $method = $reflection->getName();
        return isset(self::$methods[$class][$method]) ? self::$methods[$class][$method] : false;
    }

    public function forProperty($reflection)
    {
        $this->process($reflection->getDeclaringClass()->getFileName());
        $class = $reflection->getDeclaringClass()->getName();
        $field = $reflection->getName();
        return isset(self::$fields[$class][$field]) ? self::$fields[$class][$field] : false;

Posted from SensioLabsInsight

Annotations inheritance

Annotations should inherit from parent classes.

Annotation definition should deal with inheritance, by:

  1. Override meta value from parent class (most cases)
  2. Add some value to parent meta existing value

Example base class:

/**
 * @UpdateRoute('/content/page/update')
 * @TrashRoute('/content/page/trash')
 * @ModelRoute('/content/page/edit')
 * @ModelRoute('/content/page/publish')
 * @Label('Page item')
 */
class PageItem

Example extending class:

/**
 * @Label('Pages')
 */
class Page 

Parent class must have annotation defined in its base class, or it would have to be duplicated over and over.

In above example label of Page will be Pages, as LabelAnnotation simply overwrites label property of meta object:

class LabelAnnotation extends ManganPropertyAnnotation
{

    public $value = '';

    public function init()
    {
        $this->_entity->label = $this->value;
    }

While @ModelRoute could add up, as defined by ModelRouteAnnotation:

abstract class AbstractRoute extends IlmatarAnnotation
{
...
        if (empty($entity->routing))
        {
            $entity->routing = new RoutingMeta();
        }

        $entity->routing->{$type} = new RouteMeta($route, $params);
...

Non-critical cache error

Quick Workaround - reload resource.

Trace:

2016/10/06 21:13:03 [error] [php] array_diff_key(): Argument #2 is not an array (/var/www/websites/meno.pro/vendor/maslosoft/addendum/src/Cache/NsCache.php:159)
Stack trace:
#0 /var/www/websites/meno.pro/vendor/maslosoft/addendum/src/Cache/PhpCache.php(176): Maslosoft\Addendum\Cache\NsCache->valid()
#1 /var/www/websites/meno.pro/vendor/maslosoft/addendum/src/Builder/Builder.php(115): Maslosoft\Addendum\Cache\BuildOneCache->get()
#2 /var/www/websites/meno.pro/vendor/maslosoft/addendum/src/Builder/Builder.php(76): Maslosoft\Addendum\Builder\Builder->buildOne()
#3 /var/www/websites/meno.pro/vendor/maslosoft/addendum/src/Reflection/ReflectionAnnotatedClass.php(42): Maslosoft\Addendum\Builder\Builder->build()
#4 /var/www/websites/meno.pro/vendor/maslosoft/addendum/src/Addendum.php(238): Maslosoft\Addendum\Reflection\ReflectionAnnotatedClass->__construct()
#5 /var/www/websites/meno.pro/vendor/maslosoft/addendum/src/Collections/Meta.php(88): Maslosoft\Addendum\Addendum->annotate()
#6 /var/www/websites/meno.pro/vendor/maslosoft/ilmatar-components/src/Meta/IlmatarMeta.php(34): Maslosoft\Ilmatar\Components\Meta\IlmatarMeta->__construct()
#7 /var/www/websites/meno.pro/vendor/maslosoft/addendum/src/Collections/Meta.php(257): Maslosoft\Ilmatar\Components\Meta\IlmatarMeta->__construct()
#8 /var/www/websites/meno.pro/vendor/maslosoft/ilmatar-components/src/Meta/IlmatarMeta.php(53): create()
#9 /var/www/websites/meno.pro/vendor/maslosoft/ilmatar-components/src/MongoDocument.php(191): create()
#10 /var/www/websites/meno.pro/vendor/maslosoft/hi5edit/src/Blocks/Base/ContentBlock.php(72): Maslosoft\Hi5Edit\Blocks\Carousel\Carousel->__construct()
#11 /var/www/websites/meno.pro/vendor/maslosoft/hi5edit/src/Blocks/Carousel/Carousel.php(82): Maslosoft\Hi5Edit\Blocks\Carousel\Carousel->__construct()
#12 /var/www/websites/meno.pro/vendor/maslosoft/signals/src/Factories/SlotFactory.php(54): Maslosoft\Hi5Edit\Blocks\Carousel\Carousel->__construct()
#13 /var/www/websites/meno.pro/vendor/maslosoft/signals/src/Signal.php(271): create()
#14 /var/www/websites/meno.pro/vendor/maslosoft/hi5edit/src/Components/WidgetManager.php(38): Maslosoft\Signals\Signal->emit()
#15 /var/www/websites/meno.pro/vendor/maslosoft/hi5edit/src/Editor.php(112): Maslosoft\Hi5Edit\Components\WidgetManager->__construct()
#16 /var/www/websites/meno.pro/vendor/maslosoft/embedi/src/EmbeDi.php(329): Maslosoft\Hi5Edit\Editor->__construct()
#17 /var/www/websites/meno.pro/vendor/maslosoft/ilmatar-widgets/src/MsWidget.php(321): Maslosoft\EmbeDi\EmbeDi->apply()

Plugins infrastructure

Introduce plugins infrastructure. This is required to satisfy #16.
Proposed structure:

    class Addendum
    {

        public $plugins = [
            'matcher' => [
                ClassLiteralMatcher::class => [
                    UseStatementResolver::class,
                    [
                        'class' => OtherPlugin::class,
                        'option1' => 1,
                        'option2' => 2
                    ]
                ]
            ]
        ];

    }

Implement common cache subsystem

Addendum requires cascading cache, and cache in various methods.
Implement central cache management capable of storing Reflection* instances, which are not serializable.
Cache management must have common clear method.

[Insight] Object parameters should be type hinted - in src/Builder/DocComment.php, line 105

in src/Builder/DocComment.php, line 105

The parameter reflection, which is an object, should be typehinted.

            'fields' => isset(self::$fields[$fqn]) ? self::$fields[$fqn] : []
        ];
        return $result;
    }

    public function forMethod($reflection)
    {
        $this->process($reflection->getDeclaringClass()->getFileName());
        $class = $reflection->getDeclaringClass()->getName();
        $method = $reflection->getName();
        return isset(self::$methods[$class][$method]) ? self::$methods[$class][$method] : false;

Posted from SensioLabsInsight

[Insight] Commented code should not be committed - in src/Collections/Meta.php, line 97

in src/Collections/Meta.php, line 97

Commented out code reduces readability and lowers the code confidence for other developers. If it's common usage for debug, it should not be committed. Using a version control system, such code can be safely removed.

        // Get reflection data
        $ad = Addendum::fly($options->instanceId);
        $ad->addNamespaces($options->namespaces);
        $info = $ad->annotate($component);

//      $info = Yii::app()->addendum->annotate($component);


        if (!$info instanceof ReflectionAnnotatedClass)
        {
            throw new Exception(sprintf('Could not annotate `%s`', get_class($component)));

Posted from SensioLabsInsight

Cache file permissions

Make configurable, or a bit lessen cache permissions. It should be writable by group.

Class constant value not resolved

Example code:

namespace Maslosoft\ManganTest\Models\Validator;

use Maslosoft\Addendum\Interfaces\AnnotatedInterface;

/**
 * ModelWithRequiredValidator
 *
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
 */
class ModelWithRequiredValueValidator implements AnnotatedInterface
{

    const RequiredValue = 'test';

    /**
     * @RequiredValidator(requiredValue = ModelWithRequiredValueValidator::RequiredValue)
     * @var string
     */
    public $login = '';

}

Annotation field requiredValue should resolve to constant RequiredValue, but it doesn't - resolves to false.

Introduce namespaces to annotations

Annotations should be in namespaces too.
However when using annotation it should not require full class name (but should allow it).
Annotations should be imported and targeted.
Example addendum config pseudo code:

[
    class => Maslosoft\Addendum\Class,
    import => [
        Maslosoft\SomeProject\Annotations\*,
        Maslosoft\OtherProject\Annotations\SomeAnnotation,
        [Maslosoft\OtherProject\Annotations\SomeAnnotation => SomeOtherName],
]

Class resolving should be similar to this build in in php, but:
If two (or more) same base name annotations exists, but with different target, this should be allowed as it is possible to automatically resolve.

Example annotations with possibly conflicting annotations:

namespace One;
/**
 * @Target('Model')
 */
class Label
...
namespace Two;
/**
 * @Target('Controller')
 */
class Label
...

Some model class:

/**
 * Below should resolve to `One\Label`
 * @Label('This stores cool things in db')
 */
class SomeModel extends Model

Some controller class:

/**
 * Below should resolve to `Two\Label`
 * @Label('This makes cool things with models')
 */
class SomeController extends Controller

Name resolving of array elements with `self` and `static`

Example:

class MySettings
...
/*
* @FormRenderer(LangSelector, {self, 'getLanguagesRef'}) 

Current:
Param 2 resolves to:

[
false,// Not resolved `self` keyword
'getLanguagesRef'
]

Expected:

[
'MySettings',// Should resolve `self` keyword
'getLanguagesRef'
]

Workaround - use class name:

class MySettings
...
/*
* @FormRenderer(LangSelector, {MySettings, 'getLanguagesRef'}) 

Detect timeout when creating cache and throw exception

On large projects, filling up cache from zero might take more than default php timeout of 30 seconds.
While currently it seems to work, there is a slight chance that cache might break, so detect if timeout is near and throw new TimeoutException with some meaningfull message, like "try again".

Anonymous params do not match

Anonymous params example:

@SomeAnnotation(1, 2, 'foo')

Should set annotation $value:

[1, 2, 'foo']

Currently id doesn't work.

Wokaround:
Use array notation

@SomeAnnotation({1, 2, 'foo'})

Or named params

[Insight] Commented code should not be committed - in src/Builder/DocComment.php, line 148

in src/Builder/DocComment.php, line 148

Commented out code reduces readability and lowers the code confidence for other developers. If it's common usage for debug, it should not be committed. Using a version control system, such code can be safely removed.

        {
            $token = $tokens[$i];
            if (is_array($token))
            {
                list($code, $value) = $token;
//              $tokenName = token_name($code);

                switch ($code)
                {
                    case T_DOC_COMMENT:
                        $comment = $value;

Posted from SensioLabsInsight

Add @Require

Add @Require annotation.

This should be placed on annotations definitions, similar like @Conflicts.

This denotes that annotation require other annotation to work properly.

Syntax:

OR:

@Require('AnnOne', 'AnnTwo')
// require: AnnOne OR AnnTwo

AND:

@Require('AnnOne')
@Require('AnnTwo')
// require: AnnOne AND AnnTwo

Combine OR and AND:

@Require('AnnOne', 'AnnTwo')
@Require('AnnThree')
// require: (AnnOne OR AnnTwo) AND AnnThree

Option to use file path (or customized key) as cache key

Use case:

  1. Customer deliveded options class, which is not guaranteed to have unique namespace (or might not have namespace).
  2. When class is annotated, it will create duplicated cache keys

Possible options:

  1. Use file system path for cache key
  2. Use custom callback to get cache key
  3. Use both, selectable by enum, ie:
    CacheKeyClassName: 1; CacheKeyFilePath: 1; CacheKeyCallback: 1;
  4. Use class key namer interface and provide a fully configurable solution which is preferred solution
class Addendum
{
...
$cacheKey = ClassNameKey::class;
// or
$cacheKey = FilePathKey::class;
// or
$cacheKey = [
    'class' => CallbackKey::class
    'callback' => [UserClass::class, 'getKey']
    ];
...
}

Implement PSR-3 logger

Instead of Yii::trace, use PSR-3 compliant logger by implementing Psr\Log\LoggerAwareInterface from composer psr/log

Raw parser use statements

Raw parses should return array of use statements as there is no php method to get them.
This will be required by #15.

Optional annotation alias via use statement

Scenario:

Two annotations with same name registered:

Acme\Yoyodyne\NameAnnotation
Maslosoft\Mangan\NameAnnotation

Solution:

namespace Project\Vendor;

use Acme\Yoyodyne\NameAnnotation as AcmeName;

class MyClass
{
    /**
     * @AcmeName('foo')
     */
    public $conflicted
}

Use UseResolver for this feature.

[Insight] Commented code should not be committed - in src/Builder/Builder.php, line 152

in src/Builder/Builder.php, line 152

Commented out code reduces readability and lowers the code confidence for other developers. If it's common usage for debug, it should not be committed. Using a version control system, such code can be safely removed.

            try
            {
                // NOTE: @ need to be used here or php might complain
                if (@!class_exists($fqn))
                {
                    //$this->addendum->getLogger()->debug('Annotation class `{fqn}` not found, ignoring', ['fqn' => $fqn]);
                    Blacklister::ignore($fqn);
                }
                else
                {
                    // Class exists, exit loop

Posted from SensioLabsInsight

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.