Giter VIP home page Giter VIP logo

services's Introduction

Services

Gem Version CI

Services is a collection of modules and base classes that let you simply add a service layer to your Rails app.

Motivation

A lot has been written about service layers (service objects, SOA, etc.) for Rails. There are of course advantages and disadvantages, but after using Services since 2013 in several Rails apps, I must say that in my opinion the advantages far outweigh the disadvantages.

The biggest benefit you get when using a service layer, in my opinion, is that it gets so much easier to reason about your application, find a bug, or implement new features, when all your business logic is in services, not scattered in models, controllers, helpers etc.

Usage

For disambiguation: in this README, when you read "Services" with a uppercase "S", this gem is meant, whereas with "services", well, the plural of service is meant.

Requirements

Ruby >= 2.7

Rails >= 6.0

Redis >= 3.0

Redis is used at several points, e.g. to store information about the currently running services, so you can enforce uniqueness for specific services, i.e. make sure no more than one instance of such a service is executed simultaneously.

Postgres (optional)

The SQL that Services::Query (discussed further down) generates is optimized for Postgres. It might work with other databases but it's not guaranteed. If you're not using Postgres, you can still use all other parts of Services, just don't use Services::Query or, even better, submit a pull request that fixes it to work with your database!

Sidekiq (optional)

To process services in the background, Services uses Sidekiq. If you don't need background processing, you can still use Services without Sidekiq. When you then try to enqueue a service for background processing, an exception will be raised. If you use Sidekiq, make sure to load the Services gem after the Sidekiq gem.

Basic principles

Services is based on a couple of basic principles around what a service should be and do in your app:

A service...

  • does only one thing and does it well (Unix philosophy)
  • can be run synchronously (i.e. blocking/in the foreground) or asynchronously (i.e. non-blocking/in the background)
  • can be configured as "unique", meaning only one instance of it should be run at any time (including or ignoring parameters)
  • logs all the things (start time, end time, duration, caller, exceptions etc.)
  • has its own exception class(es) which all exceptions that might be raised inherit from
  • does not care whether certain parameters are objects or object IDs

Apart from these basic principles, you are free to implement the actual logic in a service any way you want.

Conventions

Follow these conventions when using Services in your Rails app, and you'll be fine:

  • Let your services inherit from Services::Base
  • Let your query objects inherit from Services::Query
  • Put your services in app/services/
  • Decide if you want to use a Services namespace or not. Namespacing your service allows you to use a name for them that some other class or module in your app has (e.g. you can have a Services::Maintenance service, yet also a Maintenance module in lib). Not using a namespace saves you from writing Services:: everytime you want to reference a service in your app. Both approaches are fine, pick one and stick to it.
  • Give your services "verby" names, e.g. app/services/users/delete.rb defines Users::Delete (or Services::Users::Delete, see above). If a service operates on multiple models or no models at all, don't namespace them (Services::DoStuff) or namespace them by logical groups unrelated to models (Services::Maintenance::CleanOldStuff, Services::Maintenance::SendDailySummary, etc.)
  • Some services call other services. Try to not combine multiple calls to other services and business logic in one service. Instead, some services should contain only business logic and other services only a bunch of service calls but no (or little) business logic. This keeps your services nice and modular.

Configuration

You can/should configure Services in an initializer:

# config/initializers/services.rb
Services.configure do |config|
  config.logger = Services::Logger::Redis.new(Redis.new)    # see [Logging](#Logging)
  config.redis  = Redis.new                                 # optional, if `Redis.current` is defined. Otherwise it is recommended to use
                                                            # a [connection pool](https://github.com/mperham/connection_pool) here instead of simply `Redis.new`.
end

Rails autoload fix for Services namespace

By default, Rails expects app/services/users/delete.rb to define Users::Delete. If you want to use the Services namespace for your services, we want it to expect Services::Users::Delete. To make this work, add the app folder to the autoload path:

# config/application.rb
config.autoload_paths += [config.root.join('app')]

This looks as if it might break things, but AFAIK it has never cause problems so far.

Services::Base

Services::Base is the base class you should use for all your services. It gives you a couply of helper methods and defines a custom exception class for you.

Read the source to understand what it does in more detail.

The following example service takes one or more users or user IDs as an argument and deletes the users:

