Skip to content

Commit

Permalink
makeover
Browse files Browse the repository at this point in the history
  • Loading branch information
rkh committed Feb 21, 2014
1 parent 6bd8f7b commit 6f1bb40
Show file tree
Hide file tree
Showing 17 changed files with 371 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
Gemfile.lock
.coverage
3 changes: 3 additions & 0 deletions .rspec
@@ -0,0 +1,3 @@
--color
--tty
-r support/coverage
2 changes: 2 additions & 0 deletions .travis.yml
@@ -0,0 +1,2 @@
rvm: [2.0.0, 2.1.0]
script: bundle exec rspec
2 changes: 2 additions & 0 deletions Gemfile
@@ -0,0 +1,2 @@
source 'https://rubygems.org'
gemspec
22 changes: 22 additions & 0 deletions LICENSE
@@ -0,0 +1,22 @@
Copyright (c) 2013, 2014 Konstantin Haase

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
1 change: 1 addition & 0 deletions README.md
@@ -0,0 +1 @@
General purpose Ruby library used by Sinatra 2.0, Mustermann and related projects.
21 changes: 21 additions & 0 deletions examples/frank.rb
@@ -0,0 +1,21 @@
require 'tool/decoration'

class Frank
extend Tool::Decoration
def self.get(path, &block)
decorate(block) do |method|
puts "mapping GET #{path} to #{method}"
end
end
end

class MyApp < Frank
get '/hi' do
"Hello World"
end

get '/'; get '/index.php'
def index
"This is the index page."
end
end
86 changes: 86 additions & 0 deletions lib/tool/decoration.rb
@@ -0,0 +1,86 @@
require 'tool/thread_local'

module Tool
# Mixin for easy method decorations.
#
# @example
# class Frank
# extend Tool::Decoration
# def self.get(path, &block)
# decorate(block) do |method|
# puts "mapping GET #{path} to #{method}"
# end
# end
# end
#
# # Output:
# # mapping GET /hi to __generated1
# # mapping GET / to index
# # mapping GET /index.php to index
# class MyApp < Frank
# get '/hi' do
# "Hello World"
# end
#
# get '/'; get '/index.php'
# def index
# "This is the index page."
# end
# end
module Decoration
module Initializer
def initialize(*)
setup_decorations
super
end
end

module Setup
def included(object)
super
case object
when Class then object.send(:include, Initializer)
when Module then object.extend(Setup)
end
end

def extended(object)
object.send(:setup_decorations)
end
end

extend Setup

def decorate(block = nil, name: "generated", &callback)
@decorations << callback

if block
alias_name = "__" << name.to_s.downcase.gsub(/[^a-z]+/, ?_) << ?1
alias_name = alias_name.succ while respond_to? alias_name, true
without_decorations { define_method(name, &block) }
alias_method(alias_name, name)
remove_method(name)
private(alias_name)
end
end

def without_decorations
@decorations.clear if was = @decorations.to_a.dup
yield
ensure
@decorations.replace(was) if was
end

def method_added(name)
@decorations.each { |d| d.call(name) }.clear
super
end

def setup_decorations
@decorations = Tool::ThreadLocal.new([])
end

private :method_added, :setup_decorations
private_constant :Initializer, :Setup
end
end
54 changes: 54 additions & 0 deletions lib/tool/equality_map.rb
@@ -0,0 +1,54 @@
module Tool
# A simple wrapper around ObjectSpace::WeakMap that allows matching keys by equality rather than identity.
# Used for caching.
#
# @example
# class ExpensiveComputation
# @map = Tool::EqualityMap.new
#
# def self.new(*args)
# @map.fetch(*args) { super }
# end
# end
#
# @see #fetch
class EqualityMap
attr_reader :map

def initialize
@keys = {}
@map = ObjectSpace::WeakMap.new
end

# @param [Array<#hash>] key for caching
# @yield block that will be called to populate entry if missing
# @return value stored in map or result of block
def fetch(*key)
identity = @keys[key.hash]
key = identity == key ? identity : key

# it is ok that this is not thread-safe, worst case it has double cost in
# generating, object equality is not guaranteed anyways
@map[key] ||= track(key, yield)
end

# @param [#hash] key for identifying the object
# @param [Object] object to be stored
# @return [Object] same as the second parameter
def track(key, object)
ObjectSpace.define_finalizer(object, finalizer(key.hash))
@keys[key.hash] = key
object
end

# Finalizer proc needs to be generated in different scope so it doesn't keep a reference to the object.
#
# @param [Fixnum] hash for key
# @return [Proc] finalizer callback
def finalizer(hash)
proc { @keys.delete(hash) }
end

private :track, :finalizer
end
end
19 changes: 19 additions & 0 deletions lib/tool/thread_local.rb
@@ -0,0 +1,19 @@
require 'delegate'
require 'weakref'
require 'thread'

module Tool
class ThreadLocal < Delegator
def initialize(default = {})
@mutex = Mutex.new
@default = default.dup
@map = {}
end

