├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── soa.rb └── soa │ └── version.rb ├── soa.gemspec └── test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in soa.gemspec 6 | gemspec 7 | 8 | gem "pry" 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | soa (2.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | coderay (1.1.2) 10 | method_source (0.9.0) 11 | pry (0.11.3) 12 | coderay (~> 1.1.0) 13 | method_source (~> 0.9.0) 14 | rake (10.5.0) 15 | 16 | PLATFORMS 17 | ruby 18 | 19 | DEPENDENCIES 20 | bundler (~> 1.16) 21 | pry 22 | rake (~> 10.0) 23 | soa! 24 | 25 | BUNDLED WITH 26 | 1.16.3 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SOA 2 | 3 | A lot of Ruby and Rails developers can see writing on walls that tells them 4 | small, focused services are the future. Here is a quote from a well-known 5 | Ruby thoughtleader, promoting them on the popular microservice platform 6 | called Twitter dot com: 7 | 8 | > Microservices are great for turning method calls in to distributed computing 9 | > problems 10 | > 11 | > — [Aaron Patterson](https://twitter.com/tenderlove) on [Aug. 9, 12 | 2018](https://twitter.com/tenderlove/status/1027591532847816704) 13 | 14 | I've helped many teams maintain old, slow, & confusing monolithic applications 15 | and it's taught me one thing: **monolithic codebases become more complex over 16 | time**. As a result, many companies have decided to build non-monolithic 17 | applications instead (these are called "services"; the better, more modern ones 18 | are called "microservices"). Applications built with services are initially much 19 | more difficult to create and operate, but they also tend to die sooner, which is 20 | the best known way to reduce code complexity. 21 | 22 | But how do you write services and microservices in a monolithic language like 23 | Ruby? Up until now, writing services required JavaScript and AWS Lambda. But 24 | because I prefer to write Ruby and sometimes I work offline (AWS can't be 25 | used offline yet), I wrote the SOA gem. 26 | 27 | The SOA gem is a drop-in replacement for Ruby's built-in method dispatch system. 28 | You can continue to call legacy methods like you always have alongside new 29 | service invocations registered with the SOA gem. It's the perfect companion for 30 | teams looking to make a more gradual transition to a services architecture 31 | without rewriting their entire decades-old application in JavaScript and AWS 32 | Lambda. 33 | 34 | ## Installation 35 | 36 | To install SOA, we use the command line program `gem` which communicates with 37 | the RubyGems.org microservice to download the necessary files: 38 | 39 | ``` 40 | gem install soa 41 | ``` 42 | 43 | And then, in your code, you can activate "SOA mode" in your Ruby interpreter 44 | like this 45 | 46 | ``` ruby 47 | require "soa" 48 | ``` 49 | 50 | [Note that the SOA gem is only tested with C-Ruby. ~~If you want to write 51 | services with JRuby, you'll need to wait for the release of a SOAP gem.~~ 52 | **Update: thanks to [Tom Enebo](https://github.com/enebo), SOA [now supports 53 | JRuby](https://github.com/searls/soa/pull/1) and also works as of version 54 | 2.0.0**] 55 | 56 | Once required, the SOA gem will prepare your Ruby runtime to run services and 57 | microservices instead using our easy-to-use DSL. 58 | 59 | ## Usage 60 | 61 | To create a new microservice, we use the `service` method and specify a route 62 | path like so: 63 | 64 | ``` ruby 65 | require "soa" 66 | 67 | service "/api/user/:id" do |id| 68 | User.find(id) 69 | end 70 | ``` 71 | 72 | In order to invoke a SOA microservice, we just `call_service` with the URL. The 73 | service is then looked up from the SOA Service Registry, the params are parsed, 74 | the service is invoked, and the results are returned. 75 | 76 | ``` ruby 77 | user = call_service "/api/user/45" 78 | puts user.id # => 45 79 | ``` 80 | 81 | It would barely be any easier to just define and call a legacy monolithic Ruby 82 | method! 83 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | task :default => :spec 3 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "soa" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/soa.rb: -------------------------------------------------------------------------------- 1 | require "soa/version" 2 | 3 | class SOA 4 | ServiceCall = Struct.new(:route, :service, :args) 5 | 6 | def self.register(route, blk) 7 | @services ||= {} 8 | @services[route] = blk 9 | end 10 | 11 | def self.invoke(url) 12 | service_call = service_lookup(url) 13 | service_call.service.call(*service_call.args) 14 | end 15 | 16 | private 17 | 18 | def self.service_lookup(url) 19 | route, service = @services.find { |(route, _)| 20 | parse_params(route, url).all? { |(route_component, url_component)| 21 | route_component == url_component || route_component.start_with?(":") 22 | } 23 | } 24 | 25 | return ServiceCall.new(route, service, parse_args(route, url)) 26 | end 27 | 28 | def self.parse_params(route, url) 29 | route.split("/").zip(url.split("/")) 30 | end 31 | 32 | def self.parse_args(route, url) 33 | parse_params(route, url).select { |(route_component, _)| 34 | route_component.start_with?(":") 35 | }.map { |(_, url_component)| 36 | url_component 37 | } 38 | end 39 | 40 | end 41 | 42 | module Kernel 43 | def service(route, &blk) 44 | SOA.register(route, blk) 45 | end 46 | 47 | def call_service(url, *args) 48 | SOA.invoke(url, *args) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/soa/version.rb: -------------------------------------------------------------------------------- 1 | class SOA 2 | VERSION = "2.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /soa.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "soa/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "soa" 8 | spec.version = SOA::VERSION 9 | spec.authors = ["Justin Searls"] 10 | spec.email = ["searls@gmail.com"] 11 | 12 | spec.summary = %q{Helps you migrate from monolithic Ruby to services} 13 | spec.homepage = "https://github.com/searls/soa" 14 | 15 | # Specify which files should be added to the gem when it is released. 16 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 17 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 18 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_development_dependency "bundler", "~> 1.16" 25 | spec.add_development_dependency "rake", "~> 10.0" 26 | end 27 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | require "soa" 2 | 3 | service "/api/user/:id" do |id| 4 | puts "finding user #{id}" 5 | end 6 | 7 | call_service "/api/user/45" # => puts "finding user 45" 8 | 9 | --------------------------------------------------------------------------------