module Services
  module Users
    class Delete < Services::Base
      def call(ids_or_objects)
        users = find_objects(ids_or_objects)
        users.each do |user|
          if user.posts.any?
            raise Error, "User #{user.id} has one or more posts, refusing to delete."
          end
          user.destroy
          Mailer.user_deleted(user).deliver
        end
        users
      end
    end
  end
end

This service can be called in several ways:

# Execute synchronously/in the foreground

Services::Users::Delete.call User.find(1)                # with a user object
Services::Users::Delete.call User.where(id: [1, 2, 3])   # with a ActiveRecord::Relation returning user objects
Services::Users::Delete.call [user1, user2, user3]       # with an array of user objects
Services::Users::Delete.call 1                           # with a user ID
Services::Users::Delete.call [1, 2, 3]                   # with an array of user IDs

# Execute asynchronously/in the background

Services::Users::Delete.call_async 1                     # with a user ID
Services::Users::Delete.call_async [1, 2, 3]             # with multiple user IDs

As you can see, you cannot use objects or a ActiveRecord::Relation as parameters when calling a service asynchronously since the arguments are serialized to Redis. This might change once Services works with ActiveJob and GlobalID.

The helper find_objects is used to allow the ids_or_objects parameter to be a object, object ID, array or ActiveRecord::Relation, and make sure you we dealing with an array of objects from that point on.

It's good practice to always return the objects a service has been operating on at the end of the service.

Services::Query

Services::Query on the other hand should be the base class for all query objects.

Here is an example that is used to find users:

module Services
  module Users
    class Find < Services::Query
      convert_condition_objects_to_ids :post

      private def process(scope, condition, value)
        case condition
        when :email, :name
          scope.where(condition => value)
        when :post_id
          scope.joins(:posts).where("#{Post.table_name}.id" => value)
        end
      end
    end
  end
end

A query object that inherits from Services::Query always receives two parameters: an array of IDs and a hash of conditions. It always returns an array, even if none or only one object is found.

When you write your query objects, the only method you have to write is process (preferably make it private). This method does the actual querying for all non-standard parameters (more about standard vs. non-standard parameters below).

This is how Services::Users::Find can be called:

Services::Users::Find.call []                             # find all users, neither filtered by IDs nor by conditions
Services::Users::Find.call [1, 2, 3]                      # find users with ID 1, 2 or 3
Services::Users::Find.call 1                              # find users with ID 1 (careful: returns an array containing this one user, if found, otherwise an empty array)
Services::Users::Find.call [], email: '[email protected]'       # find users with this email address
Services::Users::Find.call [1, 2], post: Post.find(1)     # find users with ID 1 or 2 and having the post with ID 1
Services::Users::Find.call [1, 2], post: [Post.find(1)]   # same as above
Services::Users::Find.call [1, 2], post: 1                # same as above

Check out the source of Services::Query to understand what it does in more detail.

Standard vs. non-standard parameters

to be described...

convert_condition_objects_to_ids

As with service objects, you want to be able to pass objects or IDs as conditions to query objects as well, and be sure that they behave the same way. This is what convert_condition_objects_to_ids :post does in the previous example: it tells the service object to convert the post condition, if present, to post_id.

For example, at some point in your app you have an array of posts and need to find the users that created these posts. Services::Users::Find.call([], post: posts) will find them for you. If you have a post ID on the other hand, simply use Services::Users::Find.call([], post: post_id), or if you have a single post, use Services::Users::Find.call([], post: post). Each of these calls will return an array of users, as you would expect.

Services::Query takes an array of IDs and a hash of conditions as parameters. It then extracts some special conditions (:order, :limit, :page, :per_page) that are handled separately and passes a ActiveRecord::Relation and the remaining conditions to the process method that the inheriting class must define. This method should handle all the conditions, extend the scope and return it.

Helpers

Your services inherit from Services::Base which makes several helper methods available to them:

  • Rails.application.routes.url_helpers is included so you use all Rails URL helpers.
  • find_objects and find_object let you automatically find object or a single object from an array of objects or object IDs, or a single object or object ID. The only difference is that find_object returns a single object whereas find_objects always returns an array.
  • object_class tries to figure out the class the service operates on. If you follow the service naming conventions and you have a service Services::Products::Find, object_class will return Product. Don't call it if you have a service like Services::DoStuff or it will raise an exception.

Your services also automatically get a custom Error class, so you can raise Error, 'Uh-oh, something has gone wrong!' in Services::MyService and a Services::MyService::Error will be raised.

Logging

