├── .gitignore ├── .rspec ├── .rubocop.yml ├── Gemfile ├── Rakefile ├── bin └── req ├── lib ├── req-cli.rb └── req-cli │ ├── config.rb │ ├── config_factory.rb │ ├── context.rb │ ├── curl_backend.rb │ ├── environment.rb │ ├── prepared_request.rb │ ├── request.rb │ ├── request_factory.rb │ ├── state.rb │ ├── state_factory.rb │ ├── state_writer.rb │ └── variable_interpolator.rb ├── readme.md ├── req-cli.gemspec └── spec ├── Reqfile.example ├── Reqstate.example ├── config_factory_spec.rb ├── config_spec.rb ├── context_spec.rb ├── environment_spec.rb ├── req_spec.rb ├── request_factory_spec.rb ├── request_spec.rb ├── spec_helper.rb ├── state_factory_spec.rb ├── state_spec.rb ├── state_writer_spec.rb ├── support ├── spec_factory.rb └── variable_provider_spec.rb └── variable_interpolator_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | Reqfile 3 | .reqstate 4 | Gemfile.lock 5 | /pkg/ 6 | *.gem 7 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2016-08-25 07:33:34 +0200 using RuboCop version 0.42.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 2 10 | #Metrics/AbcSize: 11 | #Max: 18 12 | 13 | # Offense count: 1 14 | # Configuration parameters: CountComments. 15 | Metrics/ClassLength: 16 | Max: 102 17 | 18 | # Offense count: 4 19 | Metrics/CyclomaticComplexity: 20 | Max: 13 21 | 22 | # Offense count: 12 23 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes. 24 | # URISchemes: http, https 25 | Metrics/LineLength: 26 | Max: 120 27 | 28 | # Offense count: 2 29 | Metrics/PerceivedComplexity: 30 | Max: 13 31 | 32 | # Offense count: 10 33 | Style/Documentation: 34 | Exclude: 35 | - 'spec/**/*' 36 | - 'test/**/*' 37 | - 'lib/config.rb' 38 | - 'lib/context.rb' 39 | - 'lib/curl_backend.rb' 40 | - 'lib/environment.rb' 41 | - 'lib/prepared_request.rb' 42 | - 'lib/req-cli.rb' 43 | - 'lib/request.rb' 44 | - 'lib/request_factory.rb' 45 | - 'lib/state.rb' 46 | - 'lib/variable_interpolator.rb' 47 | 48 | # Offense count: 1 49 | # Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts. 50 | Style/FileName: 51 | Exclude: 52 | - 'lib/req-cli.rb' 53 | 54 | # Offense count: 1 55 | # Cop supports --auto-correct. 56 | # Configuration parameters: EnforcedStyle, SupportedStyles, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. 57 | # SupportedStyles: ruby19, ruby19_no_mixed_keys, hash_rockets 58 | Style/HashSyntax: 59 | Exclude: 60 | - 'Rakefile' 61 | 62 | # Offense count: 3 63 | # Cop supports --auto-correct. 64 | Style/MutableConstant: 65 | Exclude: 66 | - 'lib/req-cli.rb' 67 | - 'spec/req_spec.rb' 68 | 69 | # Offense count: 1 70 | # Cop supports --auto-correct. 71 | # Configuration parameters: EnforcedStyle, SupportedStyles. 72 | # SupportedStyles: space, no_space 73 | Style/SpaceAroundEqualsInParameterDefault: 74 | Exclude: 75 | - 'lib/prepared_request.rb' 76 | 77 | # Offense count: 7 78 | # Cop supports --auto-correct. 79 | # Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. 80 | # SupportedStyles: single_quotes, double_quotes 81 | Style/StringLiterals: 82 | Exclude: 83 | - 'req-cli.gemspec' 84 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | task :default => :spec 6 | -------------------------------------------------------------------------------- /bin/req: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'req-cli' 4 | 5 | Req.start(ARGV) 6 | -------------------------------------------------------------------------------- /lib/req-cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'yaml' 3 | 4 | require 'req-cli/context' 5 | require 'req-cli/environment' 6 | require 'req-cli/request' 7 | require 'req-cli/state' 8 | require 'req-cli/config' 9 | require 'req-cli/prepared_request' 10 | require 'req-cli/request_factory' 11 | require 'req-cli/variable_interpolator' 12 | require 'req-cli/curl_backend' 13 | require 'req-cli/config_factory' 14 | require 'req-cli/state_factory' 15 | require 'req-cli/state_writer' 16 | 17 | STATEFILE = '.reqstate' 18 | REQFILE = 'Reqfile' 19 | 20 | class Req < Thor 21 | attr_accessor :config, :state 22 | @config = nil 23 | @state = nil 24 | 25 | def self.create_with_config(config) 26 | req = Req.new 27 | req.config = config 28 | req.state = State.new 29 | req 30 | end 31 | 32 | desc 'contexts', 'list all contexts' 33 | def contexts 34 | init # TODO: avoid this 35 | @config.contexts.each { |ctx| puts ctx.name } 36 | end 37 | 38 | desc 'context NAME', 'switch to context NAME' 39 | def context(name) 40 | init 41 | exit_with_message 'unknown context' unless @config.context? name 42 | puts "switching to context #{name}" 43 | @state.context = @config.get_context name 44 | save_state 45 | end 46 | 47 | desc 'environments', 'list all environments' 48 | def environments 49 | init 50 | @config.environments.each { |env| puts env.name } 51 | end 52 | 53 | desc 'environment', 'switch to environment' 54 | def environment(name) 55 | init 56 | exit_with_message 'environment not found' unless @config.environment? name 57 | puts "switching to environment #{name}" 58 | @state.environment = @config.get_environment name 59 | save_state 60 | end 61 | 62 | desc 'variable NAME VALUE', 'add variable with key NAME and value VALUE' 63 | def variable(name, value) 64 | init 65 | @state.custom_variables[name] = value 66 | save_state 67 | end 68 | 69 | desc 'variables', 'list all custom variables' 70 | def variables 71 | init 72 | @state.custom_variables.each { |key, val| put "#{key}: #{val}" } 73 | end 74 | 75 | desc 'status', 'print current environment and context info' 76 | def status 77 | init 78 | puts "context: #{@state.context.name if @state.context}" 79 | puts "environment: #{@state.environment.name if @state.environment}" 80 | puts 'variables: ' 81 | @state.custom_variables.each { |key, val| puts " #{key}: #{val}" } 82 | end 83 | 84 | desc 'clear', 'clear current context, environment and vars' 85 | def clear 86 | @state = State.new 87 | save_state 88 | end 89 | 90 | desc 'exec REQUESTNAME', 'execute request with name REQUESTNAME' 91 | option :verbose, aliases: '-v' 92 | option :head, aliases: '-I' 93 | def exec(requestname) 94 | init 95 | valid_for_execution? 96 | request = @config.get_request(requestname) 97 | prepared_request = RequestFactory.create(@config, @state, request) 98 | backend = CurlBackend.new prepared_request, options 99 | backend.execute 100 | end 101 | 102 | desc 'requests', 'list all requests' 103 | def requests 104 | init 105 | @config.requests.each { |req| puts "#{req.name} \t#{req.method} \t#{req.path}" } 106 | end 107 | 108 | no_commands do 109 | def init 110 | return if @config 111 | @config = ConfigFactory.build_from_yaml(File.read(REQFILE)) 112 | @state = StateFactory.build_from_yaml(File.read(STATEFILE), @config) if File.exist? STATEFILE 113 | @state = State.new unless File.exist? STATEFILE 114 | end 115 | 116 | def valid_for_execution? 117 | exit_with_message 'No valid environment specified, use req environment NAME to choose an environment' if @state.environment.nil? 118 | exit_with_message 'No valid context specified, use req context NAME to choose a context' if @state.context.nil? 119 | true 120 | end 121 | 122 | def save_state 123 | File.open(STATEFILE, 'w') do |file| 124 | StateWriter.write(@state, file) 125 | end 126 | end 127 | 128 | def exit_with_message(message) 129 | puts message 130 | exit 1 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/req-cli/config.rb: -------------------------------------------------------------------------------- 1 | class Config 2 | attr_reader :contexts, :environments, :requests 3 | 4 | def initialize(args = {}) 5 | @contexts = args[:contexts] 6 | @environments = args[:environments] 7 | @requests = args[:requests] 8 | end 9 | 10 | def get_context(ctx_name) 11 | find_by_name @contexts, ctx_name 12 | end 13 | 14 | def get_environment(env_name) 15 | find_by_name @environments, env_name 16 | end 17 | 18 | def get_request(req_name) 19 | find_by_name @requests, req_name 20 | end 21 | 22 | alias :context? :get_context 23 | alias :environment? :get_environment 24 | alias :request? :get_request 25 | 26 | private 27 | def find_by_name(collection, name) 28 | collection.find { |x| x.name == name } 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/req-cli/config_factory.rb: -------------------------------------------------------------------------------- 1 | # Factory responsible for building valid Config objects from YAML input 2 | module ConfigFactory 3 | def self.build_from_yaml(yaml_str) 4 | yaml = YAML.load yaml_str 5 | 6 | contexts = yaml.fetch('contexts', {}).map { |ctx| build_context ctx } 7 | environments = yaml.fetch('environments', {}).map { |env| build_environment env } 8 | requests = yaml.fetch('requests', {}).map { |req| build_request req } 9 | 10 | Config.new(contexts: contexts, 11 | environments: environments, 12 | requests: requests) 13 | end 14 | 15 | def self.build_context(args) 16 | Context.new(name: args.fetch('name'), 17 | variables: args.fetch('vars', {}), 18 | headers: args.fetch('headers', {})) 19 | end 20 | 21 | def self.build_environment(args) 22 | Environment.new(name: args.fetch('name'), 23 | endpoint: args.fetch('endpoint'), 24 | variables: args.fetch('vars', {}), 25 | headers: args.fetch('headers', {})) 26 | end 27 | 28 | def self.build_request(args) 29 | Request.new(name: args.fetch('name'), 30 | path: args.fetch('path'), 31 | method: args.fetch('method', 'get'), 32 | headers: args.fetch('headers', {}), 33 | variables: args.fetch('vars', {}), 34 | data: args.fetch('data', nil)) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/req-cli/context.rb: -------------------------------------------------------------------------------- 1 | class Context 2 | attr_reader :name, :variables, :headers 3 | 4 | def initialize(hash = {}) 5 | @name = hash[:name] || '' 6 | @variables = hash[:variables] || {} 7 | @headers = hash[:headers] || {} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/req-cli/curl_backend.rb: -------------------------------------------------------------------------------- 1 | class CurlBackend 2 | def initialize(prepared_request, options = {}) 3 | @request = prepared_request 4 | @curl_options = parse_curl_options(options) 5 | end 6 | 7 | def execute 8 | header_string = @request.headers.map { |key, val| "-H '#{key}: #{val}'" }.join ' ' 9 | data_string = @request.data ? "-d '#{@request.data}'" : '' 10 | 11 | curl_command = "curl -X #{@request.method.upcase} #{header_string} #{data_string} #{@curl_options} #{@request.uri}" 12 | system curl_command 13 | end 14 | 15 | def parse_curl_options(options) 16 | opts = '' 17 | 18 | opts += '--verbose ' if options.key? 'verbose' 19 | opts += ' --head ' if options.key? 'head' 20 | 21 | opts 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/req-cli/environment.rb: -------------------------------------------------------------------------------- 1 | class Environment 2 | attr_reader :name, :variables, :endpoint, :headers 3 | 4 | def initialize(hash = {}) 5 | @name = hash[:name] 6 | @endpoint = hash[:endpoint] 7 | @variables = hash[:variables] || {} 8 | @headers = hash[:headers] || {} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/req-cli/prepared_request.rb: -------------------------------------------------------------------------------- 1 | class PreparedRequest 2 | attr_reader :headers, :uri, :method, :data 3 | 4 | def initialize(options={}) 5 | @headers = options[:headers] || {} 6 | @uri = options[:uri] || '' 7 | @method = options[:method] || :get 8 | @data = options[:data] || nil 9 | end 10 | 11 | # Hash of HTTP headers 12 | 13 | # string with full request URI 14 | 15 | # symbol :get, :post, :put, :delete 16 | end 17 | -------------------------------------------------------------------------------- /lib/req-cli/request.rb: -------------------------------------------------------------------------------- 1 | class Request 2 | attr_reader :name, :path, :method, :headers, :variables, :data 3 | 4 | def initialize(args = {}) 5 | @name = args[:name] 6 | @path = args[:path] 7 | @method = args[:method] 8 | 9 | @headers = args[:headers] || {} 10 | @variables = args[:vars] || {} 11 | @data = args[:data] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/req-cli/request_factory.rb: -------------------------------------------------------------------------------- 1 | class RequestFactory 2 | def self.create(config, state, request) 3 | environment = state.environment 4 | context = state.context 5 | 6 | variables = merge_variables(environment, context, request, state.custom_variables) 7 | interpolator = VariableInterpolator.new(variables) 8 | 9 | PreparedRequest.new( 10 | headers: build_headers(environment, context, request, interpolator), 11 | method: request.method.downcase.to_sym, 12 | uri: build_uri(environment, request, interpolator), 13 | data: build_data(request, interpolator) 14 | ) 15 | end 16 | 17 | def self.merge_variables(environment, context, request, custom_variables) 18 | (environment.variables || {}) 19 | .merge(context.variables || {}) 20 | .merge(request.variables || {}) 21 | .merge(custom_variables || {}) 22 | end 23 | 24 | def self.build_headers(environment, context, request, interpolator) 25 | environment.headers 26 | .merge(context.headers) 27 | .merge(request.headers) 28 | .map { |key, value| [key, interpolator.interpolate(value)] }.to_h 29 | end 30 | 31 | def self.build_uri(environment, request, interpolator) 32 | uri = environment.endpoint + request.path 33 | interpolator.interpolate(uri) 34 | end 35 | 36 | def self.build_data(request, interpolator) 37 | return nil if request.data.nil? 38 | 39 | interpolator.interpolate(request.data) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/req-cli/state.rb: -------------------------------------------------------------------------------- 1 | class State 2 | attr_accessor :context, :environment, :custom_variables 3 | 4 | def initialize(args = {}) 5 | @context = args[:context] || Context.new 6 | @environment = args[:environment] || Environment.new 7 | @custom_variables = args[:custom_variables] || {} 8 | end 9 | 10 | def variables 11 | environment.variables 12 | .merge(context.variables) 13 | .merge(custom_variables) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/req-cli/state_factory.rb: -------------------------------------------------------------------------------- 1 | module StateFactory 2 | def self.build_from_yaml(yaml_str, config) 3 | yaml = YAML.load yaml_str 4 | 5 | raise 'Invalid YAML' unless yaml.kind_of? Hash 6 | 7 | State.new(environment: build_environment(yaml, config), 8 | context: build_context(yaml, config), 9 | custom_variables: build_custom_variables(yaml, config)) 10 | end 11 | 12 | def self.build_context(hash, config) 13 | name = hash['context'] 14 | config.get_context(name) || Context.new 15 | end 16 | 17 | def self.build_environment(hash, config) 18 | name = hash['environment'] 19 | config.get_environment(name) || Environment.new 20 | end 21 | 22 | def self.build_custom_variables(hash, config) 23 | hash['custom_variables'] || {} 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/req-cli/state_writer.rb: -------------------------------------------------------------------------------- 1 | module StateWriter 2 | 3 | def self.stringify(state) 4 | { 5 | 'context' => state.context.name || '', 6 | 'environment' => state.environment.name, 7 | 'custom_variables' => state.custom_variables 8 | }.to_yaml 9 | end 10 | 11 | def self.write(state, file) 12 | file.write(stringify(state)) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/req-cli/variable_interpolator.rb: -------------------------------------------------------------------------------- 1 | class VariableInterpolator 2 | attr_reader :variables 3 | 4 | def initialize(variables = {}) 5 | @variables = variables.map { |key, value| [key.to_sym, value] }.to_h 6 | end 7 | 8 | def interpolate(input) 9 | input.gsub(/\${[a-zA-Z0-9\-_]*}/) do |m| 10 | var = m.scan(/[a-zA-Z0-9\-_]+/).first.to_sym 11 | variables[var] 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Req-cli 2 | 3 | Req-cli is a CLI tool for templating HTTP requests from a config file and execute them with curl. 4 | 5 | Req-cli takes its configuration from a Reqfile. This file is typically in the root of your project and describes your environments, contexts and requests. 6 | 7 | # Installation 8 | 9 | Req can be installed through RubyGems 10 | 11 | ``` 12 | gem install req-cli 13 | ``` 14 | 15 | # Example Reqfile 16 | 17 | ```yaml 18 | --- 19 | environments: 20 | - name: production 21 | endpoint: "https://example.com" 22 | vars: 23 | env: production 24 | headers: 25 | X-App-Id: xeiHahK4feeH2oiZ 26 | 27 | - name: development 28 | endpoint: "http://localhost:3000" 29 | vars: 30 | env: development 31 | headers: 32 | X-App-Id: bienaeGaishe0EeC 33 | 34 | contexts: 35 | - name: steven 36 | vars: 37 | sessiontoken: naivahquooQuoo5OhFue5Pii3aexoh 38 | user_id: "steven" 39 | 40 | - name: dirk 41 | vars: 42 | sessiontoken: eitaeD6Oosh8va7iek8Ohch5ox1Oog 43 | user_id: "dirk" 44 | 45 | requests: 46 | - name: getItems 47 | path: "/items" 48 | method: GET 49 | headers: 50 | X-Session-Token: "${sessiontoken}" 51 | 52 | - name: addItem 53 | path: "/items" 54 | method: POST 55 | headers: 56 | Content-Type: "application/json" 57 | X-Parse-Session-Token: "${sessiontoken}" 58 | vars: 59 | title: "Default title" 60 | data: > 61 | { 62 | "title": "${title}", 63 | "author": "${user_id}" 64 | } 65 | ``` 66 | 67 | # Example Usage 68 | 69 | Given the above Reqfile, req-cli can be used like this 70 | 71 | ```bash 72 | # use production environment 73 | $ req environment production 74 | 75 | # use steven context 76 | $ req context steven 77 | 78 | # add a custom variable named title with value "My Title" 79 | $ req variable title "My Title" 80 | 81 | # show current status 82 | $ req status 83 | 84 | context: steven 85 | environment: production 86 | variables: 87 | title: My Title 88 | 89 | # execute the "examplePost" request 90 | $ req exec examplePost 91 | ``` 92 | 93 | This will invoke Curl like this: 94 | 95 | ```bash 96 | $ curl -X POST -H 'X-App-Id: xeiHahK4feeH2oiZ' -H 'Content-Type: application/json' -H 'X-Parse-Session-Token: naivahquooQuoo5OhFue5Pii3aexoh' -d '{ 97 | "title": "My Title", 98 | "author": "steven" 99 | } 100 | ' https://example.com/items 101 | ``` 102 | 103 | # Command Reference 104 | 105 | **req contexts** - List all available contexts 106 | **req context NAME** - Switch to context NAME 107 | 108 | **req environments** - List all available environments 109 | **req environment NAME** - Switch to environment NAME 110 | 111 | **req variables** - List all custom variables 112 | **req variable NAME VALUE** - Add custom variable with name NAME and value VALUE 113 | 114 | **req requests** - List all available requests 115 | 116 | **req status** - List current environment, context and custom variables 117 | **req clear** - Clear current environment, context and custom variables 118 | 119 | **req exec NAME** - Execute request with name NAME 120 | 121 | `req exec NAME` supports `--verbose/-v` and `--head/-I` options which are passed straight on to curl 122 | 123 | # Configuration Reference 124 | 125 | ## Variable interpolation 126 | 127 | Variables can be inserted by using `${variable_name}` tags in the Reqfile. Variable tags are only valid in `endpoint`, `headers` and `data` attributes. 128 | 129 | Only `a-z`, `A-Z`, `-` and `_` are valid characters in variable names 130 | 131 | Variables can be defined as `custom variables` using `req variable KEY VALUE` or in the Reqfile on `environments`, `contexts` and `requests`. 132 | 133 | When a variable is defined more than once the following priorities are used: 134 | 135 | ``` 136 | custom variables > request variables > context variables > environment variables 137 | ``` 138 | 139 | # TODO 140 | 141 | This project is a little weekend hacking project, I still have some plans for improvement 142 | 143 | - Support ${env.VAR_NAME} syntax for shell environment variables 144 | - Support ${!...} syntax for shell command interpolation 145 | - `req init` for generating an empty Reqfile 146 | - Add a cli parameter for showing the curl command 147 | - Pass extra arguments to curl 148 | 149 | 150 | -------------------------------------------------------------------------------- /req-cli.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'req-cli' 7 | s.version = '0.0.1' 8 | s.date = '2016-08-13' 9 | 10 | s.summary = "CLI tool for templating HTTP requests from a config file and execute them with curl." 11 | s.description = "" 12 | s.authors = ["Steven Van Bael"] 13 | s.email = 'steven@quantus.io' 14 | s.homepage = 'http://github.com/vbsteven/req' 15 | s.license = 'MIT' 16 | 17 | s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) } 18 | s.bindir = 'bin' 19 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | s.require_paths = ['lib'] 21 | 22 | s.add_runtime_dependency "thor", "~> 0.19" 23 | s.add_runtime_dependency "httparty", "~> 0.14" 24 | 25 | s.add_development_dependency 'rspec', '~> 3.5.0' 26 | s.add_development_dependency 'rake', '~> 11.2.2' 27 | s.add_development_dependency 'bundler' 28 | end 29 | -------------------------------------------------------------------------------- /spec/Reqfile.example: -------------------------------------------------------------------------------- 1 | --- 2 | environments: 3 | - name: production 4 | endpoint: https://example.com 5 | vars: 6 | app_id: 1 7 | environmentvar: 'production' 8 | headers: 9 | X-req-environment: 'production' 10 | X-req-environment-var: '${var}' 11 | - name: staging 12 | endpoint: https://staging.example.com 13 | vars: 14 | app_id: 2 15 | environmentvar: 'staging' 16 | headers: 17 | X-req-environment: 'staging' 18 | X-req-environment-var: '${var}' 19 | - name: development 20 | endpoint: https://localhost 21 | vars: 22 | app_id: 3 23 | environmentvar: 'development' 24 | headers: 25 | X-req-environment: 'development' 26 | X-req-environment-var: '${var}' 27 | 28 | contexts: 29 | - name: context1 30 | vars: 31 | session_token: 123456 32 | user_id: 12 33 | username: steven 34 | contextvar: 'context1' 35 | headers: 36 | X-req-context: 'context1' 37 | X-req-context-var: '${var}' 38 | - name: context2 39 | vars: 40 | session_token: 789012 41 | user_id: 42 42 | username: birgit 43 | contextvar: 'context2' 44 | headers: 45 | X-req-context: 'context2' 46 | X-req-context-var: '${var}' 47 | 48 | requests: 49 | - name: exampleRequest 50 | path: /example/path 51 | method: GET 52 | headers: 53 | X-req-request: 'exampleRequest' 54 | X-req-request-var: '${var}' 55 | Content-Type: "application/json" 56 | -------------------------------------------------------------------------------- /spec/Reqstate.example: -------------------------------------------------------------------------------- 1 | --- 2 | environment: 'production' 3 | context: 'context1' 4 | custom_variables: 5 | custom_var1: 'custom value 1' 6 | -------------------------------------------------------------------------------- /spec/config_factory_spec.rb: -------------------------------------------------------------------------------- 1 | describe ConfigFactory do 2 | context 'load from YAML' do 3 | it 'correctly parses YAML input string into Config' do 4 | yaml = SpecFactory.config_yaml_str 5 | config = ConfigFactory.build_from_yaml(yaml) 6 | 7 | expect(config).to be_kind_of Config 8 | expect(config.contexts).to be_kind_of Array 9 | expect(config.contexts.count).to eq 2 10 | expect(config.contexts.first).to be_kind_of Context 11 | expect(config.contexts.first.name).to eq 'context1' 12 | 13 | expect(config.environments).to be_kind_of Array 14 | expect(config.environments.count).to eq 3 15 | expect(config.environments.first).to be_kind_of Environment 16 | expect(config.environments.first.name).to eq 'production' 17 | 18 | expect(config.requests).to be_kind_of Array 19 | expect(config.requests.count).to eq 1 20 | expect(config.requests.first).to be_kind_of Request 21 | expect(config.requests.first.name).to eq 'exampleRequest' 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | describe Config do 2 | 3 | context 'context methods' do 4 | let(:config) { SpecFactory.config } 5 | 6 | it 'implements context?' do 7 | expect(config.context?('context1')).to be 8 | expect(config.context?('bogus_context')).not_to be 9 | end 10 | 11 | it 'implements get_context' do 12 | expect(config.get_context('context1')).to be_kind_of Context 13 | expect(config.get_context('context1').name).to eq 'context1' 14 | expect(config.get_context('bogus_context')).to be_nil 15 | end 16 | 17 | it 'implements environment?' do 18 | expect(config.environment?('production')).to be 19 | expect(config.environment?('bogus_environment')).not_to be 20 | end 21 | 22 | it 'implements get_environment' do 23 | expect(config.get_environment('production')).to be_kind_of Environment 24 | expect(config.get_environment('production').name).to eq 'production' 25 | expect(config.get_environment('bogus_environment')).to be_nil 26 | end 27 | 28 | it 'implements request?' do 29 | expect(config.request?('exampleRequest')).to be 30 | expect(config.request?('bogus_request')).not_to be 31 | end 32 | 33 | it 'implements get_request' do 34 | expect(config.get_request('exampleRequest')).to be_kind_of Request 35 | expect(config.get_request('exampleRequest').name).to eq 'exampleRequest' 36 | expect(config.get_request('bogus_request')).to be_nil 37 | end 38 | end 39 | 40 | #context 'variables method' do 41 | #it 'includes variables from environments in variables' do 42 | #env = Environment.new name: 'test', 43 | #variables: {key1: 'val1'} 44 | #config = Config.new(environments: [env]) 45 | #expect(config.variables).to have_key :key1 46 | #end 47 | #end 48 | end 49 | -------------------------------------------------------------------------------- /spec/context_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe Context do 3 | it_behaves_like 'VariableProvider' 4 | 5 | context 'initialize' do 6 | let(:ctx) { Context.new name: 'ctx', variables: { key: 'value' } } 7 | 8 | it 'extracts name from initializer hash' do 9 | expect(ctx.name).to eq 'ctx' 10 | end 11 | 12 | it 'extract variables from initializer hash' do 13 | expect(ctx.variables).to be_kind_of Hash 14 | expect(ctx.variables.keys.first).to eq :key 15 | expect(ctx.variables[:key]).to eq 'value' 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/environment_spec.rb: -------------------------------------------------------------------------------- 1 | describe Environment do 2 | it_behaves_like 'VariableProvider' 3 | 4 | context 'initialize' do 5 | let(:env) do 6 | Environment.new name: 'env', 7 | endpoint: 'http://localhost/', 8 | variables: { key: 'value' } 9 | end 10 | 11 | it 'extracts name from initializer hash' do 12 | expect(env.name).to eq 'env' 13 | end 14 | 15 | it 'extracts endpoint from initializer hash' do 16 | expect(env.endpoint).to eq 'http://localhost/' 17 | end 18 | 19 | it 'extract variables from initializer hash' do 20 | expect(env.variables).to be_kind_of Hash 21 | expect(env.variables.keys.first).to eq :key 22 | expect(env.variables[:key]).to eq 'value' 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/req_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Req' do 2 | let(:req) do 3 | config = SpecFactory.config 4 | Req.create_with_config(config) 5 | end 6 | 7 | available_commands = 8 | [:context, :contexts, :environment, :environments, :requests] 9 | available_commands.each do |cmd| 10 | it "responds to #{cmd} command" do 11 | expect(req).to respond_to cmd 12 | end 13 | end 14 | 15 | context 'contexts command' do 16 | it 'prints all contexts to stdout' do 17 | expect { req.contexts }.to output("context1\ncontext2\n").to_stdout 18 | end 19 | end 20 | 21 | context 'context command' do 22 | it 'changes current context' do 23 | expect { req.context('context1') }.to output(/.*context1.*/).to_stdout 24 | expect(req.state.context.name).to eq 'context1' 25 | end 26 | end 27 | 28 | context 'environments command' do 29 | it 'prints all environments to stdout' do 30 | expect { req.environments }.to output("production\nstaging\ndevelopment\n").to_stdout 31 | end 32 | end 33 | 34 | context 'environment command' do 35 | it 'changes current environment' do 36 | req.state.environment = 'development' 37 | expect { req.environment('production') }.to( 38 | output(/.*production.*/).to_stdout 39 | ) 40 | expect(req.state.environment.name).to eq 'production' 41 | end 42 | end 43 | 44 | context 'status command' do 45 | it 'outputs current environment' do 46 | req.environment 'production' 47 | expect { req.status }.to output(/.*environment.*production.*/).to_stdout 48 | end 49 | 50 | it 'outputs current context' do 51 | req.context 'context1' 52 | expect { req.status }.to output(/.*context.*context1.*/).to_stdout 53 | end 54 | end 55 | 56 | context 'requests command' do 57 | it 'outputs request info' do 58 | expect { req.requests }.to( 59 | output(%r{^exampleRequest[\s]+GET[\s]+/example/path$}).to_stdout 60 | ) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/request_factory_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'RequestFactory' do 2 | context 'create' do 3 | config = SpecFactory.config 4 | state = SpecFactory.state 5 | state.custom_variables.store 'var', 'custom_value' 6 | state.custom_variables.store 'customvar', 'custom_value' 7 | request = config.requests.first 8 | request.headers['X-custom-1'] = '${environmentvar}' 9 | request.headers['X-custom-2'] = '${contextvar}' 10 | request.headers['X-custom-3'] = '${customvar}' 11 | 12 | req = RequestFactory.create(config, state, config.requests.first) 13 | 14 | it 'returns a PreparedRequest' do 15 | expect(req).to be_kind_of PreparedRequest 16 | end 17 | 18 | it 'contains headers from environment' do 19 | expect(req.headers.keys).to include 'X-req-environment' 20 | end 21 | 22 | it 'contains headers from context' do 23 | expect(req.headers.keys).to include 'X-req-context' 24 | end 25 | 26 | it 'contains headers from request' do 27 | expect(req.headers.keys).to include 'X-req-request' 28 | end 29 | 30 | it 'applies var interpolation to environment headers' do 31 | expect(req.headers['X-req-environment-var']).to eq 'custom_value' 32 | end 33 | 34 | it 'applies var interpolation to context headers' do 35 | expect(req.headers['X-req-context-var']).to eq 'custom_value' 36 | end 37 | 38 | it 'applies var interpolation to request headers' do 39 | expect(req.headers['X-req-request-var']).to eq 'custom_value' 40 | end 41 | 42 | it 'applies var interpolation with environment variables' do 43 | expect(req.headers['X-custom-1']).to eq 'production' 44 | end 45 | 46 | it 'applies var interpolation with context variables' do 47 | expect(req.headers['X-custom-2']).to eq 'context1' 48 | end 49 | 50 | it 'applies var interpolation with custom variables' do 51 | expect(req.headers['X-custom-3']).to eq 'custom_value' 52 | end 53 | 54 | it 'symbolizes and downcases the request method' do 55 | expect(req.method).to eq :get 56 | end 57 | 58 | it 'builds uri from environment.endpoint and request path' do 59 | expect(req.uri).to eq 'https://example.com/example/path' 60 | end 61 | 62 | it 'interpolate variables present in the uri' do 63 | # TODO: this test needs too much setup 64 | env = Environment.new name: 'uritest', endpoint: 'https://${server}' 65 | config.environments << env 66 | state.custom_variables.store 'server', 'variableserver' 67 | state.environment = env 68 | 69 | req = RequestFactory.create(config, state, config.requests.first) 70 | expect(req.uri).to eq 'https://variableserver/example/path' 71 | end 72 | end 73 | 74 | context 'POST request' do 75 | config = SpecFactory.config 76 | state = SpecFactory.state 77 | state.custom_variables.store 'var1', 'value' 78 | request = Request.new name: 'test', path: '/path', method: 'post', data: '{"key":"${var1}"}' 79 | 80 | req = RequestFactory.create(config, state, request) 81 | 82 | it 'has a valid data attribute' do 83 | expect(req.data).to be_kind_of String 84 | end 85 | 86 | it 'interpolates values in data' do 87 | expect(req.data).to eq '{"key":"value"}' 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/request_spec.rb: -------------------------------------------------------------------------------- 1 | describe Request do 2 | it_behaves_like 'VariableProvider' 3 | 4 | end 5 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # The `.rspec` file also contains a few flags that are not defaults but that 16 | # users commonly want. 17 | # 18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 19 | 20 | load 'req-cli.rb' 21 | 22 | RSpec.configure do |config| 23 | # rspec-expectations config goes here. You can use an alternate 24 | # assertion/expectation library such as wrong or the stdlib/minitest 25 | # assertions if you prefer. 26 | config.expect_with :rspec do |expectations| 27 | # This option will default to `true` in RSpec 4. It makes the `description` 28 | # and `failure_message` of custom matchers include text for helper methods 29 | # defined using `chain`, e.g.: 30 | # be_bigger_than(2).and_smaller_than(4).description 31 | # # => "be bigger than 2 and smaller than 4" 32 | # ...rather than: 33 | # # => "be bigger than 2" 34 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 35 | end 36 | 37 | # rspec-mocks config goes here. You can use an alternate test double 38 | # library (such as bogus or mocha) by changing the `mock_with` option here. 39 | config.mock_with :rspec do |mocks| 40 | # Prevents you from mocking or stubbing a method that does not exist on 41 | # a real object. This is generally recommended, and will default to 42 | # `true` in RSpec 4. 43 | mocks.verify_partial_doubles = true 44 | end 45 | 46 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 47 | # have no way to turn it off -- the option exists only for backwards 48 | # compatibility in RSpec 3). It causes shared context metadata to be 49 | # inherited by the metadata hash of host groups and examples, rather than 50 | # triggering implicit auto-inclusion in groups with matching metadata. 51 | config.shared_context_metadata_behavior = :apply_to_host_groups 52 | end 53 | 54 | Dir["./spec/support/**/*.rb"].sort.each { |f| require f} 55 | -------------------------------------------------------------------------------- /spec/state_factory_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe StateFactory do 3 | def example_state 4 | StateFactory.build_from_yaml( 5 | SpecFactory.state_yaml_str, 6 | SpecFactory.config 7 | ) 8 | end 9 | 10 | let(:state) { example_state } 11 | 12 | context 'build_from_yaml' do 13 | it 'has a full environment' do 14 | expect(state.environment).to be_kind_of Environment 15 | expect(state.environment.name).to eq 'production' 16 | end 17 | 18 | it 'has a full context' do 19 | expect(state.context).to be_kind_of Context 20 | expect(state.context.name).to eq 'context1' 21 | end 22 | 23 | it 'has a hash with custom variables' do 24 | expect(state.custom_variables).to be_kind_of Hash 25 | end 26 | 27 | it 'raises an exception when not deserialzing into a Hash' do 28 | expect { StateFactory.build_from_yaml 'invalid_yaml', SpecFactory.config } 29 | .to raise_error 'Invalid YAML' 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/state_spec.rb: -------------------------------------------------------------------------------- 1 | describe State do 2 | it_behaves_like 'VariableProvider' 3 | 4 | context 'variables' do 5 | it 'returns variables from environment' do 6 | env = Environment.new name: 'env', variables: { key1: 'val1' } 7 | state = State.new environment: env 8 | 9 | expect(state.variables).to have_key :key1 10 | expect(state.variables[:key1]).to eq 'val1' 11 | end 12 | 13 | it 'returns variables from context' do 14 | ctx = Context.new name: 'ctx', variables: { key1: 'val1' } 15 | state = State.new context: ctx 16 | 17 | expect(state.variables).to have_key :key1 18 | expect(state.variables[:key1]).to eq 'val1' 19 | end 20 | 21 | it 'returns variables from custom_variables' do 22 | state = State.new custom_variables: { key1: 'val1' } 23 | 24 | expect(state.variables).to have_key :key1 25 | expect(state.variables[:key1]).to eq 'val1' 26 | end 27 | 28 | it 'custom_variables get priority' do 29 | state = State.new custom_variables: { key: 'custom' }, 30 | environment: Environment.new(variables: { key: 'env' }), 31 | context: Context.new(variables: { key: 'ctx' }) 32 | 33 | expect(state.variables[:key]).to eq 'custom' 34 | end 35 | 36 | it 'without custom variables, context gets priority' do 37 | state = State.new custom_variables: {}, 38 | environment: Environment.new(variables: { key: 'env' }), 39 | context: Context.new(variables: { key: 'ctx' }) 40 | 41 | expect(state.variables[:key]).to eq 'ctx' 42 | end 43 | 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /spec/state_writer_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe StateWriter do 3 | 4 | context '#stringify' do 5 | 6 | let(:state) { SpecFactory.state } 7 | 8 | it 'returns valid yaml' do 9 | expect { YAML.parse(subject.stringify(state)) }.not_to raise_error 10 | end 11 | 12 | it 'saves context name' do 13 | expect(subject.stringify(state)).to match /^context: context1/ 14 | end 15 | 16 | it 'saves environment name' do 17 | expect(subject.stringify(state)).to match /^environment: production/ 18 | end 19 | 20 | it 'saves custom variables' do 21 | expect(subject.stringify(state)).to match /^custom_variables:/ 22 | expect(subject.stringify(state)).to match /^ custom_var1: custom value 1/ 23 | end 24 | end 25 | 26 | context '#write' do 27 | 28 | let(:state) { SpecFactory.state } 29 | 30 | it 'calls #write on given file' do 31 | file = double('File') 32 | 33 | expect(file).to receive(:write).with(subject.stringify(state)) 34 | subject.write(state, file) 35 | end 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/support/spec_factory.rb: -------------------------------------------------------------------------------- 1 | module SpecFactory 2 | 3 | REQFILE_LOCATION = 'spec/Reqfile.example' 4 | REQSTATE_LOCATION = 'spec/Reqstate.example' 5 | 6 | def self.config_yaml_str 7 | File.read(REQFILE_LOCATION) 8 | end 9 | 10 | def self.config 11 | ConfigFactory.build_from_yaml(config_yaml_str) 12 | end 13 | 14 | def self.state_yaml_str 15 | File.read(REQSTATE_LOCATION) 16 | end 17 | 18 | def self.state 19 | StateFactory.build_from_yaml(state_yaml_str, config) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/variable_provider_spec.rb: -------------------------------------------------------------------------------- 1 | # VariableProvider is a ducktype for all classes that have a variables method 2 | # that should return a hash of possible replacement variables 3 | shared_examples_for 'VariableProvider' do 4 | it 'responds_to :variables' do 5 | expect(subject).to respond_to :variables 6 | end 7 | 8 | it 'returns a Hash from :variables' do 9 | expect(subject.variables).to be_kind_of Hash 10 | end 11 | end 12 | 13 | class VariableProviderDouble 14 | def variables 15 | {} 16 | end 17 | end 18 | 19 | describe VariableProviderDouble do 20 | it_behaves_like 'VariableProvider' 21 | end 22 | -------------------------------------------------------------------------------- /spec/variable_interpolator_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'VariableInterpolator' do 2 | context 'initializer' do 3 | let(:interpolator) { VariableInterpolator.new var1: 'value1', var2: 'value2' } 4 | 5 | it 'takes hash of variables as initializer param' do 6 | expect(interpolator.variables).to be_kind_of Hash 7 | expect(interpolator.variables.count).to eq 2 8 | expect(interpolator.variables[:var1]).to eq 'value1' 9 | expect(interpolator.variables[:var2]).to eq 'value2' 10 | end 11 | end 12 | 13 | context 'interpolate' do 14 | let(:interpolator) { VariableInterpolator.new var1: 'value1', var2: 'value2' } 15 | 16 | it 'does nothing when no values are present' do 17 | expect(interpolator.interpolate('test')).to eq 'test' 18 | end 19 | 20 | it 'interpolates single variables' do 21 | expect(interpolator.interpolate('test ${var1}')).to eq 'test value1' 22 | end 23 | 24 | it 'interpolates multiple variables' do 25 | expect(interpolator.interpolate('test ${var1} ${var2}')).to eq 'test value1 value2' 26 | end 27 | end 28 | end 29 | --------------------------------------------------------------------------------