├── .rspec ├── .travis.yml ├── Gemfile ├── circle.yml ├── spec ├── support │ ├── contracts │ │ └── invalid │ │ │ ├── cache │ │ │ └── service_1 │ │ │ │ └── publish.mson │ │ │ └── wrong_primitive │ │ │ └── publish.mson │ └── config.yml ├── spec_helper.rb ├── lib │ └── zeta │ │ └── local_or_remote_file_spec.rb └── zeta_spec.rb ├── Rakefile ├── lib ├── zeta │ ├── version.rb │ ├── rspec │ │ └── autorun_all.rb │ ├── rspec.rb │ ├── local_or_remote_file.rb │ ├── runner.rb │ └── instance.rb └── zeta.rb ├── bin └── zeta ├── .gitignore ├── Guardfile ├── zeta.gemspec ├── CHANGELOG.markdown └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.3 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | ruby: 3 | version: '2.3.0' 4 | -------------------------------------------------------------------------------- /spec/support/contracts/invalid/cache/service_1/publish.mson: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "lacerda/tasks" 3 | -------------------------------------------------------------------------------- /lib/zeta/version.rb: -------------------------------------------------------------------------------- 1 | class Zeta 2 | VERSION = "2.1.4" 3 | end 4 | -------------------------------------------------------------------------------- /bin/zeta: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require 'zeta' 5 | require 'zeta/runner' 6 | 7 | Zeta::Runner.run 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /spec/support/contracts/invalid/wrong_primitive/publish.mson: -------------------------------------------------------------------------------- 1 | # Data structures 2 | 3 | # ObjectWithWrongPrimitive 4 | 5 | - id: (number) 6 | - properties: (hash) 7 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :bundler do 2 | watch('Gemfile') 3 | watch(/^.+\.gemspec/) 4 | end 5 | 6 | guard :rspec, cmd: 'TEST=1 rspec', all_on_start: true do 7 | watch(%r{^spec/.+\.rb$}) { "spec" } 8 | watch(%r{^lib/(.+)\.rb$}) { "spec" } 9 | watch('spec/spec_helper.rb') { "spec" } 10 | end 11 | -------------------------------------------------------------------------------- /lib/zeta/rspec/autorun_all.rb: -------------------------------------------------------------------------------- 1 | if defined?(Rails) && Rails.application.config_for(:zeta).present? 2 | ENV['ZETA_HTTP_USER'] ||= Rails.application.config_for(:zeta)['user'] 3 | ENV['ZETA_HTTP_PASSWORD'] ||= Rails.application.config_for(:zeta)['api_key'] 4 | end 5 | 6 | require 'zeta/rspec' 7 | 8 | Zeta::RSpec.run 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'bundler' 3 | require 'webmock/rspec' 4 | require 'simplecov' 5 | require 'coveralls' 6 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 7 | SimpleCov::Formatter::HTMLFormatter, 8 | Coveralls::SimpleCov::Formatter 9 | ] 10 | SimpleCov.start{ add_filter 'spec/'} 11 | 12 | require 'zeta' 13 | -------------------------------------------------------------------------------- /lib/zeta/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'lacerda/reporters/rspec' 2 | require 'zeta' 3 | 4 | class Zeta::RSpec 5 | def self.run 6 | # Download Infrastructure 7 | Zeta.config 8 | Zeta.infrastructure 9 | 10 | # Update Contracts 11 | Zeta.verbose = false 12 | Zeta.update_contracts 13 | 14 | # Validate Infrastructure 15 | # NOTE: Expectations are defined by .contracts_fulfilled? 16 | # 17 | # Whats the structure of this expectations? 18 | # https://github.com/moviepilot/lacerda/blob/master/lib/lacerda/reporters/rspec.rb 19 | # 20 | Zeta.contracts_fulfilled?(Lacerda::Reporters::RSpec.new) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/zeta.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | require 'zeta/version' 3 | require 'zeta/instance' 4 | 5 | class Zeta 6 | include Zeta::Instance 7 | LOCK = Monitor.new 8 | 9 | def self.instance 10 | LOCK.synchronize do 11 | if @instance.nil? 12 | create_instance 13 | end 14 | @instance 15 | end 16 | end 17 | 18 | def self.create_instance(options = {verbose: true}) 19 | LOCK.synchronize do 20 | # Create a Zeta instance 21 | @instance = new(options) 22 | 23 | # Copy the current service's specifications to cache dir 24 | @instance.update_own_contracts 25 | 26 | # Convert current service's specifications so published and 27 | # consumed objects of this service can be validated at 28 | # runtime 29 | @instance.convert_all! 30 | 31 | @instance 32 | end 33 | end 34 | 35 | # Not using the SingleForwardable module here so that, when 36 | # somebody tries to figure out how Zeta works by looking at 37 | # its methods, they don't get confused. 38 | methods = Zeta::Instance.instance_methods - Object.instance_methods 39 | methods.each do |method| 40 | define_singleton_method method do |*args| 41 | send_args = [method, args].flatten.compact 42 | instance.send(*send_args) 43 | end 44 | end 45 | end 46 | 47 | 48 | -------------------------------------------------------------------------------- /zeta.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'zeta/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'zeta' 8 | spec.version = Zeta::VERSION 9 | spec.authors = ['Jannis Hermanns'] 10 | spec.email = ['jannis@gmail.com'] 11 | 12 | spec.summary = 'Collects and validates the publish/consume contracts of your infrastructure' 13 | spec.description = 'Vlad' 14 | spec.homepage = 'https://github.com/moviepilot/zeta' 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = 'bin' 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_runtime_dependency 'rake' 22 | spec.add_runtime_dependency 'lacerda', '>= 2.1.3' 23 | spec.add_runtime_dependency 'activesupport' 24 | spec.add_runtime_dependency 'httparty' 25 | spec.add_runtime_dependency 'colorize' 26 | spec.add_runtime_dependency 'webmock' 27 | 28 | spec.add_development_dependency 'bundler' 29 | spec.add_development_dependency 'guard-bundler' 30 | spec.add_development_dependency 'guard-rspec' 31 | spec.add_development_dependency "coveralls" 32 | end 33 | -------------------------------------------------------------------------------- /spec/support/config.yml: -------------------------------------------------------------------------------- 1 | common: &common 2 | service_name: test_service 3 | contracts_path: spec/support/contracts/valid 4 | contracts_cache_path: spec/support/contracts/valid/.cache 5 | 6 | with_inline_services: 7 | <<: *common 8 | services: 9 | service_1: 10 | github: 11 | repo: username/service_1 12 | branch: master 13 | path: contracts 14 | service_2: 15 | github: 16 | repo: username/service_2 17 | branch: production 18 | path: contracts 19 | 20 | with_remote_services_list: 21 | <<: *common 22 | services_file: 23 | file: 'services.yml' 24 | github: 25 | repo: username/repo 26 | branch: master 27 | 28 | rails_env: 29 | <<: *common 30 | 31 | with_broken_contracts: 32 | service_name: test_service 33 | contracts_path: spec/support/contracts/invalid 34 | contracts_cache_path: spec/support/contracts/invalid/cache 35 | services: 36 | service_1: 37 | github: 38 | repo: username/service_1 39 | branch: master 40 | path: contracts 41 | service_2: 42 | github: 43 | repo: username/service_2 44 | branch: production 45 | path: contracts 46 | 47 | missing_services: 48 | <<: *common 49 | services_file: 50 | file: 'missing.yml' 51 | github: 52 | repo: username/repo 53 | branch: master 54 | 55 | -------------------------------------------------------------------------------- /lib/zeta/local_or_remote_file.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | require 'httparty' 3 | require 'open-uri' 4 | 5 | class Zeta::LocalOrRemoteFile 6 | def initialize(options) 7 | @options = options 8 | end 9 | 10 | def read 11 | if @options[:path] 12 | read_local 13 | elsif @options[:github] 14 | read_from_github 15 | else 16 | raise "Unknown file location #{@options}" 17 | end 18 | end 19 | 20 | private 21 | 22 | def read_local 23 | open(File.join(@options[:path], @options[:file])).read 24 | end 25 | 26 | def read_from_github 27 | self.class.http_get(github_url, verbose?) 28 | end 29 | 30 | def self.http_get(url, verbose) 31 | retries ||= 3 32 | masked_url = ENV['ZETA_HTTP_PASSWORD'].blank? ? url : url.sub(ENV['ZETA_HTTP_PASSWORD'], '***') 33 | print "GET #{masked_url}... " if verbose 34 | result = HTTParty.get url 35 | raise "Error #{result.code}: #{result}" unless result.code == 200 36 | print "OK\n".green if verbose 37 | result.to_s 38 | rescue 39 | print "ERROR\n".blue if verbose 40 | raise if (retries -= 1).zero? 41 | sleep 1 42 | retry 43 | end 44 | 45 | def verbose? 46 | !!@options[:verbose] 47 | end 48 | 49 | # In order not to have git as a dependency, we'll fetch from 50 | # raw.githubusercontent.com as long as we get away with it. 51 | def github_url 52 | repo = @options[:github][:repo] 53 | branch = @options[:github][:branch] 54 | path = @options[:github][:path] 55 | file = @options[:file] 56 | 57 | uri = [branch, path, file].compact.join('/') 58 | u = ENV['ZETA_HTTP_USER'] 59 | p = ENV['ZETA_HTTP_PASSWORD'] 60 | if p 61 | "https://#{u}:#{p}@raw.githubusercontent.com/#{repo}/#{uri}" 62 | else 63 | "https://raw.githubusercontent.com/#{repo}/#{uri}" 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/lib/zeta/local_or_remote_file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fileutils' 3 | 4 | describe Zeta::LocalOrRemoteFile do 5 | it "refuses to open a file it doesn't know how to locate" do 6 | expect{ 7 | Zeta::LocalOrRemoteFile.new({}).read 8 | }.to raise_error{ |e| 9 | expect(e.to_s.include?("Unknown file location")).to be true 10 | } 11 | end 12 | 13 | it "loads a local file" do 14 | dir = Dir.mktmpdir 15 | timestamp = Time.now.to_i 16 | begin 17 | file = File.join(dir, 'test.mson') 18 | File.open(file, 'w'){ |f| f.print timestamp } 19 | o = { 20 | file: 'test.mson', 21 | path: dir 22 | } 23 | expect(Zeta::LocalOrRemoteFile.new(o).read).to eq timestamp.to_s 24 | ensure 25 | FileUtils.remove_entry dir 26 | end 27 | end 28 | 29 | context "loading from github" do 30 | let(:get_double){double(to_s: 'Something', code: 200)} 31 | let(:o){{ 32 | file: 'foo.txt', 33 | github: { user: 'user', repo: 'repo', path: 'path' } 34 | }} 35 | 36 | it "without auth tokens" do 37 | expect(HTTParty).to receive(:get).with("https://raw.githubusercontent.com/repo/path/foo.txt").and_return(get_double) 38 | Zeta::LocalOrRemoteFile.new(o).read 39 | end 40 | 41 | it "with auth tokens" do 42 | ENV['ZETA_HTTP_USER'] = 'user' 43 | ENV['ZETA_HTTP_PASSWORD'] = 'token' 44 | begin 45 | expect(HTTParty).to receive(:get).with("https://user:token@raw.githubusercontent.com/repo/path/foo.txt").and_return(get_double) 46 | Zeta::LocalOrRemoteFile.new(o).read 47 | ensure 48 | ENV['ZETA_HTTP_USER'] = nil 49 | ENV['ZETA_HTTP_PASSWORD'] = nil 50 | end 51 | end 52 | 53 | it "raises a 404" do 54 | not_found = double(to_s: 'Something', code: 404) 55 | expect(HTTParty).to receive(:get).with("https://raw.githubusercontent.com/repo/path/foo.txt").and_return(not_found).at_least(1) 56 | 57 | expect { 58 | Zeta::LocalOrRemoteFile.new(o).read 59 | }.to raise_error{ |e| 60 | expect(e.to_s.include?("Error 404")).to be true 61 | } 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/zeta/runner.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'optparse' 3 | 4 | class Zeta::Runner 5 | COMMANDS = { 6 | 'full_check' => 'Update contracts and validate infrastructure', 7 | 'validate' => 'Validate the architecture in the contracts cache dir', 8 | 'update_own_contracts' => 'Update your own contracts in the contracts cache dir', 9 | 'fetch_remote_contracts' => 'Download remote contracts and update your own contracts in the contracts cache dir' 10 | } 11 | 12 | def self.run 13 | options = {} 14 | parser = OptionParser.new do |opts| 15 | opts.banner = "#{'Usage:'.red} zeta [options] command" 16 | 17 | opts.separator "" 18 | opts.separator "Commands:".yellow 19 | 20 | longest_command = COMMANDS.keys.map(&:length).sort.last + 1 21 | command_list = [] 22 | COMMANDS.each do |cmd, desc| 23 | padded_cmd = "#{cmd}:".ljust(longest_command, " ") 24 | command_list << " #{padded_cmd} #{desc}" 25 | end 26 | opts.separator command_list 27 | 28 | opts.separator "" 29 | opts.separator "Specific options:".yellow 30 | 31 | opts.on("-c CONFIG_FILE", "--config=CONFIG_FILE", "Config file (default: config/zeta.yml)") do |c| 32 | options[:config_file] = c 33 | end 34 | 35 | opts.on("-e ENVIRONMENT", "--env=ENVIRONMENT", "Environment (default: RAILS_ENV, if it is set)") do |e| 36 | options[:env] = e 37 | end 38 | 39 | opts.on("-s", "--silent", "No output, just an appropriate return code") do |s| 40 | options[:silent] = s 41 | end 42 | 43 | opts.on("-t", "--trace", "Print exception stack traces") do |t| 44 | options[:trace] = t 45 | end 46 | 47 | opts.separator "" 48 | opts.separator "Common options:".yellow 49 | 50 | opts.on_tail("-h", "--help", "Show this message") do 51 | puts opts 52 | exit 53 | end 54 | 55 | # Another typical switch to print the version. 56 | opts.on_tail("-v", "--version", "Show version") do 57 | puts Zeta::VERSION 58 | exit 59 | end 60 | end 61 | parser.parse! 62 | 63 | commands = ARGV 64 | if commands.empty? or !(commands-COMMANDS.keys).empty? 65 | puts parser 66 | exit(-1) 67 | end 68 | 69 | options[:verbose] = !options.delete(:silent) 70 | zeta = Zeta.new(options) 71 | 72 | begin 73 | if commands.include?('fetch_remote_contracts') or commands.include?('full_check') 74 | zeta.update_contracts 75 | puts "\n" if options[:verbose] 76 | end 77 | 78 | if commands.include?('update_own_contracts') 79 | puts "Copying #{zeta.config[:service_name].to_s.camelize} contracts..." if options[:verbose] 80 | zeta.update_own_contracts 81 | puts "\n" if options[:verbose] 82 | end 83 | 84 | if commands.include?('validate') or commands.include?('full_check') 85 | puts "Validating your infrastructure with #{zeta.infrastructure.publishers.length} publishers and #{zeta.infrastructure.consumers.length} consumers..." if options[:verbose] 86 | zeta.contracts_fulfilled? 87 | unless zeta.errors.empty? 88 | exit(-1) 89 | end 90 | end 91 | rescue => e 92 | if options[:trace] 93 | raise 94 | else 95 | puts "ERROR: ".red + e.message 96 | puts "(Pssst: try the "+"--trace".yellow+" option?)" 97 | end 98 | exit(-1) 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /CHANGELOG.markdown: -------------------------------------------------------------------------------- 1 | # [2.1.4] - 2017-09-15 2 | ### Fixed 3 | - Fix `Zeta::RSpec.run`: Ensure that proper error messages are displayed if the RSpec infrastructure validation fails. 4 | 5 | # 2.1.3 6 | - ...Remove contexts, because that doesn't work either. 7 | 8 | # 2.1.2 9 | - Add a context to prevent rspec from running context before other tests in autorun_all 10 | 11 | # 2.1.1 12 | - Bump lacerda to 2.1.2 13 | - Fix issues with nested describes/its/contexts 14 | 15 | # 2.1.0 16 | - Bump lacerda to 2.1.0 (fix rspec reporter, allow to validate internal objects) 17 | - Add `zeta/rspec/autorun_all` to replace `Zeta::RSpec.run` 18 | # 2.0.0 19 | - Update to lacerda 2.0: adds enum support and raises parsing errors instead of silently returning an empty contract 20 | 21 | # 1.1.0 22 | - Update blumquist and lacerda dependencies to show better errors 23 | 24 | # 1.0.0 25 | - Use lacerda 1.0.0, which has some breaking changes. See 26 | [the changelog](https://github.com/moviepilot/lacerda/blob/master/CHANGELOG.markdown#100) 27 | - Fix `Zeta::RSpec` contract validation 28 | 29 | # 0.13.2 30 | - Fix `Zeta::RSpec` contract validation 31 | 32 | # 0.13.1 33 | - Add explicit Zeta.create_instance initializer with support to pass in options 34 | 35 | # 0.12.5 36 | - More conservative locking 37 | 38 | # 0.12.4 39 | - More conservative locking 40 | 41 | # 0.12.3 42 | - Force rspec examples (in RSpec integration of zeta, not its own tests) to run in order 43 | 44 | # 0.12.1 45 | - Retry failed downloads up to 3 times 46 | 47 | # 0.12.0 48 | - Update Lacerda (fixes required arrays being marked as invalid 49 | when they're empty) 50 | - Update a couple of dependency 51 | - Drop ruby 1.9 support, tests run on 2.3.0 52 | 53 | # 0.11.2 54 | - Make RSpec runner less verbose when downloading specifications 55 | 56 | # 0.11.1 57 | - Fix RSpec runner 58 | 59 | # 0.11.0 60 | - Use a lacerda version that makes optional attributes nullable by 61 | default. If you have an object with an optional property `foo`, 62 | now both {"foo": null} and {} will be valid objects (up to 0.11.0 63 | only the latter was valid) 64 | 65 | # 0.10.0 66 | - The Zeta singleton will only transform its own contracts after it's 67 | initialized, but not fetch remote contracts as this is not necessary 68 | at runtime see [#15](https://github.com/moviepilot/zeta/issues/15) 69 | 70 | # 0.9.0 71 | - Change http basic auth env vars to ZETA_HTTP_USER and ZETA_HTTP_PASSWORD 72 | 73 | # 0.8.0 74 | - The Zeta singleton will update its contracts after it's initialized 75 | 76 | # 0.7.4 77 | - Hint to --trace option on error 78 | - Fix cache dir cleanup on refetch 79 | 80 | # 0.7.3 81 | - Fix https://github.com/moviepilot/zeta/issues/13 82 | 83 | # 0.7.2 84 | - Broken 😱 85 | 86 | # 0.7.1 87 | - Remove require 'pry' 88 | 89 | # 0.7.0 90 | - Update Lacerda to ~> 0.12 91 | - Add Zeta.convert_all! convenience method 92 | 93 | # 0.6.0 (06-Nov-15) 94 | - Update Lacerda 95 | 96 | # 0.6.2 (04-Nov-15) 97 | - Update Lacerda 98 | - Add --trace option so we don't bother people with exception traces all the time 99 | 100 | # 0.6.1 (03-Nov-15) 101 | - Make rspec integration a little more convenient 102 | 103 | # 0.6.0 (03-Nov-15) 104 | - Make reporter configurable for contracts_fulfilled? 105 | - Add rspec integration 106 | - Add Zeta.verbose= setter 107 | 108 | # 0.5.0 (02-Nov-15) 109 | - Update lacerda 110 | - Use lacerda stdout reporter by default 111 | 112 | # 0.4.0 (30-Oct-15) 113 | - Update lacerda which uses ServiceName::Object in favor of ServiceName:Object 114 | 115 | # 0.3.0 (29-Oct-15) 116 | - Forward published/consume object validation method to the current service in the infrastructure 117 | - Forward wrapped consume object creation to the current service 118 | - Use ZETA_HTTP_USER and ZETA_HTTP_PASSWORD instead of GITHUB_USER and GITHUB_TOKEN 119 | 120 | # 0.2.5 (28-Oct-15) 121 | - Better CLI help 122 | - Update lacerda version 123 | 124 | # 0.2.3 (22-Oct-15) 125 | - Log urls of downloaded service files 126 | 127 | # 0.1.2 (20-Oct-15) 128 | - Add `zeta` runner 129 | -------------------------------------------------------------------------------- /lib/zeta/instance.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/hash/indifferent_access' 2 | require 'yaml' 3 | require 'fileutils' 4 | require 'tmpdir' 5 | require 'lacerda' 6 | 7 | require 'zeta/local_or_remote_file' 8 | 9 | class Zeta 10 | module Instance 11 | attr_reader :config 12 | 13 | def initialize(options = {}) 14 | @lock = Monitor.new 15 | @options = options 16 | end 17 | 18 | def update_contracts 19 | @lock.synchronize do 20 | i = infrastructure 21 | clear_cache 22 | puts "Updating #{cache_dir}" if verbose? 23 | update_other_contracts 24 | update_own_contracts 25 | i.convert_all! 26 | end 27 | true 28 | end 29 | 30 | def convert_all! 31 | @lock.synchronize do 32 | infrastructure.convert_all! 33 | end 34 | end 35 | 36 | def update_own_contracts 37 | @lock.synchronize do 38 | contract_files.each do |file| 39 | source_file = File.join(config[:contracts_path], file) 40 | target_file = File.join(cache_dir, config[:service_name], file) 41 | FileUtils.mkdir_p(File.join(cache_dir, config[:service_name])) 42 | puts "cp #{source_file} #{target_file}" if verbose? 43 | FileUtils.rm_f(target_file) 44 | FileUtils.cp(source_file, target_file) if File.exists?(source_file) 45 | end 46 | end 47 | end 48 | 49 | def errors 50 | infrastructure.errors 51 | end 52 | 53 | def contracts_fulfilled?(reporter = nil) 54 | reporter ||= Lacerda::Reporters::Stdout.new(verbose: verbose?) 55 | infrastructure.contracts_fulfilled?(reporter) 56 | end 57 | 58 | def infrastructure 59 | @lock.synchronize do 60 | return @infrastructure if @infrastructure 61 | @infrastructure = Lacerda::Infrastructure.new(data_dir: cache_dir, verbose: verbose?) 62 | @infrastructure 63 | end 64 | end 65 | 66 | def config_file 67 | return File.expand_path(@options[:config_file]) if @options[:config_file] 68 | File.join(Dir.pwd, 'config', 'zeta.yml') 69 | end 70 | 71 | def env 72 | return @options[:env].to_sym if @options[:env] 73 | if Object.const_defined?('Rails') 74 | Rails.env.to_sym 75 | else 76 | guessed = ENV['RAILS_ENV'] || ENV['RACK_ENV'] 77 | raise "No environment given" unless guessed 78 | guessed 79 | end 80 | end 81 | 82 | def cache_dir 83 | @lock.synchronize do 84 | return @cache_dir if @cache_dir 85 | full_path = File.expand_path(config[:contracts_cache_path]) 86 | FileUtils.mkdir_p(full_path) 87 | @cache_dir = full_path 88 | end 89 | end 90 | 91 | def clear_cache 92 | @lock.synchronize do 93 | # I'm afraid of FileUtils.rm_rf so I'll just delete all relevant files 94 | # and then rmdir all empty directories. 95 | Dir[File.join(cache_dir, "**/*.mson")].each{|f| FileUtils.rm(f) } 96 | Dir[File.join(cache_dir, "**/*.json")].each{|f| FileUtils.rm(f) } 97 | Dir[File.join(cache_dir, '*')].each do |d| 98 | next unless File.directory?(d) 99 | FileUtils.rmdir(d) rescue nil 100 | end 101 | end 102 | end 103 | 104 | def config 105 | @lock.synchronize do 106 | return @config if @config 107 | full_config = YAML.load_file(config_file).with_indifferent_access 108 | env_config = full_config[env] 109 | 110 | raise "No config for environment '#{env}' found in #{config_file}" unless env_config 111 | 112 | # TODO validate it properly 113 | [:service_name, :contracts_path, :contracts_cache_path].each do |k| 114 | raise ":#{k} missing in #{full_config.to_json}" unless env_config[k] 115 | end 116 | 117 | @config = env_config 118 | end 119 | end 120 | 121 | def validate_object_to_publish!(type, data) 122 | current_service.validate_object_to_publish!(type, data) 123 | end 124 | 125 | def validate_object_to_publish(type, data) 126 | current_service.validate_object_to_publish(type, data) 127 | end 128 | 129 | def validate_object_to_consume!(type, data) 130 | current_service.validate_object_to_consume!(type, data) 131 | end 132 | 133 | def validate_object_to_consume(type, data) 134 | current_service.validate_object_to_consume(type, data) 135 | end 136 | 137 | def consume_object(type, data) 138 | current_service.consume_object(type, data) 139 | end 140 | 141 | def current_service 142 | @current_service ||= infrastructure.services[config[:service_name]] 143 | end 144 | 145 | def verbose=(val) 146 | @options[:verbose] = !!val 147 | end 148 | 149 | private 150 | 151 | def verbose? 152 | @options[:verbose] 153 | end 154 | 155 | def fetch_service_contracts(service_name, config) 156 | target_dir = File.join(cache_dir, service_name) 157 | FileUtils.mkdir_p(target_dir) 158 | 159 | contract_files.each do |contract| 160 | file = File.join(target_dir, contract) 161 | FileUtils.rm_f(file) 162 | 163 | File.open(file, 'w') do |out| 164 | contract = LocalOrRemoteFile.new(config.merge(file: contract, verbose: verbose?)).read 165 | raise "Invalid contract:\n\n#{contract}\n#{'~'*80}" unless contract_looks_valid?(contract) 166 | out.puts contract 167 | end 168 | end 169 | end 170 | 171 | def contract_looks_valid?(contract) 172 | true # TODO 173 | end 174 | 175 | def update_other_contracts 176 | services.each do |service_name, config| 177 | fetch_service_contracts(service_name, config) 178 | end 179 | end 180 | 181 | def contract_files 182 | ['publish.mson', 'consume.mson'] 183 | end 184 | 185 | def services 186 | if config[:services] 187 | return config[:services] 188 | elsif config[:services_file] 189 | file = LocalOrRemoteFile.new(config[:services_file].merge(verbose: verbose?)) 190 | services = YAML.load(file.read) 191 | begin 192 | services.with_indifferent_access 193 | rescue 194 | raise "Could not load services from #{config[:services_file].to_json}" 195 | end 196 | end 197 | end 198 | 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /spec/zeta_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zeta do 4 | let(:config_file){ File.expand_path(File.join(__FILE__, '..', 'support', 'config.yml')) } 5 | let(:zeta){ 6 | z = Zeta.new(config_file: config_file, env: :with_inline_services) 7 | z.verbose = false 8 | z 9 | } 10 | 11 | after(:all) do 12 | # Clean up 13 | FileUtils.rm_rf(File.join(Dir.pwd, 'spec', 'support', 'contracts', 'valid', '.cache')) 14 | FileUtils.rm_rf(File.join(Dir.pwd, 'spec', 'support', 'contracts', 'valid', '.cache')) 15 | end 16 | 17 | it 'has a version number' do 18 | expect(Zeta::VERSION).not_to be nil 19 | end 20 | 21 | context "singleton" do 22 | it "creates a singleton with a default config on demand" do 23 | default = File.join(Dir.pwd, 'config', 'zeta.yml') 24 | expect{ 25 | Zeta.current_service 26 | }.to raise_error do |error| 27 | expect(error.message.include?(default)).to be true 28 | end 29 | end 30 | end 31 | 32 | context "defaults" do 33 | 34 | it "config_file in config/zeta.yml" do 35 | default = File.join(Dir.pwd, 'config', 'zeta.yml') 36 | expect{ 37 | m = Zeta.new(env: :master, verbose: false) 38 | m.update_contracts 39 | }.to raise_error do |error| 40 | expect(error.message.include?(default)).to be true 41 | end 42 | end 43 | 44 | context "environment" do 45 | 46 | it "rails env, if defined" do 47 | begin 48 | class Rails 49 | def self.env 50 | :rails_env 51 | end 52 | end 53 | m = Zeta.new(config_file: config_file) 54 | expect(m.env).to eq :rails_env 55 | ensure 56 | Object.send(:remove_const, :Rails) 57 | end 58 | end 59 | 60 | it "RAILS_ENV environment variable" do 61 | ENV['RAILS_ENV'] = 'FOO' 62 | begin 63 | m = Zeta.new(config_file: config_file, verbose: false) 64 | expect(m.env).to eq 'FOO' 65 | rescue 66 | ENV['RAILS_ENV'] = nil 67 | end 68 | end 69 | 70 | it "RACK_ENV environment variable" do 71 | ENV['RACK_ENV'] = 'FOO' 72 | begin 73 | m = Zeta.new(config_file: config_file, verbose: false) 74 | expect(m.env).to eq 'FOO' 75 | rescue 76 | ENV['RACK_ENV'] = nil 77 | end 78 | end 79 | end 80 | end 81 | 82 | context "delegating to" do 83 | let(:z){Zeta.new(env: :with_remote_services_list, config_file: config_file, verbose: false)} 84 | 85 | context "singleton" do 86 | it "updates and converts contracts when it's created" do 87 | Zeta.instance_variable_set(:@instance, nil) 88 | instance_mock = double(Zeta) 89 | expect(instance_mock).to receive(:update_own_contracts) 90 | expect(instance_mock).to receive(:convert_all!) 91 | expect(Zeta).to receive(:new).with(verbose: true).and_return instance_mock 92 | 93 | Zeta.instance 94 | end 95 | end 96 | 97 | context "infrastructure" do 98 | it ":errors" do 99 | expect(z.infrastructure).to receive(:errors).and_return [:foo] 100 | expect(z.errors).to eq [:foo] 101 | end 102 | 103 | it ":convert_all!" do 104 | expect(z.infrastructure).to receive(:convert_all!).and_return :wyclef 105 | expect(z.convert_all!).to eq :wyclef 106 | end 107 | end 108 | 109 | context "current service" do 110 | let(:services_double) { { 'test_service' => double(Lacerda::Service) } } 111 | # These should all just be forwarded to the current service 112 | 113 | context "validating objects" do 114 | methods = [ 115 | :validate_object_to_publish, 116 | :validate_object_to_publish!, 117 | :validate_object_to_consume, 118 | :validate_object_to_consume!, 119 | :consume_object 120 | ] 121 | methods.each do |m| 122 | it m do 123 | expect(z.infrastructure).to receive(:services).and_return(services_double) 124 | expect(services_double['test_service']).to receive(m) 125 | .with(:type, :data).and_return :result 126 | expect(z.send(m, :type, :data)).to eq :result 127 | end 128 | end 129 | end 130 | end 131 | end 132 | 133 | 134 | context "list of services defined inline in yaml" do 135 | 136 | it 'complains when no services could be found for an env' do 137 | get_double = double(to_s: '', code: 200) 138 | url = 'https://raw.githubusercontent.com/username/repo/master/missing.yml' 139 | o = {config_file: config_file, env: :missing_services, verbose: false} 140 | expect(HTTParty).to receive(:get).with(url).and_return(get_double) 141 | 142 | zeta = Zeta.new(o) 143 | expect{ 144 | zeta.send(:services) 145 | }.to raise_error{ |e| 146 | expected = "Could not load services from" 147 | expect(e.to_s.include?(expected)).to be true 148 | } 149 | end 150 | 151 | it 'loads a config file' do 152 | expect(zeta.config).to_not be nil 153 | end 154 | 155 | it 'updates the contracts' do 156 | get_double = double(to_s: '#Data structures', code: 200) 157 | urls = [ 158 | "https://raw.githubusercontent.com/username/service_1/master/contracts/consume.mson", 159 | "https://raw.githubusercontent.com/username/service_1/master/contracts/publish.mson", 160 | "https://raw.githubusercontent.com/username/service_2/production/contracts/publish.mson", 161 | "https://raw.githubusercontent.com/username/service_2/production/contracts/consume.mson", 162 | ] 163 | urls.each do |url| 164 | expect(HTTParty).to receive(:get).with(url).and_return(get_double) 165 | end 166 | 167 | zeta.update_contracts 168 | end 169 | end 170 | 171 | context "list of services defined in a remote file" do 172 | let(:zeta){ Zeta.new(config_file: config_file, env: :with_remote_services_list, verbose: false)} 173 | let(:services_url){ "https://raw.githubusercontent.com/username/repo/master/services.yml" } 174 | let(:remote_services_list){ 175 | < ls Zeta 217 | Zeta.methods: 218 | cache_dir current_service validate_object_to_consume 219 | clear_cache env validate_object_to_consume! 220 | config errors validate_object_to_publish 221 | config_file infrastructure validate_object_to_publish! 222 | consume_object update_contracts verbose= 223 | contracts_fulfilled? update_own_contracts 224 | [2] pry(main)> 225 | ``` 226 | 227 | Each and every one of these goes directly to your instance `Lacerda::Infrastructure`, as defined by `config/zeta.yml`. Feel free to explore them a bit, but the ones' that might be of most interest are: 228 | 229 | - `Zeta.validate_object_to_publish('Post', data_to_send)` makes sure that the content in `data_to_send` conforms to your 'Post' specification in your local `publish.mson` 230 | - `Zeta.consume_object('MessageService::Message', received_data)` will give you an instance of the [Blumquist](https://github.com/moviepilot/blumquist#readme) class, which is an obect that has getters for all properties you specified in `consume.mson` 231 | 232 | If you use these in your servies, they will help keeping the publish and consume specifications in sync with what's actually happening in the code. 233 | 234 | ### RSpec integration 235 | 236 | Of course you'll want to have your infrastructure checked in CI. If you're using RSpec, we've got you covered. Just place the following lines in, for example, `spec/zeta_spec.rb`: 237 | 238 | ```ruby 239 | require_relative 'spec_helper' 240 | 241 | # It will try to fetch the contracts from github and it's a bit slow, 242 | # so you probably just want to run this in CI 243 | if ENV['CI'] 244 | require 'zeta/rspec/autorun_all' 245 | end 246 | ``` 247 | 248 | This will try to guess your credentials and do the same as a `zeta -e test full_check` would do on the command line, but reporting to RSpec instead of printing its output directly. Whether or not you run `Zeta::RSpec.update_contracts` is up to you - perhaps you have HTTP requests disabled in your test suite, or you don't want to be network dependant for every run. If you remove it, however, make sure you run `zeta -e test fetch_remote_contracts` often enough to not be outdated. 249 | --------------------------------------------------------------------------------