You can choose between logging to Redis or to a file, or turn logging off. By default logging is turned off.

Redis

to be described...

File

to be described...

Exception wrapping

to be described...

Uniqueness checking

to be described...

Background/asynchronous processing

Each service can run synchronously (i.e. blocking/in the foreground) or asynchronously (i.e. non-blocking/in the background). If you want to run a service in the background, make sure it takes only arguments that can be serialized without problems (i.e. integers, strings, etc.). The background processing is done by Sidekiq, so you must set up Sidekiq in the Services initializer.

Installation

Add this line to your application's Gemfile:

gem 'services'

And then execute:

$ bundle

Or install it yourself as:

$ gem install services

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Testing

You need Redis to run tests, check out the Guardfile which loads it automatically when you start Guard!

Support

If you like this project, consider buying me a coffee! :)

services's People

Contributors

dependabot[bot] avatar manuelmeurer avatar mpouleijn 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  avatar  avatar  avatar  avatar

services's Issues

Lost constant in two level deep service

I have a serivce place in:

app/services/organizations/top.rb

also have the following structure in module:

module Services
  module Organizations
     class Top
       ...
    end
  end
end
  1. I get the following error for the first loading of the constant:

    LoadError:
    Unable to autoload constant Organizations::Top, expected /home/majioa/git/tradeinspect.com.1/app/services/organizations/top.rb to define it

    When I add the definition of the contains explicitly:

    module Organizations
    Top = Services::Organizations::Top
    end
  2. The errors is disappeared, but then I get the following error:

    NameError:
    uninitialized constant Services::Organizations::Top

Why the service constants isn't properly loaded by services for two level loading only, since when I load the first level service all the all (except redefinition the constant as in point 1)?

Unable to autoload constant IssuesController, expected "path"/issues_controller.rb to define it

I'm working on a Ruby On Rails application called "Redmine", the issue I'm having is, see title, that every time I change a piece of code I'm required to restart the server which is very time-consuming when trying new ideas.

Trace:

