Permalink
Browse files

makeover

  • Loading branch information...
1 parent 6bd8f7b commit 6f1bb40c4d22ccf4d40eaaae316cdebb635f7b29 @rkh committed Feb 21, 2014
View
@@ -0,0 +1,2 @@
+Gemfile.lock
+.coverage
View
3 .rspec
@@ -0,0 +1,3 @@
+--color
+--tty
+-r support/coverage
View
@@ -0,0 +1,2 @@
+rvm: [2.0.0, 2.1.0]
+script: bundle exec rspec
View
@@ -0,0 +1,2 @@
+source 'https://rubygems.org'
+gemspec
View
22 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.
View
@@ -0,0 +1 @@
+General purpose Ruby library used by Sinatra 2.0, Mustermann and related projects.
View
@@ -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
View
@@ -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
View
@@ -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
View
@@ -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
View
@@ -0,0 +1,3 @@
+module Tool
+ VERSION = '0.2.0'
+end
View
@@ -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
+
View
@@ -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
View
@@ -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
View
@@ -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
View
@@ -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
View
@@ -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.