├── LICENSE ├── .gitignore ├── map.gemspec ├── test ├── leak.rb ├── lib │ └── testing.rb └── map_test.rb ├── lib ├── map │ ├── struct.rb │ ├── params.rb │ ├── integrations │ │ └── active_record.rb │ └── options.rb └── map.rb ├── README └── Rakefile /LICENSE: -------------------------------------------------------------------------------- 1 | same as ruby's 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .todo 2 | TODO 3 | .rvmrc 4 | a.rb 5 | -------------------------------------------------------------------------------- /map.gemspec: -------------------------------------------------------------------------------- 1 | ## map.gemspec 2 | # 3 | 4 | Gem::Specification::new do |spec| 5 | spec.name = "map" 6 | spec.version = "6.5.1" 7 | spec.platform = Gem::Platform::RUBY 8 | spec.summary = "map" 9 | spec.description = "description: map kicks the ass" 10 | 11 | spec.files = 12 | ["LICENSE", 13 | "README", 14 | "Rakefile", 15 | "a.rb", 16 | "lib", 17 | "lib/map", 18 | "lib/map.rb", 19 | "lib/map/integrations", 20 | "lib/map/integrations/active_record.rb", 21 | "lib/map/options.rb", 22 | "lib/map/params.rb", 23 | "lib/map/struct.rb", 24 | "map.gemspec", 25 | "test", 26 | "test/leak.rb", 27 | "test/lib", 28 | "test/lib/testing.rb", 29 | "test/map_test.rb"] 30 | 31 | spec.executables = [] 32 | 33 | spec.require_path = "lib" 34 | 35 | spec.test_files = nil 36 | 37 | ### spec.add_dependency 'lib', '>= version' 38 | #### spec.add_dependency 'map' 39 | 40 | spec.extensions.push(*[]) 41 | 42 | spec.rubyforge_project = "codeforpeople" 43 | spec.author = "Ara T. Howard" 44 | spec.email = "ara.t.howard@gmail.com" 45 | spec.homepage = "https://github.com/ahoward/map" 46 | end 47 | -------------------------------------------------------------------------------- /test/leak.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'map' 3 | 4 | ## 5 | # 6 | gc = 7 | lambda do 8 | 10.times{ GC.start } 9 | end 10 | 11 | leak = 12 | lambda do 13 | 100.times do 14 | m = Map.new 15 | 16 | 1000.times do |i| 17 | m[rand.to_s] = rand 18 | end 19 | end 20 | 21 | gc.call() 22 | end 23 | 24 | ## 25 | # 26 | 27 | 28 | leak.call() 29 | before = Process.size 30 | 31 | 32 | leak.call() 33 | after = Process.size 34 | 35 | delta = [after.first - before.first, after.last - before.last] 36 | 37 | p :before => before 38 | p :after => after 39 | p :delta => delta 40 | 41 | 42 | ## 43 | # 44 | BEGIN { 45 | 46 | module Process 47 | def self.size pid = Process.pid 48 | stdout = `ps wwwux -p #{ pid }`.split(%r/\n/) 49 | vsize, rsize = stdout.last.split(%r/\s+/)[4,2].map{|i| i.to_i} 50 | end 51 | 52 | def self.vsize 53 | size.first.to_i 54 | end 55 | 56 | def self.rsize 57 | size.last.to_i 58 | end 59 | end 60 | 61 | } 62 | -------------------------------------------------------------------------------- /lib/map/struct.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class Map 3 | class Struct 4 | instance_methods.each { |m| undef_method m unless m =~ /^__|object_id/ } 5 | 6 | attr :map 7 | 8 | def initialize(map) 9 | @map = map 10 | end 11 | 12 | def method_missing(method, *args, &block) 13 | method = method.to_s 14 | case method 15 | when /=$/ 16 | key = method.chomp('=') 17 | value = args.shift 18 | @map[key] = value 19 | when /\?$/ 20 | key = method.chomp('?') 21 | value = @map.has?( key ) 22 | else 23 | key = method 24 | raise(IndexError, key) unless @map.has_key?(key) 25 | value = @map[key] 26 | end 27 | value.is_a?(Map) ? value.struct : value 28 | end 29 | 30 | Keys = lambda{|*keys| keys.flatten!; keys.compact!; keys.map!{|key| key.to_s}} unless defined?(Keys) 31 | 32 | delegates = %w( 33 | inspect 34 | ) 35 | 36 | delegates.each do |delegate| 37 | module_eval <<-__, __FILE__, __LINE__ 38 | def #{ delegate }(*args, &block) 39 | @map.#{ delegate }(*args, &block) 40 | end 41 | __ 42 | end 43 | end 44 | 45 | def struct 46 | @struct ||= Struct.new(self) 47 | end 48 | 49 | def Map.struct(*args, &block) 50 | new(*args, &block).struct 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/map/params.rb: -------------------------------------------------------------------------------- 1 | class Map 2 | def param_for(*args, &block) 3 | options = Map.options_for!(args) 4 | 5 | prefix = options[:prefix] || 'map' 6 | 7 | src_key = args.flatten.map{|arg| Map.alphanumeric_key_for(arg)} 8 | 9 | dst_key = src_key.map{|k| k.is_a?(Numeric) ? 0 : k} 10 | 11 | src = self 12 | dst = Map.new 13 | 14 | value = 15 | if options.has_key?(:value) 16 | options[:value] 17 | else 18 | src.get(src_key).to_s 19 | end 20 | 21 | dst.set(dst_key, value) 22 | 23 | Param.param_for(dst, prefix) 24 | end 25 | 26 | def name_for(*args, &block) 27 | options = Map.options_for!(args) 28 | options[:value] = nil 29 | args.push(options) 30 | param_for(*args, &block) 31 | end 32 | 33 | def to_params 34 | to_params = Array.new 35 | 36 | depth_first_each do |key, val| 37 | to_params.push(param_for(key)) 38 | end 39 | 40 | to_params.join('&') 41 | end 42 | 43 | module Param 44 | def param_for(value, prefix = nil) 45 | case value 46 | when Array 47 | value.map { |v| 48 | param_for(v, "#{ prefix }[]") 49 | }.join("&") 50 | 51 | when Hash 52 | value.map { |k, v| 53 | param_for(v, prefix ? "#{ prefix }[#{ escape(k) }]" : escape(k)) 54 | }.join("&") 55 | 56 | when String 57 | raise ArgumentError, "value must be a Hash" if prefix.nil? 58 | "#{ prefix }=#{ escape(value) }" 59 | 60 | else 61 | prefix 62 | end 63 | end 64 | 65 | if(''.respond_to?(:bytesize)) 66 | def bytesize(string) string.bytesize end 67 | else 68 | def bytesize(string) string.size end 69 | end 70 | 71 | require 'uri' unless defined?(URI) 72 | 73 | def escape(s) 74 | URI.encode_www_form_component(s) 75 | end 76 | 77 | extend(self) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/lib/testing.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | # simple testing support 3 | # 4 | require 'test/unit' 5 | 6 | def Testing(*args, &block) 7 | Class.new(Test::Unit::TestCase) do 8 | eval("This=self") 9 | 10 | def self.slug_for(*args) 11 | string = args.flatten.compact.join('-') 12 | words = string.to_s.scan(%r/\w+/) 13 | words.map!{|word| word.gsub %r/[^0-9a-zA-Z_-]/, ''} 14 | words.delete_if{|word| word.nil? or word.strip.empty?} 15 | words.join('-').downcase 16 | end 17 | 18 | def This.testing_subclass_count 19 | @testing_subclass_count ||= 1 20 | ensure 21 | @testing_subclass_count += 1 22 | end 23 | 24 | slug = slug_for(*args).gsub(%r/-/,'_') 25 | name = ['TESTING', '%03d' % This.testing_subclass_count, slug].delete_if{|part| part.empty?}.join('_') 26 | name = name.upcase! 27 | const_set(:Name, name) 28 | def self.name() const_get(:Name) end 29 | 30 | def self.testno() 31 | '%05d' % (@testno ||= 0) 32 | ensure 33 | @testno += 1 34 | end 35 | 36 | def self.testing(*args, &block) 37 | define_method("test_#{ testno }_#{ slug_for(*args) }", &block) 38 | end 39 | 40 | alias_method '__assert__', 'assert' 41 | 42 | def assert(*args, &block) 43 | if block 44 | label = "assert(#{ args.join ' ' })" 45 | result = nil 46 | assert_nothing_raised{ result = block.call } 47 | __assert__(result, label) 48 | result 49 | else 50 | result = args.shift 51 | label = "assert(#{ args.join ' ' })" 52 | __assert__(result, label) 53 | result 54 | end 55 | end 56 | 57 | def subclass_of exception 58 | class << exception 59 | def ==(other) super or self > other end 60 | end 61 | exception 62 | end 63 | 64 | alias_method '__assert_raises__', 'assert_raises' 65 | 66 | def assert_raises(*args, &block) 67 | args.push(subclass_of(Exception)) if args.empty? 68 | __assert_raises__(*args, &block) 69 | end 70 | 71 | module_eval(&block) 72 | self 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | NAME 2 | map.rb 3 | 4 | SYNOPSIS 5 | the awesome ruby container you've always wanted: a string/symbol indifferent 6 | ordered hash that works in all rubies 7 | 8 | maps are bitchin ordered hashes that are both ordered, string/symbol 9 | indifferent, and have all sorts of sweetness like recursive conversion, more 10 | robust implementation than HashWithIndifferentAccess, support for struct 11 | like (map.foo) access, and support for option/keyword access which avoids 12 | several nasty classes of errors in many ruby libraries 13 | 14 | INSTALL 15 | gem install map 16 | 17 | URI 18 | http://github.com/ahoward/map 19 | 20 | DESCRIPTION 21 | 22 | # maps are always ordered. constructing them in an ordered fashion builds 23 | # them that way, although the normal hash contructor is also supported 24 | # 25 | m = Map[:k, :v, :key, :val] 26 | m = Map(:k, :v, :key, :val) 27 | m = Map.new(:k, :v, :key, :val) 28 | 29 | m = Map[[:k, :v], [:key, :val]] 30 | m = Map(:k => :v, :key => :val) # ruh-oh, the input hash loses order! 31 | m = Map.new(:k => :v, :key => :val) # ruh-oh, the input hash loses order! 32 | 33 | 34 | m = Map.new 35 | m[:a] = 0 36 | m[:b] = 1 37 | m[:c] = 2 38 | 39 | p m.keys #=> ['a','b','c'] ### always ordered! 40 | p m.values #=> [0,1,2] ### always ordered! 41 | 42 | # maps don't care about symbol vs.string keys 43 | # 44 | p m[:a] #=> 0 45 | p m["a"] #=> 0 46 | 47 | # even via deep nesting 48 | # 49 | p m[:foo]['bar'][:baz] #=> 42 50 | 51 | # many functions operate in a way one would expect from an ordered container 52 | # 53 | m.update(:k2 => :v2) 54 | m.update(:k2, :v2) 55 | 56 | key_val_pair = m.shift 57 | key_val_pair = m.pop 58 | 59 | # maps keep mapiness for even deep operations 60 | # 61 | m.update :nested => {:hashes => {:are => :converted}} 62 | 63 | # maps can give back clever little struct objects 64 | # 65 | m = Map(:foo => {:bar => 42}) 66 | s = m.struct 67 | p s.foo.bar #=> 42 68 | 69 | # because option parsing is such a common use case for needing string/symbol 70 | # indifference map.rb comes out of the box loaded with option support 71 | # 72 | def foo(*args, &block) 73 | opts = Map.options(args) 74 | a = opts.getopt(:a) 75 | b = opts.getopt(:b, :default => false) 76 | end 77 | 78 | 79 | opts = Map.options(:a => 42, :b => nil, :c => false) 80 | opts.getopt(:a) #=> 42 81 | opts.getopt(:b) #=> nil 82 | opts.getopt(:b, :default => 42) #=> 42 83 | opts.getopt(:c) #=> false 84 | opts.getopt(:d, :default => false) #=> false 85 | 86 | # this avoids such bugs as 87 | # 88 | options = {:read_only => false} 89 | read_only = options[:read_only] || true # should be false but is true 90 | 91 | # with options this becomes 92 | # 93 | options = Map.options(:read_only => true) 94 | read_only = options.getopt(:read_only, :default => false) #=> true 95 | 96 | # maps support some really nice operators that hashes/orderedhashes do not 97 | # 98 | m = Map.new 99 | m.set(:h, :a, 0, 42) 100 | m.has?(:h, :a) #=> true 101 | p m #=> {'h' => {'a' => [42]}} 102 | m.set(:h, :a, 1, 42.0) 103 | p m #=> {'h' => {'a' => [42, 42.0]}} 104 | 105 | m.get(:h, :a, 1) #=> 42.0 106 | m.get(:x, :y, :z) #=> nil 107 | m[:x][:y][:z] #=> raises exception! 108 | 109 | m = Map.new(:array => [0,1]) 110 | defaults = {:array => [nil, nil, 2]} 111 | m.apply!(defaults) 112 | p m[:array] #=> [0,1,2] 113 | 114 | # they also support some different iteration styles 115 | # 116 | m = Map.new 117 | 118 | m.set( 119 | [:a, :b, :c, 0] => 0, 120 | [:a, :b, :c, 1] => 10, 121 | [:a, :b, :c, 2] => 20, 122 | [:a, :b, :c, 3] => 30 123 | ) 124 | 125 | m.set(:x, :y, 42) 126 | m.set(:x, :z, 42.0) 127 | 128 | m.depth_first_each do |key, val| 129 | p key => val 130 | end 131 | 132 | #=> [:a, :b, :c, 0] => 0 133 | #=> [:a, :b, :c, 1] => 10 134 | #=> [:a, :b, :c, 2] => 20 135 | #=> [:a, :b, :c, 3] => 30 136 | #=> [:x, :y] => 42 137 | #=> [:x, :z] => 42.0 138 | 139 | 140 | USAGE 141 | see lib/map.rb and test/map_test.rb 142 | 143 | HISTORY 144 | 4.3.0: 145 | - support for dot keys. map.set('a.b.c' => 42) #=> {'a'=>{'b'=>{'c'=>42}}} 146 | -------------------------------------------------------------------------------- /lib/map/integrations/active_record.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class Map 3 | module Integrations 4 | module ActiveRecord 5 | def self.included( klass ) 6 | klass.extend ClassMethods 7 | end 8 | 9 | module ClassMethods 10 | def to_map( record , *args ) 11 | # prep 12 | model = record.class 13 | map = Map.new 14 | map[ :model ] = model.name.underscore 15 | map[ :id ] = record.id 16 | 17 | # yank out options if they are patently obvious... 18 | if args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Hash ) 19 | options = Map.for args.last 20 | args = args.first 21 | else 22 | options = nil 23 | end 24 | 25 | # get base to_dao from class 26 | base = column_names 27 | 28 | # available options keys 29 | opts = %w( include includes with exclude excludes without ) 30 | 31 | # proc to remove options 32 | extract_options = 33 | proc do |array| 34 | to_return = Map.new 35 | last = array.last 36 | if last.is_a?( Hash ) 37 | last = Map.for last 38 | if opts.any? { | opt | last.has_key? opt } 39 | array.pop 40 | to_return = last 41 | end 42 | end 43 | to_return 44 | end 45 | 46 | # handle case where options are bundled in args... 47 | options ||= extract_options[args] 48 | 49 | # use base options iff none provided 50 | base_options = extract_options[base] 51 | if options.blank? and !base_options.blank? 52 | options = base_options 53 | end 54 | 55 | # refine the args with includes iff found in options 56 | include_opts = [ :include , :includes , :with ] 57 | if options.any? { | option | include_opts.include? option.to_sym } 58 | args.replace( base ) if args.empty? 59 | args.push( options[ :include ] ) if options[ :include ] 60 | args.push( options[ :includes ] ) if options[ :includes ] 61 | args.push( options[ :with ] ) if options[ :with ] 62 | end 63 | 64 | # take passed in args or model defaults 65 | list = args.empty? ? base : args 66 | list = column_names if list.empty? 67 | 68 | # proc to ensure we're all mapped out 69 | map_nested = 70 | proc do | value , *args | 71 | if value.is_a?( Array ) 72 | value.map { | v | map_nested[ v , *args ] } 73 | else 74 | if value.respond_to? :to_map 75 | value.to_map *args 76 | else 77 | value 78 | end 79 | end 80 | end 81 | 82 | # okay - go! 83 | list.flatten.each do | attr | 84 | if attr.is_a?( Array ) 85 | related , *argv = attr 86 | v = record.send related 87 | value = map_nested[ value , *argv ] 88 | map[ related ] = value 89 | next 90 | end 91 | 92 | if attr.is_a?( Hash ) 93 | attr.each do | related , argv | 94 | v = record.send related 95 | argv = !argv.is_a?( Array ) ? [ argv ] : argv 96 | value = map_nested[ v , *argv ] 97 | map[ related ] = value 98 | end 99 | next 100 | end 101 | 102 | value = record.send attr 103 | 104 | if value.respond_to?( :to_map ) 105 | map[ attr ] = value.to_map 106 | next 107 | end 108 | 109 | if value.is_a?( Array ) 110 | map[ attr ] = value.map &map_nested 111 | next 112 | end 113 | 114 | map[ attr ] = value 115 | end 116 | 117 | # refine the map with excludes iff passed as options 118 | exclude_opts = [ :exclude , :excludes , :without ] 119 | if options.any? { | option | exclude_opts.include? option.to_sym } 120 | [ options[ :exclude ] , options[ :excludes ] , options[ :without ] ].each do | paths | 121 | paths = Array paths 122 | next if paths.blank? 123 | paths.each { | path | map.rm path } 124 | end 125 | end 126 | 127 | map 128 | end 129 | end 130 | 131 | def to_map( *args ) 132 | self.class.to_map self , *args 133 | end 134 | end 135 | end 136 | end 137 | 138 | if defined?( ActiveRecord::Base ) 139 | ActiveRecord::Base.send :include , Map::Integrations::ActiveRecord 140 | end 141 | -------------------------------------------------------------------------------- /lib/map/options.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class Map 3 | module Options 4 | class << Options 5 | def for(arg) 6 | options = 7 | case arg 8 | when Hash 9 | arg 10 | when Array 11 | parse(arg) 12 | when String, Symbol 13 | {arg => true} 14 | else 15 | raise(ArgumentError, arg.inspect) unless arg.respond_to?(:to_hash) 16 | arg.to_hash 17 | end 18 | 19 | unless options.is_a?(Options) 20 | options = Map.for(options) 21 | options.extend(Options) 22 | end 23 | 24 | raise unless options.is_a?(Map) 25 | 26 | options 27 | end 28 | 29 | def parse(arg) 30 | case arg 31 | when Array 32 | arguments = arg 33 | arguments.extend(Arguments) unless arguments.is_a?(Arguments) 34 | options = arguments.options 35 | when Hash 36 | options = arg 37 | options = Options.for(options) 38 | else 39 | raise(ArgumentError, "`arg` should be an Array or Hash") 40 | end 41 | end 42 | end 43 | 44 | attr_accessor :arguments 45 | 46 | def pop 47 | arguments.pop if arguments.last.object_id == object_id 48 | self 49 | end 50 | 51 | def popped? 52 | !(arguments.last.object_id == object_id) 53 | end 54 | 55 | def pop! 56 | arguments.pop if arguments.last.object_id == object_id 57 | self 58 | end 59 | 60 | %w( to_options stringify_keys ).each do |method| 61 | module_eval <<-__, __FILE__, __LINE__ 62 | def #{ method }() dup end 63 | def #{ method }!() self end 64 | __ 65 | end 66 | 67 | def get_opt(opts, options = {}) 68 | options = Map.for(options.is_a?(Hash) ? options : {:default => options}) 69 | default = options[:default] 70 | [ opts ].flatten.each do |opt| 71 | return fetch(opt) if has_key?(opt) 72 | end 73 | default 74 | end 75 | alias_method('getopt', 'get_opt') 76 | 77 | def get_opts(*opts) 78 | opts.flatten.map{|opt| getopt(opt)} 79 | end 80 | alias_method('getopts', 'get_opts') 81 | 82 | def has_opt(opts) 83 | [ opts ].flatten.each do |opt| 84 | return true if has_key?(opt) 85 | end 86 | false 87 | end 88 | alias_method('hasopt', 'has_opt') 89 | alias_method('hasopt?', 'has_opt') 90 | alias_method('has_opt?', 'has_opt') 91 | 92 | def has_opts(*opts) 93 | opts.flatten.all?{|opt| hasopt(opt)} 94 | end 95 | alias_method('hasopts?', 'has_opts') 96 | alias_method('has_opts?', 'has_opts') 97 | 98 | def del_opt(opts) 99 | [ opts ].flatten.each do |opt| 100 | return delete(opt) if has_key?(opt) 101 | end 102 | nil 103 | end 104 | alias_method('delopt', 'del_opt') 105 | 106 | def del_opts(*opts) 107 | opts.flatten.map{|opt| delopt(opt)} 108 | opts 109 | end 110 | alias_method('delopts', 'del_opts') 111 | alias_method('delopts!', 'del_opts') 112 | 113 | def set_opt(opts, value = nil) 114 | [ opts ].flatten.each do |opt| 115 | return self[opt]=value 116 | end 117 | return value 118 | end 119 | alias_method('setopt', 'set_opt') 120 | alias_method('setopt!', 'set_opt') 121 | 122 | def set_opts(opts) 123 | opts.each{|key, value| setopt(key, value)} 124 | opts 125 | end 126 | alias_method('setopts', 'set_opts') 127 | alias_method('setopts!', 'set_opts') 128 | end 129 | 130 | module Arguments 131 | def options 132 | @options ||=( 133 | if last.is_a?(Hash) 134 | options = Options.for(pop) 135 | options.arguments = self 136 | push(options) 137 | options 138 | else 139 | options = Options.for({}) 140 | options.arguments = self 141 | options 142 | end 143 | ) 144 | end 145 | 146 | class << Arguments 147 | def for(args) 148 | args.extend(Arguments) unless args.is_a?(Arguments) 149 | args 150 | end 151 | 152 | def parse(args) 153 | [args, Options.parse(args)] 154 | end 155 | end 156 | end 157 | end 158 | 159 | 160 | def Map.options_for(*args, &block) 161 | Map::Options.for(*args, &block) 162 | end 163 | 164 | def Map.options_for!(*args, &block) 165 | Map::Options.for(*args, &block).pop 166 | end 167 | 168 | def Map.update_options_for!(args, &block) 169 | options = Map.options_for(args) 170 | block.call(options) 171 | end 172 | 173 | class << Map 174 | src = 'options_for' 175 | %w( options opts extract_options ).each do |dst| 176 | alias_method(dst, src) 177 | end 178 | 179 | src = 'options_for!' 180 | %w( options! opts! extract_options! ).each do |dst| 181 | alias_method(dst, src) 182 | end 183 | 184 | src = 'update_options_for!' 185 | %w( update_options! update_opts! ).each do |dst| 186 | alias_method(dst, src) 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | This.rubyforge_project = 'codeforpeople' 2 | This.author = "Ara T. Howard" 3 | This.email = "ara.t.howard@gmail.com" 4 | This.homepage = "https://github.com/ahoward/#{ This.lib }" 5 | 6 | task :license do 7 | open('LICENSE', 'w'){|fd| fd.puts "same as ruby's"} 8 | end 9 | 10 | task :default do 11 | puts((Rake::Task.tasks.map{|task| task.name.gsub(/::/,':')} - ['default']).sort) 12 | end 13 | 14 | task :test do 15 | run_tests! 16 | end 17 | 18 | namespace :test do 19 | task(:unit){ run_tests!(:unit) } 20 | task(:functional){ run_tests!(:functional) } 21 | task(:integration){ run_tests!(:integration) } 22 | end 23 | 24 | def run_tests!(which = nil) 25 | which ||= '**' 26 | test_dir = File.join(This.dir, "test") 27 | test_glob ||= File.join(test_dir, "#{ which }/**_test.rb") 28 | test_rbs = Dir.glob(test_glob).sort 29 | 30 | div = ('=' * 119) 31 | line = ('-' * 119) 32 | 33 | test_rbs.each_with_index do |test_rb, index| 34 | testno = index + 1 35 | command = "#{ This.ruby } -w -I ./lib -I ./test/lib #{ test_rb }" 36 | 37 | puts 38 | say(div, :color => :cyan, :bold => true) 39 | say("@#{ testno } => ", :bold => true, :method => :print) 40 | say(command, :color => :cyan, :bold => true) 41 | say(line, :color => :cyan, :bold => true) 42 | 43 | system(command) 44 | 45 | say(line, :color => :cyan, :bold => true) 46 | 47 | status = $?.exitstatus 48 | 49 | if status.zero? 50 | say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print) 51 | say("SUCCESS", :color => :green, :bold => true) 52 | else 53 | say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print) 54 | say("FAILURE", :color => :red, :bold => true) 55 | end 56 | say(line, :color => :cyan, :bold => true) 57 | 58 | exit(status) unless status.zero? 59 | end 60 | end 61 | 62 | 63 | task :gemspec do 64 | ignore_extensions = ['git', 'svn', 'tmp', /sw./, 'bak', 'gem'] 65 | ignore_directories = ['pkg'] 66 | ignore_files = ['test/log'] 67 | 68 | shiteless = 69 | lambda do |list| 70 | list.delete_if do |entry| 71 | next unless test(?e, entry) 72 | extension = File.basename(entry).split(%r/[.]/).last 73 | ignore_extensions.any?{|ext| ext === extension} 74 | end 75 | list.delete_if do |entry| 76 | next unless test(?d, entry) 77 | dirname = File.expand_path(entry) 78 | ignore_directories.any?{|dir| File.expand_path(dir) == dirname} 79 | end 80 | list.delete_if do |entry| 81 | next unless test(?f, entry) 82 | filename = File.expand_path(entry) 83 | ignore_files.any?{|file| File.expand_path(file) == filename} 84 | end 85 | end 86 | 87 | lib = This.lib 88 | object = This.object 89 | version = This.version 90 | files = shiteless[Dir::glob("**/**")] 91 | executables = shiteless[Dir::glob("bin/*")].map{|exe| File.basename(exe)} 92 | #has_rdoc = true #File.exist?('doc') 93 | test_files = "test/#{ lib }.rb" if File.file?("test/#{ lib }.rb") 94 | summary = object.respond_to?(:summary) ? object.summary : "summary: #{ lib } kicks the ass" 95 | description = object.respond_to?(:description) ? object.description : "description: #{ lib } kicks the ass" 96 | 97 | if This.extensions.nil? 98 | This.extensions = [] 99 | extensions = This.extensions 100 | %w( Makefile configure extconf.rb ).each do |ext| 101 | extensions << ext if File.exists?(ext) 102 | end 103 | end 104 | extensions = [extensions].flatten.compact 105 | 106 | template = 107 | if test(?e, 'gemspec.erb') 108 | Template{ IO.read('gemspec.erb') } 109 | else 110 | Template { 111 | <<-__ 112 | ## #{ lib }.gemspec 113 | # 114 | 115 | Gem::Specification::new do |spec| 116 | spec.name = #{ lib.inspect } 117 | spec.version = #{ version.inspect } 118 | spec.platform = Gem::Platform::RUBY 119 | spec.summary = #{ lib.inspect } 120 | spec.description = #{ description.inspect } 121 | 122 | spec.files =\n#{ files.sort.pretty_inspect } 123 | spec.executables = #{ executables.inspect } 124 | 125 | spec.require_path = "lib" 126 | 127 | spec.test_files = #{ test_files.inspect } 128 | 129 | ### spec.add_dependency 'lib', '>= version' 130 | #### spec.add_dependency 'map' 131 | 132 | spec.extensions.push(*#{ extensions.inspect }) 133 | 134 | spec.rubyforge_project = #{ This.rubyforge_project.inspect } 135 | spec.author = #{ This.author.inspect } 136 | spec.email = #{ This.email.inspect } 137 | spec.homepage = #{ This.homepage.inspect } 138 | end 139 | __ 140 | } 141 | end 142 | 143 | Fu.mkdir_p(This.pkgdir) 144 | gemspec = "#{ lib }.gemspec" 145 | open(gemspec, "w"){|fd| fd.puts(template)} 146 | This.gemspec = gemspec 147 | end 148 | 149 | task :gem => [:clean, :gemspec] do 150 | Fu.mkdir_p(This.pkgdir) 151 | before = Dir['*.gem'] 152 | cmd = "gem build #{ This.gemspec }" 153 | `#{ cmd }` 154 | after = Dir['*.gem'] 155 | gem = ((after - before).first || after.first) or abort('no gem!') 156 | Fu.mv(gem, This.pkgdir) 157 | This.gem = File.join(This.pkgdir, File.basename(gem)) 158 | end 159 | 160 | task :readme do 161 | samples = '' 162 | prompt = '~ > ' 163 | lib = This.lib 164 | version = This.version 165 | 166 | Dir['sample*/*'].sort.each do |sample| 167 | samples << "\n" << " <========< #{ sample } >========>" << "\n\n" 168 | 169 | cmd = "cat #{ sample }" 170 | samples << Util.indent(prompt + cmd, 2) << "\n\n" 171 | samples << Util.indent(`#{ cmd }`, 4) << "\n" 172 | 173 | cmd = "ruby #{ sample }" 174 | samples << Util.indent(prompt + cmd, 2) << "\n\n" 175 | 176 | cmd = "ruby -e'STDOUT.sync=true; exec %(ruby -I ./lib #{ sample })'" 177 | samples << Util.indent(`#{ cmd } 2>&1`, 4) << "\n" 178 | end 179 | 180 | template = 181 | if test(?e, 'readme.erb') 182 | Template{ IO.read('readme.erb') } 183 | else 184 | Template { 185 | <<-__ 186 | NAME 187 | #{ lib } 188 | 189 | DESCRIPTION 190 | 191 | INSTALL 192 | gem install #{ lib } 193 | 194 | SAMPLES 195 | #{ samples } 196 | __ 197 | } 198 | end 199 | 200 | open("README", "w"){|fd| fd.puts template} 201 | end 202 | 203 | 204 | task :clean do 205 | Dir[File.join(This.pkgdir, '**/**')].each{|entry| Fu.rm_rf(entry)} 206 | end 207 | 208 | 209 | task :release => [:clean, :gemspec, :gem] do 210 | gems = Dir[File.join(This.pkgdir, '*.gem')].flatten 211 | raise "which one? : #{ gems.inspect }" if gems.size > 1 212 | raise "no gems?" if gems.size < 1 213 | 214 | cmd = "gem push #{ This.gem }" 215 | puts cmd 216 | puts 217 | system(cmd) 218 | abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero? 219 | 220 | cmd = "rubyforge login && rubyforge add_release #{ This.rubyforge_project } #{ This.lib } #{ This.version } #{ This.gem }" 221 | puts cmd 222 | puts 223 | system(cmd) 224 | abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero? 225 | end 226 | 227 | 228 | 229 | 230 | 231 | BEGIN { 232 | # support for this rakefile 233 | # 234 | $VERBOSE = nil 235 | 236 | require 'ostruct' 237 | require 'erb' 238 | require 'fileutils' 239 | require 'rbconfig' 240 | require 'pp' 241 | 242 | # fu shortcut 243 | # 244 | Fu = FileUtils 245 | 246 | # cache a bunch of stuff about this rakefile/environment 247 | # 248 | This = OpenStruct.new 249 | 250 | This.file = File.expand_path(__FILE__) 251 | This.dir = File.dirname(This.file) 252 | This.pkgdir = File.join(This.dir, 'pkg') 253 | 254 | # grok lib 255 | # 256 | lib = ENV['LIB'] 257 | unless lib 258 | lib = File.basename(Dir.pwd).sub(/[-].*$/, '') 259 | end 260 | This.lib = lib 261 | 262 | # grok version 263 | # 264 | version = ENV['VERSION'] 265 | unless version 266 | require "./lib/#{ This.lib }" 267 | This.name = lib.capitalize 268 | This.object = eval(This.name) 269 | version = This.object.send(:version) 270 | end 271 | This.version = version 272 | 273 | # we need to know the name of the lib an it's version 274 | # 275 | abort('no lib') unless This.lib 276 | abort('no version') unless This.version 277 | 278 | # discover full path to this ruby executable 279 | # 280 | c = Config::CONFIG 281 | bindir = c["bindir"] || c['BINDIR'] 282 | ruby_install_name = c['ruby_install_name'] || c['RUBY_INSTALL_NAME'] || 'ruby' 283 | ruby_ext = c['EXEEXT'] || '' 284 | ruby = File.join(bindir, (ruby_install_name + ruby_ext)) 285 | This.ruby = ruby 286 | 287 | # some utils 288 | # 289 | module Util 290 | def indent(s, n = 2) 291 | s = unindent(s) 292 | ws = ' ' * n 293 | s.gsub(%r/^/, ws) 294 | end 295 | 296 | def unindent(s) 297 | indent = nil 298 | s.each_line do |line| 299 | next if line =~ %r/^\s*$/ 300 | indent = line[%r/^\s*/] and break 301 | end 302 | indent ? s.gsub(%r/^#{ indent }/, "") : s 303 | end 304 | extend self 305 | end 306 | 307 | # template support 308 | # 309 | class Template 310 | def initialize(&block) 311 | @block = block 312 | @template = block.call.to_s 313 | end 314 | def expand(b=nil) 315 | ERB.new(Util.unindent(@template)).result((b||@block).binding) 316 | end 317 | alias_method 'to_s', 'expand' 318 | end 319 | def Template(*args, &block) Template.new(*args, &block) end 320 | 321 | # colored console output support 322 | # 323 | This.ansi = { 324 | :clear => "\e[0m", 325 | :reset => "\e[0m", 326 | :erase_line => "\e[K", 327 | :erase_char => "\e[P", 328 | :bold => "\e[1m", 329 | :dark => "\e[2m", 330 | :underline => "\e[4m", 331 | :underscore => "\e[4m", 332 | :blink => "\e[5m", 333 | :reverse => "\e[7m", 334 | :concealed => "\e[8m", 335 | :black => "\e[30m", 336 | :red => "\e[31m", 337 | :green => "\e[32m", 338 | :yellow => "\e[33m", 339 | :blue => "\e[34m", 340 | :magenta => "\e[35m", 341 | :cyan => "\e[36m", 342 | :white => "\e[37m", 343 | :on_black => "\e[40m", 344 | :on_red => "\e[41m", 345 | :on_green => "\e[42m", 346 | :on_yellow => "\e[43m", 347 | :on_blue => "\e[44m", 348 | :on_magenta => "\e[45m", 349 | :on_cyan => "\e[46m", 350 | :on_white => "\e[47m" 351 | } 352 | def say(phrase, *args) 353 | options = args.last.is_a?(Hash) ? args.pop : {} 354 | options[:color] = args.shift.to_s.to_sym unless args.empty? 355 | keys = options.keys 356 | keys.each{|key| options[key.to_s.to_sym] = options.delete(key)} 357 | 358 | color = options[:color] 359 | bold = options.has_key?(:bold) 360 | 361 | parts = [phrase] 362 | parts.unshift(This.ansi[color]) if color 363 | parts.unshift(This.ansi[:bold]) if bold 364 | parts.push(This.ansi[:clear]) if parts.size > 1 365 | 366 | method = options[:method] || :puts 367 | 368 | Kernel.send(method, parts.join) 369 | end 370 | 371 | # always run out of the project dir 372 | # 373 | Dir.chdir(This.dir) 374 | } 375 | -------------------------------------------------------------------------------- /test/map_test.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'testing' 3 | require 'map' 4 | 5 | Testing Map do 6 | testing 'that bare constructor werks' do 7 | assert{ Map.new } 8 | end 9 | 10 | testing 'that the contructor accepts a hash' do 11 | assert{ Map.new(hash = {}) } 12 | end 13 | 14 | testing 'that the constructor accepts the empty array' do 15 | array = [] 16 | assert{ Map.new(array) } 17 | assert{ Map.new(*array) } 18 | end 19 | 20 | testing 'that the constructor does not die when passed nil or false' do 21 | assert{ Map.new(nil) } 22 | assert{ Map.new(false) } 23 | end 24 | 25 | testing 'that the contructor accepts an even sized array' do 26 | arrays = [ 27 | [ %w( k v ), %w( key val ) ], 28 | [ %w( k v ), %w( key val ), %w( a b ) ], 29 | [ %w( k v ), %w( key val ), %w( a b ), %w( x y ) ] 30 | ] 31 | arrays.each do |array| 32 | assert{ Map.new(array) } 33 | assert{ Map.new(*array) } 34 | end 35 | end 36 | 37 | testing 'that the contructor accepts an odd sized array' do 38 | arrays = [ 39 | [ %w( k v ) ], 40 | [ %w( k v ), %w( key val ), %w( a b ) ] 41 | ] 42 | arrays.each do |array| 43 | assert{ Map.new(array) } 44 | assert{ Map.new(*array) } 45 | end 46 | end 47 | 48 | testing 'that the constructor accepts arrays of pairs' do 49 | arrays = [ 50 | [], 51 | [ %w( k v ) ], 52 | [ %w( k v ), %w( key val ) ], 53 | [ %w( k v ), %w( key val ), %w( a b ) ] 54 | ] 55 | arrays.each do |array| 56 | assert{ Map.new(array) } 57 | assert{ Map.new(*array) } 58 | end 59 | end 60 | 61 | testing 'that "[]" is a synonym for "new"' do 62 | list = [ 63 | [], 64 | [{}], 65 | [[:key, :val]], 66 | [:key, :val] 67 | ] 68 | list.each do |args| 69 | map = assert{ Map[*args] } 70 | assert{ map.is_a?(Map) } 71 | assert{ Map.new(*args) == map } 72 | end 73 | end 74 | 75 | testing 'that #each yields pairs in order' do 76 | map = new_int_map 77 | i = 0 78 | map.each do |key, val| 79 | assert{ key == i.to_s } 80 | assert{ val == i } 81 | i += 1 82 | end 83 | end 84 | 85 | testing 'that keys and values are ordered' do 86 | n = 2048 87 | map = new_int_map(n) 88 | values = Array.new(n){|i| i} 89 | keys = values.map{|value| value.to_s} 90 | assert{ map.keys.size == n } 91 | assert{ map.keys == keys} 92 | assert{ map.values == values} 93 | end 94 | 95 | testing 'that maps are string/symbol indifferent for simple look-ups' do 96 | map = Map.new 97 | map[:k] = :v 98 | map['a'] = 'b' 99 | assert{ map[:k] == :v } 100 | assert{ map[:k.to_s] == :v } 101 | assert{ map[:a] == 'b' } 102 | assert{ map[:a.to_s] == 'b' } 103 | end 104 | 105 | testing 'that maps are string/symbol indifferent for recursive look-ups' do 106 | map = assert{ Map(:a => {:b => {:c => 42}}) } 107 | assert{ map[:a] = {:b => {:c => 42}} } 108 | assert{ map[:a][:b][:c] == 42 } 109 | assert{ map['a'][:b][:c] == 42 } 110 | assert{ map['a']['b'][:c] == 42 } 111 | assert{ map['a']['b']['c'] == 42 } 112 | assert{ map[:a]['b'][:c] == 42 } 113 | assert{ map[:a]['b']['c'] == 42 } 114 | assert{ map[:a][:b]['c'] == 42 } 115 | assert{ map['a'][:b]['c'] == 42 } 116 | 117 | map = assert{ Map(:a => [{:b => 42}]) } 118 | assert{ map['a'].is_a?(Array) } 119 | assert{ map['a'][0].is_a?(Map) } 120 | assert{ map['a'][0]['b'] == 42 } 121 | 122 | map = assert{ Map(:a => [ {:b => 42}, [{:c => 'forty-two'}] ]) } 123 | assert{ map['a'].is_a?(Array) } 124 | assert{ map['a'][0].is_a?(Map) } 125 | assert{ map['a'][1].is_a?(Array) } 126 | assert{ map['a'][0]['b'] == 42 } 127 | assert{ map['a'][1][0]['c'] == 'forty-two' } 128 | end 129 | 130 | testing 'that maps support shift like a good ordered container' do 131 | map = Map.new 132 | 10.times do |i| 133 | key, val = i.to_s, i 134 | assert{ map.unshift(key, val) } 135 | assert{ map[key] == val } 136 | assert{ map.keys.first.to_s == key.to_s} 137 | assert{ map.values.first.to_s == val.to_s} 138 | end 139 | 140 | map = Map.new 141 | args = [] 142 | 10.times do |i| 143 | key, val = i.to_s, i 144 | args.unshift([key, val]) 145 | end 146 | map.unshift(*args) 147 | 10.times do |i| 148 | key, val = i.to_s, i 149 | assert{ map[key] == val } 150 | assert{ map.keys[i].to_s == key.to_s} 151 | assert{ map.values[i].to_s == val.to_s} 152 | end 153 | end 154 | 155 | testing 'the match operator, which can make testing hash equality simpler!' do 156 | map = new_int_map 157 | hash = new_int_hash 158 | assert{ map =~ hash } 159 | end 160 | 161 | testing 'that inheritence works without cycles' do 162 | c = Class.new(Map){} 163 | o = assert{ c.new } 164 | assert{ Map === o } 165 | end 166 | 167 | testing 'equality' do 168 | a = assert{ Map.new } 169 | b = assert{ Map.new } 170 | assert{ a == b} 171 | assert{ a != 42 } 172 | b[:k] = :v 173 | assert{ a != b} 174 | end 175 | 176 | testing 'simple struct usage' do 177 | a = assert{ Map.new(:k => :v) } 178 | s = assert{ a.struct } 179 | assert{ s.k == :v } 180 | end 181 | 182 | testing 'nested struct usage' do 183 | a = assert{ Map.new(:k => {:l => :v}) } 184 | s = assert{ a.struct } 185 | assert{ s.k.l == :v } 186 | end 187 | 188 | testing 'that subclassing and clobbering initialize does not kill nested coersion' do 189 | c = Class.new(Map){ def initialize(arg) end } 190 | o = assert{ c.new(42) } 191 | assert{ o.is_a?(c) } 192 | assert{ o.update(:k => {:a => :b}) } 193 | end 194 | 195 | testing 'that subclassing does not kill class level coersion' do 196 | c = Class.new(Map){ } 197 | o = assert{ c.for(Map.new) } 198 | assert{ o.is_a?(c) } 199 | 200 | d = Class.new(c) 201 | o = assert{ d.for(Map.new) } 202 | assert{ o.is_a?(d) } 203 | end 204 | 205 | testing 'that subclassing creates custom conversion methods' do 206 | c = Class.new(Map) do 207 | def self.name() 208 | :C 209 | end 210 | end 211 | assert{ c.conversion_methods.map{|x| x.to_s} == %w( to_c to_map ) } 212 | o = c.new 213 | assert{ o.respond_to?(:to_map) } 214 | assert{ o.respond_to?(:to_c) } 215 | 216 | assert{ o.update(:a => {:b => :c}) } 217 | assert{ o[:a].class == c } 218 | end 219 | 220 | testing 'that custom conversion methods can be added' do 221 | c = Class.new(Map) 222 | o = c.new 223 | foobar = {:k => :v} 224 | def foobar.to_foobar() self end 225 | c.add_conversion_method!('to_foobar') 226 | assert{ c.conversion_methods.map{|x| x.to_s} == %w( to_foobar to_map ) } 227 | o[:foobar] = foobar 228 | assert{ o[:foobar] =~ foobar } 229 | end 230 | 231 | testing 'that custom conversion methods are coerced - just in case' do 232 | map = Map.new 233 | record = Class.new(Hash) do 234 | def to_map() {:id => 42} end 235 | end 236 | map.update(:list => [record.new, record.new]) 237 | assert{ map.list.all?{|x| x.is_a?(Map)} } 238 | assert{ map.list.all?{|x| x.id==42} } 239 | end 240 | 241 | testing 'that non-hashlike classes do *not* have conversion methods called on them' do 242 | map = Map.new 243 | record = Class.new do 244 | def to_map() {:id => 42} end 245 | end 246 | map.update(:record => record.new) 247 | assert{ !map.record.is_a?(Hash) } 248 | assert{ !map.record.is_a?(Map) } 249 | assert{ map.record.is_a?(record) } 250 | end 251 | 252 | testing 'that coercion is minimal' do 253 | map = Map.new 254 | a = Class.new(Map) do 255 | def to_map() {:k => :a} end 256 | end 257 | b = Class.new(a) do 258 | def to_map() {:k => :b} end 259 | end 260 | m = b.new 261 | m.update(:list => [a.new, b.new]) 262 | assert{ m.list.first.class == b } 263 | assert{ m.list.last.class == b } 264 | m = a.new 265 | m.update(:list => [a.new, b.new]) 266 | assert{ m.list.first.class == a } 267 | assert{ m.list.last.class == a } 268 | end 269 | 270 | testing 'that map supports basic option parsing for methods' do 271 | %w( options_for options opts ).each do |method| 272 | args = [0,1, {:k => :v, :a => false}] 273 | Map.send(method, args) 274 | opts = assert{ Map.send(method, args) } 275 | assert{ opts.is_a?(Map) } 276 | assert{ opts.getopt(:k)==:v } 277 | assert{ opts.getopt(:a)==false } 278 | assert{ opts.getopt(:b, :default => 42)==42 } 279 | assert{ args.last.object_id == opts.object_id } 280 | end 281 | end 282 | 283 | testing 'that bang option parsing can pop the options off' do 284 | logic = proc do |method, args| 285 | before = args.dup 286 | opts = assert{ Map.send(method, args) } 287 | after = args 288 | 289 | assert{ opts.is_a?(Map) } 290 | assert{ !args.last.is_a?(Hash) } if before.last.is_a?(Hash) 291 | assert{ args.last.object_id != opts.object_id } 292 | 293 | opts 294 | end 295 | 296 | %w( options_for! options! opts! ).each do |method| 297 | [ 298 | [0,1, {:k => :v, :a => false}], 299 | [42], 300 | [] 301 | ].each do |args| 302 | opts = logic.call(method, args) 303 | logic.call(method, [0, 1, opts]) 304 | end 305 | end 306 | end 307 | 308 | testing 'that maps can be converted to lists with numeric indexes' do 309 | m = Map[0, :a, 1, :b, 2, :c] 310 | assert{ m.to_list == [:a, :b, :c] } 311 | end 312 | 313 | testing 'that method_missing hacks allow setting values, but not getting them until they are set' do 314 | m = Map.new 315 | assert{ (m.missing rescue $!).is_a?(Exception) } 316 | assert{ m.missing = :val } 317 | assert{ m[:missing] == :val } 318 | assert{ m.missing == :val } 319 | end 320 | 321 | testing 'that method_missing hacks have sane respond_to? semantics' do 322 | m = Map.new 323 | assert{ !m.respond_to?(:missing) } 324 | assert{ m.respond_to?(:missing=) } 325 | assert{ m.missing = :val } 326 | assert{ m.respond_to?(:missing) } 327 | assert{ m.respond_to?(:missing=) } 328 | end 329 | 330 | testing 'that method missing with a block delegatets to fetch' do 331 | m = Map.new 332 | assert{ m.missing{ :val } == :val } 333 | assert{ !m.has_key?(:key) } 334 | end 335 | 336 | testing 'that #id werks' do 337 | m = Map.new 338 | assert{ (m.id rescue $!).is_a?(Exception) } 339 | m.id = 42 340 | assert{ m.id==42 } 341 | end 342 | 343 | testing 'that maps support compound key/val setting' do 344 | m = Map.new 345 | assert{ m.set(:a, :b, :c, 42) } 346 | assert{ m.get(:a, :b, :c) == 42 } 347 | 348 | m = Map.new 349 | assert{ m.set([:a, :b, :c], 42) } 350 | assert{ m.get(:a, :b, :c) == 42 } 351 | 352 | m = Map.new 353 | assert{ m.set([:a, :b, :c] => 42) } 354 | assert{ m.get(:a, :b, :c) == 42 } 355 | 356 | m = Map.new 357 | assert{ m.set([:x, :y, :z] => 42.0, [:A, 2] => 'forty-two') } 358 | assert{ m[:A].is_a?(Array) } 359 | assert{ m[:A].size == 3} 360 | assert{ m[:A][2] == 'forty-two' } 361 | assert{ m[:x][:y].is_a?(Map) } 362 | assert{ m[:x][:y][:z] == 42.0 } 363 | 364 | assert{ Map.new.tap{|nm| nm.set} =~ {} } 365 | assert{ Map.new.tap{|nm| nm.set({})} =~ {} } 366 | end 367 | 368 | testing 'that Map#get supports providing a default value in a block' do 369 | m = Map.new 370 | m.set(:a, :b, :c, 42) 371 | m.set(:z, 1) 372 | 373 | assert { m.get(:x) {1} == 1 } 374 | assert { m.get(:z) {2} == 1 } 375 | assert { m.get(:a, :b, :d) {1} == 1 } 376 | assert { m.get(:a, :b, :c) {1} == 42 } 377 | assert { m.get(:a, :b) {1} == Map.new({:c => 42}) } 378 | assert { m.get(:a, :aa) {1} == 1 } 379 | end 380 | 381 | testing 'that setting a sub-container does not eff up the container values' do 382 | m = Map.new 383 | assert{ m.set(:array => [0,1,2]) } 384 | assert{ m.get(:array, 0) == 0 } 385 | assert{ m.get(:array, 1) == 1 } 386 | assert{ m.get(:array, 2) == 2 } 387 | 388 | assert{ m.set(:array, 2, 42) } 389 | assert{ m.get(:array, 0) == 0 } 390 | assert{ m.get(:array, 1) == 1 } 391 | assert{ m.get(:array, 2) == 42 } 392 | end 393 | 394 | testing 'that #apply selectively merges non-nil values' do 395 | m = Map.new(:array => [0, 1], :hash => {:a => false, :b => nil, :c => 42}) 396 | defaults = Map.new(:array => [nil, nil, 2], :hash => {:b => true}) 397 | 398 | assert{ m.apply(defaults) } 399 | assert{ m[:array] == [0,1,2] } 400 | assert{ m[:hash] =~ {:a => false, :b => true, :c => 42} } 401 | 402 | m = Map.new 403 | assert{ m.apply :key => [{:key => :val}] } 404 | assert{ m[:key].is_a?(Array) } 405 | assert{ m[:key][0].is_a?(Map) } 406 | end 407 | 408 | testing 'that #add overlays the leaves of one hash onto another without nuking branches' do 409 | m = Map.new 410 | 411 | assert do 412 | m.add( 413 | :comments => [ 414 | { :body => 'a' }, 415 | { :body => 'b' }, 416 | ], 417 | 418 | [:comments, 0] => {'title' => 'teh title', 'description' => 'description'}, 419 | [:comments, 1] => {'description' => 'description'}, 420 | ) 421 | end 422 | 423 | assert do 424 | m =~ 425 | {"comments"=> 426 | [{"body"=>"a", "title"=>"teh title", "description"=>"description"}, 427 | {"body"=>"b", "description"=>"description"}]} 428 | end 429 | 430 | m = Map.new 431 | 432 | assert do 433 | m.add( 434 | [:a, :b, :c] => 42, 435 | 436 | [:a, :b] => {:d => 42.0} 437 | ) 438 | end 439 | 440 | assert do 441 | m =~ 442 | {"a"=>{"b"=>{"c"=>42, "d"=>42.0}}} 443 | end 444 | 445 | assert{ Map.new.tap{|i| i.add} =~ {} } 446 | assert{ Map.new.tap{|i| i.add({})} =~ {} } 447 | end 448 | 449 | testing 'that Map.combine is teh sweet' do 450 | { 451 | [{:a => {:b => 42}}, {:a => {:c => 42.0}}] => 452 | {"a"=>{"b"=>42, "c"=>42.0}}, 453 | 454 | [{:a => {:b => 42}}, {:a => {:c => 42.0, :d => [1]}}] => 455 | {"a"=>{"b"=>42, "d"=>[1], "c"=>42.0}}, 456 | 457 | [{:a => {:b => 42}}, {:a => {:c => 42.0, :d => {0=>1}}}] => 458 | {"a"=>{"b"=>42, "d"=>{0=>1}, "c"=>42.0}} 459 | 460 | }.each do |args, expected| 461 | assert{ Map.combine(*args) =~ expected } 462 | end 463 | end 464 | 465 | testing 'that maps support depth_first_each' do 466 | m = Map.new 467 | prefix = %w[ a b c ] 468 | keys = [] 469 | n = 0.42 470 | 471 | 10.times do |i| 472 | key = prefix + [i] 473 | val = n 474 | keys.push(key) 475 | assert{ m.set(key => val) } 476 | n *= 10 477 | end 478 | 479 | assert{ m.get(:a).is_a?(Hash) } 480 | assert{ m.get(:a, :b).is_a?(Hash) } 481 | assert{ m.get(:a, :b, :c).is_a?(Array) } 482 | 483 | n = 0.42 484 | m.depth_first_each do |key, val| 485 | assert{ key == keys.shift } 486 | assert{ val == n } 487 | n *= 10 488 | end 489 | end 490 | 491 | testing 'that Map.each_pair works on arrays' do 492 | each = [] 493 | array = %w( a b c ) 494 | Map.each_pair(array){|k,v| each.push(k,v)} 495 | assert{ each_pair = ['a', 'b', 'c', nil] } 496 | end 497 | 498 | testing 'that maps support breath_first_each' do 499 | map = Map[ 500 | 'hash' , {'x' => 'y'}, 501 | 'nested hash' , {'nested' => {'a' => 'b'}}, 502 | 'array' , [0, 1, 2], 503 | 'nested array' , [[3], [4], [5]], 504 | 'string' , '42' 505 | ] 506 | 507 | accum = [] 508 | Map.breadth_first_each(map){|k, v| accum.push([k, v])} 509 | expected = 510 | [[["hash"], {"x"=>"y"}], 511 | [["nested hash"], {"nested"=>{"a"=>"b"}}], 512 | [["array"], [0, 1, 2]], 513 | [["nested array"], [[3], [4], [5]]], 514 | [["string"], "42"], 515 | [["hash", "x"], "y"], 516 | [["nested hash", "nested"], {"a"=>"b"}], 517 | [["array", 0], 0], 518 | [["array", 1], 1], 519 | [["array", 2], 2], 520 | [["nested array", 0], [3]], 521 | [["nested array", 1], [4]], 522 | [["nested array", 2], [5]], 523 | [["nested hash", "nested", "a"], "b"], 524 | [["nested array", 0, 0], 3], 525 | [["nested array", 1, 0], 4], 526 | [["nested array", 2, 0], 5]] 527 | end 528 | 529 | testing 'that maps have a needle-in-a-haystack like #contains? method' do 530 | haystack = Map[ 531 | 'hash' , {'x' => 'y'}, 532 | 'nested hash' , {'nested' => {'a' => 'b'}}, 533 | 'array' , [0, 1, 2], 534 | 'nested array' , [[3], [4], [5]], 535 | 'string' , '42' 536 | ] 537 | 538 | needles = [ 539 | {'x' => 'y'}, 540 | {'nested' => {'a' => 'b'}}, 541 | {'a' => 'b'}, 542 | [0,1,2], 543 | [[3], [4], [5]], 544 | [3], 545 | [4], 546 | [5], 547 | '42', 548 | 0,1,2, 549 | 3,4,5 550 | ] 551 | 552 | needles.each do |needle| 553 | assert{ haystack.contains?(needle) } 554 | end 555 | end 556 | 557 | testing 'that #update and #replace accept map-ish objects' do 558 | o = Object.new 559 | def o.to_map() {:k => :v} end 560 | m = Map.new 561 | assert{ m.update(o) } 562 | assert{ m =~ {:k => :v} } 563 | m[:a] = :b 564 | assert{ m.replace(o) } 565 | assert{ m =~ {:k => :v} } 566 | end 567 | 568 | testing 'that maps with un-marshal-able objects can be copied' do 569 | open(__FILE__) do |f| 570 | m = Map.for(:f => f) 571 | assert{ m.copy } 572 | assert{ m.dup } 573 | assert{ m.clone } 574 | end 575 | end 576 | 577 | testing 'that maps have a blank? method that is sane' do 578 | m = Map.new(:a => 0, :b => ' ', :c => '', :d => {}, :e => [], :f => false) 579 | m.each do |key, val| 580 | assert{ m.blank?(key) } 581 | end 582 | 583 | m = Map.new(:a => 1, :b => '_', :d => {:k=>:v}, :e => [42], :f => true) 584 | m.each do |key, val| 585 | assert{ !m.blank?(key) } 586 | end 587 | 588 | assert{ Map.new.blank? } 589 | end 590 | 591 | testing 'that self referential maps do not make #inspect puke' do 592 | a = Map.new 593 | b = Map.new 594 | 595 | b[:a] = a 596 | a[:b] = b 597 | 598 | assert do 599 | begin 600 | a.inspect 601 | b.inspect 602 | true 603 | rescue Object 604 | false 605 | end 606 | end 607 | end 608 | 609 | testing 'that maps a clever little rm operator' do 610 | map = Map.new 611 | map.set :a, :b, 42 612 | map.set :x, :y, 42 613 | map.set :x, :z, 42 614 | map.set :array, [0,1,2] 615 | 616 | assert{ map.rm(:x, :y) } 617 | assert{ map.get(:x) =~ {:z => 42} } 618 | 619 | assert{ map.rm(:a, :b) } 620 | assert{ map.get(:a) =~ {} } 621 | 622 | assert{ map.rm(:array, 0) } 623 | assert{ map.get(:array) == [1,2] } 624 | assert{ map.rm(:array, 1) } 625 | assert{ map.get(:array) == [1] } 626 | assert{ map.rm(:array, 0) } 627 | assert{ map.get(:array) == [] } 628 | 629 | assert{ map.rm(:array) } 630 | assert{ map.get(:array).nil? } 631 | 632 | assert{ map.rm(:a) } 633 | assert{ map.get(:a).nil? } 634 | 635 | assert{ map.rm(:x) } 636 | assert{ map.get(:x).nil? } 637 | end 638 | 639 | testing 'that maps a clever little question method' do 640 | m = Map.new 641 | m.set(:a, :b, :c, 42) 642 | m.set([:x, :y, :z] => 42.0, [:A, 2] => 'forty-two') 643 | 644 | assert( !m.b? ) 645 | assert( m.a? ) 646 | assert( m.a.b? ) 647 | assert( m.a.b.c? ) 648 | assert( !m.a.b.d? ) 649 | 650 | assert( m.x? ) 651 | assert( m.x.y? ) 652 | assert( m.x.y.z? ) 653 | assert( !m.y? ) 654 | 655 | assert( m.A? ) 656 | end 657 | 658 | testing 'that maps have a clever little question method on Struct' do 659 | m = Map.new 660 | m.set(:a, :b, :c, 42) 661 | m.set([:x, :y, :z] => 42.0, [:A, 2] => 'forty-two') 662 | s = m.struct 663 | 664 | assert( s.a.b.c == 42 ) 665 | assert( s.x.y.z == 42.0 ) 666 | 667 | assert( !s.b? ) 668 | assert( s.a? ) 669 | assert( s.a.b? ) 670 | assert( s.a.b.c? ) 671 | assert( !s.a.b.d? ) 672 | 673 | assert( s.x? ) 674 | assert( s.x.y? ) 675 | assert( s.x.y.z? ) 676 | assert( !s.y? ) 677 | 678 | assert( s.A? ) 679 | 680 | end 681 | 682 | testing 'that Map#default= blows up until a sane strategy for dealing with it is developed' do 683 | m = Map.new 684 | 685 | assert do 686 | begin 687 | m.default = 42 688 | rescue Object => e 689 | e.is_a?(ArgumentError) 690 | end 691 | end 692 | end 693 | 694 | ## Map#keep_if tests 695 | # 696 | # See: https://github.com/rubyspec/rubyspec/blob/ebd1ea400cb06807dbd6aa481c6c3d7a0b8fc7b4/core/hash/keep_if_spec.rb 697 | # 698 | # original failing test 699 | testing 'that Map#keep_if properly removes k/v pairs for which the passed block evaluates to false' do 700 | m = Map.new( { 1 => "hi" , 2 => "there" } ) 701 | assert{ !( m.keep_if { |k,v| k == 2 }.keys.include? 1 ) } 702 | end 703 | 704 | testing 'yields two arguments: key and value' do 705 | all_args = [] 706 | m = Map.new( { 1 => 2 , 3 => 4 } ) 707 | m.keep_if { |*args| all_args << args } 708 | assert{ all_args == [[1, 2], [3, 4]] } 709 | end 710 | 711 | testing 'keeps every entry for which block is true and returns self' do 712 | m = Map.new( { :a => 1 , :b => 2 , :c => 3 , :d => 4 } ) 713 | assert{ m.keep_if { |k,v| v % 2 == 0 }.object_id == m.object_id } 714 | assert{ m == Map.new( { :b => 2 , :d => 4 } ) } 715 | end 716 | 717 | testing 'it raises a RuntimeError if called on a frozen instance' do 718 | m = Map.new( { :a => 1 } ).freeze 719 | 720 | assert do 721 | begin 722 | m.keep_if { |*_| false } 723 | rescue Object => e 724 | e.is_a?( RuntimeError ) 725 | end 726 | end 727 | end 728 | 729 | testing 'that Array-y values preserve their class' do 730 | map = Map.new 731 | list = Class.new(Array){}.new 732 | map.list = list 733 | assert{ map.list.class == list.class } 734 | assert{ map.list.class != Array } 735 | end 736 | 737 | testing 'rack compatible params' do 738 | m = Map.for(:a => [{}, {:b => 42}], :x => [ nil, [ nil, {:y => 42}] ], :A => {:B => {:C => 42}}) 739 | 740 | assert{ m.param_for(:a, 1, :b) == 'map[a][][b]=42' } 741 | assert{ m.name_for(:a, 1, :b) == 'map[a][][b]' } 742 | 743 | assert{ m.param_for(:x, 1, 1, :y) == 'map[x][][][y]=42' } 744 | assert{ m.name_for(:x, 1, 1, :y) == 'map[x][][][y]' } 745 | 746 | assert{ m.param_for(:A, :B, :C) == 'map[A][B][C]=42' } 747 | assert{ m.name_for(:A, :B, :C) == 'map[A][B][C]' } 748 | 749 | assert{ m.name_for(:A, :B, :C, :prefix => :foo) == 'foo[A][B][C]' } 750 | 751 | m = Map.for({"a"=>{"b"=>42}, "x"=>{"y"=>42}, "foo"=>:bar, "array"=>[{"k"=>:v}]}) 752 | assert{ m.to_params == "map[a][b]=42&map[x][y]=42&map[foo]=bar&map[array][][k]=v" } 753 | end 754 | 755 | testing 'delete_if' do 756 | m = Map.for(:k => :v) 757 | assert{ m.delete_if{|k| k.to_s == 'k'} } 758 | assert{ m.empty?} 759 | 760 | m = Map.for(:k => :v) 761 | assert{ m.delete_if{|k,v| v.to_s == 'v'} } 762 | assert{ m.empty?} 763 | end 764 | 765 | protected 766 | def new_int_map(n = 1024) 767 | map = assert{ Map.new } 768 | n.times{|i| map[i.to_s] = i} 769 | map 770 | end 771 | 772 | def new_int_hash(n = 1024) 773 | hash = Hash.new 774 | n.times{|i| hash[i.to_s] = i} 775 | hash 776 | end 777 | end 778 | 779 | 780 | 781 | 782 | 783 | 784 | BEGIN { 785 | testdir = File.dirname(File.expand_path(__FILE__)) 786 | testlibdir = File.join(testdir, 'lib') 787 | rootdir = File.dirname(testdir) 788 | libdir = File.join(rootdir, 'lib') 789 | $LOAD_PATH.push(libdir) 790 | $LOAD_PATH.push(testlibdir) 791 | } 792 | -------------------------------------------------------------------------------- /lib/map.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class Map < Hash 3 | Version = '6.5.1' unless defined?(Version) 4 | Load = Kernel.method(:load) unless defined?(Load) 5 | 6 | class << Map 7 | def version 8 | Map::Version 9 | end 10 | 11 | def libdir(*args, &block) 12 | @libdir ||= File.expand_path(__FILE__).sub(/\.rb$/,'') 13 | libdir = args.empty? ? @libdir : File.join(@libdir, *args.map{|arg| arg.to_s}) 14 | ensure 15 | if block 16 | begin 17 | $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.first==libdir 18 | module_eval(&block) 19 | ensure 20 | $LOAD_PATH.shift() if $LOAD_PATH.first==libdir 21 | end 22 | end 23 | end 24 | 25 | def load(*args, &block) 26 | libdir{ Load.call(*args, &block) } 27 | end 28 | 29 | def allocate 30 | super.instance_eval do 31 | @keys = [] 32 | self 33 | end 34 | end 35 | 36 | def new(*args, &block) 37 | allocate.instance_eval do 38 | initialize(*args, &block) 39 | self 40 | end 41 | end 42 | 43 | def for(*args, &block) 44 | if(args.size == 1 and block.nil?) 45 | return args.first if args.first.class == self 46 | end 47 | new(*args, &block) 48 | end 49 | 50 | def coerce(other) 51 | case other 52 | when Map 53 | other 54 | else 55 | allocate.update(other.to_hash) 56 | end 57 | end 58 | 59 | def tap(*args, &block) 60 | new(*args).tap do |map| 61 | map.tap(&block) if block 62 | end 63 | end 64 | 65 | def conversion_methods 66 | @conversion_methods ||= ( 67 | map_like = ancestors.select{|ancestor| ancestor <= Map} 68 | type_names = map_like.map do |ancestor| 69 | name = ancestor.name.to_s.strip 70 | next if name.empty? 71 | name.downcase.gsub(/::/, '_') 72 | end.compact 73 | list = type_names.map{|type_name| "to_#{ type_name }"} 74 | list.each{|method| define_conversion_method!(method)} 75 | list 76 | ) 77 | end 78 | 79 | def define_conversion_method!(method) 80 | method = method.to_s.strip 81 | raise ArguementError if method.empty? 82 | module_eval(<<-__, __FILE__, __LINE__) 83 | unless public_method_defined?(#{ method.inspect }) 84 | def #{ method } 85 | self 86 | end 87 | true 88 | else 89 | false 90 | end 91 | __ 92 | end 93 | 94 | def add_conversion_method!(method) 95 | if define_conversion_method!(method) 96 | method = method.to_s.strip 97 | raise ArguementError if method.empty? 98 | module_eval(<<-__, __FILE__, __LINE__) 99 | unless conversion_methods.include?(#{ method.inspect }) 100 | conversion_methods.unshift(#{ method.inspect }) 101 | end 102 | __ 103 | true 104 | else 105 | false 106 | end 107 | end 108 | 109 | # iterate over arguments in pairs smartly. 110 | # 111 | def each_pair(*args, &block) 112 | size = args.size 113 | first = args.first 114 | 115 | if block.nil? 116 | result = [] 117 | block = lambda{|*kv| result.push(kv)} 118 | else 119 | result = args 120 | end 121 | 122 | return args if size == 0 123 | 124 | if size == 1 125 | conversion_methods.each do |method| 126 | if first.respond_to?(method) 127 | first = first.send(method) 128 | break 129 | end 130 | end 131 | 132 | if first.respond_to?(:each_pair) 133 | first.each_pair do |key, val| 134 | block.call(key, val) 135 | end 136 | return args 137 | end 138 | 139 | if first.respond_to?(:each_slice) 140 | first.each_slice(2) do |key, val| 141 | block.call(key, val) 142 | end 143 | return args 144 | end 145 | 146 | raise(ArgumentError, 'odd number of arguments for Map') 147 | end 148 | 149 | array_of_pairs = args.all?{|a| a.is_a?(Array) and a.size == 2} 150 | 151 | if array_of_pairs 152 | args.each do |pair| 153 | k, v = pair[0..1] 154 | block.call(k, v) 155 | end 156 | else 157 | 0.step(args.size - 1, 2) do |a| 158 | key = args[a] 159 | val = args[a + 1] 160 | block.call(key, val) 161 | end 162 | end 163 | 164 | args 165 | end 166 | alias_method '[]', 'new' 167 | 168 | def intersection(a, b) 169 | a, b, i = Map.for(a), Map.for(b), Map.new 170 | a.depth_first_each{|key, val| i.set(key, val) if b.has?(key)} 171 | i 172 | end 173 | 174 | def match(haystack, needle) 175 | intersection(haystack, needle) == needle 176 | end 177 | 178 | def args_for_arity(args, arity) 179 | arity = Integer(arity.respond_to?(:arity) ? arity.arity : arity) 180 | arity < 0 ? args.dup : args.slice(0, arity) 181 | end 182 | 183 | def call(object, method, *args, &block) 184 | args = Map.args_for_arity(args, object.method(method).arity) 185 | object.send(method, *args, &block) 186 | end 187 | 188 | def bcall(*args, &block) 189 | args = Map.args_for_arity(args, block.arity) 190 | block.call(*args) 191 | end 192 | end 193 | 194 | # instance constructor 195 | # 196 | def keys 197 | @keys ||= [] 198 | end 199 | 200 | def initialize(*args, &block) 201 | case args.size 202 | when 0 203 | super(&block) 204 | 205 | when 1 206 | first = args.first 207 | case first 208 | when nil, false 209 | nil 210 | when Hash 211 | initialize_from_hash(first) 212 | when Array 213 | initialize_from_array(first) 214 | else 215 | if first.respond_to?(:to_hash) 216 | initialize_from_hash(first.to_hash) 217 | else 218 | initialize_from_hash(first) 219 | end 220 | end 221 | 222 | else 223 | initialize_from_array(args) 224 | end 225 | end 226 | 227 | def initialize_from_hash(hash) 228 | map = self 229 | map.update(hash) 230 | map.default = hash.default 231 | end 232 | 233 | def initialize_from_array(array) 234 | map = self 235 | Map.each_pair(array){|key, val| map[key] = val} 236 | end 237 | 238 | # support methods 239 | # 240 | def klass 241 | self.class 242 | end 243 | 244 | def Map.map_for(hash) 245 | map = klass.coerce(hash) 246 | map.default = hash.default 247 | map 248 | end 249 | def map_for(hash) 250 | klass.map_for(hash) 251 | end 252 | 253 | def Map.convert_key(key) 254 | key.kind_of?(Symbol) ? key.to_s : key 255 | end 256 | 257 | def convert_key(key) 258 | if klass.respond_to?(:convert_key) 259 | klass.convert_key(key) 260 | else 261 | Map.convert_key(key) 262 | end 263 | end 264 | 265 | def self.convert_value(value) 266 | conversion_methods.each do |method| 267 | #return convert_value(value.send(method)) if value.respond_to?(method) 268 | hashlike = value.is_a?(Hash) 269 | if hashlike and value.respond_to?(method) 270 | value = value.send(method) 271 | break 272 | end 273 | end 274 | 275 | case value 276 | when Hash 277 | coerce(value) 278 | when Array 279 | value.map!{|v| convert_value(v)} 280 | else 281 | value 282 | end 283 | end 284 | def convert_value(value) 285 | if klass.respond_to?(:convert_value) 286 | klass.convert_value(value) 287 | else 288 | Map.convert_value(value) 289 | end 290 | end 291 | alias_method('convert_val', 'convert_value') 292 | 293 | def convert(key, val) 294 | [convert_key(key), convert_value(val)] 295 | end 296 | 297 | def copy 298 | default = self.default 299 | self.default = nil 300 | copy = Marshal.load(Marshal.dump(self)) rescue Dup.bind(self).call() 301 | copy.default = default 302 | copy 303 | ensure 304 | self.default = default 305 | end 306 | 307 | Dup = instance_method(:dup) unless defined?(Dup) 308 | 309 | def dup 310 | copy 311 | end 312 | 313 | def clone 314 | copy 315 | end 316 | 317 | def default(key = nil) 318 | key.is_a?(Symbol) && include?(key = key.to_s) ? self[key] : super 319 | end 320 | 321 | def default=(value) 322 | raise ArgumentError.new("Map doesn't work so well with a non-nil default value!") unless value.nil? 323 | end 324 | 325 | # writer/reader methods 326 | # 327 | alias_method '__set__', '[]=' unless method_defined?('__set__') 328 | alias_method '__get__', '[]' unless method_defined?('__get__') 329 | alias_method '__update__', 'update' unless method_defined?('__update__') 330 | 331 | def []=(key, val) 332 | key, val = convert(key, val) 333 | keys.push(key) unless has_key?(key) 334 | __set__(key, val) 335 | end 336 | alias_method 'store', '[]=' 337 | 338 | def [](key) 339 | key = convert_key(key) 340 | __get__(key) 341 | end 342 | 343 | def fetch(key, *args, &block) 344 | key = convert_key(key) 345 | super(key, *args, &block) 346 | end 347 | 348 | def key?(key) 349 | super(convert_key(key)) 350 | end 351 | alias_method 'include?', 'key?' 352 | alias_method 'has_key?', 'key?' 353 | alias_method 'member?', 'key?' 354 | 355 | def update(*args) 356 | Map.each_pair(*args){|key, val| store(key, val)} 357 | self 358 | end 359 | alias_method 'merge!', 'update' 360 | 361 | def merge(*args) 362 | copy.update(*args) 363 | end 364 | alias_method '+', 'merge' 365 | 366 | def reverse_merge(hash) 367 | map = copy 368 | hash.each{|key, val| map[key] = val unless map.key?(key)} 369 | map 370 | end 371 | 372 | def reverse_merge!(hash) 373 | replace(reverse_merge(hash)) 374 | end 375 | 376 | def values 377 | array = [] 378 | keys.each{|key| array.push(self[key])} 379 | array 380 | end 381 | alias_method 'vals', 'values' 382 | 383 | def values_at(*keys) 384 | keys.map{|key| self[key]} 385 | end 386 | 387 | def first 388 | [keys.first, self[keys.first]] 389 | end 390 | 391 | def last 392 | [keys.last, self[keys.last]] 393 | end 394 | 395 | # iterator methods 396 | # 397 | def each_with_index 398 | keys.each_with_index{|key, index| yield([key, self[key]], index)} 399 | self 400 | end 401 | 402 | def each_key 403 | keys.each{|key| yield(key)} 404 | self 405 | end 406 | 407 | def each_value 408 | keys.each{|key| yield self[key]} 409 | self 410 | end 411 | 412 | def each 413 | keys.each{|key| yield(key, self[key])} 414 | self 415 | end 416 | alias_method 'each_pair', 'each' 417 | 418 | # mutators 419 | # 420 | def delete(key) 421 | key = convert_key(key) 422 | keys.delete(key) 423 | super(key) 424 | end 425 | 426 | def clear 427 | keys.clear 428 | super 429 | end 430 | 431 | def delete_if(&block) 432 | to_delete = [] 433 | 434 | each do |key, val| 435 | args = [key, val] 436 | to_delete.push(key) if !!Map.bcall(*args, &block) 437 | end 438 | 439 | to_delete.each{|key| delete(key)} 440 | 441 | self 442 | end 443 | 444 | # See: https://github.com/rubinius/rubinius/blob/98c516820d9f78bd63f29dab7d5ec9bc8692064d/kernel/common/hash19.rb#L476-L484 445 | def keep_if( &block ) 446 | raise RuntimeError.new( "can't modify frozen #{ self.class.name }" ) if frozen? 447 | return to_enum( :keep_if ) unless block_given? 448 | each { | key , val | delete key unless yield( key , val ) } 449 | self 450 | end 451 | 452 | def replace(*args) 453 | clear 454 | update(*args) 455 | end 456 | 457 | # ordered container specific methods 458 | # 459 | def shift 460 | unless empty? 461 | key = keys.first 462 | val = delete(key) 463 | [key, val] 464 | end 465 | end 466 | 467 | def unshift(*args) 468 | Map.each_pair(*args) do |key, val| 469 | key = convert_key(key) 470 | delete(key) 471 | keys.unshift(key) 472 | __set__(key, val) 473 | end 474 | self 475 | end 476 | 477 | def push(*args) 478 | Map.each_pair(*args) do |key, val| 479 | key = convert_key(key) 480 | delete(key) 481 | keys.push(key) 482 | __set__(key, val) 483 | end 484 | self 485 | end 486 | 487 | def pop 488 | unless empty? 489 | key = keys.last 490 | val = delete(key) 491 | [key, val] 492 | end 493 | end 494 | 495 | # equality / sorting / matching support 496 | # 497 | def ==(other) 498 | case other 499 | when Map 500 | return false if keys != other.keys 501 | super(other) 502 | 503 | when Hash 504 | self == Map.from_hash(other, self) 505 | 506 | else 507 | false 508 | end 509 | end 510 | 511 | def <=>(other) 512 | cmp = keys <=> klass.coerce(other).keys 513 | return cmp unless cmp.zero? 514 | values <=> klass.coerce(other).values 515 | end 516 | 517 | def =~(hash) 518 | to_hash == klass.coerce(hash).to_hash 519 | end 520 | 521 | # reordering support 522 | # 523 | def reorder(order = {}) 524 | order = Map.for(order) 525 | map = Map.new 526 | keys = order.depth_first_keys | depth_first_keys 527 | keys.each{|key| map.set(key, get(key))} 528 | map 529 | end 530 | 531 | def reorder!(order = {}) 532 | replace(reorder(order)) 533 | end 534 | 535 | # support for building ordered hasshes from a map's own image 536 | # 537 | def Map.from_hash(hash, order = nil) 538 | map = Map.for(hash) 539 | map.reorder!(order) if order 540 | map 541 | end 542 | 543 | def invert 544 | inverted = klass.allocate 545 | inverted.default = self.default 546 | keys.each{|key| inverted[self[key]] = key } 547 | inverted 548 | end 549 | 550 | def reject(&block) 551 | dup.delete_if(&block) 552 | end 553 | 554 | def reject!(&block) 555 | hash = reject(&block) 556 | self == hash ? nil : hash 557 | end 558 | 559 | def select 560 | array = [] 561 | each{|key, val| array << [key,val] if yield(key, val)} 562 | array 563 | end 564 | 565 | def inspect(*args, &block) 566 | require 'pp' unless defined?(PP) 567 | PP.pp(self, '') 568 | end 569 | 570 | # conversions 571 | # 572 | def conversion_methods 573 | self.class.conversion_methods 574 | end 575 | 576 | conversion_methods.each do |method| 577 | begin 578 | instance_method(:to_map) 579 | rescue NameError 580 | module_eval(<<-__, __FILE__, __LINE__) 581 | def #{ method } 582 | self 583 | end 584 | __ 585 | end 586 | end 587 | 588 | def to_hash 589 | hash = Hash.new(default) 590 | each do |key, val| 591 | val = val.to_hash if val.respond_to?(:to_hash) 592 | hash[key] = val 593 | end 594 | hash 595 | end 596 | 597 | def to_yaml( opts = {} ) 598 | map = self 599 | YAML.quick_emit(self.object_id, opts){|out| 600 | out.map('!omap'){|m| map.each{|k,v| m.add(k, v)}} 601 | } 602 | end 603 | 604 | def to_array 605 | array = [] 606 | each{|*pair| array.push(pair)} 607 | array 608 | end 609 | alias_method 'to_a', 'to_array' 610 | 611 | def to_list 612 | list = [] 613 | each_pair do |key, val| 614 | list[key.to_i] = val if(key.is_a?(Numeric) or key.to_s =~ %r/^\d+$/) 615 | end 616 | list 617 | end 618 | 619 | def to_s 620 | to_array.to_s 621 | end 622 | 623 | # oh rails - would that map.rb existed before all this non-sense... 624 | # 625 | def stringify_keys!; self end 626 | def stringify_keys; dup end 627 | def symbolize_keys!; self end 628 | def symbolize_keys; dup end 629 | def to_options!; self end 630 | def to_options; dup end 631 | def with_indifferent_access!; self end 632 | def with_indifferent_access; dup end 633 | 634 | # a sane method missing that only supports writing values or reading 635 | # *previously set* values 636 | # 637 | def method_missing(*args, &block) 638 | method = args.first.to_s 639 | case method 640 | when /=$/ 641 | key = args.shift.to_s.chomp('=') 642 | value = args.shift 643 | self[key] = value 644 | when /\?$/ 645 | key = args.shift.to_s.chomp('?') 646 | self.has?( key ) 647 | else 648 | key = method 649 | unless has_key?(key) 650 | return(block ? fetch(key, &block) : super(*args)) 651 | end 652 | self[key] 653 | end 654 | end 655 | 656 | def respond_to?(method, *args, &block) 657 | has_key = has_key?(method) 658 | setter = method.to_s =~ /=\Z/o 659 | !!((!has_key and setter) or has_key or super) 660 | end 661 | 662 | def id 663 | return self[:id] if has_key?(:id) 664 | return self[:_id] if has_key?(:_id) 665 | raise NoMethodError 666 | end 667 | 668 | # support for compound key indexing and depth first iteration 669 | # 670 | def get(*keys) 671 | keys = key_for(keys) 672 | 673 | if keys.size <= 1 674 | if !self.has_key?(keys.first) && block_given? 675 | return yield 676 | else 677 | return self[keys.first] 678 | end 679 | end 680 | 681 | keys, key = keys[0..-2], keys[-1] 682 | collection = self 683 | 684 | keys.each do |k| 685 | if Map.collection_has?(collection, k) 686 | collection = Map.collection_key(collection, k) 687 | else 688 | collection = nil 689 | end 690 | 691 | unless collection.respond_to?('[]') 692 | leaf = collection 693 | return leaf 694 | end 695 | end 696 | 697 | if !Map.collection_has?(collection, key) && block_given? 698 | yield #default_value 699 | else 700 | Map.collection_key(collection, key) 701 | end 702 | end 703 | 704 | def has?(*keys) 705 | keys = key_for(keys) 706 | collection = self 707 | 708 | return Map.collection_has?(collection, keys.first) if keys.size <= 1 709 | 710 | keys, key = keys[0..-2], keys[-1] 711 | 712 | keys.each do |k| 713 | if Map.collection_has?(collection, k) 714 | collection = Map.collection_key(collection, k) 715 | else 716 | collection = nil 717 | end 718 | 719 | return collection unless collection.respond_to?('[]') 720 | end 721 | 722 | return false unless(collection.is_a?(Hash) or collection.is_a?(Array)) 723 | 724 | Map.collection_has?(collection, key) 725 | end 726 | 727 | def Map.blank?(value) 728 | return value.blank? if value.respond_to?(:blank?) 729 | 730 | case value 731 | when String 732 | value.strip.empty? 733 | when Numeric 734 | value == 0 735 | when false 736 | true 737 | else 738 | value.respond_to?(:empty?) ? value.empty? : !value 739 | end 740 | end 741 | 742 | def blank?(*keys) 743 | return empty? if keys.empty? 744 | !has?(*keys) or Map.blank?(get(*keys)) 745 | end 746 | 747 | def Map.collection_key(collection, key, &block) 748 | case collection 749 | when Array 750 | begin 751 | key = Integer(key) 752 | rescue 753 | raise(IndexError, "(#{ collection.inspect })[#{ key.inspect }]") 754 | end 755 | collection[key] 756 | 757 | when Hash 758 | collection[key] 759 | 760 | else 761 | raise(IndexError, "(#{ collection.inspect })[#{ key.inspect }]") 762 | end 763 | end 764 | 765 | def collection_key(*args, &block) 766 | Map.collection_key(*args, &block) 767 | end 768 | 769 | def Map.collection_has?(collection, key, &block) 770 | has_key = 771 | case collection 772 | when Array 773 | key = (Integer(key) rescue -1) 774 | (0...collection.size).include?(key) 775 | 776 | when Hash 777 | collection.has_key?(key) 778 | 779 | else 780 | raise(IndexError, "(#{ collection.inspect })[#{ key.inspect }]") 781 | end 782 | 783 | block.call(key) if(has_key and block) 784 | 785 | has_key 786 | end 787 | 788 | def collection_has?(*args, &block) 789 | Map.collection_has?(*args, &block) 790 | end 791 | 792 | def Map.collection_set(collection, key, value, &block) 793 | set_key = false 794 | 795 | case collection 796 | when Array 797 | begin 798 | key = Integer(key) 799 | rescue 800 | raise(IndexError, "(#{ collection.inspect })[#{ key.inspect }]=#{ value.inspect }") 801 | end 802 | set_key = true 803 | collection[key] = value 804 | 805 | when Hash 806 | set_key = true 807 | collection[key] = value 808 | 809 | else 810 | raise(IndexError, "(#{ collection.inspect })[#{ key.inspect }]=#{ value.inspect }") 811 | end 812 | 813 | block.call(key) if(set_key and block) 814 | 815 | [key, value] 816 | end 817 | 818 | def collection_set(*args, &block) 819 | Map.collection_set(*args, &block) 820 | end 821 | 822 | def set(*args) 823 | case 824 | when args.empty? 825 | return [] 826 | when args.size == 1 && args.first.is_a?(Hash) 827 | hash = args.shift 828 | else 829 | hash = {} 830 | value = args.pop 831 | key = Array(args).flatten 832 | hash[key] = value 833 | end 834 | 835 | strategy = hash.map{|skey, svalue| [Array(skey), svalue]} 836 | 837 | strategy.each do |skey, svalue| 838 | leaf_for(skey, :autovivify => true) do |leaf, k| 839 | Map.collection_set(leaf, k, svalue) 840 | end 841 | end 842 | 843 | self 844 | end 845 | 846 | def add(*args) 847 | case 848 | when args.empty? 849 | return [] 850 | when args.size == 1 && args.first.is_a?(Hash) 851 | hash = args.shift 852 | else 853 | hash = {} 854 | value = args.pop 855 | key = Array(args).flatten 856 | hash[key] = value 857 | end 858 | 859 | exploded = Map.explode(hash) 860 | 861 | exploded[:branches].each do |bkey, btype| 862 | set(bkey, btype.new) unless get(bkey).is_a?(btype) 863 | end 864 | 865 | exploded[:leaves].each do |lkey, lvalue| 866 | set(lkey, lvalue) 867 | end 868 | 869 | self 870 | end 871 | 872 | def Map.explode(hash) 873 | accum = {:branches => [], :leaves => []} 874 | 875 | hash.each do |key, value| 876 | Map._explode(key, value, accum) 877 | end 878 | 879 | branches = accum[:branches] 880 | leaves = accum[:leaves] 881 | 882 | sort_by_key_size = proc{|a,b| a.first.size <=> b.first.size} 883 | 884 | branches.sort!(&sort_by_key_size) 885 | leaves.sort!(&sort_by_key_size) 886 | 887 | accum 888 | end 889 | 890 | def Map._explode(key, value, accum = {:branches => [], :leaves => []}) 891 | key = Array(key).flatten 892 | 893 | case value 894 | when Array 895 | accum[:branches].push([key, Array]) 896 | 897 | value.each_with_index do |v, k| 898 | Map._explode(key + [k], v, accum) 899 | end 900 | 901 | when Hash 902 | accum[:branches].push([key, Map]) 903 | 904 | value.each do |k, v| 905 | Map._explode(key + [k], v, accum) 906 | end 907 | 908 | else 909 | accum[:leaves].push([key, value]) 910 | end 911 | 912 | accum 913 | end 914 | 915 | def Map.add(*args) 916 | args.flatten! 917 | args.compact! 918 | 919 | Map.for(args.shift).tap do |map| 920 | args.each{|arg| map.add(arg)} 921 | end 922 | end 923 | 924 | def Map.combine(*args) 925 | Map.add(*args) 926 | end 927 | 928 | def combine!(*args, &block) 929 | add(*args, &block) 930 | end 931 | 932 | def combine(*args, &block) 933 | dup.tap do |map| 934 | map.combine!(*args, &block) 935 | end 936 | end 937 | 938 | def leaf_for(key, options = {}, &block) 939 | leaf = self 940 | key = Array(key).flatten 941 | k = key.first 942 | 943 | key.each_cons(2) do |a, b| 944 | exists = Map.collection_has?(leaf, a) 945 | 946 | case b 947 | when Numeric 948 | if options[:autovivify] 949 | Map.collection_set(leaf, a, Array.new) unless exists 950 | end 951 | 952 | when String, Symbol 953 | if options[:autovivify] 954 | Map.collection_set(leaf, a, Map.new) unless exists 955 | end 956 | end 957 | 958 | leaf = Map.collection_key(leaf, a) 959 | k = b 960 | end 961 | 962 | block ? block.call(leaf, k) : [leaf, k] 963 | end 964 | 965 | def rm(*args) 966 | paths, path = args.partition{|arg| arg.is_a?(Array)} 967 | paths.push(path) 968 | 969 | paths.each do |p| 970 | if p.size == 1 971 | delete(*p) 972 | next 973 | end 974 | 975 | branch, leaf = p[0..-2], p[-1] 976 | collection = get(branch) 977 | 978 | case collection 979 | when Hash 980 | key = leaf 981 | collection.delete(key) 982 | when Array 983 | index = leaf 984 | collection.delete_at(index) 985 | else 986 | raise(IndexError, "(#{ collection.inspect }).rm(#{ p.inspect })") 987 | end 988 | end 989 | paths 990 | end 991 | 992 | def forcing(forcing=nil, &block) 993 | @forcing ||= nil 994 | 995 | if block 996 | begin 997 | previous = @forcing 998 | @forcing = forcing 999 | block.call() 1000 | ensure 1001 | @forcing = previous 1002 | end 1003 | else 1004 | @forcing 1005 | end 1006 | end 1007 | 1008 | def forcing?(forcing=nil) 1009 | @forcing ||= nil 1010 | @forcing == forcing 1011 | end 1012 | 1013 | def apply(other) 1014 | Map.for(other).depth_first_each do |keys, value| 1015 | set(keys => value) unless !get(keys).nil? 1016 | end 1017 | self 1018 | end 1019 | 1020 | def Map.alphanumeric_key_for(key) 1021 | return key if key.is_a?(Numeric) 1022 | 1023 | digity, stringy, digits = %r/^(~)?(\d+)$/iomx.match(key).to_a 1024 | 1025 | digity ? stringy ? String(digits) : Integer(digits) : key 1026 | end 1027 | 1028 | def alphanumeric_key_for(key) 1029 | Map.alphanumeric_key_for(key) 1030 | end 1031 | 1032 | def self.key_for(*keys) 1033 | return keys.flatten 1034 | end 1035 | 1036 | def key_for(*keys) 1037 | self.class.key_for(*keys) 1038 | end 1039 | 1040 | ## TODO - technically this returns only leaves so the name isn't *quite* right. re-factor for 3.0 1041 | # 1042 | def Map.depth_first_each(enumerable, path = [], accum = [], &block) 1043 | Map.pairs_for(enumerable) do |key, val| 1044 | path.push(key) 1045 | if((val.is_a?(Hash) or val.is_a?(Array)) and not val.empty?) 1046 | Map.depth_first_each(val, path, accum) 1047 | else 1048 | accum << [path.dup, val] 1049 | end 1050 | path.pop() 1051 | end 1052 | if block 1053 | accum.each{|keys, val| block.call(keys, val)} 1054 | else 1055 | accum 1056 | end 1057 | end 1058 | 1059 | def Map.depth_first_keys(enumerable, path = [], accum = [], &block) 1060 | accum = Map.depth_first_each(enumerable, path = [], accum = [], &block) 1061 | accum.map!{|kv| kv.first} 1062 | accum 1063 | end 1064 | 1065 | def Map.depth_first_values(enumerable, path = [], accum = [], &block) 1066 | accum = Map.depth_first_each(enumerable, path = [], accum = [], &block) 1067 | accum.map!{|kv| kv.last} 1068 | accum 1069 | end 1070 | 1071 | def Map.pairs_for(enumerable, *args, &block) 1072 | if block.nil? 1073 | pairs, block = [], lambda{|*pair| pairs.push(pair)} 1074 | else 1075 | pairs = false 1076 | end 1077 | 1078 | result = 1079 | case enumerable 1080 | when Hash 1081 | enumerable.each_pair(*args, &block) 1082 | when Array 1083 | enumerable.each_with_index(*args) do |val, key| 1084 | block.call(key, val) 1085 | end 1086 | else 1087 | enumerable.each_pair(*args, &block) 1088 | end 1089 | 1090 | pairs ? pairs : result 1091 | end 1092 | 1093 | def Map.breadth_first_each(enumerable, accum = [], &block) 1094 | levels = [] 1095 | 1096 | keys = Map.depth_first_keys(enumerable) 1097 | 1098 | keys.each do |key| 1099 | key.size.times do |i| 1100 | k = key.slice(0, i + 1) 1101 | level = k.size - 1 1102 | levels[level] ||= Array.new 1103 | last = levels[level].last 1104 | levels[level].push(k) unless last == k 1105 | end 1106 | end 1107 | 1108 | levels.each do |level| 1109 | level.each do |key| 1110 | val = enumerable.get(key) 1111 | block ? block.call(key, val) : accum.push([key, val]) 1112 | end 1113 | end 1114 | 1115 | block ? enumerable : accum 1116 | end 1117 | 1118 | def Map.keys_for(enumerable) 1119 | keys = enumerable.respond_to?(:keys) ? enumerable.keys : Array.new(enumerable.size){|i| i} 1120 | keys 1121 | end 1122 | 1123 | def depth_first_each(*args, &block) 1124 | Map.depth_first_each(self, *args, &block) 1125 | end 1126 | 1127 | def depth_first_keys(*args, &block) 1128 | Map.depth_first_keys(self, *args, &block) 1129 | end 1130 | 1131 | def depth_first_values(*args, &block) 1132 | Map.depth_first_values(self, *args, &block) 1133 | end 1134 | 1135 | def breadth_first_each(*args, &block) 1136 | Map.breadth_first_each(self, *args, &block) 1137 | end 1138 | 1139 | def contains(other) 1140 | other = other.is_a?(Hash) ? Map.coerce(other) : other 1141 | breadth_first_each{|key, value| return true if value == other} 1142 | return false 1143 | end 1144 | alias_method 'contains?', 'contains' 1145 | 1146 | ## for rails' extract_options! compat 1147 | # 1148 | def extractable_options? 1149 | true 1150 | end 1151 | 1152 | ## for mongoid type system support 1153 | # 1154 | def serialize(object) 1155 | ::Map.for(object) 1156 | end 1157 | 1158 | def deserialize(object) 1159 | ::Map.for(object) 1160 | end 1161 | 1162 | def Map.demongoize(object) 1163 | Map.for(object) 1164 | end 1165 | 1166 | def Map.evolve(object) 1167 | Map.for(object) 1168 | end 1169 | 1170 | def mongoize 1171 | self 1172 | end 1173 | end 1174 | 1175 | module Kernel 1176 | private 1177 | def Map(*args, &block) 1178 | Map.new(*args, &block) 1179 | end 1180 | end 1181 | 1182 | Map.load('struct.rb') 1183 | Map.load('options.rb') 1184 | Map.load('params.rb') 1185 | --------------------------------------------------------------------------------