activesupport (4.2.7.1) lib/active_support/dependencies.rb:495:in load_missing_constant' activesupport (4.2.7.1) lib/active_support/dependencies.rb:184:in const_missing'
activesupport (4.2.7.1) lib/active_support/inflector/methods.rb:261:in const_get' activesupport (4.2.7.1) lib/active_support/inflector/methods.rb:261:in block in constantize'
activesupport (4.2.7.1) lib/active_support/inflector/methods.rb:259:in each' activesupport (4.2.7.1) lib/active_support/inflector/methods.rb:259:in inject'
activesupport (4.2.7.1) lib/active_support/inflector/methods.rb:259:in constantize' activesupport (4.2.7.1) lib/active_support/dependencies.rb:566:in get'
activesupport (4.2.7.1) lib/active_support/dependencies.rb:597:in constantize' actionpack (4.2.7.1) lib/action_dispatch/routing/route_set.rb:70:in controller_reference'
actionpack (4.2.7.1) lib/action_dispatch/routing/route_set.rb:60:in controller' actionpack (4.2.7.1) lib/action_dispatch/routing/route_set.rb:39:in serve'
actionpack (4.2.7.1) lib/action_dispatch/journey/router.rb:43:in block in serve' actionpack (4.2.7.1) lib/action_dispatch/journey/router.rb:30:in each'
actionpack (4.2.7.1) lib/action_dispatch/journey/router.rb:30:in serve' actionpack (4.2.7.1) lib/action_dispatch/routing/route_set.rb:817:in call'
rack-openid (1.4.2) lib/rack/openid.rb:98:in call' request_store (1.0.5) lib/request_store/middleware.rb:9:in call'
rack (1.6.5) lib/rack/content_length.rb:15:in call' rack (1.6.5) lib/rack/etag.rb:24:in call'
rack (1.6.5) lib/rack/conditionalget.rb:25:in call' rack (1.6.5) lib/rack/head.rb:13:in call'
actionpack-xml_parser (1.0.2) lib/action_dispatch/xml_params_parser.rb:16:in call' actionpack (4.2.7.1) lib/action_dispatch/middleware/params_parser.rb:27:in call'
actionpack (4.2.7.1) lib/action_dispatch/middleware/flash.rb:260:in call' rack (1.6.5) lib/rack/session/abstract/id.rb:225:in context'
rack (1.6.5) lib/rack/session/abstract/id.rb:220:in call' actionpack (4.2.7.1) lib/action_dispatch/middleware/cookies.rb:560:in call'
activerecord (4.2.7.1) lib/active_record/query_cache.rb:36:in call' activerecord (4.2.7.1) lib/active_record/connection_adapters/abstract/connection_pool.rb:653:in call'
actionpack (4.2.7.1) lib/action_dispatch/middleware/callbacks.rb:29:in block in call' activesupport (4.2.7.1) lib/active_support/callbacks.rb:88:in run_callbacks'
activesupport (4.2.7.1) lib/active_support/callbacks.rb:778:in _run_call_callbacks' activesupport (4.2.7.1) lib/active_support/callbacks.rb:81:in run_callbacks'
actionpack (4.2.7.1) lib/action_dispatch/middleware/callbacks.rb:27:in call' actionpack (4.2.7.1) lib/action_dispatch/middleware/reloader.rb:73:in call'
actionpack (4.2.7.1) lib/action_dispatch/middleware/remote_ip.rb:78:in call' actionpack (4.2.7.1) lib/action_dispatch/middleware/debug_exceptions.rb:17:in call'
actionpack (4.2.7.1) lib/action_dispatch/middleware/show_exceptions.rb:30:in call' railties (4.2.7.1) lib/rails/rack/logger.rb:38:in call_app'
railties (4.2.7.1) lib/rails/rack/logger.rb:20:in block in call' activesupport (4.2.7.1) lib/active_support/tagged_logging.rb:68:in block in tagged'
activesupport (4.2.7.1) lib/active_support/tagged_logging.rb:26:in tagged' activesupport (4.2.7.1) lib/active_support/tagged_logging.rb:68:in tagged'
railties (4.2.7.1) lib/rails/rack/logger.rb:20:in call' actionpack (4.2.7.1) lib/action_dispatch/middleware/request_id.rb:21:in call'
rack (1.6.5) lib/rack/methodoverride.rb:22:in call' rack (1.6.5) lib/rack/runtime.rb:18:in call'
activesupport (4.2.7.1) lib/active_support/cache/strategy/local_cache_middleware.rb:28:in call' rack (1.6.5) lib/rack/lock.rb:17:in call'
actionpack (4.2.7.1) lib/action_dispatch/middleware/static.rb:120:in call' rack (1.6.5) lib/rack/sendfile.rb:113:in call'
railties (4.2.7.1) lib/rails/engine.rb:518:in call' railties (4.2.7.1) lib/rails/application.rb:165:in call'
rack (1.6.5) lib/rack/lock.rb:17:in call' rack (1.6.5) lib/rack/content_length.rb:15:in call'
rack (1.6.5) lib/rack/handler/webrick.rb:88:in service' C:/Ruby23-x64/lib/ruby/2.3.0/webrick/httpserver.rb:140:in service'
C:/Ruby23-x64/lib/ruby/2.3.0/webrick/httpserver.rb:96:in run' C:/Ruby23-x64/lib/ruby/2.3.0/webrick/server.rb:296:in block in start_thread'

Are Redis executables needed in the RubyGems releases?

Hey there, thank you for this library.

While reviewing changes in this lib I noticed that the spec/ directory is in the RubyGems releases together with spec/support/redis-server and spec/support/redis-cli. I wanted to ask if that is needed? Binaries committed into releases pose risk as reviewing them is much harder (decompilation is needed), especially since those seem to be spec related files.

Thank you.

undefined method `del' for nil:NilClass

I was trying to use the Services gem with a Rails project but ran into this error with my service classes Services::Models::FindObjectsTest::Error: undefined methoddel' for nil:NilClass`. This lead me to fork the repo and run specs, but I see there are a bunch of failures with starting and stopping Sidekiq. I then started to investigate using Sidekiq::Testing and was able to reproduce my error with the Rails application.

ttdonovan/services@ttdonovan:master...sidekiq-testing

I'll continue to look into this but any guidance or insight to why Sidekiq::Testing is not being used would greatly be appreciated.

https://github.com/mperham/sidekiq/wiki/Testing

$ rspec ./spec/services/base_spec.rb
Run options: include {:focus=>true}

All examples were filtered out; ignoring {:focus=>true}
Waiting for Redis to start...
FFFF.FFF

