minilab 1.0.0-mswin32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. data/.document +2 -0
  2. data/CHANGES +2 -0
  3. data/LICENSE +19 -0
  4. data/README +107 -0
  5. data/Rakefile +145 -0
  6. data/config/environment.rb +15 -0
  7. data/config/objects.yml +22 -0
  8. data/lib/analog_io.rb +30 -0
  9. data/lib/digital_auxport_io.rb +49 -0
  10. data/lib/digital_configuration.rb +66 -0
  11. data/lib/digital_port_io.rb +68 -0
  12. data/lib/extension/extconf.rb +4 -0
  13. data/lib/extension/minilab_hardware.c +235 -0
  14. data/lib/extension/minilab_hardware.so +0 -0
  15. data/lib/library_translator.rb +48 -0
  16. data/lib/minilab.rb +149 -0
  17. data/lib/minilab_context.rb +39 -0
  18. data/lib/result_verifier.rb +14 -0
  19. data/test/integration/analog_input_output_test.rb +43 -0
  20. data/test/integration/connect_to_hardware_test.rb +13 -0
  21. data/test/integration/digital_input_output_test.rb +114 -0
  22. data/test/integration/integration_test.rb +53 -0
  23. data/test/integration/require_minilab_test.rb +9 -0
  24. data/test/system/analog_input.test +3 -0
  25. data/test/system/analog_output.test +37 -0
  26. data/test/system/digital_port_input.test +5 -0
  27. data/test/system/digital_port_output.test +39 -0
  28. data/test/system/digital_port_read_byte.test +26 -0
  29. data/test/system/digital_screw_terminals_input.test +2 -0
  30. data/test/system/digital_screw_terminals_output.test +11 -0
  31. data/test/system/minilab_driver.rb +85 -0
  32. data/test/test_helper.rb +11 -0
  33. data/test/unit/analog_io_test.rb +87 -0
  34. data/test/unit/digital_auxport_io_test.rb +114 -0
  35. data/test/unit/digital_configuration_test.rb +136 -0
  36. data/test/unit/digital_port_io_test.rb +117 -0
  37. data/test/unit/library_translator_test.rb +100 -0
  38. data/test/unit/minilab_context_test.rb +82 -0
  39. data/test/unit/minilab_hardware_test.rb +83 -0
  40. data/test/unit/minilab_test.rb +131 -0
  41. data/test/unit/result_verifier_test.rb +33 -0
  42. data/vendor/behaviors/lib/behaviors.rb +50 -0
  43. data/vendor/behaviors/tasks/behaviors_tasks.rake +140 -0
  44. data/vendor/behaviors/test/behaviors_tasks_test.rb +71 -0
  45. data/vendor/behaviors/test/behaviors_test.rb +50 -0
  46. data/vendor/behaviors/test/tasks_test/Rakefile +16 -0
  47. data/vendor/behaviors/test/tasks_test/doc/behaviors.html +55 -0
  48. data/vendor/behaviors/test/tasks_test/lib/user.rb +2 -0
  49. data/vendor/behaviors/test/tasks_test/test/user_test.rb +17 -0
  50. data/vendor/constructor/Rakefile +44 -0
  51. data/vendor/constructor/config/environment.rb +12 -0
  52. data/vendor/constructor/lib/constructor.rb +132 -0
  53. data/vendor/constructor/test/constructor_test.rb +366 -0
  54. data/vendor/constructor/test/helper.rb +3 -0
  55. data/vendor/diy/README +26 -0
  56. data/vendor/diy/Rakefile +18 -0
  57. data/vendor/diy/lib/constructor.rb +114 -0
  58. data/vendor/diy/lib/diy.rb +329 -0
  59. data/vendor/diy/proto/context.rb +117 -0
  60. data/vendor/diy/proto/context.yml +20 -0
  61. data/vendor/diy/test/diy_test.rb +370 -0
  62. data/vendor/diy/test/files/broken_construction.yml +7 -0
  63. data/vendor/diy/test/files/cat/cat.rb +4 -0
  64. data/vendor/diy/test/files/cat/extra_conflict.yml +5 -0
  65. data/vendor/diy/test/files/cat/heritage.rb +2 -0
  66. data/vendor/diy/test/files/cat/needs_input.yml +3 -0
  67. data/vendor/diy/test/files/cat/the_cat_lineage.rb +1 -0
  68. data/vendor/diy/test/files/dog/dog_model.rb +4 -0
  69. data/vendor/diy/test/files/dog/dog_presenter.rb +4 -0
  70. data/vendor/diy/test/files/dog/dog_view.rb +2 -0
  71. data/vendor/diy/test/files/dog/file_resolver.rb +2 -0
  72. data/vendor/diy/test/files/dog/other_thing.rb +2 -0
  73. data/vendor/diy/test/files/dog/simple.yml +11 -0
  74. data/vendor/diy/test/files/donkey/foo.rb +8 -0
  75. data/vendor/diy/test/files/donkey/foo/bar/qux.rb +7 -0
  76. data/vendor/diy/test/files/fud/objects.yml +13 -0
  77. data/vendor/diy/test/files/fud/toy.rb +15 -0
  78. data/vendor/diy/test/files/gnu/objects.yml +14 -0
  79. data/vendor/diy/test/files/gnu/thinger.rb +8 -0
  80. data/vendor/diy/test/files/goat/base.rb +8 -0
  81. data/vendor/diy/test/files/goat/can.rb +6 -0
  82. data/vendor/diy/test/files/goat/goat.rb +6 -0
  83. data/vendor/diy/test/files/goat/objects.yml +12 -0
  84. data/vendor/diy/test/files/goat/paper.rb +6 -0
  85. data/vendor/diy/test/files/goat/plane.rb +8 -0
  86. data/vendor/diy/test/files/goat/shirt.rb +6 -0
  87. data/vendor/diy/test/files/goat/wings.rb +8 -0
  88. data/vendor/diy/test/files/horse/holder_thing.rb +4 -0
  89. data/vendor/diy/test/files/horse/objects.yml +7 -0
  90. data/vendor/diy/test/files/yak/core_model.rb +4 -0
  91. data/vendor/diy/test/files/yak/core_presenter.rb +4 -0
  92. data/vendor/diy/test/files/yak/core_view.rb +1 -0
  93. data/vendor/diy/test/files/yak/data_source.rb +1 -0
  94. data/vendor/diy/test/files/yak/fringe_model.rb +4 -0
  95. data/vendor/diy/test/files/yak/fringe_presenter.rb +4 -0
  96. data/vendor/diy/test/files/yak/fringe_view.rb +1 -0
  97. data/vendor/diy/test/files/yak/my_objects.yml +21 -0
  98. data/vendor/diy/test/test_helper.rb +40 -0
  99. data/vendor/hardmock/CHANGES +8 -0
  100. data/vendor/hardmock/LICENSE +7 -0
  101. data/vendor/hardmock/README +48 -0
  102. data/vendor/hardmock/Rakefile +100 -0
  103. data/vendor/hardmock/TODO +7 -0
  104. data/vendor/hardmock/config/environment.rb +12 -0
  105. data/vendor/hardmock/homepage/demo.rb +21 -0
  106. data/vendor/hardmock/homepage/hardmock_sample.png +0 -0
  107. data/vendor/hardmock/homepage/index.html +65 -0
  108. data/vendor/hardmock/init.rb +3 -0
  109. data/vendor/hardmock/lib/hardmock.rb +634 -0
  110. data/vendor/hardmock/lib/method_cleanout.rb +14 -0
  111. data/vendor/hardmock/rcov.rake +18 -0
  112. data/vendor/hardmock/test/functional/assert_error_test.rb +52 -0
  113. data/vendor/hardmock/test/functional/auto_verify_test.rb +192 -0
  114. data/vendor/hardmock/test/functional/direct_mock_usage_test.rb +396 -0
  115. data/vendor/hardmock/test/functional/hardmock_test.rb +380 -0
  116. data/vendor/hardmock/test/test_helper.rb +23 -0
  117. data/vendor/hardmock/test/unit/expectation_builder_test.rb +18 -0
  118. data/vendor/hardmock/test/unit/expector_test.rb +56 -0
  119. data/vendor/hardmock/test/unit/method_cleanout_test.rb +35 -0
  120. data/vendor/hardmock/test/unit/mock_control_test.rb +172 -0
  121. data/vendor/hardmock/test/unit/mock_test.rb +273 -0
  122. data/vendor/hardmock/test/unit/simple_expectation_test.rb +345 -0
  123. data/vendor/hardmock/test/unit/trapper_test.rb +60 -0
  124. data/vendor/hardmock/test/unit/verify_error_test.rb +34 -0
  125. data/vendor/systir/systir.rb +403 -0
  126. data/vendor/systir/test/unit/ui/xml/testrunner.rb +192 -0
  127. data/vendor/systir/test/unit/ui/xml/xmltestrunner.xslt +109 -0
  128. metadata +235 -0
