perfume 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cdbf43b151d7f376a9858e70e92c2a86dc815a73
4
+ data.tar.gz: 44e31ee400a397f536e36ee94eea30f7abe6fc06
5
+ SHA512:
6
+ metadata.gz: 1506aa517bcc89219aabaa16765d51f140fc5d841625f3f8f8a4a7813485f76057fc83e8608bfb8c316c039224ad474bac11efb4286703a200acb98e77a69a07
7
+ data.tar.gz: cf04e57c3e583617460867bae3d1b45df456daedac7e4206c576c34ee2ed829129bdf5204b24de088bf739a30e2a444af397db8eca361b7c9f011885c02e9e67
data/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+ This project adheres to [Semantic Versioning](http://semver.org/).
4
+
5
+ ## [Unreleased]
6
+
7
+ ...
8
+
9
+ ## [0.1.0]
10
+
11
+ ### Added
12
+
13
+ - Project extracted from other library and moved into this standalone gem.
data/Dockerfile ADDED
@@ -0,0 +1,11 @@
1
+ FROM ruby:2.2
2
+
3
+ ENV GEM_NAME perfume
4
+ ENV WORKDIR /usr/local/src/$GEM_NAME
5
+
6
+ RUN mkdir -p $WORKDIR
7
+ WORKDIR $WORKDIR
8
+ ADD . $WORKDIR
9
+ RUN bundle install
10
+
11
+ CMD bundle exec rake spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in perfume.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Rake Bump
2
+
3
+ [![Status](https://codeship.com/projects/xxx/status?branch=master)](https://codeship.com/projects/xxx)
4
+
5
+ Perfume is a set of fragrances to make your (micro)services code smell nice. It's bunch of common practices and
6
+ shorthands to deal with common problems. At the moment it includes the following stuff:
7
+
8
+ * So called SuperObject - a lightweight hash-initialized struct-like object.
9
+ * So called Service - a callable object that's designed to implement your business processes.
10
+ * So called Promise - extended Service with ignorable errors.
11
+ * Unified and pre-configured logging (adapted Log4r).
12
+ * Shell services.
13
+ * Console interaction service.
14
+ * Exit (abort) service.
15
+ * Various core extensions.
16
+ * Testing utilities.
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'perfume', '0.1.0'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ $ bundle
29
+
30
+ Or install it yourself as:
31
+
32
+ $ gem install perfume --version 0.1.0
33
+
34
+ ## Usage
35
+
36
+ TODO: ...
37
+
38
+ ## Development
39
+
40
+ You have two options to work with this project. The [docker flow](#setup-with-docker) is suggested since solves problems of compatibility of tools.
41
+
42
+ ### Manual Setup
43
+
44
+ First off, make sure you have **Ruby 2.2+** and latest version of **Bundler** on your machine. After checking out the repo, you can install dependencies and prepare the project with:
45
+
46
+ $ bin/setup
47
+
48
+ Now you can run tests:
49
+
50
+ $ bundle exec rake spec
51
+
52
+ You can also connect to interactive prompt that will allow you to experiment. To do this, run:
53
+
54
+ $ bundle exec bin/console
55
+
56
+ To install this gem onto your local machine, run:
57
+
58
+ $ bundle exec rake install
59
+
60
+ ### Setup with Docker
61
+
62
+ If you're lazy and don't wanna get into how the setup works, here's something for you. This project comes fully [dockerized](http://docker.io/). Install docker toolchain and then go for:
63
+
64
+ $ docker-compose build
65
+
66
+ All done, you can do testing and fiddling around:
67
+
68
+ $ docker-compose run perfume bash
69
+ root@xyyyyxx:/usr/local/src/perfume# bundle exec rake spec
70
+ root@xyyyyxx:/usr/local/src/perfume# bundle exec bin/console
71
+
72
+ ### Releasing new version
73
+
74
+ This project is powered by rake-bump. To release gem version, follow [this continuous releasing guide](https://github.com/jobandtalent/rake-bump#continuous-releasing)
75
+
76
+ ## Contributing
77
+
78
+ Bug reports and pull requests are welcome [here](https://github.com/kkvlk/perfume/issues).
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require "bundler/gem_tasks"
2
+ require "perfume/version"
3
+ # require "rake/bump/tasks"
4
+ require "rake/testtask"
5
+
6
+ # Rake::Bump::Tasks.new do |t|
7
+ # t.gem_name = 'perfume'
8
+ # t.gem_current_version = Perfume::VERSION
9
+ # end
10
+
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << "test"
13
+ t.libs << "lib"
14
+ t.test_files = FileList['test/**/*_test.rb']
15
+ end
16
+
17
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "perfume"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ perfume:
2
+ build: .
3
+ volumes:
4
+ - .:/usr/local/src/perfume:rw
@@ -0,0 +1,9 @@
1
+ module Perfume
2
+ require 'perfume/logging'
3
+ require 'perfume/service'
4
+ require 'perfume/promise'
5
+ require 'perfume/shell'
6
+ require 'perfume/exit'
7
+ require 'perfume/console'
8
+ require 'perfume/core_ext/dir'
9
+ end
@@ -0,0 +1,62 @@
1
+ require 'perfume/super_object'
2
+
3
+ module Perfume
4
+ # Public: Sometimes there's a need to ask user a question or to get stuff from him.
5
+ # This simple class handles this. You can initialize this virtual "Console" pointed to
6
+ # given input and output stream and perform common operations like getting user input,
7
+ # asking questions (yes/no questions too), etc.
8
+ class Console < SuperObject
9
+ args :input, :output
10
+
11
+ def defaults
12
+ { input: STDIN, output: STDOUT }
13
+ end
14
+
15
+ # Public: Reads user input from given input stream. The answer can be either obtained
16
+ # from result or yielded value.
17
+ def read_user_input(&block)
18
+ value = @input.gets.to_s.strip
19
+ block_given? ? block.call(value) : value
20
+ end
21
+
22
+ # Public: Asks user a question. As with #read_user_input, the answer can be obtained
23
+ # from both, return value and yielded argument.
24
+ #
25
+ # Example
26
+ #
27
+ # console = Console.new
28
+ # console.ask("How much do you know?") do |answer|
29
+ # puts "So you know #{answer}"
30
+ # end
31
+ #
32
+ # $ How much do you know?: nothing
33
+ # $ So you know nothing...
34
+ #
35
+ def ask(question, &block)
36
+ @output.write(question + ': ')
37
+ read_user_input(&block)
38
+ end
39
+
40
+ # Public: Same like #ask, but converts the answer to boolean value. It also formats
41
+ # the question a little bit to indicate that we're asking about the boolean. Any other
42
+ # answer than case insensitive "y" or "yes" will be converted to false.
43
+ #
44
+ # Example
45
+ #
46
+ # console = Console.new
47
+ # console.ask("Are you Jon Snow?") do |answer|
48
+ # puts "Answer: #{answer.inspect}"
49
+ # end
50
+ #
51
+ # $ Are you Jon Snow? [y/N]: Yes
52
+ # $ Answer: true
53
+ #
54
+ def ask_yes_or_no(question, &block)
55
+ ask(question + ' [y/N]') do |answer|
56
+ %[y yes].include?(answer.downcase).tap do |bool_answer|
57
+ yield bool_answer if block_given?
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,16 @@
1
+ module Perfume
2
+ module CoreExt
3
+ module Dir
4
+ # Public: A helper that's a fusion of Dir.mktmpdir and Dir.chdir. Lazieness FTW!
5
+ def chtmpdir(&block)
6
+ ::Dir.mktmpdir do |dir|
7
+ ::Dir.chdir(dir) do
8
+ yield dir
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ Dir.extend(Perfume::CoreExt::Dir)
@@ -0,0 +1,23 @@
1
+ require 'perfume/service'
2
+
3
+ module Perfume
4
+ # Public: There's often need to exit from the app in a testable and graceful manner.
5
+ # Simple call to Kernel#exit isn't such. This simple wrapper around the Kernel method
6
+ # allows to say goodbye message via service logging as well as status code.
7
+ class Exit < Service
8
+ args :message, :code, :log
9
+
10
+ def defaults
11
+ { message: "Exiting...", code: 1 }
12
+ end
13
+
14
+ def log
15
+ @log ||= self.class.log
16
+ end
17
+
18
+ def call
19
+ log.error(@message)
20
+ Kernel.exit(@code)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Perfume
2
+ module Logging
3
+ # Internal: For command line apps we rather don't want to display all the logging information
4
+ # to the user. Just to avoid noise, INFO messages are displayed as is, and any other log levels
5
+ # are prefixed with the level name. Example:
6
+ #
7
+ # This is simple info message.
8
+ # And another one.
9
+ # WARN: Uups, something's not right here...
10
+ # And another info message.
11
+ # DEBUG: We need to go deeper!
12
+ # ERROR: Limbo!
13
+ #
14
+ class CommandLineOutputFormatter < Log4r::Formatter
15
+ def initialize
16
+ @info_formatter = Log4r::PatternFormatter.new(:pattern => "%m")
17
+ @else_formatter = Log4r::PatternFormatter.new(:pattern => "%l: %m")
18
+ end
19
+
20
+ def format(logevent)
21
+ (Log4r::INFO == logevent.level ? @info_formatter : @else_formatter).format(logevent)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ module Perfume
2
+ module Logging
3
+ # Internal: Adapter for Log4r logging system. It's a poor version of what Slf4j does to Log4j in
4
+ # java apps. Just tiny layer to improve on logging payloads.
5
+ class Log4rAdapter
6
+ class Event
7
+ def initialize(message, payload)
8
+ @message, @payload = message, payload || {}
9
+ @payload_formatted = @payload.map { |k,v| "#{k}=#{v.inspect}" }.join(" ")
10
+ @s = [ @message, @payload_formatted.empty? ? nil : @payload_formatted].compact.join("; ")
11
+ end
12
+
13
+ def to_s
14
+ @s
15
+ end
16
+ end
17
+
18
+ def initialize(log)
19
+ @log = log
20
+ end
21
+
22
+ def method_missing(method, *args, &block)
23
+ @log.send(method, *args, &block)
24
+ end
25
+
26
+ %w[debug info warn error fatal].each do |level|
27
+ define_method(level) do |obj_or_msg, payload=nil|
28
+ @log.send(level, obj_or_msg.is_a?(String) ? Event.new(obj_or_msg, payload).to_s : obj_or_msg)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ require 'log4r'
2
+ require 'perfume/logging/log4r_adapter'
3
+
4
+ module Perfume
5
+ module Logging
6
+ # Public: Similarly to Perfume::Logging no point to initialize global logger for your
7
+ # modules/packages. Include this mixin to define LOGGER constant and log class method
8
+ # shortcut.
9
+ #
10
+ # Often used practice is to define such top level logger as a parent with default
11
+ # outputters, formatters, log level, etc.
12
+ module PackageLogger
13
+ def self.included(klass)
14
+ klass.const_set(:LOGGER, Logging::Log4rAdapter.new(Log4r::Logger.new(klass.name)))
15
+ klass.extend(ClassMethods)
16
+ end
17
+
18
+ module ClassMethods
19
+ def log
20
+ const_get(:LOGGER)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ require 'log4r'
2
+
3
+ module Perfume
4
+ # Public: Log4r is awesome, but why initialize it for every class manually if you
5
+ # can use simple extension that will do it for you. This one injects class and
6
+ # instance `log` methods to access class logger.
7
+ module Logging
8
+ require 'perfume/logging/log4r_adapter'
9
+ require 'perfume/logging/command_line_output_formatter'
10
+ require 'perfume/logging/package_logger'
11
+
12
+ def self.included(klass)
13
+ klass.extend(ClassMethods)
14
+ end
15
+
16
+ def log
17
+ self.class.log
18
+ end
19
+
20
+ module ClassMethods
21
+ def log
22
+ @log ||= Log4rAdapter.new(Log4r::Logger.new(self.name))
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,121 @@
1
+ require 'perfume/service'
2
+
3
+ module Perfume
4
+ # Public: Very often we're in a situation that we have to catch and eventuall log some
5
+ # errors, or even more, solely catch errors for purpose of logging. Other times we must
6
+ # perform multiple actions in different places according to the results produces by our
7
+ # method or service. We'd normally have to pass these results around deppening our stack
8
+ # trace. Here comes this a bit twisted implementation of a promise. Maybe twisted is
9
+ # wrong description, it's rather simplified. It's not parallel, it's simple to the bones.
10
+ # It just allows you to define three kinds of callbacks: before call, on success and
11
+ # on failure of course. Since it inherits from our own Service, it essentially is one
12
+ # with own logging and access to class level call method.
13
+ #
14
+ # Here's few interesting properties:
15
+ #
16
+ # 1. Define your logic in #call! method. Yes call with bang.
17
+ #
18
+ # class FindWally < Perfume::Promise
19
+ # NotFoundWarning = Class.new(Warning)
20
+ #
21
+ # def call!
22
+ # Wally.latest_location.tap do |location|
23
+ # raise NotFoundWarning, "No idea where is Wally!" if location.nil?
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # 2. Failures are dependent on the kind of error thrown. It means that the flow
29
+ # is directed by raised errors. Slow you might say, but benchmarking this shows that
30
+ # the overhead is negligible in real life apps. This also allows a nifty trick. Look
31
+ # at the example of `FindWally` above. Why there's `NotFoundWarning` inhertiting from
32
+ # magical `Warning` class? The `Warning` is bundled into each new promise defined.
33
+ # This errors are not thrown like regular exceptions. Instead they're handled and
34
+ # logged as warnings. This error will be passed to our `failure` callbacks as well.
35
+ # Check example:
36
+ #
37
+ # FindWally.call do |_location|
38
+ # raise StandardError, "Something went wrong!"
39
+ # end
40
+ #
41
+ # This one raises `StandardError`, while this one:
42
+ #
43
+ # find_wally = FindWally.new
44
+ # find_wally.failure { |err| puts err.message }
45
+ # find_wally.call { |_location| raise FindWally::Warning, "Uuups!" }
46
+ #
47
+ # Will log WARNING with "Uuups!" message.
48
+ #
49
+ # 3. Block passed to call is added to on success callbacks:
50
+ #
51
+ # FindWally.call do |location|
52
+ # puts location
53
+ # end
54
+ #
55
+ # $ "Madrid"
56
+ #
57
+ class Promise < Service
58
+ Warning = Class.new(Exception)
59
+
60
+ def self.inherited(child)
61
+ child.const_set(:Warning, Warning)
62
+ end
63
+
64
+ def initialize(*)
65
+ super
66
+
67
+ @__before = []
68
+ @__on_success = []
69
+ @__on_failure = []
70
+ end
71
+
72
+ def before(&block)
73
+ @__before << block
74
+ self
75
+ end
76
+
77
+ def success(&block)
78
+ @__on_success << block
79
+ self
80
+ end
81
+
82
+ def failure(&block)
83
+ @__on_failure << block
84
+ self
85
+ end
86
+
87
+ def success?
88
+ @__ok
89
+ end
90
+
91
+ def failure?
92
+ !success?
93
+ end
94
+
95
+ # Public: Your logic goes here. Flow is broken by raising an exception of local Error
96
+ # class or child classes. Any return value will be passed to on-success callbacks.
97
+ def call!
98
+ raise NotImplementedError
99
+ end
100
+
101
+ # Pubic: Safely executes your logic defined in call!, taking care that all callbacks
102
+ # are properly called.
103
+ def call(&block)
104
+ @__ok = false
105
+ @__before.each(&:call)
106
+ success_callbacks = @__on_success + [block]
107
+ call!.tap { |result| success_callbacks.compact.each { |callback| callback.call(result) } }
108
+ @__ok = true
109
+ self
110
+ rescue Warning => err
111
+ log.warn(err)
112
+ @__on_failure.each { |callback| callback.call(err) }
113
+ return nil
114
+ end
115
+ end
116
+
117
+ # Public: Shorthand to define a Promise with accessors.
118
+ def self.Promise(*names, &block)
119
+ Class.new(Promise, &block).tap { |klass| klass.args_accessor(*names) }
120
+ end
121
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+ require 'perfume/super_object'
3
+
4
+ module Perfume
5
+ # Public: The pattern is simple, our Service is a struct with init arguments that
6
+ # additionally is callable. It also comes with class level `call` method that
7
+ # executes stuff on new instance of the object.
8
+ class Service < SuperObject
9
+ include Logging
10
+
11
+ # Public: Shorthand to instantionante with arguments and perform call in one shot.
12
+ # You should actually always intend to use this method as it's easier to mock
13
+ # in testing.
14
+ def self.call(args = {}, &block)
15
+ new(args).call(&block)
16
+ end
17
+
18
+ # Public: Implement this one on your own. Your logic goes here.
19
+ def call(&block)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ protected
24
+
25
+ def system_call(*args, &block)
26
+ Shell::SystemCall.call(*args, &block)
27
+ end
28
+
29
+ def exec(*args, &block)
30
+ Shell::Exec.call(*args, &block)
31
+ end
32
+ end
33
+
34
+ # Public: Shorthand to define a Service with accessors.
35
+ def self.Service(*names, &block)
36
+ Class.new(Service, &block).tap { |klass| klass.args_accessor(*names) }
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ require 'perfume/service'
2
+
3
+ module Perfume
4
+ module Shell
5
+ # Internal: Executing shell commands isn't good practice. Especially it's bad when
6
+ # calling ruby methods like `exec` or `system`. This is dangerous and completely
7
+ # untestable. So here it comes base wrapper around shell calls.
8
+ #
9
+ # Check Perfume::Shell::Exec and Perfume::Shell:SystemCall for two most common
10
+ # use cases.
11
+ class Base < Perfume::Service(:cmd, :root, :log)
12
+ def init
13
+ @root = Pathname.new(@root)
14
+ end
15
+
16
+ def log
17
+ @log or self.class.log
18
+ end
19
+
20
+ def defaults
21
+ { root: Dir.pwd }
22
+ end
23
+
24
+ def before
25
+ end
26
+
27
+ def to_s
28
+ cmd.to_s
29
+ end
30
+
31
+ protected
32
+
33
+ def fail!(msg = "Shell command execution failed!")
34
+ raise ExecutionError, msg
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,57 @@
1
+ module Perfume
2
+ module Shell
3
+ # Public: Go for Exec when you need to obtain standard output of the command. It uses
4
+ # the %x[] call under the hood. Note that it doesn't raise errors when return status
5
+ # is different than 0. Same as with Perfume::Shell::SystemCall, you should go for
6
+ # inheriting from this class:
7
+ #
8
+ # class GitDirtyTreeValidation < Perfume::Shell::Exec
9
+ # Error = Class.new(StandardError)
10
+ #
11
+ # def cmd
12
+ # 'git status -s'
13
+ # end
14
+ #
15
+ # def fail_on_error?
16
+ # true
17
+ # end
18
+ #
19
+ # def handle
20
+ # raise Error, "Uncommited changes detected!" unless output.strip.empty?
21
+ # end
22
+ # end
23
+ #
24
+ class Exec < Base
25
+ args :fail_on_error
26
+ attr_reader :output, :rc
27
+
28
+ def init
29
+ @__done = []
30
+ @fail_on_error = false if @fail_on_error.nil?
31
+ end
32
+
33
+ def fail_on_error?
34
+ @fail_on_error
35
+ end
36
+
37
+ def call(&block)
38
+ Dir.chdir(@root.to_s) do
39
+ before
40
+ log.debug("Executing shell command and waiting for result", cmd: cmd, root: @root.to_s)
41
+ @output, @rc = [ %x[#{cmd} 2>&1], $?.to_i ]
42
+ fail! if fail_on_error? and @rc != 0
43
+ result = respond_to?(:handle) ? handle : [ @output, @rc ]
44
+ yield result if block_given?
45
+ return result
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ def fail!(*)
52
+ $stderr.write(@output)
53
+ super
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,32 @@
1
+ module Perfume
2
+ module Shell
3
+ # Public: Simple system call. Use it when you need to just execute something, eventually
4
+ # silently and you don't care about the result. Uses `system` under the hood. Feel free
5
+ # to inherit from this class, example:
6
+ #
7
+ # class GitCommit < Perfume::Shell::SystemCall
8
+ # args :message
9
+ #
10
+ # def cmd
11
+ # 'git commit -m #{@message.inspect}'
12
+ # end
13
+ # end
14
+ #
15
+ class SystemCall < Base
16
+ args :quiet
17
+
18
+ def defaults
19
+ super.merge(quiet: false)
20
+ end
21
+
22
+ def call
23
+ Dir.chdir(@root.to_s) do
24
+ before
25
+ log.debug("Executing shell command", cmd: cmd, root: @root.to_s)
26
+ system([ cmd, !!@quiet ? ' &>/dev/null' : nil ].compact.join(' '))
27
+ fail! unless $?.to_i == 0
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ module Perfume
2
+ module Shell
3
+ # Public: Raised when shell command returned non-zero exit status.
4
+ ExecutionError = Class.new(StandardError)
5
+
6
+ require 'perfume/shell/base'
7
+ require 'perfume/shell/system_call'
8
+ require 'perfume/shell/exec'
9
+ end
10
+ end
@@ -0,0 +1,93 @@
1
+ module Perfume
2
+ # Public: Premise is tha using method arguments sucks because you need to remember the order,
3
+ # sucks even more when you might have lot of them (like structure/service initializers).
4
+ #
5
+ # It's often seen in ruby to declare classes that inherit from Struct. There we have problem
6
+ # of arguments order. Maybe I wanna specify defaults? Maybe I wanna pass only one argument
7
+ # from the middle of the list? For such cases people use OpenStruct or third-party stuff
8
+ # like Hashie::Dash. Not a good idea either because those classes are bloated with useless
9
+ # methods.
10
+ #
11
+ # Here's how this super object works:
12
+ #
13
+ # class Women < SuperObject(:name, :surname)
14
+ # args :married
15
+ #
16
+ # def title
17
+ # @married ? 'Mrs.' : 'Miss'
18
+ # end
19
+ #
20
+ # def to_s
21
+ # [ title, name, surname ].join(' ')
22
+ # end
23
+ # end
24
+ #
25
+ # ada = Women.new(name: "Ada", surname: "Lovelace", married: true)
26
+ # ada.name # => "Ada"
27
+ # ada.surname # => "Lovelace"
28
+ # ada.to_s # => "Mrs. Ada Lovelace"
29
+ #
30
+ # mary = Women.new(name: "mary", surname: "Cassat", married: false)
31
+ # mary.to_s # => "Miss Mary Cassat"
32
+ # mary.married # => NoMethodError
33
+ #
34
+ # Women.new(unknown: 'Something') # => ArgumentError
35
+ #
36
+ # By default `args` method defines only instance variables. Then we have `args_accessor`,
37
+ # which is called for all class parameters, `args_reader` and `args_writer` if you need
38
+ # to expose some stuff.
39
+ class SuperObject
40
+ # Public: Returns list of defined arguments. Note that superclass arguments are inherited
41
+ # by child class.
42
+ def self.init_args
43
+ @init_args ||= superclass.respond_to?(:init_args) ? superclass.init_args.dup : []
44
+ end
45
+
46
+ # Public: Defines unaccessible arguments.
47
+ def self.args(*names)
48
+ init_args.concat(names)
49
+ end
50
+
51
+ # Public: Defines given init arguments alongside with accessor methods.
52
+ def self.args_accessor(*names)
53
+ names = names.map(&:to_sym)
54
+ args(*names)
55
+ attr_accessor(*names)
56
+ end
57
+
58
+ # Public: Defines given init arguments alongside with reader method.
59
+ def self.args_reader(*names)
60
+ args(*names)
61
+ attr_reader(*names)
62
+ end
63
+
64
+ # Public: Defines given init arguments alongside with writer method.
65
+ def self.args_writer(*names)
66
+ args(*names)
67
+ attr_writer(*names)
68
+ end
69
+
70
+ def initialize(args = {})
71
+ args.symbolize_keys!
72
+ unknown_args = args.keys - self.class.init_args
73
+ raise ArgumentError, "Unknown arguments: #{unknown_args.map(&:inspect).join(', ')}" unless unknown_args.empty?
74
+ args = defaults.symbolize_keys.merge(args)
75
+ self.class.init_args.each { |name| instance_variable_set("@#{name}", args[name]) }
76
+ init
77
+ end
78
+
79
+ # Public: Extra initialization.
80
+ def init
81
+ end
82
+
83
+ # Public: Override it with your own default arguments.
84
+ def defaults
85
+ {}
86
+ end
87
+ end
88
+
89
+ # Public: Shorthand to define a super object with accessors.
90
+ def self.SuperObject(*names, &block)
91
+ Class.new(SuperObject, &block).tap { |klass| klass.args_accessor(*names) }
92
+ end
93
+ end
@@ -0,0 +1,38 @@
1
+ require 'pathname'
2
+ require 'forwardable'
3
+
4
+ module Perfume
5
+ module Testing
6
+ # Public: Sometimes you have to test file contents or some biggest chunks of data. Put them in fixtures
7
+ # folder and load using this helper class. Example:
8
+ #
9
+ # MY_FIXTURE_FILES = FixtureFiles.load('path/to/fixtures/*.txt')
10
+ # MY_FIXTURE_FILES.each { |content| ... }
11
+ # puts MY_FIXTURE_FILES['filename.txt']
12
+ #
13
+ class FixtureFiles
14
+ extend Forwardable
15
+ def_delegators :@fixtures, :[], :size, :each
16
+
17
+ include Enumerable
18
+
19
+ class << self
20
+ alias :load :new
21
+ end
22
+
23
+ def initialize(dir)
24
+ @fixtures = load_fixtures(dir)
25
+ end
26
+
27
+ private
28
+
29
+ def load_fixtures(dir)
30
+ {}.tap do |fixtures|
31
+ Pathname.glob(dir.to_s) do |entry|
32
+ fixtures[entry.basename.to_s] = entry.read if entry.file?
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ require 'pathname'
2
+
3
+ module Perfume
4
+ module Testing
5
+ # Public: You load support files in pretty much every test/spec helper. It's stupid to repeat yourself
6
+ # like this. Use following helper to load support stuff from given directory.
7
+ module Support
8
+ def self.require_all(dir)
9
+ Dir.glob(dir.join('*.rb').to_s) do |f|
10
+ require f
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ module Perfume
2
+ module Testing
3
+ require 'perfume/testing/support'
4
+ require 'perfume/testing/fixture_files'
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Perfume
2
+ VERSION = '0.1.0'
3
+ end
data/lib/perfume.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Perfume
2
+ require 'perfume/version'
3
+ end
data/perfume.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'perfume/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "perfume"
8
+ spec.version = Perfume::VERSION
9
+ spec.authors = ["jobandtalent", "Kris Kovalik"]
10
+ spec.email = ["kris.kovalik@jobandtalent.com", "hi@kkvlk.me"]
11
+
12
+ spec.summary = %q{Tooling for writing (micro)services in Ruby.}
13
+ spec.description = %q{Bunch of tools and utilities for common day-to-day tasks while working with microservices.!}
14
+ spec.homepage = "https://github.com/jobandtalent/perfume"
15
+ spec.license = 'Apache-2.0'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "minitest", "~> 5.8"
25
+ spec.add_development_dependency "minitest-reporters", "~> 1.1"
26
+ spec.add_development_dependency "mocha", "~> 1.1"
27
+ # spec.add_development_dependency "rake-bump", "~> 0.4"
28
+
29
+ spec.add_dependency "activesupport", "~> 4.2", ">= 3.0"
30
+ spec.add_dependency "log4r", "~> 1.1", ">= 1.1"
31
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: perfume
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - jobandtalent
8
+ - Kris Kovalik
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2016-01-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.10'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.10'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '10.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '10.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: minitest
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '5.8'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '5.8'
56
+ - !ruby/object:Gem::Dependency
57
+ name: minitest-reporters
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.1'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.1'
70
+ - !ruby/object:Gem::Dependency
71
+ name: mocha
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '1.1'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '1.1'
84
+ - !ruby/object:Gem::Dependency
85
+ name: activesupport
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '4.2'
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '3.0'
94
+ type: :runtime
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '4.2'
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ - !ruby/object:Gem::Dependency
105
+ name: log4r
106
+ requirement: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.1'
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '1.1'
114
+ type: :runtime
115
+ prerelease: false
116
+ version_requirements: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: '1.1'
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '1.1'
124
+ description: Bunch of tools and utilities for common day-to-day tasks while working
125
+ with microservices.!
126
+ email:
127
+ - kris.kovalik@jobandtalent.com
128
+ - hi@kkvlk.me
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".editorconfig"
134
+ - ".gitignore"
135
+ - ".rspec"
136
+ - CHANGELOG.md
137
+ - Dockerfile
138
+ - Gemfile
139
+ - README.md
140
+ - Rakefile
141
+ - bin/console
142
+ - bin/setup
143
+ - docker-compose.yml
144
+ - lib/perfume.rb
145
+ - lib/perfume/all.rb
146
+ - lib/perfume/console.rb
147
+ - lib/perfume/core_ext/dir.rb
148
+ - lib/perfume/exit.rb
149
+ - lib/perfume/logging.rb
150
+ - lib/perfume/logging/command_line_output_formatter.rb
151
+ - lib/perfume/logging/log4r_adapter.rb
152
+ - lib/perfume/logging/package_logger.rb
153
+ - lib/perfume/promise.rb
154
+ - lib/perfume/service.rb
155
+ - lib/perfume/shell.rb
156
+ - lib/perfume/shell/base.rb
157
+ - lib/perfume/shell/exec.rb
158
+ - lib/perfume/shell/system_call.rb
159
+ - lib/perfume/super_object.rb
160
+ - lib/perfume/testing.rb
161
+ - lib/perfume/testing/fixture_files.rb
162
+ - lib/perfume/testing/support.rb
163
+ - lib/perfume/version.rb
164
+ - perfume.gemspec
165
+ homepage: https://github.com/jobandtalent/perfume
166
+ licenses:
167
+ - Apache-2.0
168
+ metadata: {}
169
+ post_install_message:
170
+ rdoc_options: []
171
+ require_paths:
172
+ - lib
173
+ required_ruby_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ required_rubygems_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ requirements: []
184
+ rubyforge_project:
185
+ rubygems_version: 2.5.1
186
+ signing_key:
187
+ specification_version: 4
188
+ summary: Tooling for writing (micro)services in Ruby.
189
+ test_files: []