├── var ├── name ├── title ├── version ├── created ├── organization ├── summary ├── authors ├── copyrights ├── repositories ├── requirements ├── description └── resources ├── pkg └── .gitignore ├── Gemfile ├── demo ├── applique │ └── env.rb └── 01_ostruct.md ├── .gitignore ├── .travis.yml ├── lib ├── ostruct2 │ └── ostruct.rb └── ostruct2.rb ├── Rakefile ├── MANIFEST ├── etc └── qed.rb ├── work ├── deprecated │ ├── Assembly │ └── Rulefile ├── benchmarks │ └── vs_original.rb ├── consider │ ├── 01_ostructable.rdoc │ └── ostructable.rb └── reference │ ├── basic_cascade.rb │ └── basic_struct.rb ├── README.rdoc ├── Assembly.rb ├── HISTORY.rdoc ├── LICENSE.txt ├── .index └── .gemspec /var/name: -------------------------------------------------------------------------------- 1 | ostruct2 2 | -------------------------------------------------------------------------------- /var/title: -------------------------------------------------------------------------------- 1 | OStruct2 2 | -------------------------------------------------------------------------------- /var/version: -------------------------------------------------------------------------------- 1 | 0.2.1 2 | -------------------------------------------------------------------------------- /pkg/.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | -------------------------------------------------------------------------------- /var/created: -------------------------------------------------------------------------------- 1 | 2010-04-21 2 | -------------------------------------------------------------------------------- /var/organization: -------------------------------------------------------------------------------- 1 | Rubyworks 2 | -------------------------------------------------------------------------------- /var/summary: -------------------------------------------------------------------------------- 1 | A Better OpenStruct 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /var/authors: -------------------------------------------------------------------------------- 1 | --- 2 | - Trans 3 | -------------------------------------------------------------------------------- /var/copyrights: -------------------------------------------------------------------------------- 1 | --- 2 | - (c) 2010 Rubyworks (BSD-2-Clause) 3 | -------------------------------------------------------------------------------- /demo/applique/env.rb: -------------------------------------------------------------------------------- 1 | require 'ae' 2 | require 'ostruct2/ostruct' 3 | -------------------------------------------------------------------------------- /var/repositories: -------------------------------------------------------------------------------- 1 | --- 2 | upstream: git://github.com/rubyworks/ostruct2.git 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .fire/digest 2 | .yardoc 3 | doc 4 | log 5 | tmp 6 | site 7 | web 8 | DEMO.md 9 | -------------------------------------------------------------------------------- /var/requirements: -------------------------------------------------------------------------------- 1 | --- 2 | - qed (test) 3 | - ae (test) 4 | - rake (build) 5 | 6 | #- detroit (build) 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | script: "bundle exec qed" 3 | rvm: 4 | - 1.9.3 5 | - 2.0.0 6 | - 2.1.0 7 | - rbx 8 | - jruby 9 | 10 | -------------------------------------------------------------------------------- /lib/ostruct2/ostruct.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct2' 2 | 3 | class OpenStruct < OpenStruct2 4 | def __class__ 5 | OpenStruct 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | desc 'run unit tests' 4 | task 'test' do 5 | sh 'qed' 6 | end 7 | 8 | task :prep do 9 | sh 'mast -u' 10 | sh 'index -u var' 11 | end 12 | -------------------------------------------------------------------------------- /var/description: -------------------------------------------------------------------------------- 1 | OStruct2 is a reimplementation of Ruby's standard ostruct.rb library. 2 | This new OpenStruct class addresses issues the original has with conflicting 3 | member names and cloning. 4 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | #!mast .index .ruby .yardopts bin info data lib man spec test *.rdoc *.md *.txt 2 | .index 3 | lib/ostruct2/ostruct.rb 4 | lib/ostruct2.rb 5 | README.rdoc 6 | HISTORY.rdoc 7 | DEMO.md 8 | LICENSE.txt 9 | -------------------------------------------------------------------------------- /var/resources: -------------------------------------------------------------------------------- 1 | --- 2 | home: http://rubyworks.github.com/ostruct2 3 | docs: http://rubydoc.info/gems/ostruct2/frames 4 | code: http://github.com/rubyworks/ostruct2 5 | mail: http://groups.google.com/group/rubyworks-mailinglist 6 | chat: http://chat.us.freenode.net/rubyworks 7 | -------------------------------------------------------------------------------- /etc/qed.rb: -------------------------------------------------------------------------------- 1 | # 2 | # QED test coverage report using SimpleCov. 3 | # 4 | # Use `$properties.coverage_folder` to set directory in which to store 5 | # coverage report this defaults to `log/coverage`. 6 | # 7 | QED.configure 'coverage' do 8 | dir = $properties.coverage_folder 9 | require 'simplecov' 10 | SimpleCov.command_name 'QED' 11 | SimpleCov.start do 12 | coverage_dir(dir || 'log/coverage') 13 | #add_group "Label", "lib/qed/directory" 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /work/deprecated/Assembly: -------------------------------------------------------------------------------- 1 | --- 2 | github: 3 | gh_pages: web 4 | 5 | gem: 6 | active: true 7 | 8 | dnote: 9 | title: Source Notes 10 | labels: ~ 11 | output: 12 | log/dnote.html 13 | 14 | yard: 15 | yardopts: true 16 | priority: -1 17 | 18 | qedoc: 19 | files : demo/ 20 | output: DEMO.md 21 | title : OStruct2 22 | 23 | email: 24 | service : Email 25 | file : ~ 26 | subject : ~ 27 | mailto : 28 | - ruby-talk@ruby-lang.org 29 | - rubyworks-mailinglist@googlegroups.com 30 | 31 | locat: 32 | output: log/locat.html 33 | 34 | #vclog: 35 | # active: false 36 | # output: 37 | # - log/history.html 38 | # - log/changes.html 39 | 40 | 41 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = OpenStruct 2 2 | 3 | {Website}[http://rubyworks.github.com/ostruct2] / 4 | {Report Issue}[http://github.com/rubyworks/ostruct2/issues] / 5 | {Source Code}[http://github.com/rubyworks/ostruct2] 6 | 7 | {}[http://travis-ci.org/rubyworks/ostruct2] 8 | 9 | 10 | == Description 11 | 12 | OStruct2 is a reimplementation of Ruby's standard ostruct.rb library. 13 | This new OpenStruct class addresses issues the original has with conflicting 14 | member names and cloning. 15 | 16 | 17 | == Copyrights 18 | 19 | Copyright (c) 2010 Rubyworks 20 | 21 | OStruct2 is distributed under the *BSD-2-Clause* license. 22 | 23 | See LICENSE.txt for detatils. 24 | 25 | -------------------------------------------------------------------------------- /Assembly.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Detroit assembly. 4 | 5 | service :gem do |s| 6 | s.gemspec = '.gemspec' 7 | end 8 | 9 | service :github do |s| 10 | s.folder = 'web' 11 | end 12 | 13 | service :dnote do |s| 14 | s.title = 'Source Notes' 15 | s.output = 'log/notes.html' 16 | end 17 | 18 | service :locat do |s| 19 | s.output = 'log/locat.html' 20 | end 21 | 22 | service :qedoc do |s| 23 | s.files = "demo/" 24 | s.output = "DEMO.md" 25 | s.title = "OStruct2" 26 | end 27 | 28 | service :vclog do |s| 29 | s.output = ['log/history.html', 30 | 'log/changes.html'] 31 | end 32 | 33 | service :email do |s| 34 | s.mailto = ['ruby-talk@ruby-lang.org', 35 | 'rubyworks-mailinglist@googlegroups.com'] 36 | end 37 | 38 | service :yard do |s| 39 | s.yardopts = true 40 | s.priority = -1 41 | end 42 | 43 | -------------------------------------------------------------------------------- /work/deprecated/Rulefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # TODO: Do things in ruby where possible instead of shelling out! 4 | 5 | ignore 'doc', 'log', 'pkg', 'tmp', 'site', 'web', 'work' 6 | 7 | # TODO: Eventually Fire should provide access to metadata via a utility method. 8 | version = File.read('var/version').strip 9 | 10 | #desc "update manifest" 11 | #task 'manifest' do 12 | # system 'mast -u' 13 | #end 14 | 15 | #desc "release and tag" 16 | #task 'release' do 17 | # exit -1 unless system('detroit release') 18 | # system 'vclog-news | git tag -a -F - #{version}' 19 | #end 20 | 21 | file 'var' do 22 | system 'index -u var' 23 | end 24 | 25 | file 'lib' do 26 | #exit -1 unless system('detroit test') 27 | exit -1 unless system('qed') 28 | end 29 | 30 | file 'demo' do 31 | #exit -1 unless system('detroit test') 32 | exit -1 unless system('qed') 33 | end 34 | 35 | -------------------------------------------------------------------------------- /HISTORY.rdoc: -------------------------------------------------------------------------------- 1 | = RELEASE HISTORY 2 | 3 | == 0.2.1 | 2013-11-21 4 | 5 | Primarily this release cleans up the project configuration a bit 6 | and adds a missing `LICENSE.txt` file. But it also adjusts the `#to_enum` 7 | method to avoid Ruby 2.0's deprecation of `Enumerator.new(object, method)`. 8 | I don't know why Matz thought it a good thing to do. It forced us to use 9 | `Kernel.instance_method(:enum_for).bind(...).call(...)` in its place. Fugly! 10 | 11 | Changes: 12 | 13 | * Adjust to_enum to not use Enumerator.new. 14 | * Add missing LICENSE file. 15 | 16 | 17 | == 0.2.0 | 2012-05-23 18 | 19 | This release brings the new OpenStruct to a production ready state. 20 | 21 | Changes: 22 | 23 | * Constructors cascade and auto/renew are now slightly different. 24 | * Added nest/nested constructor for nests OpenStructs. (Cool!) 25 | * Boost performace via on-demand creation of singleton methods. 26 | * Add missing equality methods, hash method and dup/clone methods. 27 | * Rename main class to OpenStruct2 to avoid conflicts with original. 28 | * Require `ostruct2/ostruct` to get drop-in replacement. 29 | 30 | 31 | == 0.1.0 | 2011-05-20 32 | 33 | This is the initial relase of OpenSturct2. 34 | 35 | Changes: 36 | 37 | * First release. 38 | 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | (BSD-2-Clause License) 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 16 | COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 17 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 18 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 19 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 20 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 22 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /work/benchmarks/vs_original.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | require 'ostruct' 3 | require 'ostruct2' 4 | 5 | class OpenStructBenchmarks 6 | 7 | def initialize(num) 8 | @num = num 9 | end 10 | 11 | attr :num 12 | 13 | def bench_new(klass, data) 14 | num.times do 15 | klass.new(data) 16 | end 17 | end 18 | 19 | def bench_read(klass, data) 20 | o = klass.new(data) 21 | num.times do 22 | o.__send__(:table).keys.each do |k| 23 | eval "o.#{k}" 24 | end 25 | end 26 | end 27 | 28 | def bench_write(klass) 29 | o = klass.new 30 | num.times do 31 | (:a..:z).each do |k| 32 | eval "o.#{k}='foo'" 33 | end 34 | end 35 | end 36 | 37 | end 38 | 39 | bm = OpenStructBenchmarks.new(20000) 40 | 41 | Benchmark.bmbm do |x| 42 | puts "NEW" 43 | data = {:a=>1, :b=>2, :c=>{:x=>9}} 44 | x.report("ostruct") { bm.bench_new(OpenStruct, data) } 45 | x.report("ostruct2") { bm.bench_new(OpenStruct2, data) } 46 | end 47 | 48 | puts 49 | 50 | Benchmark.bmbm do |x| 51 | puts "READ" 52 | data = {:a=>1, :b=>2, :c=>{:x=>9}} 53 | x.report("ostruct") { bm.bench_read(OpenStruct, data) } 54 | x.report("ostruct2") { bm.bench_read(OpenStruct2, data) } 55 | end 56 | 57 | puts 58 | 59 | Benchmark.bmbm do |x| 60 | puts "WRITE" 61 | x.report("ostruct") { bm.bench_write(OpenStruct) } 62 | x.report("ostruct2") { bm.bench_write(OpenStruct2) } 63 | end 64 | 65 | -------------------------------------------------------------------------------- /.index: -------------------------------------------------------------------------------- 1 | --- 2 | revision: 2013 3 | type: ruby 4 | sources: 5 | - var 6 | authors: 7 | - name: Trans 8 | email: transfire@gmail.com 9 | organizations: [] 10 | requirements: 11 | - groups: 12 | - test 13 | development: true 14 | name: qed 15 | - groups: 16 | - test 17 | development: true 18 | name: ae 19 | - groups: 20 | - build 21 | development: true 22 | name: rake 23 | conflicts: [] 24 | alternatives: [] 25 | resources: 26 | - type: home 27 | uri: http://rubyworks.github.com/ostruct2 28 | label: Homepage 29 | - type: docs 30 | uri: http://rubydoc.info/gems/ostruct2/frames 31 | label: Documentation 32 | - type: code 33 | uri: http://github.com/rubyworks/ostruct2 34 | label: Source Code 35 | - type: mail 36 | uri: http://groups.google.com/group/rubyworks-mailinglist 37 | label: Mailing List 38 | - type: chat 39 | uri: http://chat.us.freenode.net/rubyworks 40 | label: IRC Channel 41 | repositories: 42 | - name: upstream 43 | scm: git 44 | uri: git://github.com/rubyworks/ostruct2.git 45 | categories: [] 46 | copyrights: 47 | - holder: Rubyworks 48 | year: '2010' 49 | license: BSD-2-Clause 50 | customs: [] 51 | paths: 52 | lib: 53 | - lib 54 | name: ostruct2 55 | title: OStruct2 56 | summary: A Better OpenStruct 57 | created: '2010-04-21' 58 | description: |- 59 | OStruct2 is a reimplementation of Ruby's standard ostruct.rb library. 60 | This new OpenStruct class addresses issues the original has with conflicting 61 | member names and cloning. 62 | version: 0.2.1 63 | date: '2013-11-21' 64 | -------------------------------------------------------------------------------- /work/consider/01_ostructable.rdoc: -------------------------------------------------------------------------------- 1 | = OpenStructable 2 | 3 | OpensStructable is a mixin module which can provide OpenStruct behavior to 4 | any class or object. OpenStructable allows extention of data objects 5 | with arbitrary attributes. 6 | 7 | require 'hashery/ostructable' 8 | 9 | class Record 10 | include OpenStructable 11 | end 12 | 13 | Now let's create a new record and see if we can assign values to open 14 | properties. 15 | 16 | record = Record.new 17 | record.name = "John Smith" 18 | record.age = 70 19 | record.pension = 300 20 | 21 | We can see that the values were set. 22 | 23 | record.name.assert == "John Smith" 24 | record.age.assert == 70 25 | record.pension.assert == 300 26 | 27 | If we havent' assigned a value to a property it should just return +nil+. 28 | 29 | record.address.assert == nil 30 | 31 | OpenStructable is also smart enough to adjust itself to work with a subclass 32 | of a Hash. 33 | 34 | class HashRecord < Hash 35 | include OpenStructable 36 | end 37 | 38 | We can apply similar settings as above. 39 | 40 | record = HashRecord.new 41 | record.name = "John Doe" 42 | record.age = 40 43 | record.pension = 200 44 | 45 | We can see that the values were set. 46 | 47 | record.name.assert == "John Doe" 48 | record.age.assert == 40 49 | record.pension.assert == 200 50 | 51 | The differnce here is that the data is accessible via the normal Hash 52 | methods too. 53 | 54 | record.assert == {:name=>"John Doe", :age=>40, :pension=>200} 55 | 56 | Notice that entries are converted to Symbols, not Strings. 57 | -------------------------------------------------------------------------------- /work/reference/basic_cascade.rb: -------------------------------------------------------------------------------- 1 | require 'hashery/basic_struct' 2 | 3 | # BasicCascade is subclass of BasicStruct. It differs in a few 4 | # significant ways. 5 | # 6 | # The main reason this class is labeled "cascade", every internal 7 | # Hash is transformed into an BasicCascade dynamically upon access. 8 | # This makes it easy to create "cascading" references. 9 | # 10 | # h = { :x => { :y => { :z => 1 } } } 11 | # c = BasicCascade[h] 12 | # c.x.y.z #=> 1 13 | # 14 | # As soon as you access a node it automatically becomes an BasicCascade. 15 | # 16 | # c = BasicCascade.new #=> # 17 | # c.r #=> # 18 | # c.a.b #=> # 19 | # 20 | # But if you set a node, then that will be it's value. 21 | # 22 | # c.a.b = 4 #=> 4 23 | # 24 | # To query a node without causing the auto-creation of an OpenCasade 25 | # object, use the ?-mark. 26 | # 27 | # c.a.z? #=> nil 28 | # 29 | # BasicCascade also transforms Hashes within Arrays. 30 | # 31 | # h = { :x=>[ {:a=>1}, {:a=>2} ], :y=>1 } 32 | # c = BasicCascade[h] 33 | # c.x.first.a.assert == 1 34 | # c.x.last.a.assert == 2 35 | # 36 | # Finally, you can set a node and get the reciever back using 37 | # the !-mark. 38 | # 39 | # c = BasicCascade.new #=> # 40 | # c.x!(4).y!(3) #=> #4, :y=>3}> 41 | # 42 | class BasicCascade < BasicStruct 43 | 44 | # 45 | def method_missing(sym, *args, &blk) 46 | type = sym.to_s[-1,1] 47 | name = sym.to_s.gsub(/[=!?]$/, '').to_sym 48 | case type 49 | when '=' 50 | self[name] = args.first 51 | when '!' 52 | #@hash.__send__(name, *args, &blk) 53 | __send__(name, *args, &blk) 54 | when '?' 55 | self[name] 56 | else 57 | if key?(name) 58 | self[name] = transform_entry(self[name]) 59 | else 60 | self[name] = ::BasicCascade.new # TODO: can't get `self.class` ? 61 | end 62 | end 63 | end 64 | 65 | def each 66 | super do |key, entry| 67 | yield([key, transform_entry(entry)]) 68 | end 69 | end 70 | 71 | private 72 | 73 | # 74 | def transform_entry(entry) 75 | case entry 76 | when ::Hash 77 | ::BasicCascade.new(entry) #self.class.new(entry) 78 | when ::Array 79 | entry.map{ |e| transform_entry(e) } 80 | else 81 | entry 82 | end 83 | end 84 | 85 | end 86 | 87 | #-- 88 | # Last, when an entry is not found, 'null' is returned rather then 'nil'. 89 | # This allows for run-on entries withuot error. Eg. 90 | # 91 | # o = BasicCascade.new 92 | # o.a.b.c #=> null 93 | # 94 | # Unfortuately this requires an explict test for null? in 'if' conditions. 95 | # 96 | # if o.a.b.c.null? # true if null 97 | # if o.a.b.c.nil? # true if nil or null 98 | # if o.a.b.c.not? # true if nil or null or false 99 | # 100 | # So be sure to take that into account. 101 | #++ 102 | 103 | -------------------------------------------------------------------------------- /demo/01_ostruct.md: -------------------------------------------------------------------------------- 1 | # OpenStruct 2 | 3 | The constructor can take a priming hash. 4 | 5 | o = OpenStruct.new(:a=>1,:b=>2) 6 | o.a #=> 1 7 | o.b #=> 2 8 | 9 | It can also take a default procedure, just like an Hash. 10 | 11 | o = OpenStruct.new{ |h,k| h[k] = {} } 12 | o.a #=> {} 13 | 14 | Common usage of an OpenStruct is via the missing method dynamic calls. 15 | An entry can be made by making an assignment and read back via the 16 | same method call. 17 | 18 | o = OpenStruct.new 19 | o.a = 1 20 | o.a #=> 1 21 | 22 | Key existence can also be checked by adding a question mark. 23 | 24 | o.a? #=> true 25 | 26 | OpenStruct "circa 2" has a CRUDified design. There are only a few primary 27 | methods that handle access to the underlying table and all other methods 28 | route through these. Primarily they are `#read!`, `#store!`, `#delete!` 29 | as well as `#key?` and `#keys!`. 30 | 31 | o = OpenStruct.new 32 | o.store!(:a, 1) 33 | o.read!(:a) #=> 1 34 | o.key?(:a) #=> true 35 | o.keys! #=> [:a] 36 | o.delete!(:a) 37 | o.empty? #=> true 38 | 39 | OpenStruct offers a number of methods to access the underlying table. 40 | Each of these ends in a exlimation mark, and include `#fetch!`, `#update!`, 41 | and `merge!`. 42 | 43 | o = OpenStruct.new 44 | o.update!(:a=>1, :b=>2) 45 | o.fetch!(:a) #=> 1 46 | 47 | Note that `#merge!` is akin to `Hash#merge`, not `Hash#merge!` --it does not 48 | act in-place. 49 | 50 | o = OpenStruct.new 51 | x = o.merge!(:a=>1, :b=>2) 52 | o.a #=> nil 53 | x.a #=> 1 54 | 55 | OpenStruct also supports Hash-like read and write operators, #[] and #[]=. 56 | 57 | o = OpenStruct.new 58 | o[:a] = 1 59 | o[:a] #=> 1 60 | o.a #=> 1 61 | 62 | The OpenStruct object can be converted to a simple Hash, via `#to_h`. 63 | 64 | o = OpenStruct.new(:a=>1,:b=>2) 65 | o.to_h #=> {:a=>1, :b=>2} 66 | 67 | Iteration can be achieved with `#each!`. 68 | 69 | o = OpenStruct.new(:a=>1,:b=>2) 70 | a = {} 71 | o.each! do |k,v| 72 | a[k] = v 73 | end 74 | a #=> {:a=>1, :b=>2} 75 | 76 | Currently all Enumerable methods will work if suffixed with an exclemation mark. But they operate 77 | directly on the underlying Hash rather than at the level of the "CRUDified" OpenStruct itself. 78 | 79 | o = OpenStruct.new(:a=>1,:b=>2) 80 | a = o.map!{ |k,v| [k,v] } 81 | a #=> [[:a,1], [:b,2]] 82 | 83 | In most cases this will work fine. But it may cause some minor discrepencies in how Enumerable 84 | methods work presently and how ultimatley they should work. A fix is a bit tricky so this is an 85 | endeavor left a future release. In the rare cases where it does matters, proper enumeratorion 86 | can be assured by calling `#to_enum` first. 87 | 88 | o = OpenStruct.new(:a=>1,:b=>2) 89 | a = {} 90 | o.to_enum.each do |k,v| 91 | a[k] = v 92 | end 93 | a #=> {:a=>1, :b=>2} 94 | 95 | OpenStruct also has a unique method called `#key!` that is used to check for an key entry, 96 | and raise a KeyError if it not found. 97 | 98 | o = OpenStruct.new 99 | expect KeyError do 100 | o.key!(:x) 101 | end 102 | 103 | Lastly, OpenStruct has a convenient feature for creating cascading OpenStructs. 104 | 105 | o = OpenStruct.cascade 106 | o.x.a = 1 107 | o.x.a #=> 1 108 | OpenStruct.assert === o.a 109 | 110 | -------------------------------------------------------------------------------- /work/consider/ostructable.rb: -------------------------------------------------------------------------------- 1 | # OpensStructable is a mixin module which can provide OpenStruct behavior to 2 | # any class or object. OpenStructable allows extention of data objects 3 | # with arbitrary attributes. 4 | # 5 | # require 'ostructable' 6 | # 7 | # class Record 8 | # include OpenStructable 9 | # end 10 | # 11 | # record = Record.new 12 | # record.name = "John Smith" 13 | # record.age = 70 14 | # record.pension = 300 15 | # 16 | # puts record.name # -> "John Smith" 17 | # puts record.address # -> nil 18 | # 19 | # @author 7rans 20 | # @author Yukihiro Matsumoto 21 | # @author Gavin Sinclair 22 | # 23 | module OpenStructable 24 | 25 | # TODO: Update to matchh current OpenStruct class. 26 | 27 | # TODO: Keep this uptodate with ostruct.rb. 28 | 29 | # TODO: See if Matz will accept it into core so we don't have to anymore. 30 | 31 | # TODO: As with OpenStruct, marshalling is problematic at the moment. 32 | 33 | def self.include(base) 34 | if Hash > base 35 | base.module_eval do 36 | define_method(:__table__) do 37 | self 38 | end 39 | end 40 | protected :__table__ 41 | end 42 | end 43 | 44 | def initialize(hash=nil) 45 | @__table__ = {} 46 | if hash 47 | for k,v in hash 48 | __table__[k.to_sym] = v 49 | new_ostruct_member(k) 50 | end 51 | end 52 | end 53 | 54 | # 55 | def __table__ 56 | @__table__ ||= {} 57 | end 58 | protected :__table__ 59 | 60 | # duplicate an OpenStruct object members. 61 | def initialize_copy(orig) 62 | super 63 | __table__.replace(__table__.dup) 64 | end 65 | 66 | def marshal_dump 67 | __table__ 68 | end 69 | 70 | def marshal_load(hash) 71 | __table__.replace(hash) 72 | __table__.each_key{|key| new_ostruct_member(key)} 73 | end 74 | 75 | def new_ostruct_member(name) 76 | unless self.respond_to?(name) 77 | self.instance_eval %{ 78 | def #{name}; __table__[:#{name}]; end 79 | def #{name}=(x); __table__[:#{name}] = x; end 80 | } 81 | end 82 | end 83 | 84 | # 85 | # Generate additional attributes and values. 86 | # 87 | def update(hash) 88 | #__table__ ||= {} 89 | if hash 90 | for k,v in hash 91 | __table__[k.to_sym] = v 92 | new_ostruct_member(k) 93 | end 94 | end 95 | end 96 | 97 | # 98 | def method_missing(mid, *args) # :nodoc: 99 | mname = mid.to_s 100 | len = args.length 101 | if mname =~ /=$/ 102 | if len != 1 103 | raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1) 104 | end 105 | if self.frozen? 106 | raise TypeError, "can't modify frozen #{self.class}", caller(1) 107 | end 108 | mname.chop! 109 | #@__table__ ||= {} 110 | __table__[mname.intern] = args[0] 111 | self.new_ostruct_member(mname) 112 | elsif len == 0 113 | #@__table__ ||= {} 114 | __table__[mid] 115 | else 116 | raise NoMethodError, "undefined method `#{mname}' for #{self}", caller(1) 117 | end 118 | end 119 | 120 | # 121 | # Remove the named field from the object. 122 | # 123 | def delete_field(name) 124 | #@__table__ ||= {} 125 | __table__.delete(name.to_sym) 126 | end 127 | 128 | # 129 | # Returns a string containing a detailed summary of the keys and values. 130 | # 131 | def inspect 132 | str = "<#{self.class}" 133 | for k,v in (@__table__ ||= {}) 134 | str << " #{k}=#{v.inspect}" 135 | end 136 | str << ">" 137 | end 138 | 139 | # TODO: OpenStruct could be compared too, but only if it is loaded. How? 140 | 141 | # 142 | # Compare this object and +other+ for equality. 143 | # 144 | def ==(other) 145 | case other 146 | when OpenStructable 147 | __table__ == other.__table__ 148 | #when OpenStruct 149 | # __table__ == other.__table__ 150 | when Hash 151 | __table__ == other 152 | else 153 | false 154 | end 155 | end 156 | 157 | end 158 | 159 | =begin 160 | # 161 | # It is possibe to implement OpenStruct itself with 162 | # this OpenStructable module as follows: 163 | # 164 | class OpenStruct 165 | include OpenStructable 166 | end 167 | =end 168 | -------------------------------------------------------------------------------- /work/reference/basic_struct.rb: -------------------------------------------------------------------------------- 1 | unless defined?(BasicObject) 2 | require 'blankslate' 3 | BasicObject = BlankSlate 4 | end 5 | 6 | # = BasicStruct 7 | # 8 | # BasicStruct is very similar to Ruby's own OpenStruct, but it offers some 9 | # advantages. With OpenStruct, slots with the same name as predefined 10 | # Object methods cannot be used. With BasicStruct, almost any slot can be 11 | # defined. BasicStruct is a subclass of BasicObject to ensure all method 12 | # slots, except those that are absolutely essential, are open for use. 13 | # 14 | #-- 15 | # If you wish to pass a BasicStruct to a routine that normal takes a Hash, 16 | # but are uncertain it can handle the distictions properly you can convert 17 | # easily to a Hash using #as_hash! and the result will automatically be 18 | # converted back to an BasicStruct on return. 19 | # 20 | # o = BasicStruct.new(:a=>1,:b=>2) 21 | # o.as_hash!{ |h| h.update(:a=>6) } 22 | # o #=> #6,:b=>2}> 23 | #++ 24 | # 25 | # Unlike a Hash, all BasicStruct's keys are symbols and all keys are converted 26 | # to such using #to_sym on the fly. 27 | 28 | class BasicStruct < BasicObject 29 | 30 | def self.[](hash=nil) 31 | new(hash) 32 | end 33 | 34 | # Inititalizer for BasicStruct is slightly different than that of Hash. 35 | # It does not take a default parameter, but an initial priming Hash, 36 | # like OpenStruct. The initializer can still take a default block 37 | # however. To set the default value use #default!(value). 38 | # 39 | # BasicStruct.new(:a=>1).default!(0) 40 | # 41 | def initialize(hash=nil, &yld) 42 | super(&yld) 43 | if hash 44 | hash.each{ |k,v| store(k,v) } 45 | end 46 | end 47 | 48 | # 49 | def initialize_copy(orig) 50 | orig.each{ |k,v| store(k,v) } 51 | end 52 | 53 | # Object inspection. 54 | # TODO: Need to get __class__ and __id__ in hex form. 55 | def inspect 56 | #@table.inspect 57 | hexid = __id__ 58 | klass = "BasicStruct" # __class__ 59 | "#<#{klass}:#{hexid} #{@table.inspect}>" 60 | end 61 | 62 | # Convert to an associative array. 63 | def to_a 64 | super 65 | end 66 | 67 | # 68 | def to_hash 69 | h = {} 70 | each do |k,v| 71 | h[k] = v 72 | end 73 | h 74 | end 75 | 76 | # 77 | alias_method :to_h, :to_hash 78 | 79 | # 80 | def to_basicstruct 81 | self 82 | end 83 | 84 | # Convert to an assignment procedure. 85 | def to_proc(response=false) 86 | hash = self #@table 87 | if response 88 | ::Proc.new do |o| 89 | hash.each do |k,v| 90 | o.__send__("#{k}=", v) rescue nil 91 | end 92 | end 93 | else 94 | ::Proc.new do |o| 95 | hash.each{ |k,v| o.__send__("#{k}=", v) } 96 | end 97 | end 98 | end 99 | 100 | # NOT SURE ABOUT THIS 101 | #def as_hash 102 | # @table 103 | #end 104 | 105 | # Is a given +key+ defined? 106 | def key?(key) 107 | super(key.to_sym) 108 | end 109 | 110 | # 111 | def is_a?(klass) 112 | return true if klass == ::Hash # TODO: Is this wise? How to fake a subclass? 113 | return true if klass == ::BasicStruct 114 | false 115 | end 116 | 117 | # Iterate over each key-value pair. 118 | def each(&yld) 119 | super(&yld) 120 | end 121 | 122 | # Set the default value. 123 | def default=(default) 124 | #@table.default = default 125 | super(default) 126 | end 127 | 128 | # Check equality. 129 | def ==( other ) 130 | case other 131 | when ::BasicStruct 132 | to_hash == other.to_hash # as_hash 133 | when ::Hash 134 | to_hash == other 135 | else 136 | if other.respond_to?(:to_hash) 137 | to_hash == other.to_hash 138 | else 139 | false 140 | end 141 | end 142 | end 143 | 144 | # 145 | def eql?( other ) 146 | case other 147 | when ::BasicStruct 148 | super(other.to_hash) # other.as_hash 149 | else 150 | false 151 | end 152 | end 153 | 154 | # 155 | def <<(x) 156 | case x 157 | when ::Hash 158 | self.update(x) 159 | when ::Array 160 | x.each_slice(2) do |(k,v)| 161 | self[k] = v 162 | end 163 | end 164 | end 165 | 166 | # 167 | def []=(key, value) 168 | super(key.to_sym, value) 169 | end 170 | 171 | # 172 | def [](key) 173 | super(key.to_sym) 174 | end 175 | 176 | # TODO: Should this work like #merge or #update ? 177 | def merge!(other) 178 | ::BasicStruct.new(to_hash.merge(other)) 179 | end 180 | 181 | # 182 | def update!(other) 183 | self.update(other) 184 | self 185 | end 186 | 187 | # 188 | def respond_to?(key) 189 | key?(key) 190 | end 191 | 192 | # NOTE: These were protected, why? 193 | 194 | # 195 | def store(k, v) 196 | super(k.to_sym, v) 197 | end 198 | 199 | # 200 | def fetch(k, *d, &b) 201 | super(k.to_sym, *d, &b) 202 | end 203 | 204 | protected 205 | 206 | #def as_hash! 207 | # Functor.new do |op,*a,&b| 208 | # result = @table.__send__(op,*a,&b) 209 | # case result 210 | # when Hash 211 | # BasicObject.new(result) 212 | # else 213 | # result 214 | # end 215 | # end 216 | #end 217 | 218 | #def define_slot(key, value=nil) 219 | # @table[key.to_sym] = value 220 | #end 221 | 222 | #def protect_slot( key ) 223 | # (class << self; self; end).class_eval { 224 | # protected key rescue nil 225 | # } 226 | #end 227 | 228 | def method_missing(sym, *args, &blk) 229 | type = sym.to_s[-1,1] 230 | key = sym.to_s.sub(/[=?!]$/,'').to_sym 231 | case type 232 | when '=' 233 | store(key, args[0]) 234 | when '!' 235 | __send__(key, *args, &blk) 236 | # if key?(key) 237 | # fetch(key) 238 | # else 239 | # store(key, BasicObject.new) 240 | # end 241 | when '?' 242 | fetch(key) 243 | else 244 | fetch(key) 245 | end 246 | end 247 | 248 | end 249 | 250 | # Core Extensions 251 | 252 | class Hash 253 | # Convert a Hash into a BasicStruct. 254 | def to_basicstruct 255 | BasicStruct[self] 256 | end 257 | end 258 | 259 | =begin 260 | class NilClass 261 | # Nil converts to an empty BasicObject. 262 | def to_basicstruct 263 | BasicObject.new 264 | end 265 | end 266 | 267 | class Proc 268 | # Translates a Proc into an BasicObject. By droping an BasicObject into 269 | # the Proc, the resulting assignments incured as the procedure is 270 | # evaluated produce the BasicObject. This technique is simlar to that 271 | # of MethodProbe. 272 | # 273 | # p = lambda { |x| 274 | # x.word = "Hello" 275 | # } 276 | # o = p.to_basicstruct 277 | # o.word #=> "Hello" 278 | # 279 | # NOTE The Proc must have an arity of one --no more and no less. 280 | def to_basicstruct 281 | raise ArgumentError, 'bad arity for converting Proc to basicstruct' if arity != 1 282 | o = BasicObject.new 283 | self.call( o ) 284 | o 285 | end 286 | end 287 | =end 288 | 289 | -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | require 'pathname' 5 | 6 | module Indexer 7 | 8 | # Convert index data into a gemspec. 9 | # 10 | # Notes: 11 | # * Assumes all executables are in bin/. 12 | # * Does not yet handle default_executable setting. 13 | # * Does not yet handle platform setting. 14 | # * Does not yet handle required_ruby_version. 15 | # * Support for rdoc entries is weak. 16 | # 17 | class GemspecExporter 18 | 19 | # File globs to include in package --unless a manifest file exists. 20 | FILES = ".index .yardopts alt bin data demo ext features lib man spec test try* [A-Z]*.*" unless defined?(FILES) 21 | 22 | # File globs to omit from FILES. 23 | OMIT = "Config.rb" unless defined?(OMIT) 24 | 25 | # Standard file patterns. 26 | PATTERNS = { 27 | :root => '{.index,Gemfile}', 28 | :bin => 'bin/*', 29 | :lib => 'lib/{**/}*', #.rb', 30 | :ext => 'ext/{**/}extconf.rb', 31 | :doc => '*.{txt,rdoc,md,markdown,tt,textile}', 32 | :test => '{test,spec}/{**/}*.rb' 33 | } unless defined?(PATTERNS) 34 | 35 | # For which revision of indexer spec is this converter intended? 36 | REVISION = 2013 unless defined?(REVISION) 37 | 38 | # 39 | def self.gemspec 40 | new.to_gemspec 41 | end 42 | 43 | # 44 | attr :metadata 45 | 46 | # 47 | def initialize(metadata=nil) 48 | @root_check = false 49 | 50 | if metadata 51 | root_dir = metadata.delete(:root) 52 | if root_dir 53 | @root = root_dir 54 | @root_check = true 55 | end 56 | metadata = nil if metadata.empty? 57 | end 58 | 59 | @metadata = metadata || YAML.load_file(root + '.index') 60 | 61 | if @metadata['revision'].to_i != REVISION 62 | warn "This gemspec exporter was not designed for this revision of index metadata." 63 | end 64 | end 65 | 66 | # 67 | def has_root? 68 | root ? true : false 69 | end 70 | 71 | # 72 | def root 73 | return @root if @root || @root_check 74 | @root_check = true 75 | @root = find_root 76 | end 77 | 78 | # 79 | def manifest 80 | return nil unless root 81 | @manifest ||= Dir.glob(root + 'manifest{,.txt}', File::FNM_CASEFOLD).first 82 | end 83 | 84 | # 85 | def scm 86 | return nil unless root 87 | @scm ||= %w{git hg}.find{ |m| (root + ".#{m}").directory? }.to_sym 88 | end 89 | 90 | # 91 | def files 92 | return [] unless root 93 | @files ||= \ 94 | if manifest 95 | File.readlines(manifest). 96 | map{ |line| line.strip }. 97 | reject{ |line| line.empty? || line[0,1] == '#' } 98 | else 99 | list = [] 100 | Dir.chdir(root) do 101 | FILES.split(/\s+/).each do |pattern| 102 | list.concat(glob(pattern)) 103 | end 104 | OMIT.split(/\s+/).each do |pattern| 105 | list = list - glob(pattern) 106 | end 107 | end 108 | list 109 | end.select{ |path| File.file?(path) }.uniq 110 | end 111 | 112 | # 113 | def glob_files(pattern) 114 | return [] unless root 115 | Dir.chdir(root) do 116 | Dir.glob(pattern).select do |path| 117 | File.file?(path) && files.include?(path) 118 | end 119 | end 120 | end 121 | 122 | def patterns 123 | PATTERNS 124 | end 125 | 126 | def executables 127 | @executables ||= \ 128 | glob_files(patterns[:bin]).map do |path| 129 | File.basename(path) 130 | end 131 | end 132 | 133 | def extensions 134 | @extensions ||= \ 135 | glob_files(patterns[:ext]).map do |path| 136 | File.basename(path) 137 | end 138 | end 139 | 140 | def name 141 | metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_') 142 | end 143 | 144 | def homepage 145 | page = ( 146 | metadata['resources'].find{ |r| r['type'] =~ /^home/i } || 147 | metadata['resources'].find{ |r| r['name'] =~ /^home/i } || 148 | metadata['resources'].find{ |r| r['name'] =~ /^web/i } 149 | ) 150 | page ? page['uri'] : false 151 | end 152 | 153 | def licenses 154 | metadata['copyrights'].map{ |c| c['license'] }.compact 155 | end 156 | 157 | def require_paths 158 | paths = metadata['paths'] || {} 159 | paths['load'] || ['lib'] 160 | end 161 | 162 | # 163 | # Convert to gemnspec. 164 | # 165 | def to_gemspec 166 | if has_root? 167 | Gem::Specification.new do |gemspec| 168 | to_gemspec_data(gemspec) 169 | to_gemspec_paths(gemspec) 170 | end 171 | else 172 | Gem::Specification.new do |gemspec| 173 | to_gemspec_data(gemspec) 174 | to_gemspec_paths(gemspec) 175 | end 176 | end 177 | end 178 | 179 | # 180 | # Convert pure data settings. 181 | # 182 | def to_gemspec_data(gemspec) 183 | gemspec.name = name 184 | gemspec.version = metadata['version'] 185 | gemspec.summary = metadata['summary'] 186 | gemspec.description = metadata['description'] 187 | 188 | metadata['authors'].each do |author| 189 | gemspec.authors << author['name'] 190 | 191 | if author.has_key?('email') 192 | if gemspec.email 193 | gemspec.email << author['email'] 194 | else 195 | gemspec.email = [author['email']] 196 | end 197 | end 198 | end 199 | 200 | gemspec.licenses = licenses 201 | 202 | requirements = metadata['requirements'] || [] 203 | requirements.each do |req| 204 | next if req['optional'] 205 | next if req['external'] 206 | 207 | name = req['name'] 208 | groups = req['groups'] || [] 209 | 210 | version = gemify_version(req['version']) 211 | 212 | if groups.empty? or groups.include?('runtime') 213 | # populate runtime dependencies 214 | if gemspec.respond_to?(:add_runtime_dependency) 215 | gemspec.add_runtime_dependency(name,*version) 216 | else 217 | gemspec.add_dependency(name,*version) 218 | end 219 | else 220 | # populate development dependencies 221 | if gemspec.respond_to?(:add_development_dependency) 222 | gemspec.add_development_dependency(name,*version) 223 | else 224 | gemspec.add_dependency(name,*version) 225 | end 226 | end 227 | end 228 | 229 | # convert external dependencies into gemspec requirements 230 | requirements.each do |req| 231 | next unless req['external'] 232 | gemspec.requirements << ("%s-%s" % req.values_at('name', 'version')) 233 | end 234 | 235 | gemspec.homepage = homepage 236 | gemspec.require_paths = require_paths 237 | gemspec.post_install_message = metadata['install_message'] 238 | end 239 | 240 | # 241 | # Set gemspec settings that require a root directory path. 242 | # 243 | def to_gemspec_paths(gemspec) 244 | gemspec.files = files 245 | gemspec.extensions = extensions 246 | gemspec.executables = executables 247 | 248 | if Gem::VERSION < '1.7.' 249 | gemspec.default_executable = gemspec.executables.first 250 | end 251 | 252 | gemspec.test_files = glob_files(patterns[:test]) 253 | 254 | unless gemspec.files.include?('.document') 255 | gemspec.extra_rdoc_files = glob_files(patterns[:doc]) 256 | end 257 | end 258 | 259 | # 260 | # Return a copy of this file. This is used to generate a local 261 | # .gemspec file that can automatically read the index file. 262 | # 263 | def self.source_code 264 | File.read(__FILE__) 265 | end 266 | 267 | private 268 | 269 | def find_root 270 | root_files = patterns[:root] 271 | if Dir.glob(root_files).first 272 | Pathname.new(Dir.pwd) 273 | elsif Dir.glob("../#{root_files}").first 274 | Pathname.new(Dir.pwd).parent 275 | else 276 | #raise "Can't find root of project containing `#{root_files}'." 277 | warn "Can't find root of project containing `#{root_files}'." 278 | nil 279 | end 280 | end 281 | 282 | def glob(pattern) 283 | if File.directory?(pattern) 284 | Dir.glob(File.join(pattern, '**', '*')) 285 | else 286 | Dir.glob(pattern) 287 | end 288 | end 289 | 290 | def gemify_version(version) 291 | case version 292 | when /^(.*?)\+$/ 293 | ">= #{$1}" 294 | when /^(.*?)\-$/ 295 | "< #{$1}" 296 | when /^(.*?)\~$/ 297 | "~> #{$1}" 298 | else 299 | version 300 | end 301 | end 302 | 303 | end 304 | 305 | end 306 | 307 | Indexer::GemspecExporter.gemspec -------------------------------------------------------------------------------- /lib/ostruct2.rb: -------------------------------------------------------------------------------- 1 | # OpenStruct2 is a better OpenStruct class. 2 | # 3 | # To demonstrate the weakness of the original OpenStruct, try this IRB session: 4 | # 5 | # irb(main):001:0> o = OpenStruct.new 6 | # => # 7 | # irb(main):002:0> o.display = "Hello, World!" 8 | # => "Hello, World!" 9 | # irb(main):003:0> o.display 10 | # #=> nil 11 | # 12 | # This new OpenStruct class allows *almost* any member name to be used. 13 | # The only exceptions are methods starting with double underscores, 14 | # such as `__id__` and `__send__`, and a few neccessary public 15 | # methods: `clone`, `dup`, `freeze`, `hash`, `to_enum`, `to_h`, 16 | # `to_s` and `inspect`, as well as `instance_eval` and `instance_exec`. 17 | # 18 | # Also note that `empty`, `eql`, `equal`, `frozen` and `key` can be used as 19 | # members but the key-check shorthand of using `?`-methods cannot be used since 20 | # these have special definitions. 21 | # 22 | # To offset the loss of most methods, OpenStruct provides numerous 23 | # bang-methods which can be used to manipulate the data, e.g. `#each!`. 24 | # Currently most bang-methods route directly to the underlying hash table, 25 | # so developers should keep that in mind when using this feature. A future 26 | # version may add an intermediate interface to always ensure proper "CRUD", 27 | # functonality but in the vast majority of cases it will make no difference, 28 | # so it is left for later consideration. 29 | # 30 | # This improved version of OpenStruct also has no issues with being cloned 31 | # since it does not depend on singleton methods to work. But singleton methods 32 | # are used to help boost performance. But instead of always creating singleton 33 | # methods, it only creates them on the first attempt to use them. 34 | # 35 | class OpenStruct2 < BasicObject 36 | 37 | class << self 38 | # 39 | # Create autovivified OpenStruct. 40 | # 41 | # @example 42 | # o = OpenStruct2.renew 43 | # o.a #=> # 44 | # 45 | def auto(data=nil) 46 | leet = lambda{ |h,k| new(&leet) } 47 | new(&leet) 48 | end 49 | 50 | # 51 | # Another name for #auto method. 52 | # 53 | # TODO: Still wondering waht the best name is for this. 54 | # 55 | alias :renew :auto 56 | 57 | # 58 | # Create a nested OpenStruct, such that all sub-hashes 59 | # added to the table also become OpenStruct objects. 60 | # 61 | def nested(data=nil) 62 | o = new 63 | o.nested!(true) 64 | o.update!(data) if data 65 | o 66 | end 67 | 68 | # 69 | # Shorter name for `nested`. 70 | # 71 | alias :nest :nested 72 | 73 | # 74 | # Constructor that is both autovivified and nested. 75 | # 76 | def cascade(data=nil) 77 | o = renew 78 | o.nested!(true) 79 | o.update!(data) if data 80 | o 81 | end 82 | 83 | private 84 | 85 | def const_missing(name) 86 | ::Object.const_get(name) 87 | end 88 | end 89 | 90 | # 91 | # Initialize new instance of OpenStruct. 92 | # 93 | # @param [Hash] data 94 | # 95 | def initialize(data=nil, &block) 96 | @table = ::Hash.new(&block) 97 | update!(data || {}) 98 | end 99 | 100 | # 101 | # Because there is no means of getting the class via a BasicObject instance, 102 | # we define such a method manually. 103 | # 104 | def __class__ 105 | OpenStruct2 106 | end 107 | 108 | # 109 | # Duplicate underlying table when OpenStruct is duplicated or cloned. 110 | # 111 | # @param [OpenStruct] original 112 | # 113 | def initialize_copy(original) 114 | super 115 | @table = @table.dup 116 | end 117 | 118 | # 119 | # Dispatch unrecognized member calls. 120 | # 121 | def method_missing(sym, *args, &blk) 122 | str = sym.to_s 123 | type = str[-1,1] 124 | name = str.chomp('=').chomp('!').chomp('?') 125 | 126 | case type 127 | when '!' 128 | # TODO: Probably should have an indirect interface to ensure proper 129 | # functonality in all cases. 130 | @table.public_send(name, *args, &blk) 131 | when '=' 132 | new_ostruct_member(name) 133 | store!(name, args.first) 134 | when '?' 135 | new_ostruct_member(name) 136 | key?(name) 137 | else 138 | new_ostruct_member(name) 139 | read!(name) 140 | end 141 | end 142 | 143 | # 144 | # Get/set nested flag. 145 | # 146 | def nested!(boolean=nil) 147 | if boolean.nil? 148 | @nested 149 | else 150 | @nested = !!boolean 151 | end 152 | end 153 | 154 | # 155 | # CRUD method for listing all keys. 156 | # 157 | def keys! 158 | @table.keys 159 | end 160 | 161 | # 162 | # Also a CRUD method like #read!, but for checking for the existence of a key. 163 | # 164 | def key?(key) 165 | @table.key?(key.to_sym) 166 | end 167 | 168 | # 169 | # The CRUD method for read. 170 | # 171 | def read!(key) 172 | @table[key.to_sym] 173 | end 174 | 175 | # 176 | # The CRUD method for create and update. 177 | # 178 | def store!(key, value) 179 | if @nested && Hash === value # value.respond_to?(:to_hash) 180 | value = OpenStruct2.new(value) 181 | end 182 | 183 | #new_ostruct_member(key) # this is here only for speed bump 184 | 185 | @table[key.to_sym] = value 186 | end 187 | 188 | # 189 | # The CRUD method for destroy. 190 | # 191 | def delete!(key) 192 | @table.delete(key.to_sym) 193 | end 194 | 195 | # 196 | # Same as `#delete!`. This method provides compatibility 197 | # with the original OpenStruct class. 198 | # 199 | # @deprecated Use `#delete!` method instead. 200 | # 201 | def delete_field(key) 202 | @table.delete(key.to_sym) 203 | end 204 | 205 | # 206 | # Like #read but will raise a KeyError if key is not found. 207 | # 208 | def fetch!(key) 209 | key!(key) 210 | read!(key) 211 | end 212 | 213 | # 214 | # If key is not present raise a KeyError. 215 | # 216 | # @param [#to_sym] key 217 | # 218 | # @raise [KeyError] If key is not present. 219 | # 220 | # @return key 221 | # 222 | def key!(key) 223 | return key if key?(key) 224 | ::Kernel.raise ::KeyError, ("key not found: %s" % [key.inspect]) 225 | end 226 | 227 | # 228 | # Alias for `#read!`. 229 | # 230 | # @param [#to_sym] key 231 | # 232 | # @return [Object] 233 | # 234 | def [](key) 235 | read!(key) 236 | end 237 | 238 | # 239 | # Alias for `#store!`. 240 | # 241 | # @param [#to_sym] key 242 | # 243 | # @param [Object] value 244 | # 245 | # @return value. 246 | # 247 | def []=(key, value) 248 | store!(key, value) 249 | end 250 | 251 | # 252 | # CRUDified each. 253 | # 254 | # @return nothing 255 | # 256 | def each! 257 | @table.each_key do |key| 258 | yield(key, read!(key)) 259 | end 260 | end 261 | 262 | # 263 | def map!(&block) 264 | to_enum.map(&block) 265 | end 266 | 267 | # 268 | # CRUDified update method. 269 | # 270 | # @return [self] 271 | # 272 | def update!(other) 273 | other.each do |k,v| 274 | store!(k,v) 275 | end 276 | self 277 | end 278 | 279 | # 280 | # Merge this OpenStruct with another OpenStruct or Hash object 281 | # returning a new OpenStruct instance. 282 | # 283 | # IMPORTANT! This method does not act in-place like `Hash#merge!`, 284 | # rather it works like `Hash#merge`. 285 | # 286 | # @return [OpenStruct] 287 | # 288 | def merge!(other) 289 | o = dup 290 | other.each do |k,v| 291 | o.store!(k,v) 292 | end 293 | o 294 | end 295 | 296 | # 297 | # Inspect OpenStruct object. 298 | # 299 | # @return [String] 300 | # 301 | def inspect 302 | "#<#{__class__}: #{@table.inspect}>" 303 | end 304 | 305 | alias :to_s :inspect 306 | 307 | # 308 | # Get a duplicate of the underlying table. 309 | # 310 | # @return [Hash] 311 | # 312 | def to_h 313 | @table.dup 314 | end 315 | 316 | # TODO: Should OpenStruct2 support #to_hash ? 317 | #alias :to_hash :to_h 318 | 319 | # 320 | # Create an enumerator based on `#each!`. 321 | # 322 | # @return [Enumerator] 323 | # 324 | def to_enum(methname=:each!) 325 | # Why has Ruby 2 deprecated this form? 326 | #::Enumerator.new(self, methname) 327 | ::Enumerator.new do |y| 328 | __send__(methname) do |*a| 329 | y.yield *a 330 | end 331 | end 332 | end 333 | 334 | # 335 | # Duplicate OpenStruct object. 336 | # 337 | # @return [OpenStruct] Duplicate instance. 338 | # 339 | def dup 340 | __class__.new(@table, &@table.default_proc) 341 | end 342 | 343 | alias :clone :dup 344 | 345 | # 346 | # Hash number. 347 | # 348 | def hash 349 | @table.hash 350 | end 351 | 352 | # 353 | # Freeze OpenStruct instance. 354 | # 355 | def freeze 356 | @table.freeze 357 | end 358 | 359 | # 360 | # Is the OpenStruct instance frozen? 361 | # 362 | # @return [Boolean] 363 | # 364 | def frozen? 365 | @table.frozen? 366 | end 367 | 368 | # 369 | # Is the OpenStruct void of entries? 370 | # 371 | def empty? 372 | @table.empty? 373 | end 374 | 375 | # 376 | # Two OpenStructs are equal if they are the same class and their 377 | # underlying tables are equal. 378 | # 379 | def eql?(other) 380 | return false unless(other.kind_of?(__class__)) 381 | return @table == other.table #to_h 382 | end 383 | 384 | # 385 | # Two OpenStructs are equal if they are the same class and their 386 | # underlying tables are equal. 387 | # 388 | # TODO: Why not equal for other hash types, e.g. via #to_h? 389 | # 390 | def ==(other) 391 | return false unless(other.kind_of?(__class__)) 392 | return @table == other.table #to_h 393 | end 394 | 395 | protected 396 | 397 | def table 398 | @table 399 | end 400 | 401 | def new_ostruct_member(name) 402 | name = name.to_sym 403 | # TODO: Check `#respond_to?` is needed? And if so how to do this in BasicObject? 404 | #return name if self.respond_to?(name) 405 | (class << self; self; end).class_eval do 406 | define_method(name) { read!(name) } 407 | define_method("#{name}?") { key?(name) } 408 | define_method("#{name}=") { |value| store!(name, value) } 409 | end 410 | name 411 | end 412 | 413 | end 414 | --------------------------------------------------------------------------------