@@ -0,0 +1,140 @@
1
+ desc "List behavioral definitions for the classes under test (use for=<term> also)"
2
+ task :behaviors do
3
+ specifications.each do |spec|
4
+ puts "#{spec.name} should:\n"
5
+ spec.requirements.each do |req|
6
+ puts " - #{req}"
7
+ end
8
+ end
9
+ end
10
+
11
+ desc "List behavioral definitions for the classes under test (HTML output)"
12
+ task :behaviors_html do
13
+ require 'erb'
14
+ txt =<<-EOS
15
+ <html>
16
+ <head>
17
+ <style>
18
+
19
+ div.title
20
+ {
21
+ width: 600px;
22
+ font: bold 14pt trebuchet ms;
23
+ }
24
+
25
+ div.specification
26
+ {
27
+ font: bold 12pt trebuchet ms;
28
+ border: solid 1px black;
29
+ width: 600px;
30
+ padding: 5px;
31
+ margin: 5px;
32
+ }
33
+
34
+ ul.requirements
35
+ {
36
+ font: normal 11pt verdana;
37
+ padding-left: 0;
38
+ margin-left: 0;
39
+ border-bottom: 1px solid gray;
40
+ width: 600px;
41
+ }
42
+
43
+ ul.requirements li
44
+ {
45
+ list-style: none;
46
+ margin: 0;
47
+ padding: 0.25em;
48
+ border-top: 1px solid gray;
49
+ }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <div class="title">Specifications for <%= title %></div>
54
+ <% specifications.each do |spec| %>
55
+ <div class="specification">
56
+ <%= spec.name %> should:
57
+ <ul class="requirements">
58
+ <% spec.requirements.each do |req| %>
59
+ <li><%= req %></li>
60
+ <% end %>
61
+ </ul>
62
+ </div>
63
+ <% end %>
64
+ </body>
65
+ </html>
66
+ EOS
67
+ output_dir = File.expand_path("#{APP_ROOT}/doc")
68
+ mkdir_p output_dir
69
+ output_filename = output_dir + "/behaviors.html"
70
+ File.open(output_filename,"w") do |f|
71
+ f.write ERB.new(txt).result(binding)
72
+ end
73
+ puts "(Wrote #{output_filename})"
74
+ end
75
+
76
+ def title
77
+ File.basename(File.expand_path(APP_ROOT))
78
+ end
79
+
80
+ def specifications
81
+ test_files.map do |file|
82
+ spec = OpenStruct.new
83
+ m = %r".*/([^/].*)_test.rb".match(file)
84
+ class_name = titleize(m[1]) if m[1]
85
+ spec.name = class_name
86
+ spec.requirements = []
87
+ File::readlines(file).each do |line|
88
+ if line =~ /^\s*should\s+\(?\s*["'](.*)["']/
89
+ spec.requirements << $1
90
+ end
91
+ end
92
+ spec
93
+ end
94
+ end
95
+
96
+ def test_files
97
+ test_list = FileList["#{APP_ROOT}/test/**/*_test.rb"]
98
+ if ENV['for']
99
+ test_list = test_list.grep(/#{ENV['for']}/i)
100
+ end
101
+ test_list
102
+ end
103
+
104
+
105
+ ############################################################
106
+ # STOLEN FROM inflector.rb
107
+ ############################################################
108
+ #--
109
+ # Copyright (c) 2005 David Heinemeier Hansson
110
+ #
111
+ # Permission is hereby granted, free of charge, to any person obtaining
112
+ # a copy of this software and associated documentation files (the
113
+ # "Software"), to deal in the Software without restriction, including
114
+ # without limitation the rights to use, copy, modify, merge, publish,
115
+ # distribute, sublicense, and/or sell copies of the Software, and to
116
+ # permit persons to whom the Software is furnished to do so, subject to
117
+ # the following conditions:
118
+ #
119
+ # The above copyright notice and this permission notice shall be
120
+ # included in all copies or substantial portions of the Software.
121
+ #
122
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
123
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
124
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
125
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
126
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
127
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
128
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
129
+ #++
130
+ def titleize(word)
131
+ humanize(underscore(word)).gsub(/\b([a-z])/) { $1.capitalize }
132
+ end
133
+
134
+ def underscore(camel_cased_word) camel_cased_word.to_s.gsub(/::/, '/').
135
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
136
+ end
137
+
138
+ def humanize(lower_case_and_underscored_word)
139
+ lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
140
+ end
@@ -0,0 +1,71 @@
1
+ require 'test/unit'
2
+
3
+ APP_ROOT = File.expand_path(File.dirname(__FILE__) + '/../')
4
+
5
+ class BehaviorsTasksTest < Test::Unit::TestCase
6
+
7
+ def setup
8
+ here = File.expand_path(File.dirname(__FILE__))
9
+ cmd = RUBY_PLATFORM[/mswin/] ? 'rake.cmd' : 'rake'
10
+ @base_cmd = "#{cmd} -f \"#{here}/tasks_test/Rakefile\" "
11
+ end
12
+
13
+ #
14
+ # HELPERS
15
+ #
16
+ def run_behaviors_task
17
+ run_cmd "behaviors"
18
+ end
19
+
20
+ def run_behaviors_html_task
21
+ run_cmd "behaviors_html"
22
+ end
23
+
24
+ def run_cmd(cmd)
25
+ @report = %x[ #{@base_cmd} #{cmd} ]
26
+ end
27
+
28
+ def see_html_task_output_message
29
+ @html_output_filename = "#{APP_ROOT}/test/tasks_test/doc/behaviors.html"
30
+ assert_match /Wrote #{@html_output_filename}/, @report
31
+ end
32
+
33
+ def see_that_html_report_file_exits
34
+ assert File.exists?(@html_output_filename), "html output file should exist"
35
+ end
36
+
37
+ def html_report_file_should_contain(user_behaviors)
38
+ file_contents = File.read(@html_output_filename)
39
+ user_behaviors.each do |line|
40
+ assert_match /#{line}/, file_contents
41
+ end
42
+ end
43
+
44
+ #
45
+ # TESTS
46
+ #
47
+ def test_that_behaviors_tasks_should_list_behavioral_definitions_for_the_classes_under_test
48
+ run_behaviors_task
49
+ user_behaviors = [
50
+ "User should:",
51
+ " - be able set user name and age during construction",
52
+ " - be able to get user name and age",
53
+ " - be able to ask if a user is an adult"
54
+ ]
55
+ assert_match /#{user_behaviors.join("\n")}/, @report
56
+ end
57
+
58
+ def test_that_behaviors_tasks_should_list_behavioral_definitions_for_the_classes_under_test_in_html_output
59
+ run_behaviors_html_task
60
+ see_html_task_output_message
61
+ see_that_html_report_file_exits
62
+ user_behaviors = [
63
+ "User should:",
64
+ "be able set user name and age during construction",
65
+ "be able to get user name and age",
66
+ "be able to ask if a user is an adult"
67
+ ]
68
+ html_report_file_should_contain user_behaviors
69
+ end
70
+
71
+ end
@@ -0,0 +1,50 @@
1
+ require 'test/unit'
2
+ require File.expand_path(File.dirname(__FILE__)) + '/../lib/behaviors'
3
+ require 'stringio'
4
+
5
+ loading_developer_test_class_stdout = StringIO.new
6
+ saved_stdout = $stdout.dup
7
+ $stdout = loading_developer_test_class_stdout
8
+
9
+ class DeveloperTest
10
+ extend Behaviors
11
+ attr_accessor :flunk_msg, :tested_code
12
+
13
+ should "test their code" do
14
+ @tested_code = true
15
+ end
16
+ should "go to meetings"
17
+ end
18
+
19
+ $stdout = saved_stdout
20
+ loading_developer_test_class_stdout.rewind
21
+ $loading_developer_test_class_output = loading_developer_test_class_stdout.read
22
+
23
+ class BehaviorsTest < Test::Unit::TestCase
24
+
25
+
26
+ def setup
27
+ @target = DeveloperTest.new
28
+ assert_nil @target.tested_code, "block called too early"
29
+ end
30
+
31
+ #
32
+ # TESTS
33
+ #
34
+ def test_should_called_with_a_block_defines_a_test
35
+ assert @target.methods.include?("test_should_test their code"), "Missing test method"
36
+
37
+ @target.send("test_should_test their code")
38
+
39
+ assert @target.tested_code, "block not called"
40
+ end
41
+
42
+ def test_should_called_without_a_block_does_not_create_a_test_method
43
+ assert !@target.methods.include?("test_should_go to meetings"), "Should not have method"
44
+ end
45
+
46
+ def test_should_called_without_a_block_will_give_unimplemented_output_when_class_loads
47
+ unimplemented_output = "UNIMPLEMENTED CASE: Developer should go to meetings"
48
+ assert_match /#{unimplemented_output}/, $loading_developer_test_class_output
49
+ end
50
+ end
@@ -0,0 +1,16 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ APP_ROOT = File.expand_path(File.dirname(__FILE__))
5
+ load "#{APP_ROOT}/../../tasks/behaviors_tasks.rake"
6
+
7
+ cd APP_ROOT
8
+
9
+ desc 'Default: run unit tests.'
10
+ task :default => :test
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << "#{APP_ROOT}/../../lib"
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = true
16
+ end
@@ -0,0 +1,55 @@
1
+ <html>
2
+ <head>
3
+ <style>
4
+
5
+ div.title
6
+ {
7
+ width: 600px;
8
+ font: bold 14pt trebuchet ms;
9
+ }
10
+
11
+ div.specification
12
+ {
13
+ font: bold 12pt trebuchet ms;
14
+ border: solid 1px black;
15
+ width: 600px;
16
+ padding: 5px;
17
+ margin: 5px;
18
+ }
19
+
20
+ ul.requirements
21
+ {
22
+ font: normal 11pt verdana;
23
+ padding-left: 0;
24
+ margin-left: 0;
25
+ border-bottom: 1px solid gray;
26
+ width: 600px;
27
+ }
28
+
29
+ ul.requirements li
30
+ {
31
+ list-style: none;
32
+ margin: 0;
33
+ padding: 0.25em;
34
+ border-top: 1px solid gray;
35
+ }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <div class="title">Specifications for tasks_test</div>
40
+
41
+ <div class="specification">
42
+ User should:
43
+ <ul class="requirements">
44
+
45
+ <li>be able set user name and age during construction</li>
46
+
47
+ <li>be able to get user name and age</li>
48
+
49
+ <li>be able to ask if a user is an adult</li>
50
+
51
+ </ul>
52
+ </div>
53
+
54
+ </body>
55
+ </html>
@@ -0,0 +1,2 @@
1
+ class User
2
+ end
@@ -0,0 +1,17 @@
1
+ require 'test/unit'
2
+ require 'behaviors'
3
+
4
+ require 'user'
5
+
6
+ class UserTest < Test::Unit::TestCase
7
+ extend Behaviors
8
+
9
+ def setup
10
+ end
11
+
12
+ should "be able set user name and age during construction"
13
+ should "be able to get user name and age"
14
+ should "be able to ask if a user is an adult"
15
+ def test_DELETEME
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rubygems'
5
+ Gem::manage_gems
6
+ require 'rake/gempackagetask'
7
+
8
+ desc 'Default: run unit tests.'
9
+ task :default => :test
10
+
11
+ GEM_VERSION = '1.0.0'
12
+ GEM_NAME = 'constructor'
13
+
14
+ gem_spec = Gem::Specification.new do |s|
15
+ s.name = GEM_NAME
16
+ s.version = GEM_VERSION
17
+ s.author = "Atomic Object LLC"
18
+ s.email = "dev@atomicobject.com"
19
+ s.homepage = "http://atomicobject.com"
20
+ s.platform = Gem::Platform::RUBY
21
+ s.summary = "Declarative, named constructor arguments"
22
+ s.files = FileList["{test,lib}/**/*"].exclude("rdoc").to_a
23
+ s.has_rdoc = true
24
+ s.add_dependency "behaviors", ">= 1.0.0"
25
+ end
26
+
27
+ Rake::GemPackageTask.new(gem_spec) do |pkg|
28
+ pkg.need_tar = true
29
+ end
30
+
31
+ desc 'Run all tests.'
32
+ Rake::TestTask.new(:test) do |t|
33
+ t.libs << 'lib'
34
+ t.pattern = 'test/*_test.rb'
35
+ t.verbose = true
36
+ end
37
+
38
+ desc 'Generate documentation.'
39
+ Rake::RDocTask.new(:rdoc) do |rdoc|
40
+ rdoc.rdoc_dir = 'rdoc'
41
+ rdoc.title = 'Constructor'
42
+ rdoc.options << '--line-numbers' << '--inline-source'
43
+ rdoc.rdoc_files.include('lib/**/*.rb')
44
+ end
@@ -0,0 +1,12 @@
1
+ # The path to the root directory of your application.
2
+ APP_ROOT = File.join(File.dirname(__FILE__), '..')
3
+
4
+ ADDITIONAL_LOAD_PATHS = []
5
+ ADDITIONAL_LOAD_PATHS.concat %w(
6
+ src
7
+ ).map { |dir| "#{APP_ROOT}/#{dir}" }.select { |dir| File.directory?(dir) }
8
+
9
+ # Prepend to $LOAD_PATH
10
+ ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) }
11
+
12
+ # Require any additional libraries needed
@@ -0,0 +1,132 @@
1
+ class Class
2
+ #
3
+ # Declarative means to define object properties by passing a hash
4
+ # to the constructor, which will set the corresponding ivars.
5
+ # Eg,
6
+ # class Horse
7
+ # constructor :name, :breed, :weight
8
+ # end
9
+ # Horse.new :name => 'Ed', :breed => 'Mustang', :weight => 342
10
+ #
11
+ # By default the ivars do not get accessors defined.
12
+ # But you can get them auto-made if you want:
13
+ # class Horse
14
+ # constructor :name, :breed, :weight, :accessors => true
15
+ # end
16
+ # ...
17
+ # puts my_horse.weight
18
+ #
19
+ # Arguments specified are required by default. You can disable
20
+ # strict argument checking with :strict option. This means that
21
+ # the constructor will not raise an error if you pass more or
22
+ # fewer arguments than declared.
23
+ # Eg,
24
+ # class Donkey
25
+ # constructor :age, :odor, :strict => false
26
+ # end
27
+ # ... this allows you to pass either an age or odor key (or neither) to
28
+ # the Donkey constructor.
29
+ #
30
+ def constructor(*attrs)
31
+ # Look for embedded options in the listing:
32
+ opts = attrs.find { |a| a.kind_of?(Hash) and attrs.delete(a) }
33
+ do_acc = opts.nil? ? false : opts[:accessors] == true
34
+ require_args = opts.nil? ? true : opts[:strict] != false
35
+ super_args = opts.nil? ? nil : opts[:super]
36
+
37
+ # Incorporate superclass's constructor keys, if our superclass
38
+ if superclass.constructor_keys
39
+ attrs = [attrs,superclass.constructor_keys].flatten
40
+ end
41
+ # Generate ivar assigner code lines
42
+ assigns = ''
43
+ attrs.each do |k|
44
+ assigns += "@#{k.to_s} = args[:#{k.to_s}]\n"
45
+ end
46
+
47
+ # If accessors option is on, declare accessors for the attributes:
48
+ if do_acc
49
+ self.class_eval "attr_accessor " + attrs.map {|x| ":#{x.to_s}"}.join(',')
50
+ end
51
+
52
+ # If user supplied super-constructor hints:
53
+ super_call = ''
54
+ if super_args
55
+ list = super_args.map do |a|
56
+ case a
57
+ when String
58
+ %|"#{a}"|
59
+ when Symbol
60
+ %|:#{a}|
61
+ end
62
+ end
63
+ super_call = %|super(#{list.join(',')})|
64
+ end
65
+
66
+ # If strict is on, define the constructor argument validator method,
67
+ # and setup the initializer to invoke the validator method.
68
+ # Otherwise, insert lax code into the initializer.
69
+ validation_code = "return if args.nil?"
70
+ if require_args
71
+ self.class_eval do
72
+ def _validate_constructor_args(args)
73
+ # First, make sure we've got args of some kind
74
+ unless args and args.keys and args.keys.size > 0
75
+ raise ConstructorArgumentError.new(self.class.constructor_keys)
76
+ end
77
+ # Scan for missing keys in the argument hash
78
+ a_keys = args.keys
79
+ missing = []
80
+ self.class.constructor_keys.each do |ck|
81
+ unless a_keys.member?(ck)
82
+ missing << ck
83
+ end
84
+ a_keys.delete(ck) # Delete inbound keys as we address them
85
+ end
86
+ if missing.size > 0 || a_keys.size > 0
87
+ raise ConstructorArgumentError.new(missing,a_keys)
88
+ end
89
+ end
90
+ end
91
+ # Setup the code to insert into the initializer:
92
+ validation_code = "_validate_constructor_args args "
93
+ end
94
+
95
+ # Generate the initializer code
96
+ self.class_eval %{
97
+ def initialize(args=nil)
98
+ #{super_call}
99
+ #{validation_code}
100
+ #{assigns}
101
+ setup if respond_to?(:setup)
102
+ end
103
+ }
104
+
105
+ # Remember our constructor keys
106
+ @_ctor_keys = attrs
107
+ end
108
+
109
+ # Access the constructor keys for this class
110
+ def constructor_keys; @_ctor_keys; end
111
+ end
112
+
113
+ # Fancy validation exception, based on missing and extraneous keys.
114
+ class ConstructorArgumentError < RuntimeError
115
+ def initialize(missing,rejected=[])
116
+ err_msg = ''
117
+ if missing.size > 0
118
+ err_msg = "Missing constructor args [#{missing.join(',')}]"
119
+ end
120
+ if rejected.size > 0
121
+ # Some inbound keys were not addressed earlier; this means they're unwanted
122
+ if err_msg
123
+ err_msg << "; " # Appending to earlier message about missing items
124
+ else
125
+ err_msg = ''
126
+ end
127
+ # Enumerate the rejected key names
128
+ err_msg << "Rejected constructor args [#{rejected.join(',')}]"
129
+ end
130
+ super err_msg
131
+ end
132
+ end