def __getobj__
ref = Thread.current[:weakref] ||= WeakRef.new(Thread.current)
@map.delete_if { |key, value| !key.weakref_alive? }
@mutex.synchronize { @map[ref] ||= @default.dup }
end
end
end
3 changes: 3 additions & 0 deletions lib/tool/version.rb
@@ -0,0 +1,3 @@
module Tool
VERSION = '0.2.0'
end
13 changes: 13 additions & 0 deletions lib/tool/warning_filter.rb
@@ -0,0 +1,13 @@
require 'delegate'

module Tool
class WarningFilter < DelegateClass(IO)
$stderr = new($stderr)
$VERBOSE = true

def write(line)
super if line !~ /^\S+gems\/ruby\-\S+:\d+: warning:/
end
end
end

57 changes: 57 additions & 0 deletions spec/decoration_spec.rb
@@ -0,0 +1,57 @@
require 'tool/decoration'

describe Tool::Decoration do
shared_examples :decorate do
specify "with block" do
method = nil
subject.decorate(-> { 42 }) { |m| method = m }
expect(method).not_to be_nil
expect(subject.new.send(method)).to be == 42
end

specify "without block" do
method = nil
subject.decorate { |m| method = m }
expect(method).to be_nil
subject.send(:define_method, :foo) { }
expect(method).to be == :foo
end

specify "multiple decorations" do
calls = []
subject.decorate { |m| calls << :a }
subject.decorate { |m| calls << :b }
expect(calls).to be_empty
subject.send(:define_method, :foo) { }
expect(calls).to be == [:a, :b]
end

specify "multiple methods" do
calls = []
subject.decorate { |m| calls << :a }
subject.send(:define_method, :foo) { }
subject.send(:define_method, :bar) { }
expect(calls).to be == [:a]
end
end

context "extending a class" do
subject { Class.new.extend(Tool::Decoration) }
include_examples(:decorate)
end

context "including in a module" do
subject { Class.new.extend(Module.new { include Tool::Decoration }) }
include_examples(:decorate)
end

context "including in a module" do
subject do
Class.new(Module) do
def new(*) Object.new.extend(self) end
include Tool::Decoration
end.new
end
include_examples(:decorate)
end
end
21 changes: 21 additions & 0 deletions spec/equality_map_spec.rb
@@ -0,0 +1,21 @@
require 'tool/equality_map'

describe Tool::EqualityMap do
before { GC.disable }
after { GC.enable }

describe :fetch do
specify 'with existing entry' do
subject.fetch("foo") { "foo" }
result = subject.fetch("foo") { "bar" }
expect(result).to be == "foo"
end

specify 'with GC-removed entry' do
subject.fetch("foo") { "foo" }
expect(subject.map).to receive(:[]).and_return(nil)
result = subject.fetch("foo") { "bar" }
expect(result).to be == "bar"
end
end
end
17 changes: 17 additions & 0 deletions spec/support/coverage.rb
@@ -0,0 +1,17 @@
require 'tool/warning_filter'
require 'simplecov'
require 'coveralls'

SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
SimpleCov::Formatter::HTMLFormatter,
Coveralls::SimpleCov::Formatter
]

SimpleCov.start do
project_name 'tool'
minimum_coverage 100
coverage_dir '.coverage'

add_filter '/spec/'
add_group 'Library', 'lib'
end
26 changes: 26 additions & 0 deletions spec/thread_local_spec.rb
@@ -0,0 +1,26 @@
require 'tool/thread_local'

describe Tool::ThreadLocal do
specify 'normal access' do
subject[:foo] = 'bar'
expect(subject[:foo]).to be == 'bar'
end

specify 'concurrent access' do
subject[:foo] = 'bar'
value = Thread.new { subject[:foo] = 'baz' }.value
expect(value).to be == 'baz'
expect(subject[:foo]).to be == 'bar'
end

specify 'with an array as value' do
list = Tool::ThreadLocal.new([])
foo = Thread.new { 10.times { list << :foo; sleep(0.01) }; list.to_a }
bar = Thread.new { 10.times { list << :bar; sleep(0.01) }; list.to_a }
expect(list).to be_empty
list << :list
expect(list) .to be == [ :list ]
expect(foo.value) .to be == [ :foo ] * 10
expect(bar.value) .to be == [ :bar ] * 10
end
end
22 changes: 22 additions & 0 deletions tool.gemspec
@@ -0,0 +1,22 @@
$:.unshift File.expand_path("../lib", __FILE__)
require "tool/version"

Gem::Specification.new do |s|
s.name = "tool"
s.version = Tool::VERSION
s.author = "Konstantin Haase"
s.email = "konstantin.mailinglists@googlemail.com"
s.homepage = "https://github.com/rkh/tool"
s.summary = %q{general purpose library}
s.description = %q{general purpose Ruby library used by Sinatra 2.0, Mustermann and related projects}
s.license = 'MIT'
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.extra_rdoc_files = `git ls-files -- *.md`.split("\n")
s.required_ruby_version = '>= 2.0.0'

s.add_development_dependency 'rspec', '~> 3.0.0.beta'
s.add_development_dependency 'simplecov'
s.add_development_dependency 'coveralls'
end

0 comments on commit 6f1bb40

Please sign in to comment.