Failures:

  1) Services::Base#find_objects when passing in IDs returns the objects for the IDs
     Failure/Error: expect(Services::Models::FindObjectsTest.call(model_objects.map(&:id))).to eq(model_objects)
     Services::Models::FindObjectsTest::Error:
       undefined method `del' for nil:NilClass
     # ./lib/services/modules/exception_wrapper.rb:10:in `rescue in call'
     # ./lib/services/modules/exception_wrapper.rb:5:in `call'
     # ./lib/services/modules/call_logger.rb:29:in `call'
     # ./lib/services/base.rb:15:in `call'
     # ./spec/services/base_spec.rb:15:in `block (4 levels) in <top (required)>'

Possible dsl improvement

From the README:

# Execute synchronously/in the foreground
Services::Users::Delete.call 1

# Execute asynchronously/in the background
Services::Users::Delete.perform_async 1

Maybe perform will be better then call?

ArgumentError: comparison of String with 0 failed

I wrote a few services and when I began to test them, got a

ArgumentError: comparison of String with 0 failed

I'm running ruby 2.4.4 and Rails 5.2.1

I tried a completely empty service with no params just to test.

Difference between 7.3.3 and 8.0.0

What is the difference between versions 7.3.3 and 8.0.0? There are no commits on master branch after 7.3.0, the last release tag was v6.0.5 and the last changelog entry was at 4.1.3.

Reuse redis

I defined a service as:

class Services::Top < Services::Base
def do
end
end

do returns a hash of parsed results of sql query. how can I reuse redis results for the specific service?

LoadError: Unable to autoload constant

Hi Manuel,

I read your blogpost and decided to give it a try because i really liked the idea.

I wrote the following services and config:

# config/initializers/services.rb
Services.configure do |config|
  config.redis  = Redis.new
  config.logger = Services::Logger::File.new("#{Rails.root}/log/")
end

# app/services/users/find.rb
module Services
  module Users
    class Find < Services::BaseFinder
      private

      def process(scope, conditions)
        conditions.each do |k, v|
          case k
            when :email, :name
              scope = scope.where(k => v)
            else
              raise ArgumentError, "Unexpected condition: #{k}"
          end
        end
        scope
      end
    end
  end
end

# app/services/users/delete.rb
module Services
  module Users
    class Delete < Services::Base
      def call(ids_or_objects)
        users = find_objects(ids_or_objects)
        users.each do |user|
          user.destroy
        end
        users
      end
    end
  end
end

As you can see they are almost identical to your examples.

But when I want to execute them in the console I got the following:

2.1.4 :001 > Services::Users::Delete.call([2])
LoadError: Unable to autoload constant Users::Delete, expected /Users/mpouleijn/development/sx-core/app/services/users/delete.rb to define it
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/activesupport-4.1.7/lib/active_support/dependencies.rb:481:in `load_missing_constant'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/activesupport-4.1.7/lib/active_support/dependencies.rb:180:in `const_missing'
  from (irb):1
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/railties-4.1.7/lib/rails/commands/console.rb:90:in `start'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/railties-4.1.7/lib/rails/commands/console.rb:9:in `start'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/railties-4.1.7/lib/rails/commands/commands_tasks.rb:69:in `console'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/railties-4.1.7/lib/rails/commands/commands_tasks.rb:40:in `run_command!'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/railties-4.1.7/lib/rails/commands.rb:17:in `<top (required)>'
  from bin/rails:4:in `require'
  from bin/rails:4:in `<main>'
2.1.4 :002 > Services::Users::Delete.call([2])
Services::Users::Delete::Error: uninitialized constant Services::Users::Find
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/services-1.0.0/lib/services/modules/exception_wrapper.rb:10:in `rescue in call'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/services-1.0.0/lib/services/modules/exception_wrapper.rb:5:in `call'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/services-1.0.0/lib/services/modules/call_logger.rb:11:in `call'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/services-1.0.0/lib/services/base.rb:15:in `call'
  from (irb):2
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/railties-4.1.7/lib/rails/commands/console.rb:90:in `start'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/railties-4.1.7/lib/rails/commands/console.rb:9:in `start'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/railties-4.1.7/lib/rails/commands/commands_tasks.rb:69:in `console'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/railties-4.1.7/lib/rails/commands/commands_tasks.rb:40:in `run_command!'
  from /Users/mpouleijn/.rvm/gems/ruby-2.1.4@sx/gems/railties-4.1.7/lib/rails/commands.rb:17:in `<top (required)>'
  from bin/rails:4:in `require'
  from bin/rails:4:in `<main>'

Do you have any ideas how to solve this?

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.