├── .gitignore ├── .rspec ├── .rvmrc.example ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── zss.rb └── zss │ ├── client.rb │ ├── configuration.rb │ ├── environment.rb │ ├── error.rb │ ├── errors.json │ ├── message.rb │ ├── message │ ├── message_address.rb │ ├── message_type.rb │ └── smi.rb │ ├── permit.rb │ ├── require.rb │ ├── router.rb │ ├── runner.rb │ ├── service.rb │ ├── socket.rb │ ├── validate.rb │ └── version.rb ├── spec ├── integration │ ├── client_spec.rb │ ├── service_spec.rb │ └── socket_spec.rb ├── spec_broker_helper.rb ├── spec_helper.rb └── unit │ ├── client_spec.rb │ ├── environment_spec.rb │ ├── error_spec.rb │ ├── message_address_spec.rb │ ├── message_spec.rb │ ├── permit_spec.rb │ ├── require_spec.rb │ ├── router_spec.rb │ ├── service_spec.rb │ ├── socket_spec.rb │ └── validate_spec.rb └── zss.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /lib/bundler/man/ 26 | 27 | # for a library or gem, you might want to ignore these files since the code is 28 | # intended to run in multiple environments; otherwise, check them in: 29 | # Gemfile.lock 30 | # .ruby-version 31 | # .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | -f doc --color 2 | -------------------------------------------------------------------------------- /.rvmrc.example: -------------------------------------------------------------------------------- 1 | rvm 2.1.2@zss --create 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: 3 | - bundler 4 | - apt 5 | rvm: 6 | - 2.1.2 7 | branches: 8 | only: 9 | - master 10 | env: 11 | global: 12 | - ZSS_ENV=test 13 | - DISPLAY=:99.0 14 | before_install: 15 | - sudo apt-get install libzmq3-dev 16 | - gem install bundler --no-rdoc --no-ri 1>/dev/null 17 | script: 18 | - "bundle exec rspec" 19 | addons: 20 | code_climate: 21 | repo_token: 22 | secure: U6d5emmLhWdFAqrVgtHtXDs1lR2f40is89mZfOp1HbnTQKnClGbmCuEGfeHL8HbaMZjpxFb7g9Ery26E3g1gCsE82sP8SkD0qY46LbWlufRctZWsD4d+TZZZttqo2eNNObKkW5JbzW2pGiRqQb7RCfsxsOnqqhXl3uXVOHK24x4= 23 | # deploy: 24 | # provider: rubygems 25 | # api_key: 26 | # secure: OsOTnuz+qiZe4RY2hOhYsv46LJE7eQf3jZAX5oBBxsSaC6V91SgyHgl1mexMM6r1CPvgOlG4b33AU2Um9NIVrO11fwl/psSeh6zrMf46l5tmn+QCtjOuoM1jtKpomYAPXioH0KkZWoRcqPyNZemZx14YJQW0/fLtrc5xUS20Re0= 27 | # gemspec: zss.gemspec 28 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in zss.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | zss (0.3.4) 5 | activesupport (~> 4.2) 6 | daemons (~> 1.1) 7 | em-zeromq (~> 0.5) 8 | ffi-rzmq (~> 2.0) 9 | hashie (~> 3.2) 10 | logger_facade (~> 0.4.1) 11 | msgpack (= 0.5.12) 12 | 13 | GEM 14 | remote: https://rubygems.org/ 15 | specs: 16 | activesupport (4.2.1) 17 | i18n (~> 0.7) 18 | json (~> 1.7, >= 1.7.7) 19 | minitest (~> 5.1) 20 | thread_safe (~> 0.3, >= 0.3.4) 21 | tzinfo (~> 1.1) 22 | airbrake (4.1.0) 23 | builder 24 | multi_json 25 | builder (3.2.2) 26 | bump (0.5.1) 27 | celluloid (0.16.0) 28 | timers (~> 4.0.0) 29 | codeclimate-test-reporter (0.4.6) 30 | simplecov (>= 0.7.1, < 1.0.0) 31 | coderay (1.1.0) 32 | daemons (1.2.2) 33 | diff-lcs (1.2.5) 34 | docile (1.1.5) 35 | em-zeromq (0.5.0) 36 | eventmachine (>= 1.0.0) 37 | ffi (>= 1.0.0) 38 | ffi-rzmq (~> 2.0.1) 39 | eventmachine (1.0.7) 40 | ffi (1.9.8) 41 | ffi-rzmq (2.0.4) 42 | ffi-rzmq-core (>= 1.0.1) 43 | ffi-rzmq-core (1.0.3) 44 | ffi (~> 1.9) 45 | hashie (3.4.1) 46 | hitimes (1.2.2) 47 | i18n (0.7.0) 48 | json (1.8.2) 49 | logger_facade (0.4.1) 50 | airbrake (~> 4.0) 51 | hashie (~> 3.2) 52 | sucker_punch (~> 1.1) 53 | method_source (0.8.2) 54 | minitest (5.6.1) 55 | msgpack (0.5.12) 56 | multi_json (1.10.1) 57 | pry (0.10.1) 58 | coderay (~> 1.1.0) 59 | method_source (~> 0.8.1) 60 | slop (~> 3.4) 61 | rake (10.4.2) 62 | rspec (3.2.0) 63 | rspec-core (~> 3.2.0) 64 | rspec-expectations (~> 3.2.0) 65 | rspec-mocks (~> 3.2.0) 66 | rspec-core (3.2.1) 67 | rspec-support (~> 3.2.0) 68 | rspec-expectations (3.2.0) 69 | diff-lcs (>= 1.2.0, < 2.0) 70 | rspec-support (~> 3.2.0) 71 | rspec-mocks (3.2.1) 72 | diff-lcs (>= 1.2.0, < 2.0) 73 | rspec-support (~> 3.2.0) 74 | rspec-support (3.2.2) 75 | simplecov (0.9.2) 76 | docile (~> 1.1.0) 77 | multi_json (~> 1.0) 78 | simplecov-html (~> 0.9.0) 79 | simplecov-html (0.9.0) 80 | slop (3.6.0) 81 | sucker_punch (1.5.0) 82 | celluloid (~> 0.16.0) 83 | thread_safe (0.3.5) 84 | timers (4.0.1) 85 | hitimes 86 | tzinfo (1.2.2) 87 | thread_safe (~> 0.1) 88 | 89 | PLATFORMS 90 | ruby 91 | 92 | DEPENDENCIES 93 | bump (~> 0.5) 94 | bundler (~> 1.6) 95 | codeclimate-test-reporter (~> 0.3) 96 | pry (~> 0.10) 97 | rake (~> 10.3) 98 | rspec (~> 3.0) 99 | simplecov (~> 0.9) 100 | zss! 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Pedro Januário 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/pjanuario/zmq-service-suite-ruby.svg?branch=master)](https://travis-ci.org/pjanuario/zmq-service-suite-ruby) 2 | [![Code Climate](https://codeclimate.com/github/pjanuario/zmq-service-suite-ruby.png)](https://codeclimate.com/github/pjanuario/zmq-service-suite-ruby) 3 | [![Coverage](https://codeclimate.com/github/pjanuario/zmq-service-suite-ruby/coverage.png)](https://codeclimate.com/github/pjanuario/zmq-service-suite-ruby) 4 | [![Dependency Status](https://gemnasium.com/pjanuario/zmq-service-suite-ruby.svg)](https://gemnasium.com/pjanuario/zmq-service-suite-ruby) 5 | [![Gem Version](https://badge.fury.io/rb/zss.svg)](http://badge.fury.io/rb/zss) 6 | 7 | # ZMQ SOA Suite - Ruby Client & Service 8 | 9 | This project is a ruby client&service implementation for [ZMQ Service Suite](http://pjanuario.github.io/zmq-service-suite-specs/). 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | gem 'zss' 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install zss 24 | 25 | **NOTE:** 26 | 27 | You need to have [0MQ installed](http://zeromq.org/area:download). 28 | 29 | If you use MacOS just do 30 | 31 | $ brew install zeromq 32 | 33 | 34 | ## ZSS Client 35 | 36 | ```ruby 37 | require 'zss' 38 | 39 | # default Client config 40 | config = { 41 | frontend: 'tcp://127.0.0.1:5560', 42 | identity: 'client', 43 | timeout: 1000 # in ms 44 | } 45 | 46 | # it uses default configs 47 | PongClient = ZSS::Client.new(:pong) 48 | 49 | # it uses configs 50 | # who can override just a single property 51 | PongClient = ZSS::Client.new(:pong, config) 52 | 53 | # call Pong service on verb PING with "payload" 54 | PongClient.ping("payload") 55 | #or 56 | PongClient.call("ping", "payload") 57 | 58 | # call Pong service on verb PING with "payload" and headers 59 | PongClient.ping("payload", headers: { something: "data" }) 60 | # or 61 | PongClient.call("ping", "payload", headers: { something: "data" }) 62 | 63 | # call Pong service on verb PING with "payload" 64 | PongClient.ping_pong("payload") 65 | # or 66 | PongClient.call("ping/pong", "payload") 67 | 68 | ``` 69 | 70 | ### Client errors 71 | 72 | The client raises services errors using ZSS::Error class, with code, developer message and user message. 73 | 74 | ```ruby 75 | require 'zss' 76 | 77 | PongClient = ZSS::Client.new(:pong) 78 | 79 | begin 80 | PongClient.ping("payload") 81 | rescue ZSS::Error => error 82 | puts "Status code: #{error.code}" 83 | puts "User Message: #{error.user_message}" 84 | puts "Developer Message: #{error.developer_message}" 85 | end 86 | 87 | ``` 88 | 89 | ## ZSS Service 90 | 91 | The ZSS Service is responsible for receiving ZSS Request and execute configured service handlers. 92 | 93 | **The main components are:** 94 | * ZSS::Service.new(:sid, config) 95 | 96 | This component responsible for receiving and routing ZSS requests. 97 | The first parameter is service identifier (sid), you can use either symbol or string. 98 | The configuration parameter will be used to pass heartbeat interval and broker backend address. 99 | 100 | ```ruby 101 | config = { 102 | backend: 'tcp://127.0.0.1:7776', # default: tcp://127.0.0.1:7776 103 | heartbeat: 1000 # ms, default: 1s 104 | } 105 | service = ZSS::Service.new(:pong, config) 106 | ``` 107 | 108 | * ZSS::Runner.run(:sid) 109 | 110 | This component is responsible to execute a ZSS::Service as daemon, when in background it redirects stdout to file and manages pid files. It uses [daemon gem](https://rubygems.org/gems/daemon). 111 | 112 | $ bin/pong run 113 | 114 | or in background, where pidfile and logs are under /log 115 | 116 | $ bin/pong start/stop 117 | 118 | * ZSS::ServiceRegister 119 | 120 | This component is the glue entry point between Runner & Service. 121 | 122 | 123 | **NOTE:** You can run services without using the runner and service register, they are just one way to run services, you can use your own. Using Runner and service registry all services are done on the same way and using shared infra. 124 | 125 | ### Creating a new service step by step 126 | 127 | Let's create our first service sample, Ping-Pong, step by step. 128 | Note: For your next services you will be able to use [Service Generation rake](#zss-service-generation-rake), but for now you learn what the rake does and why! 129 | 130 | * Create you service logic class, adding a pong_service.rb under /lib folder. 131 | 132 | ```ruby 133 | class PongService 134 | 135 | # it print's payload and headers on the console 136 | def ping(payload, headers) 137 | puts "Payload => #{payload}" 138 | puts "Headers => #{headers}" 139 | 140 | return "PONG" 141 | end 142 | 143 | end 144 | 145 | ``` 146 | **NOTE:** Headers parameter is optional! 147 | 148 | * Create the service registration, adding a service_register.rb under /lib folder. 149 | 150 | ```ruby 151 | module ZSS 152 | class ServiceRegister 153 | 154 | def self.get_service 155 | config = Hashie::Mash.new( 156 | # this data should be received from a config file instead! 157 | backend: 'tcp://127.0.0.1:7776' 158 | ) 159 | 160 | # create a service instance for sid :pong 161 | service = ZSS::Service.new(:pong, config) 162 | 163 | instance = PongService.new 164 | # register route ping for the service 165 | service.add_route(instance, :ping) 166 | 167 | service 168 | end 169 | end 170 | end 171 | 172 | ``` 173 | 174 | * Hook your files by creating start.rb under /lib folder. 175 | 176 | ```ruby 177 | require 'zss/service' 178 | 179 | require_relative 'service_register' 180 | require_relative 'pong_service' 181 | ``` 182 | 183 | * Register LoggerFacade plugin/s 184 | 185 | The ZSS library uses the [LoggerFacade](https://github.com/pjanuario/logger-facade-ruby) library to abstract logging info, so you should hook your plugins on start.rb. 186 | 187 | 188 | ```ruby 189 | # log level should be retrieved from a configuration file 190 | plugin = LoggerFacade::Plugins::Console.new({ level: :debug }) 191 | LoggerFacade::Manager.use(plugin) 192 | ``` 193 | 194 | * Create a binary file to run the service as daemon, such as bin/pong 195 | 196 | ```ruby 197 | #!/usr/bin/env ruby 198 | require 'rubygems' unless defined?(Gem) 199 | 200 | require 'bundler/setup' 201 | Bundler.require 202 | 203 | $env = ENV['ZSS_ENV'] || 'development' 204 | 205 | require 'zss/runner' 206 | require_relative '../lib/start' 207 | 208 | # Runner receives the identifier used for pid and log filename 209 | ZSS::Runner.run(:pong) 210 | ``` 211 | 212 | **NOTES:** 213 | * ZSS_ENV: is used to identify the running environment 214 | 215 | 216 | You running service example [here](https://github.com/pjanuario/zss-service-sample) 217 | 218 | ### Returning errors 219 | 220 | Every exception that is raised by the service is shield and result on a response with status code 500 with default user and developer messages. 221 | 222 | The available errors dictionary is defined in [error.json](https://github.com/pjanuario/zmq-service-suite-ruby/blob/master/lib/zss/errors.json). 223 | 224 | **Raising different errors** 225 | 226 | ```ruby 227 | raise Error[500] 228 | # or 229 | raise Error.new 230 | # or 231 | raise Error.new(500) 232 | # or with developer message override 233 | raise Error.new(500, "this message should helpfull for developer!") 234 | ``` 235 | When relevant errors should be raised be with developer messages! 236 | 237 | **New Error Types** 238 | 239 | New error types should be added to [error.json](https://github.com/pjanuario/zmq-service-suite-ruby/blob/master/lib/zss/errors.json) using pull request. 240 | 241 | 242 | ### ZSS Service Generation Rake 243 | 244 | [**#TODO**](https://github.com/pjanuario/zmq-service-suite-ruby/issues/11) 245 | 246 | rake zss:service sid (--airbrake) 247 | 248 | It generates the service skeleton, adding several files into root directory: 249 | * sid - binary file named with sid 250 | * start.rb with console plugin attached (--airbrake will add Airbrake plugin also) 251 | * sid_service.rb 252 | * service_register.rb 253 | * config/application.yml 254 | * travis.yml 255 | * .rvmrc.sample 256 | * .rvmrc 257 | * .rspec 258 | * Gemfile 259 | * .gitignore 260 | 261 | ## Contributing 262 | 263 | 1. Fork it 264 | 2. Create your feature branch (`git checkout -b my-new-feature`) 265 | 3. Commit your changes (`git commit -am 'Add some feature'`) 266 | 4. Push to the branch (`git push origin my-new-feature`) 267 | 5. Create new Pull Request 268 | 269 | ## Bump versioning 270 | 271 | We use [bump gem](https://github.com/gregorym/bump) to control gem versioning. 272 | 273 | Bump Patch version 274 | 275 | $ bump patch --tag 276 | 277 | Bump Minor version 278 | 279 | $ bump minor --tag 280 | 281 | Bump Major version 282 | 283 | $ bump major --tag 284 | 285 | ## Running Specs 286 | 287 | $ rspec 288 | 289 | ## Coverage Report 290 | 291 | $ open ./coverage/index.html 292 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Rakefile 2 | require "bump/tasks" 3 | -------------------------------------------------------------------------------- /lib/zss.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'active_support/string_inquirer' 3 | require 'active_support/core_ext/object' 4 | require 'active_support/core_ext/hash/indifferent_access' 5 | 6 | require 'logger_facade' 7 | 8 | require_relative 'zss/version' 9 | require_relative 'zss/environment' 10 | require_relative 'zss/configuration' 11 | require_relative 'zss/message' 12 | require_relative 'zss/error' 13 | 14 | 15 | module ZSS 16 | extend self 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/zss/client.rb: -------------------------------------------------------------------------------- 1 | require_relative '../zss' 2 | require_relative 'socket' 3 | 4 | module ZSS 5 | class Client 6 | 7 | include LoggerFacade::Loggable 8 | 9 | attr_reader :sid, :frontend, :identity, :timeout 10 | 11 | def initialize sid, config = {} 12 | @frontend = config[:frontend] || Configuration.default.frontend 13 | @sid = sid.to_s.upcase 14 | @identity = config[:identity] || "client" 15 | @timeout = config[:timeout] || 1000 16 | @config = Hashie::Mash.new( 17 | socket_address: frontend, 18 | identity: identity, 19 | timeout: timeout 20 | ) 21 | end 22 | 23 | def call verb, payload, headers: {}, timeout: nil 24 | action = verb.to_s.upcase 25 | address = Message::Address.new(sid: sid, verb: action) 26 | 27 | request = Message.new( 28 | address: address, 29 | headers: headers, 30 | payload: payload) 31 | 32 | timeout ||= config.timeout 33 | metadata = metadata(timeout, request) 34 | log.info("Request #{request.rid} sent to #{request.address} with #{timeout/1000.0}s timeout", metadata) 35 | 36 | response = socket.call(request, timeout) 37 | metadata = metadata(timeout, response) 38 | 39 | log.info("Received response to #{request.rid} with status #{response.status}", metadata) 40 | 41 | fail ZSS::Error.new(response.status, payload: response.payload) if response.is_error? 42 | 43 | response.payload 44 | end 45 | 46 | private 47 | 48 | attr_reader :config 49 | 50 | def method_missing method, *args 51 | # since we cannot use / on method names we replace _ with / 52 | verb = method.to_s.gsub('_', '/') 53 | payload = args[0] 54 | options = args[1] || {} 55 | call verb, payload, options 56 | end 57 | 58 | def socket 59 | Socket.new config 60 | end 61 | 62 | def metadata(timeout, message) 63 | metadata = { 64 | identity: identity, 65 | timeout: timeout, 66 | pid: Process.pid, 67 | request: message.to_log 68 | } 69 | metadata 70 | end 71 | 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/zss/configuration.rb: -------------------------------------------------------------------------------- 1 | module ZSS 2 | class Configuration 3 | 4 | def self.default 5 | Hashie::Mash.new( 6 | frontend: 'tcp://127.0.0.1:7777', 7 | backend: 'tcp://127.0.0.1:7776' 8 | ) 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/zss/environment.rb: -------------------------------------------------------------------------------- 1 | module ZSS 2 | module Environment 3 | 4 | extend self 5 | 6 | def env 7 | environment = ENV['ZSS_ENV'] || 'development' 8 | ActiveSupport::StringInquirer.new environment 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/zss/error.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module ZSS 4 | class Error < ::StandardError 5 | 6 | attr_reader :code, :user_message 7 | attr_accessor :developer_message 8 | 9 | def initialize(code = 500, developer_message = nil, payload: nil) 10 | 11 | if payload 12 | fail "Invalid error code: #{code}" if code.blank? 13 | else 14 | data = self.class.get_errors[code.to_s] 15 | payload = data.body if data 16 | end 17 | 18 | fail "Invalid error with code: #{code}" unless payload 19 | 20 | @code = code.to_i 21 | @developer_message = developer_message || payload.developerMessage 22 | @user_message = payload.userMessage 23 | super @developer_message 24 | set_backtrace caller 25 | end 26 | 27 | def self.[](code) 28 | data = get_errors[code.to_s] 29 | Error.new(code.to_i, data.body) 30 | end 31 | 32 | private 33 | 34 | def self.get_errors 35 | @errors ||= begin 36 | path = File.join( 37 | File.dirname(File.absolute_path(__FILE__)), 38 | 'errors.json' 39 | ) 40 | file = File.read(path) 41 | Hashie::Mash.new(JSON.parse(file)) 42 | end 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/zss/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "400": { 3 | "code": 400, 4 | "body": { 5 | "developerMessage": "The request cannot be fulfilled due to bad syntax.", 6 | "userMessage": "An error occured", 7 | "errorCode": 400 8 | } 9 | }, 10 | "401": { 11 | "code": 401, 12 | "body": { 13 | "developerMessage": "User authentication token has expired or is missing", 14 | "userMessage": "This resource is only available after logging in.", 15 | "errorCode": 401 16 | } 17 | }, 18 | "403": { 19 | "code": 403, 20 | "body": { 21 | "developerMessage": "User does not have enough privileges to access this resource.", 22 | "userMessage": "You do not have access to this resource.", 23 | "errorCode": 403 24 | }, 25 | "headers": {} 26 | }, 27 | "404": { 28 | "code": 404, 29 | "body": { 30 | "developerMessage": "The resource could not be found.", 31 | "userMessage": "The content you requested was not found.", 32 | "errorCode": 404 33 | } 34 | }, 35 | "409": { 36 | "code": 409, 37 | "body": { 38 | "developerMessage": "There is a conflict with the resource you are trying to access.", 39 | "userMessage": "You cannot perform this action on the current resource due to a conflict.", 40 | "errorCode": 409 41 | } 42 | }, 43 | "410": { 44 | "code": 410, 45 | "body": { 46 | "developerMessage": "The resource is no longer available and will not be available again.", 47 | "userMessage": "The content you request is no longer available.", 48 | "errorCode": 410 49 | } 50 | }, 51 | "429": { 52 | "code": 429, 53 | "body": { 54 | "developerMessage": "You have sent too many requests in a given amount of time.", 55 | "userMessage": "Please wait a while before trying to access this content again", 56 | "errorCode": 429 57 | } 58 | }, 59 | "500": { 60 | "code": 500, 61 | "body": { 62 | "developerMessage": "There was an error while processing this request. There is probably something wrong with the API server.", 63 | "userMessage": "There was an error while processing this request.", 64 | "errorCode": 500 65 | } 66 | }, 67 | "599": { 68 | "code": 599, 69 | "body": { 70 | "developerMessage": "Connection timeout while processing this request.", 71 | "userMessage": "Connection timeout while processing this request.", 72 | "errorCode": 599 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/zss/message.rb: -------------------------------------------------------------------------------- 1 | require 'msgpack' 2 | require 'hashie' 3 | 4 | require_relative 'message/message_type' 5 | require_relative 'message/message_address' 6 | 7 | module ZSS 8 | class Message 9 | 10 | CLIENT_ID_REGEX = /^(.+?)#/ 11 | 12 | PROTOCOL_VERSION = "ZSS:0.0" 13 | 14 | attr_accessor :client, 15 | :identity, 16 | :protocol, 17 | :type, 18 | :rid, 19 | :address, 20 | :headers, 21 | :status, 22 | :payload, 23 | :payload_size 24 | 25 | def initialize(args = {}) 26 | 27 | @identity = args[:identity] 28 | @protocol = args[:protocol] || PROTOCOL_VERSION 29 | @type = args[:type] || Type::REQ 30 | @rid = args[:rid] || SecureRandom.uuid 31 | @address = args[:address] 32 | @headers = args[:headers] || {} 33 | @status = args[:status] 34 | @payload = args[:payload] 35 | @client = nil 36 | @payload_size = args[:payload_size] 37 | 38 | match = identity.try(:match, CLIENT_ID_REGEX) 39 | @client = match.captures.first if match 40 | end 41 | 42 | def payload=(payload) 43 | @payload = payload 44 | @payload_msgpack_data = nil 45 | @payload_size = payload_msgpack.length 46 | end 47 | 48 | def req? 49 | type == Type::REQ 50 | end 51 | 52 | def self.parse(frames) 53 | 54 | frames.unshift(nil) if frames.length == 7 55 | 56 | payload_data = frames[7] 57 | payload_size = payload_data.length 58 | payload = MessagePack.unpack(payload_data) 59 | payload = Hashie::Mash.new(payload) if payload.kind_of? Hash 60 | 61 | msg = Message.new( 62 | identity: frames.shift, 63 | protocol: frames.shift, 64 | type: frames.shift, 65 | rid: frames.shift, 66 | address: Address.new( 67 | MessagePack.unpack(frames.shift).with_indifferent_access 68 | ), 69 | headers: Hashie::Mash.new(MessagePack.unpack(frames.shift)), 70 | status: frames.shift.to_i, 71 | payload: payload, 72 | payload_size: payload_size 73 | ) 74 | 75 | msg 76 | end 77 | 78 | def to_s 79 | <<-out 80 | FRAME 0: 81 | IDENTITY : #{identity} 82 | FRAME 1: 83 | PROTOCOL : #{protocol} 84 | FRAME 2: 85 | TYPE : #{type} 86 | FRAME 3: 87 | RID : #{rid} 88 | FRAME 4: 89 | SID : #{address.sid} 90 | VERB : #{address.verb} 91 | SVERSION : #{address.sversion} 92 | FRAME 5: 93 | HEADERS : #{headers.to_h} 94 | FRAME 6: 95 | STATUS : #{status} 96 | FRAME 7: 97 | PAYLOAD : #{payload} 98 | out 99 | end 100 | 101 | def to_frames 102 | [ 103 | identity, 104 | protocol, 105 | type, 106 | rid, 107 | address.instance_values.to_msgpack, 108 | headers.to_h.to_msgpack, 109 | status.to_s, 110 | payload_msgpack 111 | ] 112 | end 113 | 114 | def to_log 115 | { 116 | client: client, 117 | identity: identity, 118 | protocol: protocol, 119 | type: type, 120 | rid: rid, 121 | address: address, 122 | headers: headers, 123 | status: status, 124 | payload: big? ? "<>" : payload, 125 | "payload-size" => payload_size 126 | } 127 | end 128 | 129 | def is_error? 130 | status != 200 131 | end 132 | 133 | def big? 134 | payload_size = payload_msgpack.length unless payload_size 135 | payload_size > 1024 136 | end 137 | 138 | def payload_msgpack 139 | # this will avoid executing multiple serializations 140 | @payload_msgpack_data ||= payload.to_msgpack 141 | end 142 | 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /lib/zss/message/message_address.rb: -------------------------------------------------------------------------------- 1 | module ZSS 2 | 3 | class Message 4 | 5 | class Address 6 | attr_accessor :sid, 7 | :sversion, 8 | :verb 9 | 10 | def initialize args = {} 11 | @sid = args[:sid].try(:upcase) 12 | @verb = args[:verb].try(:upcase) 13 | @sversion = args[:sversion].try(:upcase) || '*' 14 | end 15 | 16 | def to_s 17 | "#{sid}:#{sversion}##{verb}" 18 | end 19 | 20 | end 21 | 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/zss/message/message_type.rb: -------------------------------------------------------------------------------- 1 | module ZSS 2 | 3 | class Message 4 | 5 | class Type 6 | REQ = "REQ" 7 | REP = "REP" 8 | end 9 | 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/zss/message/smi.rb: -------------------------------------------------------------------------------- 1 | module ZSS 2 | 3 | class Message 4 | 5 | class SMI 6 | 7 | SMI = 'SMI' 8 | 9 | def self.down sid 10 | Message.new( 11 | address: Message::Address.new( 12 | sid: SMI, 13 | verb: 'DOWN' 14 | ), 15 | payload: sid 16 | ) 17 | end 18 | 19 | def self.up sid 20 | Message.new( 21 | address: Message::Address.new( 22 | sid: SMI, 23 | verb: 'UP' 24 | ), 25 | payload: sid 26 | ) 27 | end 28 | 29 | def self.heartbeat sid 30 | Message.new( 31 | address: Message::Address.new( 32 | sid: SMI, 33 | verb: 'HEARTBEAT' 34 | ), 35 | payload: sid 36 | ) 37 | end 38 | 39 | end 40 | 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/zss/permit.rb: -------------------------------------------------------------------------------- 1 | module ZSS 2 | module Permit 3 | 4 | class Permitter 5 | 6 | def self.permit! params, attributes 7 | params.keep_if do |k, _| 8 | Array(attributes).include? k.to_sym 9 | end 10 | end 11 | 12 | end 13 | 14 | def permit! params, *attributes 15 | Permitter.permit!(params, attributes) 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/zss/require.rb: -------------------------------------------------------------------------------- 1 | module ZSS 2 | module Require 3 | 4 | class Requirer 5 | 6 | def self.requires params, attributes 7 | Array(attributes).each do |attribute| 8 | unless params[attribute].present? 9 | raise ZSS::Error.new(400, "Invalid parameter '#{attribute}'") 10 | end 11 | end 12 | end 13 | 14 | end 15 | 16 | def requires params, *attributes 17 | Requirer.requires(params, attributes) 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/zss/router.rb: -------------------------------------------------------------------------------- 1 | module ZSS 2 | class Router 3 | 4 | def add(context, route, handler = nil) 5 | 6 | fail "Invalid context!" unless context 7 | fail "Invalid route: #{route}" unless route 8 | 9 | handler ||= route.to_sym 10 | 11 | fail "Invalid handler: #{handler}" unless context.respond_to? handler 12 | 13 | routes[route.to_s.upcase] = get_proc(context, handler) 14 | end 15 | 16 | def get(route) 17 | handler = routes[route.to_s.upcase] 18 | return handler if handler 19 | 20 | error = Error[404] 21 | error.developer_message = "Invalid route #{route}!" 22 | fail error 23 | end 24 | 25 | private 26 | 27 | def routes 28 | @routes ||= {} 29 | end 30 | 31 | def get_proc(context, handler) 32 | receive_headers = context.method(handler).parameters.size == 2 33 | 34 | if receive_headers 35 | Proc.new { |p,h| context.send(handler, p, h) } 36 | else 37 | Proc.new { |p,h| context.send(handler, p) } 38 | end 39 | end 40 | 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/zss/runner.rb: -------------------------------------------------------------------------------- 1 | require 'daemons' 2 | 3 | module ZSS 4 | class Runner 5 | 6 | def self.run(proc_name) 7 | proc_name = proc_name.to_s 8 | pid_path = log_path = './log' 9 | 10 | FileUtils.mkdir_p pid_path 11 | FileUtils.mkdir_p log_path 12 | 13 | daemon_opts = { 14 | multiple: true, 15 | dir_mode: :normal, 16 | dir: pid_path, 17 | log_output: true, 18 | stop_proc: lambda do 19 | puts "stop #{proc_name} daemon..." 20 | $stop_requested = true 21 | end 22 | } 23 | 24 | puts "Starting #{proc_name}:\n\tPID: #{pid_path}\n\tLOGS: #{log_path}" 25 | 26 | Daemons.run_proc proc_name, daemon_opts do 27 | daemon = if ZSS::ServiceRegister.respond_to?(:get_services) 28 | daemons = ZSS::ServiceRegister.get_services 29 | daemons.find { |daemon| daemon.sid.downcase == proc_name } 30 | else 31 | ZSS::ServiceRegister.get_service 32 | end 33 | 34 | if daemon.nil? 35 | puts "Daemon #{proc_name} not found!" 36 | exit 1 37 | end 38 | puts "Started #{proc_name} daemon..." 39 | daemon.run 40 | 41 | puts "Stoping #{proc_name} daemon" 42 | exit 0 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/zss/service.rb: -------------------------------------------------------------------------------- 1 | require 'em-zeromq' 2 | require_relative '../zss' 3 | require_relative 'router' 4 | require_relative 'message/smi' 5 | 6 | module ZSS 7 | class Service 8 | 9 | include LoggerFacade::Loggable 10 | 11 | attr_reader :sid, :heartbeat, :backend, :identity 12 | 13 | def initialize(sid, config = {}) 14 | 15 | fail Error[500] if sid.blank? 16 | 17 | @sid = sid.to_s.upcase 18 | @heartbeat = config.try(:heartbeat) || 1000 19 | @backend = config.try(:backend) || Configuration.default.backend 20 | @router = ZSS::Router.new 21 | @identity = "#{sid}##{SecureRandom.uuid}" 22 | end 23 | 24 | def run 25 | Thread.abort_on_exception = true 26 | 27 | context = EM::ZeroMQ::Context.new(1) 28 | fail RuntimeError, 'failed to create create_context' unless context 29 | 30 | log.info("Starting SID: '#{sid}' ID: '#{identity}' Env: '#{ZSS::Environment.env}' Broker: '#{backend}'") 31 | event_machine_start(context) 32 | end 33 | 34 | def add_route(context, route, handler = nil) 35 | router.add(context, route, handler) 36 | end 37 | 38 | def stop 39 | timer.cancel if timer 40 | 41 | EM.add_timer do 42 | 43 | log.info("Stoping SID: '#{sid}' ID: '#{identity}'", metadata) 44 | 45 | send Message::SMI.down(sid) 46 | socket.disconnect backend 47 | EM::stop 48 | end 49 | end 50 | 51 | private 52 | 53 | attr_accessor :socket, :router, :timer 54 | 55 | def event_machine_start(context) 56 | EM.run do 57 | handle_interrupts 58 | 59 | connect_socket context 60 | 61 | start_heartbeat_worker 62 | 63 | # send up message 64 | send Message::SMI.up(sid) 65 | end 66 | end 67 | 68 | def handle_interrupts 69 | Signal.trap("INT") { stop } 70 | Signal.trap("TERM") { stop } 71 | end 72 | 73 | def connect_socket(context) 74 | 75 | @socket = context.socket ZMQ::DEALER 76 | fail RuntimeError, 'failed to create socket' unless socket 77 | 78 | socket.identity = identity 79 | socket.setsockopt(ZMQ::LINGER, 0) 80 | socket.on(:message, &method(:handle_frames)) 81 | 82 | socket.connect(backend) 83 | end 84 | 85 | def start_heartbeat_worker 86 | @timer = EventMachine::PeriodicTimer.new(heartbeat / 1000) do 87 | send Message::SMI.heartbeat(sid) 88 | end 89 | end 90 | 91 | def handle_frames(*frames) 92 | # we need to close frame to avoid memory leaks 93 | frames = frames.map do |frame| 94 | out_frame = frame.copy_out_string 95 | frame.close 96 | out_frame 97 | end 98 | 99 | handle Message.parse(frames) 100 | end 101 | 102 | def handle(message) 103 | EM.defer do 104 | begin 105 | if message.req? 106 | handle_request(message) 107 | else 108 | context = request_metadata(message) 109 | log.trace("SMI response received: \n #{message}", context) if log.is_debug 110 | end 111 | rescue ZSS::Error => error 112 | if error.code >= 400 and error.code < 500 113 | log.info("ZSS::Error raised while processing request: #{error}", 114 | metadata({ error: error })) 115 | end 116 | 117 | reply_error error, message 118 | rescue => e 119 | log.error("Unexpected error occurred while processing request: #{e}", metadata({ exception: e })) 120 | reply_error Error[500], message 121 | end 122 | end 123 | end 124 | 125 | def handle_request(message) 126 | start_time = Time.now.utc 127 | 128 | log.info("Handle request for #{message.address}", request_metadata(message)) 129 | log.trace("Request message:\n #{message}") if log.is_debug 130 | 131 | check_sid!(message.address.sid) 132 | 133 | # the router returns an handler that receives payload and headers 134 | handler = router.get(message.address.verb) 135 | 136 | message.payload = handler.call(message.payload, message.headers) 137 | message.headers["zss-response-time"] = get_time(start_time) 138 | 139 | reply message 140 | end 141 | 142 | def reply_error(error, message) 143 | message.status = error.code 144 | message.payload = { 145 | errorCode: error.code, 146 | userMessage: error.user_message, 147 | developerMessage: error.developer_message 148 | } 149 | message.type = Message::Type::REP 150 | 151 | log.info("Reply with status: #{message.status}", request_metadata(message)) 152 | 153 | send message 154 | end 155 | 156 | def reply(message) 157 | message.status = 200 158 | message.type = Message::Type::REP 159 | 160 | log.info("Reply with status: #{message.status}", request_metadata(message)) 161 | log.trace("Reply with message:\n #{message}") if log.is_debug 162 | 163 | send message 164 | end 165 | 166 | def send(msg) 167 | log.trace("sending: \n #{msg}") if log.is_debug 168 | 169 | frames = msg.to_frames 170 | #remove identity frame on request 171 | frames.shift if msg.req? 172 | success = socket.send_msg(*frames) 173 | 174 | log.error("An Error ocurred while sending message", request_metadata(message)) unless success 175 | end 176 | 177 | def check_sid!(message_sid) 178 | return unless message_sid != sid 179 | 180 | error = Error[404] 181 | error.developer_message = "Invalid SID: #{message_sid}!" 182 | fail error 183 | end 184 | 185 | def metadata(metadata = {}) 186 | metadata ||= {} 187 | metadata[:sid] = sid 188 | metadata[:identity] = identity 189 | metadata[:pid] = Process.pid 190 | metadata 191 | end 192 | 193 | def request_metadata(message, metadata = {}) 194 | metadata = metadata(metadata) 195 | 196 | metadata[:request] = message.to_log 197 | metadata 198 | end 199 | 200 | def get_time(start_time) 201 | # return time in ms 202 | ((Time.now.utc - start_time) * 1_000).to_i 203 | end 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /lib/zss/socket.rb: -------------------------------------------------------------------------------- 1 | require 'ffi-rzmq' 2 | require 'timeout' 3 | 4 | module ZSS 5 | class Socket 6 | 7 | include LoggerFacade::Loggable 8 | 9 | class Error < StandardError; end 10 | class TimeoutError < Socket::Error; end 11 | 12 | attr_reader :timeout, :socket_address, :identity 13 | 14 | def initialize config 15 | @identity = config.identity 16 | @timeout = config.timeout || 1000 17 | @socket_address = config.socket_address 18 | end 19 | 20 | def call request, call_timeout = nil 21 | fail Socket::Error, 'invalid request' unless request 22 | 23 | response = nil 24 | t = (call_timeout || timeout) / 1000.0 25 | 26 | context do |ctx| 27 | socket ctx do |sock| 28 | begin 29 | ::Timeout.timeout t do 30 | log.trace("Request #{request.rid} sent to #{request.address} with #{t}s timeout") 31 | 32 | send_message sock, request 33 | 34 | log.trace("Waiting for #{request.rid}") 35 | response = receive_message(sock) 36 | 37 | end 38 | rescue ::Timeout::Error 39 | log.info("Request #{request.rid} exit with timeout after #{t}s") 40 | raise ZSS::Socket::TimeoutError, "call timeout after #{t}s" 41 | end 42 | end 43 | end 44 | 45 | response 46 | end 47 | 48 | private 49 | 50 | def context 51 | ctx = ZMQ::Context.create(1) 52 | fail Socket::Error, 'failed to create create_context' unless ctx 53 | yield ctx 54 | ensure 55 | check!(ctx.terminate) if ctx 56 | end 57 | 58 | def socket(context) 59 | socket = context.socket ZMQ::DEALER 60 | fail Socket::Error, 'failed to create socket' unless socket 61 | socket.identity = "#{identity}##{SecureRandom.uuid}" 62 | socket.setsockopt(ZMQ::LINGER, 0) 63 | socket.connect(socket_address) 64 | 65 | log.trace("#{socket.identity} connected to #{socket_address}") 66 | 67 | yield socket 68 | ensure 69 | check!(socket.close) if socket 70 | end 71 | 72 | def send_message socket, message 73 | 74 | log.trace("Sending:\n #{message}") if log.is_debug 75 | 76 | frames = message.to_frames 77 | 78 | # if it's a reply should send identity 79 | frames.shift if message.req? 80 | last = frames.pop 81 | frames.each { |f| check! socket.send_string f.to_s, ZMQ::SNDMORE } 82 | check! socket.send_string last.to_s 83 | end 84 | 85 | def receive_message socket 86 | check! socket.recv_strings(frames = []) 87 | message = Message.parse frames 88 | 89 | log.trace("Receiving: \n #{message}") if log.is_debug 90 | 91 | message 92 | end 93 | 94 | def check! result_code 95 | return if ZMQ::Util.resultcode_ok? result_code 96 | 97 | fail Socket::Error, "operation failed, errno [#{ZMQ::Util.errno}], " + 98 | "description [#{ZMQ::Util.error_string}]" 99 | end 100 | 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/zss/validate.rb: -------------------------------------------------------------------------------- 1 | module ZSS 2 | module Validate 3 | 4 | class Validator 5 | 6 | def self.is_valid_uri?(uri) 7 | !URI::parse(uri).relative? 8 | rescue 9 | false 10 | end 11 | 12 | end 13 | 14 | def is_valid_uri?(uri) 15 | Validator.is_valid_uri?(uri) 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/zss/version.rb: -------------------------------------------------------------------------------- 1 | module ZSS 2 | VERSION = '0.3.4' 3 | end 4 | -------------------------------------------------------------------------------- /spec/integration/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'spec_broker_helper' 3 | require 'zss/client' 4 | 5 | describe ZSS::Client do 6 | include BrokerHelper 7 | 8 | let(:broker_frontend) { "ipc://socket_test" } 9 | 10 | let(:config) do 11 | Hashie::Mash.new( 12 | frontend: broker_frontend, 13 | identity: "spec_client", 14 | timeout: 500 15 | ) 16 | end 17 | 18 | after :each do 19 | return unless @broker 20 | @broker.join 21 | @broker = nil 22 | end 23 | 24 | subject do 25 | described_class.new(:pong, config) 26 | end 27 | 28 | it('returns service response') do 29 | @broker = run_broker_for_client(broker_frontend) do |msg| 30 | expect(msg.payload).to eq("ping") 31 | msg.status = 200 32 | msg.payload = "PONG" 33 | end 34 | 35 | result = subject.ping("ping", headers: { something: "data" }) 36 | expect(result).to eq("PONG") 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /spec/integration/service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'spec_broker_helper' 3 | require 'em-zeromq' 4 | require 'zss/service' 5 | 6 | describe ZSS::Service do 7 | include BrokerHelper 8 | 9 | class DummyService 10 | def ping payload, headers 11 | headers[:took] = "0s" 12 | return "PONG" 13 | end 14 | end 15 | 16 | let(:broker_backend) { "ipc://socket_test" } 17 | 18 | let(:config) do 19 | Hashie::Mash.new( 20 | backend: broker_backend, 21 | heartbeat: 2000 22 | ) 23 | end 24 | 25 | let(:address) { ZSS::Message::Address.new(sid: "pong", verb: "ping") } 26 | 27 | let(:message) { ZSS::Message.new(address: address, payload: "PING") } 28 | 29 | subject { described_class.new(:pong, config) } 30 | 31 | it('handles request') do 32 | EM.run do 33 | run_broker_for_service(broker_backend, message) do |msg| 34 | expect(msg.payload).to eq("PONG") 35 | expect(msg.status).to eq(200) 36 | expect(msg.headers.took).to eq("0s") 37 | subject.stop 38 | EM.stop 39 | end 40 | 41 | EM.defer do 42 | service = DummyService.new 43 | subject.add_route(service, :ping) 44 | subject.run 45 | end 46 | end 47 | 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /spec/integration/socket_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'spec_broker_helper' 3 | require 'zss/socket' 4 | 5 | describe ZSS::Socket do 6 | include BrokerHelper 7 | 8 | let(:broker_frontend) { "ipc://socket_test" } 9 | 10 | let(:config) { Hashie::Mash.new(socket_address: broker_frontend) } 11 | 12 | let(:address) { ZSS::Message::Address.new(sid: "service", verb: "verb") } 13 | 14 | let(:message) { ZSS::Message.new(address: address, payload: "PING") } 15 | 16 | after :each do 17 | return unless @broker 18 | @broker.join 19 | @broker = nil 20 | end 21 | 22 | describe("#call") do 23 | 24 | subject { ZSS::Socket.new(config) } 25 | 26 | it('returns service response') do 27 | @broker = run_broker_for_client(broker_frontend) do |msg| 28 | msg.status = 200 29 | msg.payload = "PONG" 30 | end 31 | 32 | result = subject.call(message) 33 | expect(result).to be_truthy 34 | expect(result.payload).to eq("PONG") 35 | expect(result.status).to eq(200) 36 | end 37 | 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/spec_broker_helper.rb: -------------------------------------------------------------------------------- 1 | require 'em-zeromq' 2 | require 'msgpack' 3 | 4 | module BrokerHelper 5 | 6 | LOG_BROKER_INFO = false 7 | 8 | def run_broker_for_client(endpoint) 9 | 10 | Thread.new do 11 | puts "broker running" if LOG_BROKER_INFO 12 | 13 | Thread.handle_interrupt(RuntimeError => :immediate) do 14 | 15 | begin 16 | context = ZMQ::Context.create 17 | socket = context.socket ZMQ::ROUTER 18 | socket.setsockopt(ZMQ::LINGER, 0) 19 | socket.bind(endpoint) 20 | puts "broker connected" if LOG_BROKER_INFO 21 | 22 | socket.recv_strings(frames = []) 23 | 24 | puts "broker received #{frames}" if LOG_BROKER_INFO 25 | 26 | if block_given? 27 | msg = ZSS::Message.parse(frames) 28 | 29 | yield(msg) 30 | 31 | frames = msg.to_frames 32 | socket.send_strings(frames) 33 | 34 | puts "broker reply #{frames}" if LOG_BROKER_INFO 35 | end 36 | rescue => e 37 | puts "WTF => #{e}" 38 | ensure 39 | # You can write resource deallocation code safely. 40 | puts "broker stoping" if LOG_BROKER_INFO 41 | socket.close if socket 42 | context.terminate if context 43 | end 44 | end 45 | 46 | end 47 | 48 | end 49 | 50 | def run_broker_for_service(endpoint, send_message = nil) 51 | context = EM::ZeroMQ::Context.new(1) 52 | socket = context.socket(ZMQ::ROUTER) 53 | socket.setsockopt(ZMQ::LINGER, 0) 54 | socket.on(:message) do |*frames| 55 | frames = get_frames_and_close_message(frames) 56 | puts "broker received #{frames}" if LOG_BROKER_INFO 57 | service_id = frames.shift if frames.length == 9 58 | msg = ZSS::Message.parse(frames) 59 | 60 | if msg.address.sid == 'SMI' 61 | handle_smi_verbs(socket, msg, send_message) 62 | else 63 | puts "check service response" if LOG_BROKER_INFO 64 | yield(msg) if block_given? 65 | end 66 | 67 | end 68 | 69 | socket.bind(endpoint) 70 | end 71 | 72 | private 73 | 74 | def handle_smi_verbs(socket, msg, send_message) 75 | 76 | if msg.address.verb == 'UP' 77 | msg.status = 200 78 | msg.type = ZSS::Message::Type::REP 79 | puts "broker reply to UP" if LOG_BROKER_INFO 80 | socket.send_msg(*msg.to_frames) 81 | 82 | if send_message 83 | send_message.identity = msg.identity 84 | puts "broker sending emulated client request" if LOG_BROKER_INFO 85 | # emulate client request 86 | socket.send_msg(*send_message.to_frames) 87 | end 88 | 89 | end 90 | end 91 | 92 | def get_frames_and_close_message(frames) 93 | frames.map do |frame| 94 | out_frame = frame.copy_out_string 95 | frame.close 96 | out_frame 97 | end 98 | end 99 | 100 | end 101 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['ZSS_ENV'] ||= 'test' 2 | 3 | if ENV['CODECLIMATE_REPO_TOKEN'] 4 | require "codeclimate-test-reporter" 5 | CodeClimate::TestReporter.start 6 | else 7 | require 'simplecov' 8 | SimpleCov.start do 9 | add_filter '/spec/' 10 | end 11 | end 12 | 13 | $:.push '.' 14 | 15 | require 'rspec' 16 | require 'pry' 17 | require 'zss' 18 | require 'timeout' 19 | 20 | Dir['spec/support/**/*.rb'].each &method(:require) 21 | -------------------------------------------------------------------------------- /spec/unit/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zss/client' 3 | 4 | describe ZSS::Client do 5 | 6 | describe('#ctor') do 7 | 8 | it('returns a client with default config') do 9 | subject = described_class.new(:pong) 10 | expect(subject.frontend).to eq(ZSS::Configuration.default.frontend) 11 | expect(subject.identity).to eq("client") 12 | expect(subject.timeout).to eq(1000) 13 | end 14 | 15 | it('returns a client with config') do 16 | config = Hashie::Mash.new( 17 | frontend: "socket", 18 | identity: "identity", 19 | timeout: 2000 20 | ) 21 | subject = described_class.new(:pong, config) 22 | expect(subject.frontend).to eq("socket") 23 | expect(subject.identity).to eq("identity") 24 | expect(subject.timeout).to eq(2000) 25 | end 26 | 27 | it('returns a client with sid') do 28 | subject = described_class.new(:pong) 29 | expect(subject.sid).to eq("PONG") 30 | end 31 | 32 | end 33 | 34 | describe("#call") do 35 | 36 | subject do 37 | described_class.new(:pong) 38 | end 39 | 40 | it('creates a socket with configs') do 41 | socket = double('socket').as_null_object 42 | allow(ZSS::Socket).to receive(:new) { socket } 43 | allow(socket).to receive(:call) do |msg| 44 | msg.status = 200 45 | msg 46 | end 47 | expect(ZSS::Socket).to receive(:new) 48 | .with( 49 | Hashie::Mash.new( 50 | socket_address: ZSS::Configuration.default.frontend, 51 | identity: "client", 52 | timeout: 1000 53 | ) 54 | ) 55 | subject.call(:ping, nil) 56 | end 57 | 58 | context('on success') do 59 | 60 | it('calls pong service') do 61 | socket = double('socket') 62 | allow(ZSS::Socket).to receive(:new) { socket } 63 | expect(socket).to receive(:call) do |msg| 64 | expect(msg).to be_truthy 65 | expect(msg.address.sid).to eq("PONG") 66 | expect(msg.address.verb).to eq("PING") 67 | expect(msg.payload).to eq("ping") 68 | msg.status = 200 69 | msg 70 | end 71 | result = subject.call(:ping, "ping") 72 | end 73 | 74 | it('calls pong service with headers') do 75 | socket = double('socket') 76 | allow(ZSS::Socket).to receive(:new) { socket } 77 | expect(socket).to receive(:call) do |msg| 78 | expect(msg).to be_truthy 79 | expect(msg.headers[:something]).to eq("something") 80 | msg.status = 200 81 | msg 82 | end 83 | result = subject.call(:ping, "ping", headers: { something: "something" }) 84 | end 85 | 86 | it('returns service message from pong service') do 87 | socket = double('socket') 88 | allow(ZSS::Socket).to receive(:new) { socket } 89 | allow(socket).to receive(:call) do |msg| 90 | msg.payload = "pong" 91 | msg.status = 200 92 | msg 93 | end 94 | result = subject.call(:ping, "ping") 95 | expect(result).to eq("pong") 96 | end 97 | 98 | context('with implicit method implementation') do 99 | 100 | it('returns service message from pong service') do 101 | socket = double('socket') 102 | allow(ZSS::Socket).to receive(:new) { socket } 103 | allow(socket).to receive(:call) do |msg| 104 | msg.payload = "pong" 105 | msg.status = 200 106 | msg 107 | end 108 | result = subject.ping("ping") 109 | expect(result).to eq("pong") 110 | end 111 | 112 | it('calls pong service with special verb') do 113 | socket = double('socket') 114 | allow(ZSS::Socket).to receive(:new) { socket } 115 | expect(socket).to receive(:call) do |msg| 116 | expect(msg).to be_truthy 117 | expect(msg.address.sid).to eq("PONG") 118 | expect(msg.address.verb).to eq("PONG/PING") 119 | 120 | msg.payload = "pong" 121 | msg.status = 200 122 | msg 123 | end 124 | result = subject.pong_ping("ping") 125 | expect(result).to eq("pong") 126 | end 127 | 128 | end 129 | 130 | end 131 | 132 | context('on error') do 133 | 134 | it('raise service error') do 135 | socket = double('socket') 136 | allow(ZSS::Socket).to receive(:new) { socket } 137 | allow(socket).to receive(:call) do |msg| 138 | msg.payload = Hashie::Mash.new( 139 | userMessage: "user info", 140 | developerMessage: "dev info" 141 | ) 142 | msg.status = 999 143 | msg 144 | end 145 | 146 | expect { subject.call(:ping, "ping") }.to raise_exception(ZSS::Error) do |error| 147 | expect(error.code).to eq(999) 148 | end 149 | end 150 | 151 | end 152 | 153 | end 154 | 155 | end 156 | -------------------------------------------------------------------------------- /spec/unit/environment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ZSS::Environment do 4 | 5 | it('returns environment string') do 6 | expect(subject.env).to eq('test') 7 | end 8 | 9 | it('returns true when query by current env') do 10 | expect(subject.env.test?).to be true 11 | end 12 | 13 | it('returns false when query for different env') do 14 | expect(subject.env.production?).to be false 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/unit/error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ZSS::Error do 4 | 5 | let! :response do 6 | Hashie::Mash.new(userMessage: "user info", developerMessage: "dev info") 7 | end 8 | 9 | describe('#ctor') do 10 | 11 | context('on invalid code') do 12 | 13 | it('raises an exception when code is nil') do 14 | expect{ described_class.new(nil, payload: response) }.to raise_exception(RuntimeError) 15 | end 16 | 17 | it('raises an exception when code does not exist on errors.json') do 18 | expect{ described_class.new(0) }.to raise_exception(RuntimeError) 19 | end 20 | 21 | end 22 | 23 | context('with payload') do 24 | 25 | subject do 26 | described_class.new(500, payload: response) 27 | end 28 | 29 | it('returns a fullfilled error.message') do 30 | expect(subject.code).to eq(500) 31 | expect(subject.developer_message).to eq("dev info") 32 | expect(subject.user_message).to eq("user info") 33 | end 34 | 35 | it('returns error with dev info as error.message') do 36 | expect(subject.message).to eq("dev info") 37 | end 38 | 39 | it('returns error with stacktrace') do 40 | expect(subject.backtrace).not_to be_nil 41 | end 42 | 43 | end 44 | 45 | context('with default error') do 46 | 47 | subject { described_class.new } 48 | 49 | it('returns an error') do 50 | expect(subject.code).to eq(500) 51 | expect(subject.developer_message).not_to be_nil 52 | expect(subject.user_message).not_to be_nil 53 | end 54 | 55 | end 56 | 57 | 58 | context('with default error') do 59 | 60 | subject { described_class.new(500) } 61 | 62 | it('returns an error') do 63 | expect(subject.code).to eq(500) 64 | expect(subject.developer_message).not_to be_nil 65 | expect(subject.user_message).not_to be_nil 66 | end 67 | 68 | 69 | context('with override on developer message') do 70 | 71 | subject { described_class.new(500, "dev info") } 72 | 73 | it('returns an error') do 74 | expect(subject.code).to eq(500) 75 | expect(subject.developer_message).to eq('dev info') 76 | end 77 | 78 | end 79 | end 80 | 81 | end 82 | 83 | describe('.[]') do 84 | 85 | it('returns the error') do 86 | result = described_class[500] 87 | expect(result.code).to eq(500) 88 | end 89 | 90 | end 91 | 92 | end 93 | -------------------------------------------------------------------------------- /spec/unit/message_address_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ZSS::Message::Address do 4 | 5 | describe('#ctor') do 6 | 7 | it('returns a address object with default version') do 8 | actual = described_class.new sid: "service", verb: "verb" 9 | expect(actual.sid).to eq("SERVICE") 10 | expect(actual.verb).to eq("VERB") 11 | expect(actual.sversion).to eq("*") 12 | end 13 | 14 | it('returns a address object with specific version') do 15 | actual = described_class.new sid: "service", verb: "verb", sversion: 'v.1' 16 | expect(actual.sid).to eq("SERVICE") 17 | expect(actual.verb).to eq("VERB") 18 | expect(actual.sversion).to eq("V.1") 19 | end 20 | 21 | end 22 | 23 | describe("#to_s") do 24 | 25 | it('returns a address in string format') do 26 | actual = described_class.new sid: "service", verb: "verb", sversion: 'v.1' 27 | expect(actual.to_s).to eq("SERVICE:V.1#VERB") 28 | end 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /spec/unit/message_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'msgpack' 3 | 4 | describe ZSS::Message do 5 | 6 | let(:address) { ZSS::Message::Address.new(sid: "service", verb: "verb") } 7 | 8 | let(:headers) { { headerval: "something" } } 9 | 10 | let :frames do 11 | [ 12 | "identity", 13 | ZSS::Message::PROTOCOL_VERSION, 14 | ZSS::Message::Type::REP, 15 | "RID", 16 | address.instance_values.to_msgpack, 17 | headers.to_msgpack, 18 | 200, 19 | "data".to_msgpack 20 | ] 21 | end 22 | 23 | let(:message) { described_class.parse(frames) } 24 | 25 | describe('#ctor') do 26 | 27 | it('returns a fullfilled message with defaults') do 28 | allow(SecureRandom).to receive(:uuid) { "uuid" } 29 | 30 | actual = described_class.new({ 31 | address: address, 32 | headers: headers, 33 | payload: "some data" 34 | }) 35 | 36 | expect(actual.identity).to be_nil 37 | expect(actual.protocol).to eq(ZSS::Message::PROTOCOL_VERSION) 38 | expect(actual.type).to eq(ZSS::Message::Type::REQ) 39 | expect(actual.rid).to eq("uuid") 40 | expect(actual.address).to eq(address) 41 | expect(actual.headers).to eq(headers) 42 | expect(actual.payload).to eq("some data") 43 | end 44 | 45 | end 46 | 47 | describe('#parse') do 48 | 49 | context('without identity') do 50 | 51 | let :frames do 52 | [ 53 | ZSS::Message::PROTOCOL_VERSION, 54 | ZSS::Message::Type::REP, 55 | "RID", 56 | address.instance_values.to_msgpack, 57 | headers.to_msgpack, 58 | "200", 59 | "data".to_msgpack 60 | ] 61 | end 62 | 63 | it('returns a fullfilled message') do 64 | 65 | address.sversion = "sversion".upcase 66 | 67 | actual = described_class.parse(frames) 68 | 69 | expect(actual.identity).to be_nil 70 | expect(actual.protocol).to eq(ZSS::Message::PROTOCOL_VERSION) 71 | expect(actual.type).to eq(ZSS::Message::Type::REP) 72 | expect(actual.rid).to eq("RID") 73 | expect(actual.address.sid).to eq(address.sid) 74 | expect(actual.address.verb).to eq(address.verb) 75 | expect(actual.address.sversion).to eq(address.sversion) 76 | expect(actual.headers["headerval"]).to eq(headers[:headerval]) 77 | expect(actual.status).to eq(200) 78 | expect(actual.payload).to eq("data") 79 | end 80 | 81 | end 82 | 83 | context('with identity') do 84 | 85 | it('returns a fullfilled message') do 86 | 87 | actual = described_class.parse(frames) 88 | 89 | expect(actual.identity).to eq("identity") 90 | expect(actual.protocol).to eq(ZSS::Message::PROTOCOL_VERSION) 91 | expect(actual.type).to eq(ZSS::Message::Type::REP) 92 | expect(actual.rid).to eq("RID") 93 | expect(actual.address.sid).to eq(address.sid) 94 | expect(actual.address.verb).to eq(address.verb) 95 | expect(actual.address.sversion).to eq(address.sversion) 96 | expect(actual.headers["headerval"]).to eq(headers[:headerval]) 97 | expect(actual.status).to eq(200) 98 | expect(actual.payload).to eq("data") 99 | end 100 | 101 | end 102 | 103 | it('returns a message with hashie headers') do 104 | 105 | actual = described_class.parse(frames) 106 | 107 | expect(actual.headers["headerval"]).to eq(headers[:headerval]) 108 | expect(actual.headers[:headerval]).to eq(headers[:headerval]) 109 | expect(actual.headers.headerval).to eq(headers[:headerval]) 110 | end 111 | 112 | it('returns a message with hashie payload') do 113 | frames[7] = { something: "data" }.to_msgpack 114 | actual = described_class.parse(frames) 115 | expect(actual.payload.something).to eq("data") 116 | expect(actual.payload[:something]).to eq("data") 117 | expect(actual.payload["something"]).to eq("data") 118 | end 119 | 120 | end 121 | 122 | describe('#to_s') do 123 | let :string_representation do 124 | <<-out 125 | FRAME 0: 126 | IDENTITY : #{message.identity} 127 | FRAME 1: 128 | PROTOCOL : #{message.protocol} 129 | FRAME 2: 130 | TYPE : #{message.type} 131 | FRAME 3: 132 | RID : #{message.rid} 133 | FRAME 4: 134 | SID : #{message.address.sid} 135 | VERB : #{message.address.verb} 136 | SVERSION : #{message.address.sversion} 137 | FRAME 5: 138 | HEADERS : #{message.headers.to_h} 139 | FRAME 6: 140 | STATUS : #{message.status} 141 | FRAME 7: 142 | PAYLOAD : #{message.payload} 143 | out 144 | end 145 | 146 | it('returns a string with formated frame') do 147 | expect(message.to_s).to eq(string_representation) 148 | end 149 | end 150 | 151 | describe('.to_frames') do 152 | 153 | let :output_frames do 154 | [ 155 | "identity", 156 | ZSS::Message::PROTOCOL_VERSION, 157 | ZSS::Message::Type::REP, 158 | "RID", 159 | address.instance_values.to_msgpack, 160 | headers.to_msgpack, 161 | "200", 162 | "data".to_msgpack 163 | ] 164 | end 165 | 166 | it('returns message in frames') do 167 | expect(message.to_frames).to eq(output_frames) 168 | end 169 | 170 | end 171 | 172 | describe('.is_error') do 173 | 174 | it('returns false when status is 200') do 175 | message.status = 200 176 | expect(message.is_error?).to eq false 177 | end 178 | 179 | it('returns true when status is not 200') do 180 | message.status = 500 181 | expect(message.is_error?).to eq true 182 | end 183 | 184 | end 185 | 186 | describe('.req?') do 187 | 188 | it('returns true on request message') do 189 | message.type = ZSS::Message::Type::REQ 190 | expect(message.req?).to eq(true) 191 | end 192 | 193 | it('returns false on response message') do 194 | message.type = ZSS::Message::Type::REP 195 | expect(message.req?).to eq(false) 196 | end 197 | 198 | end 199 | 200 | end 201 | -------------------------------------------------------------------------------- /spec/unit/permit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zss/permit' 3 | 4 | describe ZSS::Permit do 5 | 6 | let!(:params) do 7 | { arg1: 'somevalue', arg2: 'someothervalue' } 8 | end 9 | 10 | it 'filters all non permitted params' do 11 | ZSS::Permit::Permitter.permit!(params, [ :arg1 ]) 12 | expect( 13 | params 14 | ).to eq({ arg1: 'somevalue' }) 15 | end 16 | 17 | it 'can be included' do 18 | 19 | class SomeClass 20 | include ZSS::Permit 21 | end 22 | 23 | testObject = SomeClass.new 24 | testObject.permit!(params, :arg1) 25 | 26 | expect( 27 | params 28 | ).to eq({ arg1: 'somevalue' }) 29 | 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/require_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zss/require' 3 | 4 | describe ZSS::Require do 5 | 6 | it 'raises 400 if any attributes are missing' do 7 | 8 | expect{ 9 | ZSS::Require::Requirer.requires({ arg1: 'somevalue' }, [ :arg1, :arg2 ]) 10 | }.to raise_error(ZSS::Error) do |e| 11 | expect(e.code).to eq(400) 12 | end 13 | 14 | end 15 | 16 | it 'accepts when all params are set' do 17 | expect( 18 | ZSS::Require::Requirer.requires({ arg1: 'somevalue', arg2: 'someothervalue' }, [ :arg1, :arg2 ]) 19 | ).to be 20 | end 21 | 22 | it 'can be included' do 23 | 24 | class SomeClass 25 | include ZSS::Require 26 | end 27 | 28 | testObject = SomeClass.new 29 | 30 | expect{ 31 | testObject.requires({ arg1: 'somevalue' }, [ :arg1, :arg2 ]) 32 | }.to raise_error(ZSS::Error) do |e| 33 | expect(e.code).to eq(400) 34 | end 35 | 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /spec/unit/router_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zss/router' 3 | 4 | describe ZSS::Router do 5 | 6 | class Dummy 7 | 8 | def print(payload); end 9 | 10 | def print_with_headers(payload, header); end 11 | 12 | end 13 | 14 | subject { described_class.new } 15 | 16 | let(:context_object) { Dummy.new } 17 | 18 | describe('#ctor') do 19 | 20 | end 21 | 22 | describe('#add_route') do 23 | 24 | it('register route successfully') do 25 | subject.add(context_object, "print", :print) 26 | end 27 | 28 | it('register route successfully without handler') do 29 | subject.add(context_object, :print) 30 | end 31 | 32 | 33 | context('on error') do 34 | 35 | it('raises a Error on invalid context') do 36 | expect { subject.add(nil, :route) }. 37 | to raise_exception 38 | end 39 | 40 | it('raises a Error on invalid route') do 41 | expect { subject.add(context_object, nil) }. 42 | to raise_exception 43 | end 44 | 45 | it('raises a Error on invalid handler') do 46 | expect { subject.add(context_object, :route) }. 47 | to raise_exception 48 | end 49 | 50 | end 51 | 52 | end 53 | 54 | describe('#get_route') do 55 | 56 | before(:each) do 57 | subject.add(context_object, "print", :print) 58 | subject.add(context_object, "print_with_headers", :print_with_headers) 59 | end 60 | 61 | context('on error') do 62 | 63 | it('raises a ZSS::Error on invalid verb') do 64 | expect { subject.get("route") }. 65 | to raise_exception(ZSS::Error) 66 | end 67 | 68 | end 69 | 70 | context('on success') do 71 | 72 | it('returns route handler') do 73 | expect(subject.get("print")).to be_truthy 74 | end 75 | 76 | end 77 | 78 | context('call route handler') do 79 | 80 | it('successfully call with payload') do 81 | expect(context_object).to receive(:print).with("print") 82 | subject.get("print").call("print") 83 | end 84 | 85 | it('successfully call with payload and headers') do 86 | expect(context_object).to receive(:print_with_headers) 87 | .with("print_with_headers", { header: "x" }) 88 | 89 | subject.get("print_with_headers") 90 | .call("print_with_headers", { header: "x" }) 91 | end 92 | 93 | end 94 | 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /spec/unit/service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zss' 3 | require 'zss/service' 4 | require 'em-zeromq' 5 | 6 | describe ZSS::Service do 7 | 8 | let(:socket_address) { "ipc://socket_spec" } 9 | 10 | let :config do 11 | Hashie::Mash.new( 12 | backend: socket_address, 13 | heartbeat: 60000 14 | ) 15 | end 16 | 17 | class DummyService 18 | def ping payload, headers 19 | headers[:took] = "0s" 20 | return "PONG" 21 | end 22 | 23 | def ping_fail payload, headers 24 | fail 'should raise exception' 25 | end 26 | end 27 | 28 | def done 29 | subject.stop 30 | EM.stop 31 | end 32 | 33 | let(:context) { double('EM::ZeroMQ::Context').as_null_object } 34 | let(:socket) { double('EM::ZMQ::Socket').as_null_object } 35 | 36 | before :each do 37 | allow(EM::ZeroMQ::Context).to receive(:new) { context } 38 | allow(context).to receive(:socket) { socket } 39 | allow(SecureRandom).to receive(:uuid) { "uuid" } 40 | config.heartbeat = 60000 41 | end 42 | 43 | after :each do 44 | EM.stop if EM.reactor_running? 45 | end 46 | 47 | subject do 48 | described_class.new(:pong, config) 49 | end 50 | 51 | describe('#ctor') do 52 | 53 | it('returns a service with default config') do 54 | subject = described_class.new(:pong) 55 | expect(subject.sid).to eq("PONG") 56 | expect(subject.backend).to eq(ZSS::Configuration.default.backend) 57 | expect(subject.heartbeat).to eq(1000) 58 | end 59 | 60 | it('returns a service with config') do 61 | config = Hashie::Mash.new( 62 | backend: "socket", 63 | heartbeat: 2000 64 | ) 65 | subject = described_class.new(:pong, config) 66 | expect(subject.sid).to eq("PONG") 67 | expect(subject.backend).to eq("socket") 68 | expect(subject.heartbeat).to eq(2000) 69 | end 70 | 71 | it('raises an error on invalid sid') do 72 | 73 | expect do 74 | described_class.new(nil, nil) 75 | end.to raise_exception(ZSS::Error) do |error| 76 | expect(error.code).to eq(500) 77 | end 78 | 79 | end 80 | 81 | end 82 | 83 | describe('#run') do 84 | 85 | context('on error') do 86 | 87 | it('raises RuntimeError on invalid context') do 88 | expect(EM::ZeroMQ::Context).to receive(:new) { nil } 89 | expect { subject.run }.to raise_exception(RuntimeError) 90 | end 91 | 92 | it('raises RuntimeError on invalid socket') do 93 | expect(context).to receive(:socket) { nil } 94 | expect { subject.run }.to raise_exception(RuntimeError) 95 | end 96 | 97 | end 98 | 99 | context('open ZMQ Socket') do 100 | 101 | it('with dealer type') do 102 | expect(context).to receive(:socket).with(ZMQ::DEALER) do 103 | done 104 | socket 105 | end 106 | subject.run 107 | end 108 | 109 | it('with identity set') do 110 | expect(socket).to receive(:identity=).with("pong#uuid") { done } 111 | subject.run 112 | end 113 | 114 | it('with linger set to 0') do 115 | expect(socket).to receive(:setsockopt).with(ZMQ::LINGER, 0) { done } 116 | subject.run 117 | end 118 | 119 | it('connect to socket address') do 120 | expect(socket).to receive(:connect).with(socket_address) { done } 121 | subject.run 122 | end 123 | 124 | end 125 | 126 | it('register service on broker') do 127 | expect(socket).to receive(:send_msg) do |*frames| 128 | message = ZSS::Message.parse(frames) 129 | expect(message.address.sid).to eq('SMI') 130 | expect(message.address.verb).to eq('UP') 131 | expect(message.payload).to eq('PONG') 132 | done 133 | true 134 | end 135 | 136 | subject.run 137 | end 138 | 139 | context('handling requests') do 140 | 141 | let(:address) { ZSS::Message::Address.new(sid: "PONG", verb: "PING") } 142 | 143 | let(:message) { ZSS::Message.new(address: address, payload: "PING") } 144 | 145 | let :message_parts do 146 | message.to_frames.map { |f| ZMQ::Message.new(f) } 147 | end 148 | 149 | it('returns payload and headers') do 150 | service = DummyService.new 151 | subject.add_route(service, :ping) 152 | 153 | EM.run do 154 | allow(socket).to receive(:on) do |event, &block| 155 | EM.add_timer { block.call *message_parts } 156 | end 157 | 158 | subject.run 159 | 160 | expect(socket).to receive(:send_msg) do |*frames| 161 | message = ZSS::Message.parse(frames) 162 | 163 | expect(message.type).to eq(ZSS::Message::Type::REP) 164 | expect(message.status).to eq(200) 165 | expect(message.headers.took).to eq("0s") 166 | 167 | done 168 | 169 | true 170 | end 171 | end 172 | 173 | end 174 | 175 | it('returns response time header') do 176 | service = DummyService.new 177 | subject.add_route(service, :ping) 178 | 179 | EM.run do 180 | allow(socket).to receive(:on) do |event, &block| 181 | EM.add_timer { block.call *message_parts } 182 | end 183 | 184 | subject.run 185 | 186 | expect(socket).to receive(:send_msg) do |*frames| 187 | message = ZSS::Message.parse(frames) 188 | expect(message.headers["zss-response-time"]).to be 189 | done 190 | 191 | true 192 | end 193 | end 194 | 195 | end 196 | 197 | context('on error') do 198 | 199 | it('returns 404 on invalid sid') do 200 | message.address.sid = "something" 201 | 202 | EM.run do 203 | allow(socket).to receive(:on) do |event, &block| 204 | EM.add_timer { block.call *message_parts } 205 | end 206 | 207 | subject.run 208 | 209 | expect(socket).to receive(:send_msg) do |*frames| 210 | message = ZSS::Message.parse(frames) 211 | 212 | expect(message.type).to eq(ZSS::Message::Type::REP) 213 | expect(message.status).to eq(404) 214 | 215 | done 216 | 217 | true 218 | end 219 | end 220 | 221 | end 222 | 223 | it('returns 404 on invalid verb') do 224 | 225 | message.address.verb = "something" 226 | 227 | EM.run do 228 | allow(socket).to receive(:on) do |event, &block| 229 | EM.add_timer { block.call *message_parts } 230 | end 231 | 232 | subject.run 233 | 234 | expect(socket).to receive(:send_msg) do |*frames| 235 | message = ZSS::Message.parse(frames) 236 | 237 | expect(message.type).to eq(ZSS::Message::Type::REP) 238 | expect(message.status).to eq(404) 239 | 240 | done 241 | 242 | true 243 | end 244 | end 245 | 246 | end 247 | 248 | it('returns 500 when an error occurred while handling request') do 249 | 250 | service = DummyService.new 251 | subject.add_route(service, "PING/FAIL", :ping_fail) 252 | message.address.verb = "PING/FAIL" 253 | 254 | EM.run do 255 | allow(socket).to receive(:on) do |event, &block| 256 | EM.add_timer { block.call *message_parts } 257 | end 258 | 259 | subject.run 260 | 261 | expect(socket).to receive(:send_msg) do |*frames| 262 | message = ZSS::Message.parse(frames) 263 | 264 | expect(message.type).to eq(ZSS::Message::Type::REP) 265 | expect(message.status).to eq(500) 266 | 267 | done 268 | 269 | true 270 | end 271 | end 272 | 273 | end 274 | 275 | end 276 | 277 | end 278 | 279 | it('sends heartbeat message') do 280 | config.heartbeat = 500 281 | subject = described_class.new(:pong, config) 282 | 283 | EM.run do 284 | 285 | subject.run 286 | 287 | expect(socket).to receive(:send_msg) do |*frames| 288 | message = ZSS::Message.parse(frames) 289 | 290 | expect(message.type).to eq(ZSS::Message::Type::REQ) 291 | expect(message.address.sid).to eq('SMI') 292 | expect(message.address.verb).to eq('HEARTBEAT') 293 | expect(message.payload).to eq('PONG') 294 | 295 | done 296 | 297 | true 298 | end 299 | end 300 | end 301 | 302 | end 303 | 304 | describe('#stop') do 305 | 306 | it('disconnects the socket') do 307 | EM.run do 308 | subject.run 309 | 310 | expect(socket).to receive(:disconnect) { done } 311 | 312 | subject.stop 313 | end 314 | end 315 | 316 | it('unregister service on broker') do 317 | EM.run do 318 | 319 | subject.run 320 | 321 | expect(socket).to receive(:send_msg) do |*frames| 322 | message = ZSS::Message.parse(frames) 323 | expect(message.address.sid).to eq('SMI') 324 | expect(message.address.verb).to eq('DOWN') 325 | expect(message.payload).to eq('PONG') 326 | done 327 | 1 328 | end 329 | 330 | subject.stop 331 | end 332 | end 333 | 334 | end 335 | 336 | end 337 | -------------------------------------------------------------------------------- /spec/unit/socket_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zss/socket' 3 | require 'ffi-rzmq' 4 | 5 | describe ZSS::Socket do 6 | 7 | let(:socket_address) { "ipc://socket_spec" } 8 | 9 | let :config do 10 | Hashie::Mash.new( 11 | socket_address: socket_address, 12 | identity: 'socket-identity', 13 | timeout: 300 14 | ) 15 | end 16 | 17 | let(:address) { ZSS::Message::Address.new(sid: "service", verb: "verb") } 18 | 19 | let(:message) { ZSS::Message.new(address: address, payload: "PING") } 20 | 21 | let(:context) { double('ZMQ::Context').as_null_object } 22 | 23 | let(:socket) { double('ZMQ::Socket').as_null_object } 24 | 25 | before(:each) do 26 | allow(SecureRandom).to receive(:uuid) { "uuid" } 27 | end 28 | 29 | subject { ZSS::Socket.new(config) } 30 | 31 | describe("#call") do 32 | 33 | context('open ZMQ Socket') do 34 | 35 | before :each do 36 | allow(ZMQ::Context).to receive(:create) { context } 37 | allow(context).to receive(:socket) { socket } 38 | allow(socket).to receive(:send_string) { -1 } 39 | end 40 | 41 | it('with dealer type') do 42 | expect(context).to receive(:socket).with(ZMQ::DEALER) 43 | expect { subject.call(message) }.to raise_exception(ZSS::Socket::Error) 44 | end 45 | 46 | it('with identity set') do 47 | expect(socket).to receive(:identity=).with("socket-identity#uuid") 48 | expect { subject.call(message) }.to raise_exception(ZSS::Socket::Error) 49 | end 50 | 51 | it('with linger set to 0') do 52 | expect(socket).to receive(:setsockopt).with(ZMQ::LINGER, 0) 53 | expect { subject.call(message) }.to raise_exception(ZSS::Socket::Error) 54 | end 55 | 56 | it('connect to socket address') do 57 | expect(socket).to receive(:connect).with(socket_address) 58 | expect { subject.call(message) }.to raise_exception(ZSS::Socket::Error) 59 | end 60 | 61 | end 62 | 63 | context('on error') do 64 | 65 | it('raises Socket::Error on invalid request') do 66 | expect { subject.call(nil) }. 67 | to raise_exception(ZSS::Socket::Error) 68 | end 69 | 70 | it('raises ::Timeout::Error on subject timeout') do 71 | expect { subject.call(message) }. 72 | to raise_exception(ZSS::Socket::TimeoutError, "call timeout after 0.3s") 73 | end 74 | 75 | it('raises ::Timeout::Error on call timeout') do 76 | expect { subject.call(message, 200) }. 77 | to raise_exception(ZSS::Socket::TimeoutError, "call timeout after 0.2s") 78 | end 79 | 80 | it('raises Socket::Error on invalid context') do 81 | allow(ZMQ::Context).to receive(:create) { nil } 82 | expect { subject.call(message) }.to raise_exception(ZSS::Socket::Error) 83 | end 84 | 85 | it('raises Socket::Error on invalid socket') do 86 | allow(ZMQ::Context).to receive(:create) { context } 87 | allow(context).to receive(:socket) { nil } 88 | expect { subject.call(message) }.to raise_exception(ZSS::Socket::Error) 89 | end 90 | 91 | it('raises Socket::Error on invalid send send_string') do 92 | allow(ZMQ::Context).to receive(:create) { context } 93 | allow(context).to receive(:socket) { socket } 94 | allow(socket).to receive(:send_string) { -1 } 95 | expect { subject.call(message) }.to raise_exception(ZSS::Socket::Error) 96 | end 97 | 98 | end 99 | 100 | context('clean up resources') do 101 | 102 | it('terminates context') do 103 | allow(ZMQ::Context).to receive(:create) { context } 104 | allow(context).to receive(:socket) { nil } 105 | expect(context).to receive(:terminate) 106 | expect { subject.call(message) }.to raise_exception(ZSS::Socket::Error) 107 | end 108 | 109 | it('closes socket') do 110 | allow(ZMQ::Context).to receive(:create) { context } 111 | allow(context).to receive(:socket) { socket } 112 | allow(socket).to receive(:send_string) { -1 } 113 | expect(socket).to receive(:close) 114 | expect { subject.call(message) }.to raise_exception(ZSS::Socket::Error) 115 | end 116 | 117 | end 118 | 119 | describe('on success') do 120 | 121 | before :each do 122 | allow(ZMQ::Context).to receive(:create) { context } 123 | allow(context).to receive(:socket) { socket } 124 | end 125 | 126 | it('returns a result') do 127 | frames = [] 128 | allow(socket).to receive(:send_string) do |frame| 129 | frames << frame 130 | 0 # return success result code 131 | end 132 | allow(socket).to receive(:recv_strings) do |buffer| 133 | frames.each { |f| buffer << f } 134 | 0 # return success result code 135 | end 136 | 137 | result = subject.call(message) 138 | expect(result.payload).to eq(message.payload) 139 | end 140 | 141 | end 142 | 143 | end 144 | 145 | end 146 | -------------------------------------------------------------------------------- /spec/unit/validate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zss/validate' 3 | 4 | describe ZSS::Validate do 5 | 6 | it 'accepts a valid uri' do 7 | 8 | expect(ZSS::Validate::Validator.is_valid_uri?('https://clubjudge.com')).to be 9 | 10 | end 11 | 12 | it 'denies an inclomplete uri' do 13 | 14 | expect(ZSS::Validate::Validator.is_valid_uri?('www.sapo.pt')).not_to be 15 | 16 | end 17 | 18 | it 'denies an erroneous url' do 19 | expect(URI).to receive(:parse).and_raise(StandardError) 20 | expect(ZSS::Validate::Validator.is_valid_uri?('somestring')).not_to be 21 | end 22 | 23 | it 'denies an invalid uri' do 24 | 25 | expect(ZSS::Validate::Validator.is_valid_uri?('some1nval/dUrl')).not_to be 26 | 27 | end 28 | 29 | it 'can be included' do 30 | 31 | class SomeClass 32 | include ZSS::Validate 33 | end 34 | 35 | testObject = SomeClass.new 36 | 37 | expect(testObject.is_valid_uri?('https://clubjudge.com')).to be 38 | 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /zss.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require './lib/zss/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'zss' 7 | spec.version = ZSS::VERSION 8 | spec.authors = ["Pedro Januário"] 9 | spec.email = ["prnjanuario@gmail.com"] 10 | spec.description = %q{ZeroMQ SOA Suite} 11 | spec.summary = %q{This project is a ruby client&service implementation for ZMQ Service Suite, 12 | check http://pjanuario.github.io/zmq-service-suite-specs/ for more info.} 13 | spec.homepage = "https://github.com/pjanuario/zmq-service-suite-ruby" 14 | spec.metadata = { 15 | "source_code" => "https://github.com/pjanuario/zmq-service-suite-ruby", 16 | "issue_tracker" => "https://github.com/pjanuario/zmq-service-suite-ruby/issues" 17 | } 18 | spec.license = "MIT" 19 | 20 | spec.files = `git ls-files`.split($/) 21 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 22 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 23 | spec.require_paths = ["lib"] 24 | 25 | spec.add_development_dependency 'bundler', '~> 1.6' 26 | spec.add_development_dependency 'rake', '~> 10.3' 27 | spec.add_development_dependency 'rspec', '~> 3.0' 28 | spec.add_development_dependency 'pry', '~> 0.10' 29 | spec.add_development_dependency 'simplecov', '~> 0.9' 30 | spec.add_development_dependency 'codeclimate-test-reporter', '~> 0.3' 31 | spec.add_development_dependency 'bump', '~> 0.5' 32 | 33 | spec.add_dependency 'msgpack', '0.5.12' 34 | spec.add_dependency 'ffi-rzmq', '~> 2.0' 35 | spec.add_dependency 'activesupport', '~> 4.2' 36 | spec.add_dependency 'hashie', '~> 3.2' 37 | spec.add_dependency 'daemons', '~> 1.1' 38 | spec.add_dependency 'em-zeromq', '~> 0.5' 39 | spec.add_dependency 'logger_facade', '~> 0.4.1' 40 | end 41 | --------------------------------------------------------------------------------