aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionmailer/CHANGELOG19
-rw-r--r--actionmailer/MIT-LICENSE21
-rwxr-xr-xactionmailer/README102
-rwxr-xr-xactionmailer/Rakefile107
-rw-r--r--actionmailer/install.rb61
-rwxr-xr-xactionmailer/lib/action_mailer.rb43
-rw-r--r--actionmailer/lib/action_mailer/base.rb152
-rw-r--r--actionmailer/lib/action_mailer/mail_helper.rb17
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/text/format.rb1447
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail.rb4
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/address.rb223
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/base64.rb52
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/config.rb50
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/encode.rb447
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/facade.rb531
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/header.rb893
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/info.rb16
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/loader.rb1
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/mail.rb420
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/mailbox.rb414
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/mbox.rb1
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/net.rb261
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/obsolete.rb116
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/parser.rb1503
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/port.rb358
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/scanner.rb22
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb244
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/stringio.rb260
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/tmail.rb1
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/tmail/utils.rb215
-rw-r--r--actionmailer/test/fixtures/templates/signed_up.rhtml3
-rw-r--r--actionmailer/test/fixtures/test_mailer/signed_up.rhtml3
-rwxr-xr-xactionmailer/test/mail_service_test.rb92
-rw-r--r--actionpack/CHANGELOG738
-rw-r--r--actionpack/MIT-LICENSE21
-rwxr-xr-xactionpack/README418
-rw-r--r--actionpack/RUNNING_UNIT_TESTS25
-rwxr-xr-xactionpack/Rakefile105
-rw-r--r--actionpack/examples/.htaccess24
-rw-r--r--actionpack/examples/address_book/index.rhtml33
-rw-r--r--actionpack/examples/address_book/layout.rhtml8
-rwxr-xr-xactionpack/examples/address_book_controller.cgi9
-rwxr-xr-xactionpack/examples/address_book_controller.fcgi6
-rw-r--r--actionpack/examples/address_book_controller.rb52
-rw-r--r--actionpack/examples/address_book_controller.rbx4
-rw-r--r--actionpack/examples/benchmark.rb52
-rwxr-xr-xactionpack/examples/benchmark_with_ar.fcgi89
-rwxr-xr-xactionpack/examples/blog_controller.cgi53
-rw-r--r--actionpack/examples/debate/index.rhtml14
-rw-r--r--actionpack/examples/debate/new_topic.rhtml22
-rw-r--r--actionpack/examples/debate/topic.rhtml32
-rwxr-xr-xactionpack/examples/debate_controller.cgi57
-rw-r--r--actionpack/install.rb97
-rwxr-xr-xactionpack/lib/action_controller.rb51
-rw-r--r--actionpack/lib/action_controller/assertions/action_pack_assertions.rb199
-rw-r--r--actionpack/lib/action_controller/assertions/active_record_assertions.rb65
-rwxr-xr-xactionpack/lib/action_controller/base.rb689
-rw-r--r--actionpack/lib/action_controller/benchmarking.rb49
-rwxr-xr-xactionpack/lib/action_controller/cgi_ext/cgi_ext.rb43
-rwxr-xr-xactionpack/lib/action_controller/cgi_ext/cgi_methods.rb91
-rw-r--r--actionpack/lib/action_controller/cgi_process.rb124
-rw-r--r--actionpack/lib/action_controller/dependencies.rb49
-rw-r--r--actionpack/lib/action_controller/filters.rb279
-rw-r--r--actionpack/lib/action_controller/flash.rb65
-rw-r--r--actionpack/lib/action_controller/helpers.rb100
-rw-r--r--actionpack/lib/action_controller/layout.rb149
-rwxr-xr-xactionpack/lib/action_controller/request.rb99
-rw-r--r--actionpack/lib/action_controller/rescue.rb94
-rwxr-xr-xactionpack/lib/action_controller/response.rb15
-rw-r--r--actionpack/lib/action_controller/scaffolding.rb183
-rw-r--r--actionpack/lib/action_controller/session/active_record_store.rb72
-rw-r--r--actionpack/lib/action_controller/session/drb_server.rb9
-rw-r--r--actionpack/lib/action_controller/session/drb_store.rb31
-rw-r--r--actionpack/lib/action_controller/support/class_attribute_accessors.rb57
-rw-r--r--actionpack/lib/action_controller/support/class_inheritable_attributes.rb37
-rw-r--r--actionpack/lib/action_controller/support/clean_logger.rb10
-rw-r--r--actionpack/lib/action_controller/support/cookie_performance_fix.rb121
-rw-r--r--actionpack/lib/action_controller/support/inflector.rb78
-rw-r--r--actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml28
-rw-r--r--actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml22
-rw-r--r--actionpack/lib/action_controller/templates/rescues/layout.rhtml29
-rw-r--r--actionpack/lib/action_controller/templates/rescues/missing_template.rhtml2
-rw-r--r--actionpack/lib/action_controller/templates/rescues/template_error.rhtml26
-rw-r--r--actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml2
-rw-r--r--actionpack/lib/action_controller/templates/scaffolds/edit.rhtml6
-rw-r--r--actionpack/lib/action_controller/templates/scaffolds/layout.rhtml29
-rw-r--r--actionpack/lib/action_controller/templates/scaffolds/list.rhtml24
-rw-r--r--actionpack/lib/action_controller/templates/scaffolds/new.rhtml5
-rw-r--r--actionpack/lib/action_controller/templates/scaffolds/show.rhtml9
-rw-r--r--actionpack/lib/action_controller/test_process.rb195
-rw-r--r--actionpack/lib/action_controller/url_rewriter.rb170
-rw-r--r--actionpack/lib/action_view.rb49
-rw-r--r--actionpack/lib/action_view/base.rb264
-rw-r--r--actionpack/lib/action_view/helpers/active_record_helper.rb171
-rwxr-xr-xactionpack/lib/action_view/helpers/date_helper.rb230
-rw-r--r--actionpack/lib/action_view/helpers/debug_helper.rb17
-rw-r--r--actionpack/lib/action_view/helpers/form_helper.rb182
-rw-r--r--actionpack/lib/action_view/helpers/form_options_helper.rb212
-rw-r--r--actionpack/lib/action_view/helpers/tag_helper.rb59
-rw-r--r--actionpack/lib/action_view/helpers/text_helper.rb111
-rw-r--r--actionpack/lib/action_view/helpers/url_helper.rb78
-rw-r--r--actionpack/lib/action_view/partials.rb64
-rw-r--r--actionpack/lib/action_view/template_error.rb84
-rw-r--r--actionpack/lib/action_view/vendor/builder.rb13
-rw-r--r--actionpack/lib/action_view/vendor/builder/blankslate.rb51
-rw-r--r--actionpack/lib/action_view/vendor/builder/xmlbase.rb143
-rw-r--r--actionpack/lib/action_view/vendor/builder/xmlevents.rb63
-rw-r--r--actionpack/lib/action_view/vendor/builder/xmlmarkup.rb288
-rw-r--r--actionpack/test/abstract_unit.rb9
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb323
-rw-r--r--actionpack/test/controller/active_record_assertions_test.rb119
-rwxr-xr-xactionpack/test/controller/cgi_test.rb142
-rw-r--r--actionpack/test/controller/cookie_test.rb38
-rw-r--r--actionpack/test/controller/filters_test.rb159
-rw-r--r--actionpack/test/controller/flash_test.rb69
-rw-r--r--actionpack/test/controller/helper_test.rb110
-rw-r--r--actionpack/test/controller/layout_test.rb49
-rwxr-xr-xactionpack/test/controller/redirect_test.rb44
-rw-r--r--actionpack/test/controller/render_test.rb178
-rw-r--r--actionpack/test/controller/send_file_test.rb68
-rw-r--r--actionpack/test/controller/url_test.rb368
-rw-r--r--actionpack/test/fixtures/helpers/abc_helper.rb5
-rw-r--r--actionpack/test/fixtures/layouts/builder.rxml3
-rw-r--r--actionpack/test/fixtures/layouts/standard.rhtml1
-rw-r--r--actionpack/test/fixtures/scope/test/modgreet.rhtml1
-rw-r--r--actionpack/test/fixtures/test/_customer.rhtml1
-rw-r--r--actionpack/test/fixtures/test/greeting.rhtml1
-rw-r--r--actionpack/test/fixtures/test/hello.rxml4
-rw-r--r--actionpack/test/fixtures/test/hello_world.rhtml1
-rw-r--r--actionpack/test/fixtures/test/hello_xml_world.rxml11
-rw-r--r--actionpack/test/fixtures/test/list.rhtml1
-rw-r--r--actionpack/test/template/active_record_helper_test.rb76
-rwxr-xr-xactionpack/test/template/date_helper_test.rb104
-rw-r--r--actionpack/test/template/form_helper_test.rb124
-rw-r--r--actionpack/test/template/form_options_helper_test.rb165
-rw-r--r--actionpack/test/template/tag_helper_test.rb18
-rw-r--r--actionpack/test/template/text_helper_test.rb62
-rw-r--r--actionpack/test/template/url_helper_test.rb49
-rw-r--r--activerecord/CHANGELOG757
-rw-r--r--activerecord/MIT-LICENSE20
-rwxr-xr-xactiverecord/README361
-rw-r--r--activerecord/RUNNING_UNIT_TESTS36
-rwxr-xr-xactiverecord/Rakefile126
-rw-r--r--activerecord/benchmarks/benchmark.rb26
-rw-r--r--activerecord/benchmarks/mysql_benchmark.rb19
-rw-r--r--activerecord/dev-utils/eval_debugger.rb14
-rw-r--r--activerecord/examples/associations.pngbin0 -> 40623 bytes
-rw-r--r--activerecord/examples/associations.rb87
-rw-r--r--activerecord/examples/shared_setup.rb15
-rw-r--r--activerecord/examples/validation.rb88
-rw-r--r--activerecord/install.rb60
-rwxr-xr-xactiverecord/lib/active_record.rb50
-rw-r--r--activerecord/lib/active_record/aggregations.rb165
-rwxr-xr-xactiverecord/lib/active_record/associations.rb576
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb129
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb107
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb102
-rwxr-xr-xactiverecord/lib/active_record/base.rb1051
-rwxr-xr-xactiverecord/lib/active_record/callbacks.rb337
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/abstract_adapter.rb371
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/mysql_adapter.rb131
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb170
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb105
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb298
-rw-r--r--activerecord/lib/active_record/deprecated_associations.rb70
-rwxr-xr-xactiverecord/lib/active_record/fixtures.rb208
-rw-r--r--activerecord/lib/active_record/observer.rb71
-rw-r--r--activerecord/lib/active_record/reflection.rb126
-rw-r--r--activerecord/lib/active_record/support/class_attribute_accessors.rb43
-rw-r--r--activerecord/lib/active_record/support/class_inheritable_attributes.rb37
-rw-r--r--activerecord/lib/active_record/support/clean_logger.rb10
-rw-r--r--activerecord/lib/active_record/support/inflector.rb78
-rw-r--r--activerecord/lib/active_record/transactions.rb119
-rwxr-xr-xactiverecord/lib/active_record/validations.rb205
-rw-r--r--activerecord/lib/active_record/vendor/mysql.rb1117
-rw-r--r--activerecord/lib/active_record/vendor/simple.rb702
-rw-r--r--activerecord/lib/active_record/wrappers/yaml_wrapper.rb15
-rw-r--r--activerecord/lib/active_record/wrappings.rb59
-rwxr-xr-xactiverecord/test/abstract_unit.rb22
-rw-r--r--activerecord/test/aggregations_test.rb34
-rwxr-xr-xactiverecord/test/all.sh8
-rwxr-xr-xactiverecord/test/associations_test.rb549
-rwxr-xr-xactiverecord/test/base_test.rb544
-rw-r--r--activerecord/test/class_inheritable_attributes_test.rb33
-rw-r--r--activerecord/test/connections/native_mysql/connection.rb24
-rw-r--r--activerecord/test/connections/native_postgresql/connection.rb24
-rw-r--r--activerecord/test/connections/native_sqlite/connection.rb34
-rw-r--r--activerecord/test/connections/native_sqlserver/connection.rb15
-rwxr-xr-xactiverecord/test/deprecated_associations_test.rb335
-rwxr-xr-xactiverecord/test/finder_test.rb67
-rw-r--r--activerecord/test/fixtures/accounts.yml8
-rw-r--r--activerecord/test/fixtures/auto_id.rb4
-rw-r--r--activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char1
-rw-r--r--activerecord/test/fixtures/bad_fixtures/attr_with_spaces1
-rw-r--r--activerecord/test/fixtures/bad_fixtures/blank_line3
-rw-r--r--activerecord/test/fixtures/bad_fixtures/duplicate_attributes3
-rw-r--r--activerecord/test/fixtures/bad_fixtures/missing_value1
-rw-r--r--activerecord/test/fixtures/column_name.rb3
-rwxr-xr-xactiverecord/test/fixtures/companies/first_client6
-rwxr-xr-xactiverecord/test/fixtures/companies/first_firm4
-rwxr-xr-xactiverecord/test/fixtures/companies/second_client6
-rwxr-xr-xactiverecord/test/fixtures/company.rb37
-rw-r--r--activerecord/test/fixtures/company_in_module.rb47
-rw-r--r--activerecord/test/fixtures/course.rb3
-rw-r--r--activerecord/test/fixtures/courses/java2
-rw-r--r--activerecord/test/fixtures/courses/ruby2
-rw-r--r--activerecord/test/fixtures/customer.rb30
-rw-r--r--activerecord/test/fixtures/customers/david6
-rwxr-xr-xactiverecord/test/fixtures/db_definitions/mysql.sql97
-rw-r--r--activerecord/test/fixtures/db_definitions/mysql2.sql4
-rw-r--r--activerecord/test/fixtures/db_definitions/postgresql.sql114
-rw-r--r--activerecord/test/fixtures/db_definitions/postgresql2.sql4
-rw-r--r--activerecord/test/fixtures/db_definitions/sqlite.sql86
-rw-r--r--activerecord/test/fixtures/db_definitions/sqlite2.sql4
-rw-r--r--activerecord/test/fixtures/db_definitions/sqlserver.sql96
-rw-r--r--activerecord/test/fixtures/db_definitions/sqlserver2.sql4
-rw-r--r--activerecord/test/fixtures/default.rb2
-rw-r--r--activerecord/test/fixtures/developer.rb8
-rw-r--r--activerecord/test/fixtures/developers.yml13
-rw-r--r--activerecord/test/fixtures/developers_projects/david_action_controller3
-rw-r--r--activerecord/test/fixtures/developers_projects/david_active_record3
-rw-r--r--activerecord/test/fixtures/developers_projects/jamis_active_record2
-rw-r--r--activerecord/test/fixtures/entrant.rb3
-rw-r--r--activerecord/test/fixtures/entrants/first3
-rw-r--r--activerecord/test/fixtures/entrants/second3
-rw-r--r--activerecord/test/fixtures/entrants/third3
-rw-r--r--activerecord/test/fixtures/movie.rb5
-rw-r--r--activerecord/test/fixtures/movies/first2
-rw-r--r--activerecord/test/fixtures/movies/second2
-rw-r--r--activerecord/test/fixtures/project.rb4
-rw-r--r--activerecord/test/fixtures/projects/action_controller2
-rw-r--r--activerecord/test/fixtures/projects/active_record2
-rwxr-xr-xactiverecord/test/fixtures/reply.rb21
-rw-r--r--activerecord/test/fixtures/subscriber.rb5
-rw-r--r--activerecord/test/fixtures/subscribers/first2
-rw-r--r--activerecord/test/fixtures/subscribers/second2
-rwxr-xr-xactiverecord/test/fixtures/topic.rb20
-rwxr-xr-xactiverecord/test/fixtures/topics/first9
-rwxr-xr-xactiverecord/test/fixtures/topics/second8
-rwxr-xr-xactiverecord/test/fixtures_test.rb84
-rw-r--r--activerecord/test/inflector_test.rb121
-rwxr-xr-xactiverecord/test/inheritance_test.rb125
-rwxr-xr-xactiverecord/test/lifecycle_test.rb110
-rw-r--r--activerecord/test/modules_test.rb29
-rw-r--r--activerecord/test/multiple_db_test.rb46
-rw-r--r--activerecord/test/pk_test.rb59
-rw-r--r--activerecord/test/reflection_test.rb78
-rw-r--r--activerecord/test/thread_safety_test.rb33
-rw-r--r--activerecord/test/transactions_test.rb110
-rwxr-xr-xactiverecord/test/unconnected_test.rb24
-rwxr-xr-xactiverecord/test/validations_test.rb126
-rw-r--r--railties/CHANGELOG265
-rw-r--r--railties/MIT-LICENSE20
-rw-r--r--railties/README121
-rw-r--r--railties/Rakefile279
-rwxr-xr-xrailties/bin/rails28
-rwxr-xr-xrailties/configs/apache.conf31
-rw-r--r--railties/configs/database.yml20
-rwxr-xr-xrailties/dispatches/dispatch.fcgi7
-rwxr-xr-xrailties/dispatches/dispatch.rb10
-rw-r--r--railties/dispatches/dispatch.servlet49
-rw-r--r--railties/dispatches/start_server1
-rw-r--r--railties/doc/README_FOR_APP2
-rw-r--r--railties/doc/apache_protection3
-rw-r--r--railties/doc/index.html94
-rw-r--r--railties/environments/development.rb2
-rw-r--r--railties/environments/production.rb6
-rw-r--r--railties/environments/shared.rb35
-rw-r--r--railties/environments/shared_for_gem.rb23
-rw-r--r--railties/environments/test.rb2
-rwxr-xr-xrailties/fresh_rakefile104
-rwxr-xr-xrailties/generators/new_controller.rb43
-rwxr-xr-xrailties/generators/new_crud.rb34
-rw-r--r--railties/generators/new_mailer.rb43
-rwxr-xr-xrailties/generators/new_model.rb31
-rw-r--r--railties/generators/templates/controller.erb19
-rw-r--r--railties/generators/templates/controller_test.erb17
-rw-r--r--railties/generators/templates/controller_view.rhtml10
-rw-r--r--railties/generators/templates/helper.erb2
-rw-r--r--railties/generators/templates/mailer.erb15
-rw-r--r--railties/generators/templates/mailer_action.rhtml3
-rw-r--r--railties/generators/templates/mailer_fixture.rhtml4
-rw-r--r--railties/generators/templates/mailer_test.erb37
-rw-r--r--railties/generators/templates/model.erb2
-rw-r--r--railties/generators/templates/model_test.erb11
-rw-r--r--railties/helpers/abstract_application.rb5
-rw-r--r--railties/helpers/application_helper.rb3
-rw-r--r--railties/helpers/test_helper.rb16
-rw-r--r--railties/html/404.html6
-rw-r--r--railties/html/500.html6
-rw-r--r--railties/html/index.html1
-rw-r--r--railties/lib/code_statistics.rb71
-rw-r--r--railties/lib/dispatcher.rb55
-rw-r--r--railties/lib/generator.rb112
-rw-r--r--railties/lib/webrick_server.rb159
-rw-r--r--railties/test/webrick_dispatcher_test.rb30
296 files changed, 30881 insertions, 0 deletions
diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG
new file mode 100644
index 0000000000..f6c6961711
--- /dev/null
+++ b/actionmailer/CHANGELOG
@@ -0,0 +1,19 @@
+*0.4* (5)
+
+* Consolidated the server configuration options into Base#server_settings= and expanded that with controls for authentication and more [Marten]
+ NOTE: This is an API change that could potentially break your application if you used the old application form. Please do change!
+
+* Added Base#deliveries as an accessor for an array of emails sent out through that ActionMailer class when using the :test delivery option. [bitsweat]
+
+* Added Base#perform_deliveries= which can be set to false to turn off the actual delivery of the email through smtp or sendmail.
+ This is especially useful for functional testing that shouldn't send off real emails, but still trigger delivery_* methods.
+
+* Added option to specify delivery method with Base#delivery_method=. Default is :smtp and :sendmail is currently the only other option.
+ Sendmail is assumed to be present at "/usr/sbin/sendmail" if that option is used. [Kent Sibilev]
+
+* Dropped "include TMail" as it added to much baggage into the default namespace (like Version) [Chad Fowler]
+
+
+*0.3*
+
+* First release \ No newline at end of file
diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE
new file mode 100644
index 0000000000..26f55e7799
--- /dev/null
+++ b/actionmailer/MIT-LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2004 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/actionmailer/README b/actionmailer/README
new file mode 100755
index 0000000000..263da4f401
--- /dev/null
+++ b/actionmailer/README
@@ -0,0 +1,102 @@
+= Action Mailer -- Easy email delivery and testing
+
+Action Mailer is framework for designing email-service layers. These layers
+are used to consolidate code for sending out forgotten passwords, welcoming
+wishes on signup, invoices for billing, and any other use case that requires
+a written notification to either a person or another system.
+
+The framework works by setting up all the email details, except the body,
+in methods on the service layer. Subject, recipients, sender, and timestamp
+are all set up this way. An example of such a method:
+
+ def signed_up(recipient)
+ @recipients = recipient
+ @subject = "[Signed up] Welcome #{recipient}"
+ @from = "system@loudthinking.com"
+ @sent_on = Time.local(2004, 12, 12)
+
+ @body["recipient"] = recipient
+ end
+
+The body of the email is created by using an Action View template (regular
+ERb) that has the content of the @body hash available as instance variables.
+So the corresponding body template for the method above could look like this:
+
+ Hello there,
+
+ Mr. <%= @recipient %>
+
+And if the recipient was given as "david@loudthinking.com", the email
+generated would look like this:
+
+ Date: Sun, 12 Dec 2004 00:00:00 +0100
+ From: system@loudthinking.com
+ To: david@loudthinking.com
+ Subject: [Signed up] Welcome david@loudthinking.com
+
+ Hello there,
+
+ Mr. david@loudthinking.com
+
+You never actually call the instance methods like signed_up directly. Instead,
+you call class methods like deliver_* and create_* that are automatically
+created for each instance method. So if the signed_up method sat on
+ApplicationMailer, it would look like this:
+
+ ApplicationMailer.create_signed_up("david@loudthinking.com") # => tmail object for testing
+ ApplicationMailer.deliver_signed_up("david@loudthinking.com") # sends the email
+ ApplicationMailer.new.signed_up("david@loudthinking.com") # won't work!
+
+
+== Dependencies
+
+Action Mailer requires that the Action Pack is either available to be required immediately
+or is accessible as a GEM.
+
+
+== Bundled software
+
+* tmail 0.10.8 by Minero Aoki released under LGPL
+ Read more on http://i.loveruby.net/en/prog/tmail.html
+
+* Text::Format 0.63 by Austin Ziegler released under OpenSource
+ Read more on http://www.halostatue.ca/ruby/Text__Format.html
+
+
+== Download
+
+The latest version of Action Mailer can be found at
+
+* http://rubyforge.org/project/showfiles.php?group_id=361
+
+Documentation can be found at
+
+* http://actionmailer.rubyonrails.org
+
+
+== Installation
+
+You can install Action Mailer with the following command.
+
+ % [sudo] ruby install.rb
+
+from its distribution directory.
+
+
+== License
+
+Action Mailer is released under the MIT license.
+
+
+== Support
+
+The Action Mailer homepage is http://actionmailer.rubyonrails.org. You can find
+the Action Mailer RubyForge page at http://rubyforge.org/projects/actionmailer.
+And as Jim from Rake says:
+
+ Feel free to submit commits or feature requests. If you send a patch,
+ remember to update the corresponding unit tests. If fact, I prefer
+ new feature to be submitted in the form of new unit tests.
+
+For other information, feel free to ask on the ruby-talk mailing list (which
+is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com. \ No newline at end of file
diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile
new file mode 100755
index 0000000000..8ddc3179f2
--- /dev/null
+++ b/actionmailer/Rakefile
@@ -0,0 +1,107 @@
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+
+PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
+PKG_NAME = 'actionmailer'
+PKG_VERSION = '0.4.0' + PKG_BUILD
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+
+desc "Default Task"
+task :default => [ :test ]
+
+# Run the unit tests
+
+Rake::TestTask.new { |t|
+ t.libs << "test"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
+
+# Genereate the RDoc documentation
+
+Rake::RDocTask.new { |rdoc|
+ rdoc.rdoc_dir = 'doc'
+ rdoc.title = "Action Mailer -- Easy email delivery and testing"
+ rdoc.options << '--line-numbers --inline-source --main README'
+ rdoc.rdoc_files.include('README', 'CHANGELOG')
+ rdoc.rdoc_files.include('lib/action_mailer.rb')
+ rdoc.rdoc_files.include('lib/action_mailer/*.rb')
+}
+
+
+# Create compressed packages
+
+
+spec = Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = PKG_NAME
+ s.summary = "Service layer for easy email delivery and testing."
+ s.description = %q{Makes it trivial to test and deliver emails sent from a single service layer.}
+ s.version = PKG_VERSION
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.rubyforge_project = "actionmailer"
+ s.homepage = "http://actionmailer.rubyonrails.org"
+
+ s.add_dependency('actionpack', '>= 0.9.5')
+
+ s.has_rdoc = true
+ s.requirements << 'none'
+ s.require_path = 'lib'
+ s.autorequire = 'action_mailer'
+
+ s.files = [ "rakefile", "install.rb", "README", "CHANGELOG", "MIT-LICENSE" ]
+ s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "CVS" ) }
+ s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "CVS" ) }
+end
+
+Rake::GemPackageTask.new(spec) do |p|
+ p.gem_spec = spec
+ p.need_tar = true
+ p.need_zip = true
+end
+
+
+# Publish beta gem
+desc "Publish the API documentation"
+task :pgem => [:package] do
+ Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
+end
+
+# Publish documentation
+desc "Publish the API documentation"
+task :pdoc => [:rdoc] do
+ Rake::SshDirPublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/am", "doc").upload
+end
+
+desc "Publish to RubyForge"
+task :rubyforge do
+ Rake::RubyForgePublisher.new('actionmailer', 'webster132').upload
+end
+
+
+desc "Count lines in the main rake file"
+task :lines do
+ lines = 0
+ codelines = 0
+ Dir.foreach("lib/action_mailer") { |file_name|
+ next unless file_name =~ /.*rb/
+
+ f = File.open("lib/action_mailer/" + file_name)
+
+ while line = f.gets
+ lines += 1
+ next if line =~ /^\s*$/
+ next if line =~ /^\s*#/
+ codelines += 1
+ end
+ }
+ puts "Lines #{lines}, LOC #{codelines}"
+end
diff --git a/actionmailer/install.rb b/actionmailer/install.rb
new file mode 100644
index 0000000000..4791ab1f4c
--- /dev/null
+++ b/actionmailer/install.rb
@@ -0,0 +1,61 @@
+require 'rbconfig'
+require 'find'
+require 'ftools'
+
+include Config
+
+# this was adapted from rdoc's install.rb by ways of Log4r
+
+$sitedir = CONFIG["sitelibdir"]
+unless $sitedir
+ version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
+ $libdir = File.join(CONFIG["libdir"], "ruby", version)
+ $sitedir = $:.find {|x| x =~ /site_ruby/ }
+ if !$sitedir
+ $sitedir = File.join($libdir, "site_ruby")
+ elsif $sitedir !~ Regexp.quote(version)
+ $sitedir = File.join($sitedir, version)
+ end
+end
+
+makedirs = %w{ action_mailer/vendor action_mailer/vendor/text action_mailer/vendor/tmail }
+makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))}
+
+# deprecated files that should be removed
+# deprecated = %w{ }
+
+# files to install in library path
+files = %w-
+ action_mailer.rb
+ action_mailer/base.rb
+ action_mailer/mail_helper.rb
+ action_mailer/vendor/text/format.rb
+ action_mailer/vendor/tmail.rb
+ action_mailer/vendor/tmail/address.rb
+ action_mailer/vendor/tmail/base64.rb
+ action_mailer/vendor/tmail/config.rb
+ action_mailer/vendor/tmail/encode.rb
+ action_mailer/vendor/tmail/facade.rb
+ action_mailer/vendor/tmail/header.rb
+ action_mailer/vendor/tmail/info.rb
+ action_mailer/vendor/tmail/loader.rb
+ action_mailer/vendor/tmail/mail.rb
+ action_mailer/vendor/tmail/mailbox.rb
+ action_mailer/vendor/tmail/mbox.rb
+ action_mailer/vendor/tmail/net.rb
+ action_mailer/vendor/tmail/obsolete.rb
+ action_mailer/vendor/tmail/parser.rb
+ action_mailer/vendor/tmail/port.rb
+ action_mailer/vendor/tmail/scanner.rb
+ action_mailer/vendor/tmail/scanner_r.rb
+ action_mailer/vendor/tmail/stringio.rb
+ action_mailer/vendor/tmail/tmail.rb
+ action_mailer/vendor/tmail/utils.rb
+-
+
+# the acual gruntwork
+Dir.chdir("lib")
+# File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
+files.each {|f|
+ File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
+}
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
new file mode 100755
index 0000000000..48fe4ccb16
--- /dev/null
+++ b/actionmailer/lib/action_mailer.rb
@@ -0,0 +1,43 @@
+#--
+# Copyright (c) 2004 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+begin
+ require 'action_controller'
+rescue LoadError
+ # Action Pack is not already available, try RubyGems
+ require 'rubygems'
+ require_gem 'actionpack', '>= 0.9.0'
+end
+
+$:.unshift(File.dirname(__FILE__) + "/action_mailer/vendor/")
+
+require 'action_mailer/base'
+require 'action_mailer/mail_helper'
+require 'action_mailer/vendor/tmail'
+require 'net/smtp'
+
+ActionView::Base.class_eval { include MailHelper }
+
+old_verbose, $VERBOSE = $VERBOSE, nil
+TMail::Encoder.const_set("MAX_LINE_LEN", 200)
+$VERBOSE = old_verbose \ No newline at end of file
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
new file mode 100644
index 0000000000..389499c8fa
--- /dev/null
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -0,0 +1,152 @@
+module ActionMailer #:nodoc:
+ # Usage:
+ #
+ # class ApplicationMailer < ActionMailer::Base
+ # def post_notification(recipients, post)
+ # @recipients = recipients
+ # @subject = "[#{post.account.name} #{post.title}]"
+ # @body["post"] = post
+ # @from = post.author.email_address_with_name
+ # end
+ #
+ # def comment_notification(recipient, comment)
+ # @recipients = recipient.email_address_with_name
+ # @subject = "[#{comment.post.project.client.firm.account.name}]" +
+ # " Re: #{comment.post.title}"
+ # @body["comment"] = comment
+ # @from = comment.author.email_address_with_name
+ # @sent_on = comment.posted_on
+ # end
+ # end
+ #
+ # # After this post_notification will look for "templates/application_mailer/post_notification.rhtml"
+ # ApplicationMailer.template_root = "templates"
+ #
+ # ApplicationMailer.create_comment_notification(david, hello_world) # => a tmail object
+ # ApplicationMailer.deliver_comment_notification(david, hello_world) # sends the email
+ class Base
+ private_class_method :new
+
+ # Template root determines the base from which template references will be made.
+ cattr_accessor :template_root
+
+ # The logger is used for generating information on the mailing run if available.
+ # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
+ cattr_accessor :logger
+
+ # Allows detailed configuration of the server:
+ # * <tt>:address</tt> Allows you to use a remote mail server. Just change it away from it's default "localhost" setting.
+ # * <tt>:port</tt> On the off change that your mail server doesn't run on port 25, you can change it.
+ # * <tt>:domain</tt> If you need to specify a HELO domain, you can do it here.
+ # * <tt>:user_name</tt> If your mail server requires authentication, set the username and password in these two settings.
+ # * <tt>:password</tt> If your mail server requires authentication, set the username and password in these two settings.
+ # * <tt>:authentication</tt> If your mail server requires authentication, you need to specify the authentication type here.
+ # This is a symbol and one of :plain, :login, :cram_md5
+ @@server_settings = {
+ :address => "localhost",
+ :port => 25,
+ :domain => 'localhost.localdomain',
+ :user_name => nil,
+ :password => nil,
+ :authentication => nil
+ }
+ cattr_accessor :server_settings
+
+
+ # Whether or not errors should be raised if the email fails to be delivered
+ @@raise_delivery_errors = true
+ cattr_accessor :raise_delivery_errors
+
+ # Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test.
+ # Sendmail is assumed to be present at "/usr/sbin/sendmail".
+ @@delivery_method = :smtp
+ cattr_accessor :delivery_method
+
+ # Determines whether deliver_* methods are actually carried out. By default they are,
+ # but this can be turned off to help functional testing.
+ @@perform_deliveries = true
+ cattr_accessor :perform_deliveries
+
+ # Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful
+ # for unit and functional testing.
+ @@deliveries = []
+ cattr_accessor :deliveries
+
+ attr_accessor :recipients, :subject, :body, :from, :sent_on, :bcc, :cc
+
+ class << self
+ def method_missing(method_symbol, *parameters)#:nodoc:
+ case method_symbol.id2name
+ when /^create_([_a-z]*)/
+ create_from_action($1, *parameters)
+ when /^deliver_([_a-z]*)/
+ begin
+ deliver(send("create_" + $1, *parameters))
+ rescue Object => e
+ raise e if raise_delivery_errors
+ end
+ end
+ end
+
+ def mail(to, subject, body, from, timestamp = nil) #:nodoc:
+ deliver(create(to, subject, body, from, timestamp))
+ end
+
+ def create(to, subject, body, from, timestamp = nil) #:nodoc:
+ m = TMail::Mail.new
+ m.to, m.subject, m.body, m.from = to, subject, body, from
+ m.date = timestamp.respond_to?("to_time") ? timestamp.to_time : (timestamp || Time.now)
+ return m
+ end
+
+ def deliver(mail) #:nodoc:
+ logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil?
+ send("perform_delivery_#{delivery_method}", mail) if perform_deliveries
+ end
+
+ private
+ def perform_delivery_smtp(mail)
+ Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain],
+ server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
+ smtp.sendmail(mail.encoded, mail.from_address, mail.destinations)
+ end
+ end
+
+ def perform_delivery_sendmail(mail)
+ IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm|
+ sm.print(mail.encoded)
+ sm.flush
+ end
+ end
+
+ def perform_delivery_test(mail)
+ deliveries << mail
+ end
+
+ def create_from_action(method_name, *parameters)
+ mailer = new
+ mailer.body = {}
+ mailer.send(method_name, *parameters)
+
+ if String === mailer.body
+ mail = create(mailer.recipients, mailer.subject, mailer.body, mailer.from, mailer.sent_on)
+ else
+ mail = create(mailer.recipients, mailer.subject, render_body(mailer, method_name), mailer.from, mailer.sent_on)
+ end
+
+ mail.bcc = @bcc if @bcc
+ mail.cc = @cc if @cc
+
+ return mail
+ end
+
+ def render_body(mailer, method_name)
+ ActionView::Base.new(template_path, mailer.body).render_file(method_name)
+ end
+
+ def template_path
+ template_root + "/" + Inflector.underscore(self.to_s)
+ end
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb
new file mode 100644
index 0000000000..0235fcdc61
--- /dev/null
+++ b/actionmailer/lib/action_mailer/mail_helper.rb
@@ -0,0 +1,17 @@
+require 'action_mailer/vendor/text/format'
+
+module MailHelper#:nodoc:
+ def block_format(text)
+ formatted = text.split(/\n\r\n/).collect { |paragraph|
+ Text::Format.new(
+ :columns => 72, :first_indent => 2, :body_indent => 2, :text => paragraph
+ ).format
+ }.join("\n")
+
+ # Make list points stand on their own line
+ formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" }
+ formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" }
+
+ formatted
+ end
+end \ No newline at end of file
diff --git a/actionmailer/lib/action_mailer/vendor/text/format.rb b/actionmailer/lib/action_mailer/vendor/text/format.rb
new file mode 100755
index 0000000000..591c69cf91
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/text/format.rb
@@ -0,0 +1,1447 @@
+#--
+# Text::Format for Ruby
+# Version 0.63
+#
+# Copyright (c) 2002 - 2003 Austin Ziegler
+#
+# $Id: format.rb,v 1.1.1.1 2004/10/14 11:59:57 webster132 Exp $
+#
+# ==========================================================================
+# Revision History ::
+# YYYY.MM.DD Change ID Developer
+# Description
+# --------------------------------------------------------------------------
+# 2002.10.18 Austin Ziegler
+# Fixed a minor problem with tabs not being counted. Changed
+# abbreviations from Hash to Array to better suit Ruby's
+# capabilities. Fixed problems with the way that Array arguments
+# are handled in calls to the major object types, excepting in
+# Text::Format#expand and Text::Format#unexpand (these will
+# probably need to be fixed).
+# 2002.10.30 Austin Ziegler
+# Fixed the ordering of the <=> for binary tests. Fixed
+# Text::Format#expand and Text::Format#unexpand to handle array
+# arguments better.
+# 2003.01.24 Austin Ziegler
+# Fixed a problem with Text::Format::RIGHT_FILL handling where a
+# single word is larger than #columns. Removed Comparable
+# capabilities (<=> doesn't make sense; == does). Added Symbol
+# equivalents for the Hash initialization. Hash initialization has
+# been modified so that values are set as follows (Symbols are
+# highest priority; strings are middle; defaults are lowest):
+# @columns = arg[:columns] || arg['columns'] || @columns
+# Added #hard_margins, #split_rules, #hyphenator, and #split_words.
+# 2003.02.07 Austin Ziegler
+# Fixed the installer for proper case-sensitive handling.
+# 2003.03.28 Austin Ziegler
+# Added the ability for a hyphenator to receive the formatter
+# object. Fixed a bug for strings matching /\A\s*\Z/ failing
+# entirely. Fixed a test case failing under 1.6.8.
+# 2003.04.04 Austin Ziegler
+# Handle the case of hyphenators returning nil for first/rest.
+# 2003.09.17 Austin Ziegler
+# Fixed a problem where #paragraphs(" ") was raising
+# NoMethodError.
+#
+# ==========================================================================
+#++
+
+module Text #:nodoc:
+ # = Introduction
+ #
+ # Text::Format provides the ability to nicely format fixed-width text with
+ # knowledge of the writeable space (number of columns), margins, and
+ # indentation settings.
+ #
+ # Copyright:: Copyright (c) 2002 - 2003 by Austin Ziegler
+ # Version:: 0.63
+ # Based On:: Perl
+ # Text::Format[http://search.cpan.org/author/GABOR/Text-Format0.52/lib/Text/Format.pm],
+ # Copyright (c) 1998 Gábor Egressy
+ # Licence:: Ruby's, Perl Artistic, or GPL version 2 (or later)
+ #
+ class Format
+ VERSION = '0.63'
+
+ # Local abbreviations. More can be added with Text::Format.abbreviations
+ ABBREV = [ 'Mr', 'Mrs', 'Ms', 'Jr', 'Sr' ]
+
+ # Formatting values
+ LEFT_ALIGN = 0
+ RIGHT_ALIGN = 1
+ RIGHT_FILL = 2
+ JUSTIFY = 3
+
+ # Word split modes (only applies when #hard_margins is true).
+ SPLIT_FIXED = 1
+ SPLIT_CONTINUATION = 2
+ SPLIT_HYPHENATION = 4
+ SPLIT_CONTINUATION_FIXED = SPLIT_CONTINUATION | SPLIT_FIXED
+ SPLIT_HYPHENATION_FIXED = SPLIT_HYPHENATION | SPLIT_FIXED
+ SPLIT_HYPHENATION_CONTINUATION = SPLIT_HYPHENATION | SPLIT_CONTINUATION
+ SPLIT_ALL = SPLIT_HYPHENATION | SPLIT_CONTINUATION | SPLIT_FIXED
+
+ # Words forcibly split by Text::Format will be stored as split words.
+ # This class represents a word forcibly split.
+ class SplitWord
+ # The word that was split.
+ attr_reader :word
+ # The first part of the word that was split.
+ attr_reader :first
+ # The remainder of the word that was split.
+ attr_reader :rest
+
+ def initialize(word, first, rest) #:nodoc:
+ @word = word
+ @first = first
+ @rest = rest
+ end
+ end
+
+ private
+ LEQ_RE = /[.?!]['"]?$/
+
+ def brk_re(i) #:nodoc:
+ %r/((?:\S+\s+){#{i}})(.+)/
+ end
+
+ def posint(p) #:nodoc:
+ p.to_i.abs
+ end
+
+ public
+ # Compares two Text::Format objects. All settings of the objects are
+ # compared *except* #hyphenator. Generated results (e.g., #split_words)
+ # are not compared, either.
+ def ==(o)
+ (@text == o.text) &&
+ (@columns == o.columns) &&
+ (@left_margin == o.left_margin) &&
+ (@right_margin == o.right_margin) &&
+ (@hard_margins == o.hard_margins) &&
+ (@split_rules == o.split_rules) &&
+ (@first_indent == o.first_indent) &&
+ (@body_indent == o.body_indent) &&
+ (@tag_text == o.tag_text) &&
+ (@tabstop == o.tabstop) &&
+ (@format_style == o.format_style) &&
+ (@extra_space == o.extra_space) &&
+ (@tag_paragraph == o.tag_paragraph) &&
+ (@nobreak == o.nobreak) &&
+ (@abbreviations == o.abbreviations) &&
+ (@nobreak_regex == o.nobreak_regex)
+ end
+
+ # The text to be manipulated. Note that value is optional, but if the
+ # formatting functions are called without values, this text is what will
+ # be formatted.
+ #
+ # *Default*:: <tt>[]</tt>
+ # <b>Used in</b>:: All methods
+ attr_accessor :text
+
+ # The total width of the format area. The margins, indentation, and text
+ # are formatted into this space.
+ #
+ # COLUMNS
+ # <-------------------------------------------------------------->
+ # <-----------><------><---------------------------><------------>
+ # left margin indent text is formatted into here right margin
+ #
+ # *Default*:: <tt>72</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
+ # <tt>#center</tt>
+ attr_reader :columns
+
+ # The total width of the format area. The margins, indentation, and text
+ # are formatted into this space. The value provided is silently
+ # converted to a positive integer.
+ #
+ # COLUMNS
+ # <-------------------------------------------------------------->
+ # <-----------><------><---------------------------><------------>
+ # left margin indent text is formatted into here right margin
+ #
+ # *Default*:: <tt>72</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
+ # <tt>#center</tt>
+ def columns=(c)
+ @columns = posint(c)
+ end
+
+ # The number of spaces used for the left margin.
+ #
+ # columns
+ # <-------------------------------------------------------------->
+ # <-----------><------><---------------------------><------------>
+ # LEFT MARGIN indent text is formatted into here right margin
+ #
+ # *Default*:: <tt>0</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
+ # <tt>#center</tt>
+ attr_reader :left_margin
+
+ # The number of spaces used for the left margin. The value provided is
+ # silently converted to a positive integer value.
+ #
+ # columns
+ # <-------------------------------------------------------------->
+ # <-----------><------><---------------------------><------------>
+ # LEFT MARGIN indent text is formatted into here right margin
+ #
+ # *Default*:: <tt>0</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
+ # <tt>#center</tt>
+ def left_margin=(left)
+ @left_margin = posint(left)
+ end
+
+ # The number of spaces used for the right margin.
+ #
+ # columns
+ # <-------------------------------------------------------------->
+ # <-----------><------><---------------------------><------------>
+ # left margin indent text is formatted into here RIGHT MARGIN
+ #
+ # *Default*:: <tt>0</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
+ # <tt>#center</tt>
+ attr_reader :right_margin
+
+ # The number of spaces used for the right margin. The value provided is
+ # silently converted to a positive integer value.
+ #
+ # columns
+ # <-------------------------------------------------------------->
+ # <-----------><------><---------------------------><------------>
+ # left margin indent text is formatted into here RIGHT MARGIN
+ #
+ # *Default*:: <tt>0</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
+ # <tt>#center</tt>
+ def right_margin=(r)
+ @right_margin = posint(r)
+ end
+
+ # The number of spaces to indent the first line of a paragraph.
+ #
+ # columns
+ # <-------------------------------------------------------------->
+ # <-----------><------><---------------------------><------------>
+ # left margin INDENT text is formatted into here right margin
+ #
+ # *Default*:: <tt>4</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_reader :first_indent
+
+ # The number of spaces to indent the first line of a paragraph. The
+ # value provided is silently converted to a positive integer value.
+ #
+ # columns
+ # <-------------------------------------------------------------->
+ # <-----------><------><---------------------------><------------>
+ # left margin INDENT text is formatted into here right margin
+ #
+ # *Default*:: <tt>4</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ def first_indent=(f)
+ @first_indent = posint(f)
+ end
+
+ # The number of spaces to indent all lines after the first line of a
+ # paragraph.
+ #
+ # columns
+ # <-------------------------------------------------------------->
+ # <-----------><------><---------------------------><------------>
+ # left margin INDENT text is formatted into here right margin
+ #
+ # *Default*:: <tt>0</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_reader :body_indent
+
+ # The number of spaces to indent all lines after the first line of
+ # a paragraph. The value provided is silently converted to a
+ # positive integer value.
+ #
+ # columns
+ # <-------------------------------------------------------------->
+ # <-----------><------><---------------------------><------------>
+ # left margin INDENT text is formatted into here right margin
+ #
+ # *Default*:: <tt>0</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ def body_indent=(b)
+ @body_indent = posint(b)
+ end
+
+ # Normally, words larger than the format area will be placed on a line
+ # by themselves. Setting this to +true+ will force words larger than the
+ # format area to be split into one or more "words" each at most the size
+ # of the format area. The first line and the original word will be
+ # placed into <tt>#split_words</tt>. Note that this will cause the
+ # output to look *similar* to a #format_style of JUSTIFY. (Lines will be
+ # filled as much as possible.)
+ #
+ # *Default*:: +false+
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_accessor :hard_margins
+
+ # An array of words split during formatting if #hard_margins is set to
+ # +true+.
+ # #split_words << Text::Format::SplitWord.new(word, first, rest)
+ attr_reader :split_words
+
+ # The object responsible for hyphenating. It must respond to
+ # #hyphenate_to(word, size) or #hyphenate_to(word, size, formatter) and
+ # return an array of the word split into two parts; if there is a
+ # hyphenation mark to be applied, responsibility belongs to the
+ # hyphenator object. The size is the MAXIMUM size permitted, including
+ # any hyphenation marks. If the #hyphenate_to method has an arity of 3,
+ # the formatter will be provided to the method. This allows the
+ # hyphenator to make decisions about the hyphenation based on the
+ # formatting rules.
+ #
+ # *Default*:: +nil+
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_reader :hyphenator
+
+ # The object responsible for hyphenating. It must respond to
+ # #hyphenate_to(word, size) and return an array of the word hyphenated
+ # into two parts. The size is the MAXIMUM size permitted, including any
+ # hyphenation marks.
+ #
+ # *Default*:: +nil+
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ def hyphenator=(h)
+ raise ArgumentError, "#{h.inspect} is not a valid hyphenator." unless h.respond_to?(:hyphenate_to)
+ arity = h.method(:hyphenate_to).arity
+ raise ArgumentError, "#{h.inspect} must have exactly two or three arguments." unless [2, 3].include?(arity)
+ @hyphenator = h
+ @hyphenator_arity = arity
+ end
+
+ # Specifies the split mode; used only when #hard_margins is set to
+ # +true+. Allowable values are:
+ # [+SPLIT_FIXED+] The word will be split at the number of
+ # characters needed, with no marking at all.
+ # repre
+ # senta
+ # ion
+ # [+SPLIT_CONTINUATION+] The word will be split at the number of
+ # characters needed, with a C-style continuation
+ # character. If a word is the only item on a
+ # line and it cannot be split into an
+ # appropriate size, SPLIT_FIXED will be used.
+ # repr\
+ # esen\
+ # tati\
+ # on
+ # [+SPLIT_HYPHENATION+] The word will be split according to the
+ # hyphenator specified in #hyphenator. If there
+ # is no #hyphenator specified, works like
+ # SPLIT_CONTINUATION. The example is using
+ # TeX::Hyphen. If a word is the only item on a
+ # line and it cannot be split into an
+ # appropriate size, SPLIT_CONTINUATION mode will
+ # be used.
+ # rep-
+ # re-
+ # sen-
+ # ta-
+ # tion
+ #
+ # *Default*:: <tt>Text::Format::SPLIT_FIXED</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_reader :split_rules
+
+ # Specifies the split mode; used only when #hard_margins is set to
+ # +true+. Allowable values are:
+ # [+SPLIT_FIXED+] The word will be split at the number of
+ # characters needed, with no marking at all.
+ # repre
+ # senta
+ # ion
+ # [+SPLIT_CONTINUATION+] The word will be split at the number of
+ # characters needed, with a C-style continuation
+ # character.
+ # repr\
+ # esen\
+ # tati\
+ # on
+ # [+SPLIT_HYPHENATION+] The word will be split according to the
+ # hyphenator specified in #hyphenator. If there
+ # is no #hyphenator specified, works like
+ # SPLIT_CONTINUATION. The example is using
+ # TeX::Hyphen as the #hyphenator.
+ # rep-
+ # re-
+ # sen-
+ # ta-
+ # tion
+ #
+ # These values can be bitwise ORed together (e.g., <tt>SPLIT_FIXED |
+ # SPLIT_CONTINUATION</tt>) to provide fallback split methods. In the
+ # example given, an attempt will be made to split the word using the
+ # rules of SPLIT_CONTINUATION; if there is not enough room, the word
+ # will be split with the rules of SPLIT_FIXED. These combinations are
+ # also available as the following values:
+ # * +SPLIT_CONTINUATION_FIXED+
+ # * +SPLIT_HYPHENATION_FIXED+
+ # * +SPLIT_HYPHENATION_CONTINUATION+
+ # * +SPLIT_ALL+
+ #
+ # *Default*:: <tt>Text::Format::SPLIT_FIXED</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ def split_rules=(s)
+ raise ArgumentError, "Invalid value provided for split_rules." if ((s < SPLIT_FIXED) || (s > SPLIT_ALL))
+ @split_rules = s
+ end
+
+ # Indicates whether sentence terminators should be followed by a single
+ # space (+false+), or two spaces (+true+).
+ #
+ # *Default*:: +false+
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_accessor :extra_space
+
+ # Defines the current abbreviations as an array. This is only used if
+ # extra_space is turned on.
+ #
+ # If one is abbreviating "President" as "Pres." (abbreviations =
+ # ["Pres"]), then the results of formatting will be as illustrated in
+ # the table below:
+ #
+ # extra_space | include? | !include?
+ # true | Pres. Lincoln | Pres. Lincoln
+ # false | Pres. Lincoln | Pres. Lincoln
+ #
+ # *Default*:: <tt>{}</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_accessor :abbreviations
+
+ # Indicates whether the formatting of paragraphs should be done with
+ # tagged paragraphs. Useful only with <tt>#tag_text</tt>.
+ #
+ # *Default*:: +false+
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_accessor :tag_paragraph
+
+ # The array of text to be placed before each paragraph when
+ # <tt>#tag_paragraph</tt> is +true+. When <tt>#format()</tt> is called,
+ # only the first element of the array is used. When <tt>#paragraphs</tt>
+ # is called, then each entry in the array will be used once, with
+ # corresponding paragraphs. If the tag elements are exhausted before the
+ # text is exhausted, then the remaining paragraphs will not be tagged.
+ # Regardless of indentation settings, a blank line will be inserted
+ # between all paragraphs when <tt>#tag_paragraph</tt> is +true+.
+ #
+ # *Default*:: <tt>[]</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_accessor :tag_text
+
+ # Indicates whether or not the non-breaking space feature should be
+ # used.
+ #
+ # *Default*:: +false+
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_accessor :nobreak
+
+ # A hash which holds the regular expressions on which spaces should not
+ # be broken. The hash is set up such that the key is the first word and
+ # the value is the second word.
+ #
+ # For example, if +nobreak_regex+ contains the following hash:
+ #
+ # { '^Mrs?\.$' => '\S+$', '^\S+$' => '^(?:S|J)r\.$'}
+ #
+ # Then "Mr. Jones", "Mrs. Jones", and "Jones Jr." would not be broken.
+ # If this simple matching algorithm indicates that there should not be a
+ # break at the current end of line, then a backtrack is done until there
+ # are two words on which line breaking is permitted. If two such words
+ # are not found, then the end of the line will be broken *regardless*.
+ # If there is a single word on the current line, then no backtrack is
+ # done and the word is stuck on the end.
+ #
+ # *Default*:: <tt>{}</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_accessor :nobreak_regex
+
+ # Indicates the number of spaces that a single tab represents.
+ #
+ # *Default*:: <tt>8</tt>
+ # <b>Used in</b>:: <tt>#expand</tt>, <tt>#unexpand</tt>,
+ # <tt>#paragraphs</tt>
+ attr_reader :tabstop
+
+ # Indicates the number of spaces that a single tab represents.
+ #
+ # *Default*:: <tt>8</tt>
+ # <b>Used in</b>:: <tt>#expand</tt>, <tt>#unexpand</tt>,
+ # <tt>#paragraphs</tt>
+ def tabstop=(t)
+ @tabstop = posint(t)
+ end
+
+ # Specifies the format style. Allowable values are:
+ # [+LEFT_ALIGN+] Left justified, ragged right.
+ # |A paragraph that is|
+ # |left aligned.|
+ # [+RIGHT_ALIGN+] Right justified, ragged left.
+ # |A paragraph that is|
+ # | right aligned.|
+ # [+RIGHT_FILL+] Left justified, right ragged, filled to width by
+ # spaces. (Essentially the same as +LEFT_ALIGN+ except
+ # that lines are padded on the right.)
+ # |A paragraph that is|
+ # |left aligned. |
+ # [+JUSTIFY+] Fully justified, words filled to width by spaces,
+ # except the last line.
+ # |A paragraph that|
+ # |is justified.|
+ #
+ # *Default*:: <tt>Text::Format::LEFT_ALIGN</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ attr_reader :format_style
+
+ # Specifies the format style. Allowable values are:
+ # [+LEFT_ALIGN+] Left justified, ragged right.
+ # |A paragraph that is|
+ # |left aligned.|
+ # [+RIGHT_ALIGN+] Right justified, ragged left.
+ # |A paragraph that is|
+ # | right aligned.|
+ # [+RIGHT_FILL+] Left justified, right ragged, filled to width by
+ # spaces. (Essentially the same as +LEFT_ALIGN+ except
+ # that lines are padded on the right.)
+ # |A paragraph that is|
+ # |left aligned. |
+ # [+JUSTIFY+] Fully justified, words filled to width by spaces.
+ # |A paragraph that|
+ # |is justified.|
+ #
+ # *Default*:: <tt>Text::Format::LEFT_ALIGN</tt>
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ def format_style=(fs)
+ raise ArgumentError, "Invalid value provided for format_style." if ((fs < LEFT_ALIGN) || (fs > JUSTIFY))
+ @format_style = fs
+ end
+
+ # Indicates that the format style is left alignment.
+ #
+ # *Default*:: +true+
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ def left_align?
+ return @format_style == LEFT_ALIGN
+ end
+
+ # Indicates that the format style is right alignment.
+ #
+ # *Default*:: +false+
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ def right_align?
+ return @format_style == RIGHT_ALIGN
+ end
+
+ # Indicates that the format style is right fill.
+ #
+ # *Default*:: +false+
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ def right_fill?
+ return @format_style == RIGHT_FILL
+ end
+
+ # Indicates that the format style is full justification.
+ #
+ # *Default*:: +false+
+ # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
+ def justify?
+ return @format_style == JUSTIFY
+ end
+
+ # The default implementation of #hyphenate_to implements
+ # SPLIT_CONTINUATION.
+ def hyphenate_to(word, size)
+ [word[0 .. (size - 2)] + "\\", word[(size - 1) .. -1]]
+ end
+
+ private
+ def __do_split_word(word, size) #:nodoc:
+ [word[0 .. (size - 1)], word[size .. -1]]
+ end
+
+ def __format(to_wrap) #:nodoc:
+ words = to_wrap.split(/\s+/).compact
+ words.shift if words[0].nil? or words[0].empty?
+ to_wrap = []
+
+ abbrev = false
+ width = @columns - @first_indent - @left_margin - @right_margin
+ indent_str = ' ' * @first_indent
+ first_line = true
+ line = words.shift
+ abbrev = __is_abbrev(line) unless line.nil? || line.empty?
+
+ while w = words.shift
+ if (w.size + line.size < (width - 1)) ||
+ ((line !~ LEQ_RE || abbrev) && (w.size + line.size < width))
+ line << " " if (line =~ LEQ_RE) && (not abbrev)
+ line << " #{w}"
+ else
+ line, w = __do_break(line, w) if @nobreak
+ line, w = __do_hyphenate(line, w, width) if @hard_margins
+ if w.index(/\s+/)
+ w, *w2 = w.split(/\s+/)
+ words.unshift(w2)
+ words.flatten!
+ end
+ to_wrap << __make_line(line, indent_str, width, w.nil?) unless line.nil?
+ if first_line
+ first_line = false
+ width = @columns - @body_indent - @left_margin - @right_margin
+ indent_str = ' ' * @body_indent
+ end
+ line = w
+ end
+
+ abbrev = __is_abbrev(w) unless w.nil?
+ end
+
+ loop do
+ break if line.nil? or line.empty?
+ line, w = __do_hyphenate(line, w, width) if @hard_margins
+ to_wrap << __make_line(line, indent_str, width, w.nil?)
+ line = w
+ end
+
+ if (@tag_paragraph && (to_wrap.size > 0)) then
+ clr = %r{`(\w+)'}.match([caller(1)].flatten[0])[1]
+ clr = "" if clr.nil?
+
+ if ((not @tag_text[0].nil?) && (@tag_cur.size < 1) &&
+ (clr != "__paragraphs")) then
+ @tag_cur = @tag_text[0]
+ end
+
+ fchar = /(\S)/.match(to_wrap[0])[1]
+ white = to_wrap[0].index(fchar)
+ if ((white - @left_margin - 1) > @tag_cur.size) then
+ white = @tag_cur.size + @left_margin
+ to_wrap[0].gsub!(/^ {#{white}}/, "#{' ' * @left_margin}#{@tag_cur}")
+ else
+ to_wrap.unshift("#{' ' * @left_margin}#{@tag_cur}\n")
+ end
+ end
+ to_wrap.join('')
+ end
+
+ # format lines in text into paragraphs with each element of @wrap a
+ # paragraph; uses Text::Format.format for the formatting
+ def __paragraphs(to_wrap) #:nodoc:
+ if ((@first_indent == @body_indent) || @tag_paragraph) then
+ p_end = "\n"
+ else
+ p_end = ''
+ end
+
+ cnt = 0
+ ret = []
+ to_wrap.each do |tw|
+ @tag_cur = @tag_text[cnt] if @tag_paragraph
+ @tag_cur = '' if @tag_cur.nil?
+ line = __format(tw)
+ ret << "#{line}#{p_end}" if (not line.nil?) && (line.size > 0)
+ cnt += 1
+ end
+
+ ret[-1].chomp! unless ret.empty?
+ ret.join('')
+ end
+
+ # center text using spaces on left side to pad it out empty lines
+ # are preserved
+ def __center(to_center) #:nodoc:
+ tabs = 0
+ width = @columns - @left_margin - @right_margin
+ centered = []
+ to_center.each do |tc|
+ s = tc.strip
+ tabs = s.count("\t")
+ tabs = 0 if tabs.nil?
+ ct = ((width - s.size - (tabs * @tabstop) + tabs) / 2)
+ ct = (width - @left_margin - @right_margin) - ct
+ centered << "#{s.rjust(ct)}\n"
+ end
+ centered.join('')
+ end
+
+ # expand tabs to spaces should be similar to Text::Tabs::expand
+ def __expand(to_expand) #:nodoc:
+ expanded = []
+ to_expand.split("\n").each { |te| expanded << te.gsub(/\t/, ' ' * @tabstop) }
+ expanded.join('')
+ end
+
+ def __unexpand(to_unexpand) #:nodoc:
+ unexpanded = []
+ to_unexpand.split("\n").each { |tu| unexpanded << tu.gsub(/ {#{@tabstop}}/, "\t") }
+ unexpanded.join('')
+ end
+
+ def __is_abbrev(word) #:nodoc:
+ # remove period if there is one.
+ w = word.gsub(/\.$/, '') unless word.nil?
+ return true if (!@extra_space || ABBREV.include?(w) || @abbreviations.include?(w))
+ false
+ end
+
+ def __make_line(line, indent, width, last = false) #:nodoc:
+ lmargin = " " * @left_margin
+ fill = " " * (width - line.size) if right_fill? && (line.size <= width)
+
+ if (justify? && ((not line.nil?) && (not line.empty?)) && line =~ /\S+\s+\S+/ && !last)
+ spaces = width - line.size
+ words = line.split(/(\s+)/)
+ ws = spaces / (words.size / 2)
+ spaces = spaces % (words.size / 2) if ws > 0
+ words.reverse.each do |rw|
+ next if (rw =~ /^\S/)
+ rw.sub!(/^/, " " * ws)
+ next unless (spaces > 0)
+ rw.sub!(/^/, " ")
+ spaces -= 1
+ end
+ line = words.join('')
+ end
+ line = "#{lmargin}#{indent}#{line}#{fill}\n" unless line.nil?
+ if right_align? && (not line.nil?)
+ line.sub(/^/, " " * (@columns - @right_margin - (line.size - 1)))
+ else
+ line
+ end
+ end
+
+ def __do_hyphenate(line, next_line, width) #:nodoc:
+ rline = line.dup rescue line
+ rnext = next_line.dup rescue next_line
+ loop do
+ if rline.size == width
+ break
+ elsif rline.size > width
+ words = rline.strip.split(/\s+/)
+ word = words[-1].dup
+ size = width - rline.size + word.size
+ if (size <= 0)
+ words[-1] = nil
+ rline = words.join(' ').strip
+ rnext = "#{word} #{rnext}".strip
+ next
+ end
+
+ first = rest = nil
+
+ if ((@split_rules & SPLIT_HYPHENATION) != 0)
+ if @hyphenator_arity == 2
+ first, rest = @hyphenator.hyphenate_to(word, size)
+ else
+ first, rest = @hyphenator.hyphenate_to(word, size, self)
+ end
+ end
+
+ if ((@split_rules & SPLIT_CONTINUATION) != 0) and first.nil?
+ first, rest = self.hyphenate_to(word, size)
+ end
+
+ if ((@split_rules & SPLIT_FIXED) != 0) and first.nil?
+ first.nil? or @split_rules == SPLIT_FIXED
+ first, rest = __do_split_word(word, size)
+ end
+
+ if first.nil?
+ words[-1] = nil
+ rest = word
+ else
+ words[-1] = first
+ @split_words << SplitWord.new(word, first, rest)
+ end
+ rline = words.join(' ').strip
+ rnext = "#{rest} #{rnext}".strip
+ break
+ else
+ break if rnext.nil? or rnext.empty? or rline.nil? or rline.empty?
+ words = rnext.split(/\s+/)
+ word = words.shift
+ size = width - rline.size - 1
+
+ if (size <= 0)
+ rnext = "#{word} #{words.join(' ')}".strip
+ break
+ end
+
+ first = rest = nil
+
+ if ((@split_rules & SPLIT_HYPHENATION) != 0)
+ if @hyphenator_arity == 2
+ first, rest = @hyphenator.hyphenate_to(word, size)
+ else
+ first, rest = @hyphenator.hyphenate_to(word, size, self)
+ end
+ end
+
+ first, rest = self.hyphenate_to(word, size) if ((@split_rules & SPLIT_CONTINUATION) != 0) and first.nil?
+
+ first, rest = __do_split_word(word, size) if ((@split_rules & SPLIT_FIXED) != 0) and first.nil?
+
+ if (rline.size + (first ? first.size : 0)) < width
+ @split_words << SplitWord.new(word, first, rest)
+ rline = "#{rline} #{first}".strip
+ rnext = "#{rest} #{words.join(' ')}".strip
+ end
+ break
+ end
+ end
+ [rline, rnext]
+ end
+
+ def __do_break(line, next_line) #:nodoc:
+ no_brk = false
+ words = []
+ words = line.split(/\s+/) unless line.nil?
+ last_word = words[-1]
+
+ @nobreak_regex.each { |k, v| no_brk = ((last_word =~ /#{k}/) and (next_line =~ /#{v}/)) }
+
+ if no_brk && words.size > 1
+ i = words.size
+ while i > 0
+ no_brk = false
+ @nobreak_regex.each { |k, v| no_brk = ((words[i + 1] =~ /#{k}/) && (words[i] =~ /#{v}/)) }
+ i -= 1
+ break if not no_brk
+ end
+ if i > 0
+ l = brk_re(i).match(line)
+ line.sub!(brk_re(i), l[1])
+ next_line = "#{l[2]} #{next_line}"
+ line.sub!(/\s+$/, '')
+ end
+ end
+ [line, next_line]
+ end
+
+ def __create(arg = nil, &block) #:nodoc:
+ # Format::Text.new(text-to-wrap)
+ @text = arg unless arg.nil?
+ # Defaults
+ @columns = 72
+ @tabstop = 8
+ @first_indent = 4
+ @body_indent = 0
+ @format_style = LEFT_ALIGN
+ @left_margin = 0
+ @right_margin = 0
+ @extra_space = false
+ @text = Array.new if @text.nil?
+ @tag_paragraph = false
+ @tag_text = Array.new
+ @tag_cur = ""
+ @abbreviations = Array.new
+ @nobreak = false
+ @nobreak_regex = Hash.new
+ @split_words = Array.new
+ @hard_margins = false
+ @split_rules = SPLIT_FIXED
+ @hyphenator = self
+ @hyphenator_arity = self.method(:hyphenate_to).arity
+
+ instance_eval(&block) unless block.nil?
+ end
+
+ public
+ # Formats text into a nice paragraph format. The text is separated
+ # into words and then reassembled a word at a time using the settings
+ # of this Format object. If a word is larger than the number of
+ # columns available for formatting, then that word will appear on the
+ # line by itself.
+ #
+ # If +to_wrap+ is +nil+, then the value of <tt>#text</tt> will be
+ # worked on.
+ def format(to_wrap = nil)
+ to_wrap = @text if to_wrap.nil?
+ if to_wrap.class == Array
+ __format(to_wrap[0])
+ else
+ __format(to_wrap)
+ end
+ end
+
+ # Considers each element of text (provided or internal) as a paragraph.
+ # If <tt>#first_indent</tt> is the same as <tt>#body_indent</tt>, then
+ # paragraphs will be separated by a single empty line in the result;
+ # otherwise, the paragraphs will follow immediately after each other.
+ # Uses <tt>#format</tt> to do the heavy lifting.
+ def paragraphs(to_wrap = nil)
+ to_wrap = @text if to_wrap.nil?
+ __paragraphs([to_wrap].flatten)
+ end
+
+ # Centers the text, preserving empty lines and tabs.
+ def center(to_center = nil)
+ to_center = @text if to_center.nil?
+ __center([to_center].flatten)
+ end
+
+ # Replaces all tab characters in the text with <tt>#tabstop</tt> spaces.
+ def expand(to_expand = nil)
+ to_expand = @text if to_expand.nil?
+ if to_expand.class == Array
+ to_expand.collect { |te| __expand(te) }
+ else
+ __expand(to_expand)
+ end
+ end
+
+ # Replaces all occurrences of <tt>#tabstop</tt> consecutive spaces
+ # with a tab character.
+ def unexpand(to_unexpand = nil)
+ to_unexpand = @text if to_unexpand.nil?
+ if to_unexpand.class == Array
+ to_unexpand.collect { |te| v << __unexpand(te) }
+ else
+ __unexpand(to_unexpand)
+ end
+ end
+
+ # This constructor takes advantage of a technique for Ruby object
+ # construction introduced by Andy Hunt and Dave Thomas (see reference),
+ # where optional values are set using commands in a block.
+ #
+ # Text::Format.new {
+ # columns = 72
+ # left_margin = 0
+ # right_margin = 0
+ # first_indent = 4
+ # body_indent = 0
+ # format_style = Text::Format::LEFT_ALIGN
+ # extra_space = false
+ # abbreviations = {}
+ # tag_paragraph = false
+ # tag_text = []
+ # nobreak = false
+ # nobreak_regex = {}
+ # tabstop = 8
+ # text = nil
+ # }
+ #
+ # As shown above, +arg+ is optional. If +arg+ is specified and is a
+ # +String+, then arg is used as the default value of <tt>#text</tt>.
+ # Alternately, an existing Text::Format object can be used or a Hash can
+ # be used. With all forms, a block can be specified.
+ #
+ # *Reference*:: "Object Construction and Blocks"
+ # <http://www.pragmaticprogrammer.com/ruby/articles/insteval.html>
+ #
+ def initialize(arg = nil, &block)
+ case arg
+ when Text::Format
+ __create(arg.text) do
+ @columns = arg.columns
+ @tabstop = arg.tabstop
+ @first_indent = arg.first_indent
+ @body_indent = arg.body_indent
+ @format_style = arg.format_style
+ @left_margin = arg.left_margin
+ @right_margin = arg.right_margin
+ @extra_space = arg.extra_space
+ @tag_paragraph = arg.tag_paragraph
+ @tag_text = arg.tag_text
+ @abbreviations = arg.abbreviations
+ @nobreak = arg.nobreak
+ @nobreak_regex = arg.nobreak_regex
+ @text = arg.text
+ @hard_margins = arg.hard_margins
+ @split_words = arg.split_words
+ @split_rules = arg.split_rules
+ @hyphenator = arg.hyphenator
+ end
+ instance_eval(&block) unless block.nil?
+ when Hash
+ __create do
+ @columns = arg[:columns] || arg['columns'] || @columns
+ @tabstop = arg[:tabstop] || arg['tabstop'] || @tabstop
+ @first_indent = arg[:first_indent] || arg['first_indent'] || @first_indent
+ @body_indent = arg[:body_indent] || arg['body_indent'] || @body_indent
+ @format_style = arg[:format_style] || arg['format_style'] || @format_style
+ @left_margin = arg[:left_margin] || arg['left_margin'] || @left_margin
+ @right_margin = arg[:right_margin] || arg['right_margin'] || @right_margin
+ @extra_space = arg[:extra_space] || arg['extra_space'] || @extra_space
+ @text = arg[:text] || arg['text'] || @text
+ @tag_paragraph = arg[:tag_paragraph] || arg['tag_paragraph'] || @tag_paragraph
+ @tag_text = arg[:tag_text] || arg['tag_text'] || @tag_text
+ @abbreviations = arg[:abbreviations] || arg['abbreviations'] || @abbreviations
+ @nobreak = arg[:nobreak] || arg['nobreak'] || @nobreak
+ @nobreak_regex = arg[:nobreak_regex] || arg['nobreak_regex'] || @nobreak_regex
+ @hard_margins = arg[:hard_margins] || arg['hard_margins'] || @hard_margins
+ @split_rules = arg[:split_rules] || arg['split_rules'] || @split_rules
+ @hyphenator = arg[:hyphenator] || arg['hyphenator'] || @hyphenator
+ end
+ instance_eval(&block) unless block.nil?
+ when String
+ __create(arg, &block)
+ when NilClass
+ __create(&block)
+ else
+ raise TypeError
+ end
+ end
+ end
+end
+
+if __FILE__ == $0
+ require 'test/unit'
+
+ class TestText__Format < Test::Unit::TestCase #:nodoc:
+ attr_accessor :format_o
+
+ GETTYSBURG = <<-'EOS'
+ Four score and seven years ago our fathers brought forth on this
+ continent a new nation, conceived in liberty and dedicated to the
+ proposition that all men are created equal. Now we are engaged in
+ a great civil war, testing whether that nation or any nation so
+ conceived and so dedicated can long endure. We are met on a great
+ battlefield of that war. We have come to dedicate a portion of
+ that field as a final resting-place for those who here gave their
+ lives that that nation might live. It is altogether fitting and
+ proper that we should do this. But in a larger sense, we cannot
+ dedicate, we cannot consecrate, we cannot hallow this ground.
+ The brave men, living and dead who struggled here have consecrated
+ it far above our poor power to add or detract. The world will
+ little note nor long remember what we say here, but it can never
+ forget what they did here. It is for us the living rather to be
+ dedicated here to the unfinished work which they who fought here
+ have thus far so nobly advanced. It is rather for us to be here
+ dedicated to the great task remaining before us--that from these
+ honored dead we take increased devotion to that cause for which
+ they gave the last full measure of devotion--that we here highly
+ resolve that these dead shall not have died in vain, that this
+ nation under God shall have a new birth of freedom, and that
+ government of the people, by the people, for the people shall
+ not perish from the earth.
+
+ -- Pres. Abraham Lincoln, 19 November 1863
+ EOS
+
+ FIVE_COL = "Four \nscore\nand s\neven \nyears\nago o\nur fa\nthers\nbroug\nht fo\nrth o\nn thi\ns con\ntinen\nt a n\new na\ntion,\nconce\nived \nin li\nberty\nand d\nedica\nted t\no the\npropo\nsitio\nn tha\nt all\nmen a\nre cr\neated\nequal\n. Now\nwe ar\ne eng\naged \nin a \ngreat\ncivil\nwar, \ntesti\nng wh\nether\nthat \nnatio\nn or \nany n\nation\nso co\nnceiv\ned an\nd so \ndedic\nated \ncan l\nong e\nndure\n. We \nare m\net on\na gre\nat ba\nttlef\nield \nof th\nat wa\nr. We\nhave \ncome \nto de\ndicat\ne a p\nortio\nn of \nthat \nfield\nas a \nfinal\nresti\nng-pl\nace f\nor th\nose w\nho he\nre ga\nve th\neir l\nives \nthat \nthat \nnatio\nn mig\nht li\nve. I\nt is \naltog\nether\nfitti\nng an\nd pro\nper t\nhat w\ne sho\nuld d\no thi\ns. Bu\nt in \na lar\nger s\nense,\nwe ca\nnnot \ndedic\nate, \nwe ca\nnnot \nconse\ncrate\n, we \ncanno\nt hal\nlow t\nhis g\nround\n. The\nbrave\nmen, \nlivin\ng and\ndead \nwho s\ntrugg\nled h\nere h\nave c\nonsec\nrated\nit fa\nr abo\nve ou\nr poo\nr pow\ner to\nadd o\nr det\nract.\nThe w\norld \nwill \nlittl\ne not\ne nor\nlong \nremem\nber w\nhat w\ne say\nhere,\nbut i\nt can\nnever\nforge\nt wha\nt the\ny did\nhere.\nIt is\nfor u\ns the\nlivin\ng rat\nher t\no be \ndedic\nated \nhere \nto th\ne unf\ninish\ned wo\nrk wh\nich t\nhey w\nho fo\nught \nhere \nhave \nthus \nfar s\no nob\nly ad\nvance\nd. It\nis ra\nther \nfor u\ns to \nbe he\nre de\ndicat\ned to\nthe g\nreat \ntask \nremai\nning \nbefor\ne us-\n-that\nfrom \nthese\nhonor\ned de\nad we\ntake \nincre\nased \ndevot\nion t\no tha\nt cau\nse fo\nr whi\nch th\ney ga\nve th\ne las\nt ful\nl mea\nsure \nof de\nvotio\nn--th\nat we\nhere \nhighl\ny res\nolve \nthat \nthese\ndead \nshall\nnot h\nave d\nied i\nn vai\nn, th\nat th\nis na\ntion \nunder\nGod s\nhall \nhave \na new\nbirth\nof fr\needom\n, and\nthat \ngover\nnment\nof th\ne peo\nple, \nby th\ne peo\nple, \nfor t\nhe pe\nople \nshall\nnot p\nerish\nfrom \nthe e\narth.\n-- Pr\nes. A\nbraha\nm Lin\ncoln,\n19 No\nvembe\nr 186\n3 \n"
+
+ FIVE_CNT = "Four \nscore\nand \nseven\nyears\nago \nour \nfath\\\ners \nbrou\\\nght \nforth\non t\\\nhis \ncont\\\ninent\na new\nnati\\\non, \nconc\\\neived\nin l\\\niber\\\nty a\\\nnd d\\\nedic\\\nated \nto t\\\nhe p\\\nropo\\\nsiti\\\non t\\\nhat \nall \nmen \nare \ncrea\\\nted \nequa\\\nl. N\\\now we\nare \nenga\\\nged \nin a \ngreat\ncivil\nwar, \ntest\\\ning \nwhet\\\nher \nthat \nnati\\\non or\nany \nnati\\\non so\nconc\\\neived\nand \nso d\\\nedic\\\nated \ncan \nlong \nendu\\\nre. \nWe a\\\nre m\\\net on\na gr\\\neat \nbatt\\\nlefi\\\neld \nof t\\\nhat \nwar. \nWe h\\\nave \ncome \nto d\\\nedic\\\nate a\nport\\\nion \nof t\\\nhat \nfield\nas a \nfinal\nrest\\\ning-\\\nplace\nfor \nthose\nwho \nhere \ngave \ntheir\nlives\nthat \nthat \nnati\\\non m\\\night \nlive.\nIt is\nalto\\\ngeth\\\ner f\\\nitti\\\nng a\\\nnd p\\\nroper\nthat \nwe s\\\nhould\ndo t\\\nhis. \nBut \nin a \nlarg\\\ner s\\\nense,\nwe c\\\nannot\ndedi\\\ncate,\nwe c\\\nannot\ncons\\\necra\\\nte, \nwe c\\\nannot\nhall\\\now t\\\nhis \ngrou\\\nnd. \nThe \nbrave\nmen, \nlivi\\\nng a\\\nnd d\\\nead \nwho \nstru\\\nggled\nhere \nhave \ncons\\\necra\\\nted \nit f\\\nar a\\\nbove \nour \npoor \npower\nto a\\\ndd or\ndetr\\\nact. \nThe \nworld\nwill \nlitt\\\nle n\\\note \nnor \nlong \nreme\\\nmber \nwhat \nwe s\\\nay h\\\nere, \nbut \nit c\\\nan n\\\never \nforg\\\net w\\\nhat \nthey \ndid \nhere.\nIt is\nfor \nus t\\\nhe l\\\niving\nrath\\\ner to\nbe d\\\nedic\\\nated \nhere \nto t\\\nhe u\\\nnfin\\\nished\nwork \nwhich\nthey \nwho \nfoug\\\nht h\\\nere \nhave \nthus \nfar \nso n\\\nobly \nadva\\\nnced.\nIt is\nrath\\\ner f\\\nor us\nto be\nhere \ndedi\\\ncated\nto t\\\nhe g\\\nreat \ntask \nrema\\\nining\nbefo\\\nre u\\\ns--t\\\nhat \nfrom \nthese\nhono\\\nred \ndead \nwe t\\\nake \nincr\\\neased\ndevo\\\ntion \nto t\\\nhat \ncause\nfor \nwhich\nthey \ngave \nthe \nlast \nfull \nmeas\\\nure \nof d\\\nevot\\\nion-\\\n-that\nwe h\\\nere \nhigh\\\nly r\\\nesol\\\nve t\\\nhat \nthese\ndead \nshall\nnot \nhave \ndied \nin v\\\nain, \nthat \nthis \nnati\\\non u\\\nnder \nGod \nshall\nhave \na new\nbirth\nof f\\\nreed\\\nom, \nand \nthat \ngove\\\nrnme\\\nnt of\nthe \npeop\\\nle, \nby t\\\nhe p\\\neopl\\\ne, f\\\nor t\\\nhe p\\\neople\nshall\nnot \nperi\\\nsh f\\\nrom \nthe \neart\\\nh. --\nPres.\nAbra\\\nham \nLinc\\\noln, \n19 N\\\novem\\\nber \n1863 \n"
+
+ # Tests both abbreviations and abbreviations=
+ def test_abbreviations
+ abbr = [" Pres. Abraham Lincoln\n", " Pres. Abraham Lincoln\n"]
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal([], @format_o.abbreviations)
+ assert_nothing_raised { @format_o.abbreviations = [ 'foo', 'bar' ] }
+ assert_equal([ 'foo', 'bar' ], @format_o.abbreviations)
+ assert_equal(abbr[0], @format_o.format(abbr[0]))
+ assert_nothing_raised { @format_o.extra_space = true }
+ assert_equal(abbr[1], @format_o.format(abbr[0]))
+ assert_nothing_raised { @format_o.abbreviations = [ "Pres" ] }
+ assert_equal([ "Pres" ], @format_o.abbreviations)
+ assert_equal(abbr[0], @format_o.format(abbr[0]))
+ assert_nothing_raised { @format_o.extra_space = false }
+ assert_equal(abbr[0], @format_o.format(abbr[0]))
+ end
+
+ # Tests both body_indent and body_indent=
+ def test_body_indent
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal(0, @format_o.body_indent)
+ assert_nothing_raised { @format_o.body_indent = 7 }
+ assert_equal(7, @format_o.body_indent)
+ assert_nothing_raised { @format_o.body_indent = -3 }
+ assert_equal(3, @format_o.body_indent)
+ assert_nothing_raised { @format_o.body_indent = "9" }
+ assert_equal(9, @format_o.body_indent)
+ assert_nothing_raised { @format_o.body_indent = "-2" }
+ assert_equal(2, @format_o.body_indent)
+ assert_match(/^ [^ ]/, @format_o.format(GETTYSBURG).split("\n")[1])
+ end
+
+ # Tests both columns and columns=
+ def test_columns
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal(72, @format_o.columns)
+ assert_nothing_raised { @format_o.columns = 7 }
+ assert_equal(7, @format_o.columns)
+ assert_nothing_raised { @format_o.columns = -3 }
+ assert_equal(3, @format_o.columns)
+ assert_nothing_raised { @format_o.columns = "9" }
+ assert_equal(9, @format_o.columns)
+ assert_nothing_raised { @format_o.columns = "-2" }
+ assert_equal(2, @format_o.columns)
+ assert_nothing_raised { @format_o.columns = 40 }
+ assert_equal(40, @format_o.columns)
+ assert_match(/this continent$/,
+ @format_o.format(GETTYSBURG).split("\n")[1])
+ end
+
+ # Tests both extra_space and extra_space=
+ def test_extra_space
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert(!@format_o.extra_space)
+ assert_nothing_raised { @format_o.extra_space = true }
+ assert(@format_o.extra_space)
+ # The behaviour of extra_space is tested in test_abbreviations. There
+ # is no need to reproduce it here.
+ end
+
+ # Tests both first_indent and first_indent=
+ def test_first_indent
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal(4, @format_o.first_indent)
+ assert_nothing_raised { @format_o.first_indent = 7 }
+ assert_equal(7, @format_o.first_indent)
+ assert_nothing_raised { @format_o.first_indent = -3 }
+ assert_equal(3, @format_o.first_indent)
+ assert_nothing_raised { @format_o.first_indent = "9" }
+ assert_equal(9, @format_o.first_indent)
+ assert_nothing_raised { @format_o.first_indent = "-2" }
+ assert_equal(2, @format_o.first_indent)
+ assert_match(/^ [^ ]/, @format_o.format(GETTYSBURG).split("\n")[0])
+ end
+
+ def test_format_style
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal(Text::Format::LEFT_ALIGN, @format_o.format_style)
+ assert_match(/^November 1863$/,
+ @format_o.format(GETTYSBURG).split("\n")[-1])
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::RIGHT_ALIGN
+ }
+ assert_equal(Text::Format::RIGHT_ALIGN, @format_o.format_style)
+ assert_match(/^ +November 1863$/,
+ @format_o.format(GETTYSBURG).split("\n")[-1])
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::RIGHT_FILL
+ }
+ assert_equal(Text::Format::RIGHT_FILL, @format_o.format_style)
+ assert_match(/^November 1863 +$/,
+ @format_o.format(GETTYSBURG).split("\n")[-1])
+ assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY }
+ assert_equal(Text::Format::JUSTIFY, @format_o.format_style)
+ assert_match(/^of freedom, and that government of the people, by the people, for the$/,
+ @format_o.format(GETTYSBURG).split("\n")[-3])
+ assert_raises(ArgumentError) { @format_o.format_style = 33 }
+ end
+
+ def test_tag_paragraph
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert(!@format_o.tag_paragraph)
+ assert_nothing_raised { @format_o.tag_paragraph = true }
+ assert(@format_o.tag_paragraph)
+ assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG]),
+ Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG]))
+ end
+
+ def test_tag_text
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal([], @format_o.tag_text)
+ assert_equal(@format_o.format(GETTYSBURG),
+ Text::Format.new.format(GETTYSBURG))
+ assert_nothing_raised {
+ @format_o.tag_paragraph = true
+ @format_o.tag_text = ["Gettysburg Address", "---"]
+ }
+ assert_not_equal(@format_o.format(GETTYSBURG),
+ Text::Format.new.format(GETTYSBURG))
+ assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG]),
+ Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG]))
+ assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG,
+ GETTYSBURG]),
+ Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG,
+ GETTYSBURG]))
+ end
+
+ def test_justify?
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert(!@format_o.justify?)
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::RIGHT_ALIGN
+ }
+ assert(!@format_o.justify?)
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::RIGHT_FILL
+ }
+ assert(!@format_o.justify?)
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::JUSTIFY
+ }
+ assert(@format_o.justify?)
+ # The format testing is done in test_format_style
+ end
+
+ def test_left_align?
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert(@format_o.left_align?)
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::RIGHT_ALIGN
+ }
+ assert(!@format_o.left_align?)
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::RIGHT_FILL
+ }
+ assert(!@format_o.left_align?)
+ assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY }
+ assert(!@format_o.left_align?)
+ # The format testing is done in test_format_style
+ end
+
+ def test_left_margin
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal(0, @format_o.left_margin)
+ assert_nothing_raised { @format_o.left_margin = -3 }
+ assert_equal(3, @format_o.left_margin)
+ assert_nothing_raised { @format_o.left_margin = "9" }
+ assert_equal(9, @format_o.left_margin)
+ assert_nothing_raised { @format_o.left_margin = "-2" }
+ assert_equal(2, @format_o.left_margin)
+ assert_nothing_raised { @format_o.left_margin = 7 }
+ assert_equal(7, @format_o.left_margin)
+ assert_nothing_raised {
+ ft = @format_o.format(GETTYSBURG).split("\n")
+ assert_match(/^ {11}Four score/, ft[0])
+ assert_match(/^ {7}November/, ft[-1])
+ }
+ end
+
+ def test_hard_margins
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert(!@format_o.hard_margins)
+ assert_nothing_raised {
+ @format_o.hard_margins = true
+ @format_o.columns = 5
+ @format_o.first_indent = 0
+ @format_o.format_style = Text::Format::RIGHT_FILL
+ }
+ assert(@format_o.hard_margins)
+ assert_equal(FIVE_COL, @format_o.format(GETTYSBURG))
+ assert_nothing_raised {
+ @format_o.split_rules |= Text::Format::SPLIT_CONTINUATION
+ assert_equal(Text::Format::SPLIT_CONTINUATION_FIXED,
+ @format_o.split_rules)
+ }
+ assert_equal(FIVE_CNT, @format_o.format(GETTYSBURG))
+ end
+
+ # Tests both nobreak and nobreak_regex, since one is only useful
+ # with the other.
+ def test_nobreak
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert(!@format_o.nobreak)
+ assert(@format_o.nobreak_regex.empty?)
+ assert_nothing_raised {
+ @format_o.nobreak = true
+ @format_o.nobreak_regex = { '^this$' => '^continent$' }
+ @format_o.columns = 77
+ }
+ assert(@format_o.nobreak)
+ assert_equal({ '^this$' => '^continent$' }, @format_o.nobreak_regex)
+ assert_match(/^this continent/,
+ @format_o.format(GETTYSBURG).split("\n")[1])
+ end
+
+ def test_right_align?
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert(!@format_o.right_align?)
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::RIGHT_ALIGN
+ }
+ assert(@format_o.right_align?)
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::RIGHT_FILL
+ }
+ assert(!@format_o.right_align?)
+ assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY }
+ assert(!@format_o.right_align?)
+ # The format testing is done in test_format_style
+ end
+
+ def test_right_fill?
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert(!@format_o.right_fill?)
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::RIGHT_ALIGN
+ }
+ assert(!@format_o.right_fill?)
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::RIGHT_FILL
+ }
+ assert(@format_o.right_fill?)
+ assert_nothing_raised {
+ @format_o.format_style = Text::Format::JUSTIFY
+ }
+ assert(!@format_o.right_fill?)
+ # The format testing is done in test_format_style
+ end
+
+ def test_right_margin
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal(0, @format_o.right_margin)
+ assert_nothing_raised { @format_o.right_margin = -3 }
+ assert_equal(3, @format_o.right_margin)
+ assert_nothing_raised { @format_o.right_margin = "9" }
+ assert_equal(9, @format_o.right_margin)
+ assert_nothing_raised { @format_o.right_margin = "-2" }
+ assert_equal(2, @format_o.right_margin)
+ assert_nothing_raised { @format_o.right_margin = 7 }
+ assert_equal(7, @format_o.right_margin)
+ assert_nothing_raised {
+ ft = @format_o.format(GETTYSBURG).split("\n")
+ assert_match(/^ {4}Four score.*forth on$/, ft[0])
+ assert_match(/^November/, ft[-1])
+ }
+ end
+
+ def test_tabstop
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal(8, @format_o.tabstop)
+ assert_nothing_raised { @format_o.tabstop = 7 }
+ assert_equal(7, @format_o.tabstop)
+ assert_nothing_raised { @format_o.tabstop = -3 }
+ assert_equal(3, @format_o.tabstop)
+ assert_nothing_raised { @format_o.tabstop = "9" }
+ assert_equal(9, @format_o.tabstop)
+ assert_nothing_raised { @format_o.tabstop = "-2" }
+ assert_equal(2, @format_o.tabstop)
+ end
+
+ def test_text
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal([], @format_o.text)
+ assert_nothing_raised { @format_o.text = "Test Text" }
+ assert_equal("Test Text", @format_o.text)
+ assert_nothing_raised { @format_o.text = ["Line 1", "Line 2"] }
+ assert_equal(["Line 1", "Line 2"], @format_o.text)
+ end
+
+ def test_s_new
+ # new(NilClass) { block }
+ assert_nothing_raised do
+ @format_o = Text::Format.new {
+ self.text = "Test 1, 2, 3"
+ }
+ end
+ assert_equal("Test 1, 2, 3", @format_o.text)
+
+ # new(Hash Symbols)
+ assert_nothing_raised { @format_o = Text::Format.new(:columns => 72) }
+ assert_equal(72, @format_o.columns)
+
+ # new(Hash String)
+ assert_nothing_raised { @format_o = Text::Format.new('columns' => 72) }
+ assert_equal(72, @format_o.columns)
+
+ # new(Hash) { block }
+ assert_nothing_raised do
+ @format_o = Text::Format.new('columns' => 80) {
+ self.text = "Test 4, 5, 6"
+ }
+ end
+ assert_equal("Test 4, 5, 6", @format_o.text)
+ assert_equal(80, @format_o.columns)
+
+ # new(Text::Format)
+ assert_nothing_raised do
+ fo = Text::Format.new(@format_o)
+ assert(fo == @format_o)
+ end
+
+ # new(Text::Format) { block }
+ assert_nothing_raised do
+ fo = Text::Format.new(@format_o) { self.columns = 79 }
+ assert(fo != @format_o)
+ end
+
+ # new(String)
+ assert_nothing_raised { @format_o = Text::Format.new("Test A, B, C") }
+ assert_equal("Test A, B, C", @format_o.text)
+
+ # new(String) { block }
+ assert_nothing_raised do
+ @format_o = Text::Format.new("Test X, Y, Z") { self.columns = -5 }
+ end
+ assert_equal("Test X, Y, Z", @format_o.text)
+ assert_equal(5, @format_o.columns)
+ end
+
+ def test_center
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_nothing_raised do
+ ct = @format_o.center(GETTYSBURG.split("\n")).split("\n")
+ assert_match(/^ Four score and seven years ago our fathers brought forth on this/, ct[0])
+ assert_match(/^ not perish from the earth./, ct[-3])
+ end
+ end
+
+ def test_expand
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal(" ", @format_o.expand("\t "))
+ assert_nothing_raised { @format_o.tabstop = 4 }
+ assert_equal(" ", @format_o.expand("\t "))
+ end
+
+ def test_unexpand
+ assert_nothing_raised { @format_o = Text::Format.new }
+ assert_equal("\t ", @format_o.unexpand(" "))
+ assert_nothing_raised { @format_o.tabstop = 4 }
+ assert_equal("\t ", @format_o.unexpand(" "))
+ end
+
+ def test_space_only
+ assert_equal("", Text::Format.new.format(" "))
+ assert_equal("", Text::Format.new.format("\n"))
+ assert_equal("", Text::Format.new.format(" "))
+ assert_equal("", Text::Format.new.format(" \n"))
+ assert_equal("", Text::Format.new.paragraphs("\n"))
+ assert_equal("", Text::Format.new.paragraphs(" "))
+ assert_equal("", Text::Format.new.paragraphs(" "))
+ assert_equal("", Text::Format.new.paragraphs(" \n"))
+ assert_equal("", Text::Format.new.paragraphs(["\n"]))
+ assert_equal("", Text::Format.new.paragraphs([" "]))
+ assert_equal("", Text::Format.new.paragraphs([" "]))
+ assert_equal("", Text::Format.new.paragraphs([" \n"]))
+ end
+
+ def test_splendiferous
+ h = nil
+ test = "This is a splendiferous test"
+ assert_nothing_raised { @format_o = Text::Format.new(:columns => 6, :left_margin => 0, :indent => 0, :first_indent => 0) }
+ assert_match(/^splendiferous$/, @format_o.format(test))
+ assert_nothing_raised { @format_o.hard_margins = true }
+ assert_match(/^lendif$/, @format_o.format(test))
+ assert_nothing_raised { h = Object.new }
+ assert_nothing_raised do
+ @format_o.split_rules = Text::Format::SPLIT_HYPHENATION
+ class << h #:nodoc:
+ def hyphenate_to(word, size)
+ return ["", word] if size < 2
+ [word[0 ... size], word[size .. -1]]
+ end
+ end
+ @format_o.hyphenator = h
+ end
+ assert_match(/^iferou$/, @format_o.format(test))
+ assert_nothing_raised { h = Object.new }
+ assert_nothing_raised do
+ class << h #:nodoc:
+ def hyphenate_to(word, size, formatter)
+ return ["", word] if word.size < formatter.columns
+ [word[0 ... size], word[size .. -1]]
+ end
+ end
+ @format_o.hyphenator = h
+ end
+ assert_match(/^ferous$/, @format_o.format(test))
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/vendor/tmail.rb b/actionmailer/lib/action_mailer/vendor/tmail.rb
new file mode 100755
index 0000000000..013f3020c2
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail.rb
@@ -0,0 +1,4 @@
+require 'tmail/info'
+require 'tmail/mail'
+require 'tmail/mailbox'
+require 'tmail/obsolete'
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/address.rb b/actionmailer/lib/action_mailer/vendor/tmail/address.rb
new file mode 100755
index 0000000000..5367633205
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/address.rb
@@ -0,0 +1,223 @@
+#
+# address.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'tmail/encode'
+require 'tmail/parser'
+
+
+module TMail
+
+ class Address
+
+ include TextUtils
+
+ def Address.parse( str )
+ Parser.parse :ADDRESS, str
+ end
+
+ def address_group?
+ false
+ end
+
+ def initialize( local, domain )
+ if domain
+ domain.each do |s|
+ raise SyntaxError, 'empty word in domain' if s.empty?
+ end
+ end
+ @local = local
+ @domain = domain
+ @name = nil
+ @routes = []
+ end
+
+ attr_reader :name
+
+ def name=( str )
+ @name = str
+ @name = nil if str and str.empty?
+ end
+
+ alias phrase name
+ alias phrase= name=
+
+ attr_reader :routes
+
+ def inspect
+ "#<#{self.class} #{address()}>"
+ end
+
+ def local
+ return nil unless @local
+ return '""' if @local.size == 1 and @local[0].empty?
+ @local.map {|i| quote_atom(i) }.join('.')
+ end
+
+ def domain
+ return nil unless @domain
+ join_domain(@domain)
+ end
+
+ def spec
+ s = self.local
+ d = self.domain
+ if s and d
+ s + '@' + d
+ else
+ s
+ end
+ end
+
+ alias address spec
+
+
+ def ==( other )
+ other.respond_to? :spec and self.spec == other.spec
+ end
+
+ alias eql? ==
+
+ def hash
+ @local.hash ^ @domain.hash
+ end
+
+ def dup
+ obj = self.class.new(@local.dup, @domain.dup)
+ obj.name = @name.dup if @name
+ obj.routes.replace @routes
+ obj
+ end
+
+ include StrategyInterface
+
+ def accept( strategy, dummy1 = nil, dummy2 = nil )
+ unless @local
+ strategy.meta '<>' # empty return-path
+ return
+ end
+
+ spec_p = (not @name and @routes.empty?)
+ if @name
+ strategy.phrase @name
+ strategy.space
+ end
+ tmp = spec_p ? '' : '<'
+ unless @routes.empty?
+ tmp << @routes.map {|i| '@' + i }.join(',') << ':'
+ end
+ tmp << self.spec
+ tmp << '>' unless spec_p
+ strategy.meta tmp
+ strategy.lwsp ''
+ end
+
+ end
+
+
+ class AddressGroup
+
+ include Enumerable
+
+ def address_group?
+ true
+ end
+
+ def initialize( name, addrs )
+ @name = name
+ @addresses = addrs
+ end
+
+ attr_reader :name
+
+ def ==( other )
+ other.respond_to? :to_a and @addresses == other.to_a
+ end
+
+ alias eql? ==
+
+ def hash
+ map {|i| i.hash }.hash
+ end
+
+ def []( idx )
+ @addresses[idx]
+ end
+
+ def size
+ @addresses.size
+ end
+
+ def empty?
+ @addresses.empty?
+ end
+
+ def each( &block )
+ @addresses.each(&block)
+ end
+
+ def to_a
+ @addresses.dup
+ end
+
+ alias to_ary to_a
+
+ def include?( a )
+ @addresses.include? a
+ end
+
+ def flatten
+ set = []
+ @addresses.each do |a|
+ if a.respond_to? :flatten
+ set.concat a.flatten
+ else
+ set.push a
+ end
+ end
+ set
+ end
+
+ def each_address( &block )
+ flatten.each(&block)
+ end
+
+ def add( a )
+ @addresses.push a
+ end
+
+ alias push add
+
+ def delete( a )
+ @addresses.delete a
+ end
+
+ include StrategyInterface
+
+ def accept( strategy, dummy1 = nil, dummy2 = nil )
+ strategy.phrase @name
+ strategy.meta ':'
+ strategy.space
+ first = true
+ each do |mbox|
+ if first
+ first = false
+ else
+ strategy.meta ','
+ end
+ strategy.space
+ mbox.accept strategy
+ end
+ strategy.meta ';'
+ strategy.lwsp ''
+ end
+
+ end
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/base64.rb b/actionmailer/lib/action_mailer/vendor/tmail/base64.rb
new file mode 100755
index 0000000000..d08a4fca7e
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/base64.rb
@@ -0,0 +1,52 @@
+#
+# base64.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+module TMail
+
+ module Base64
+
+ module_function
+
+ def rb_folding_encode( str, eol = "\n", limit = 60 )
+ [str].pack('m')
+ end
+
+ def rb_encode( str )
+ [str].pack('m').tr( "\r\n", '' )
+ end
+
+ def rb_decode( str, strict = false )
+ str.unpack('m')
+ end
+
+ begin
+ require 'tmail/base64.so'
+ alias folding_encode c_folding_encode
+ alias encode c_encode
+ alias decode c_decode
+ class << self
+ alias folding_encode c_folding_encode
+ alias encode c_encode
+ alias decode c_decode
+ end
+ rescue LoadError
+ alias folding_encode rb_folding_encode
+ alias encode rb_encode
+ alias decode rb_decode
+ class << self
+ alias folding_encode rb_folding_encode
+ alias encode rb_encode
+ alias decode rb_decode
+ end
+ end
+
+ end
+
+end
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/config.rb b/actionmailer/lib/action_mailer/vendor/tmail/config.rb
new file mode 100755
index 0000000000..c40e5fb384
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/config.rb
@@ -0,0 +1,50 @@
+#
+# config.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+module TMail
+
+ class Config
+
+ def initialize( strict )
+ @strict_parse = strict
+ @strict_base64decode = strict
+ end
+
+ def strict_parse?
+ @strict_parse
+ end
+
+ attr_writer :strict_parse
+
+ def strict_base64decode?
+ @strict_base64decode
+ end
+
+ attr_writer :strict_base64decode
+
+ def new_body_port( mail )
+ StringPort.new
+ end
+
+ alias new_preamble_port new_body_port
+ alias new_part_port new_body_port
+
+ end
+
+ DEFAULT_CONFIG = Config.new(false)
+ DEFAULT_STRICT_CONFIG = Config.new(true)
+
+ def Config.to_config( arg )
+ return DEFAULT_STRICT_CONFIG if arg == true
+ return DEFAULT_CONFIG if arg == false
+ arg or DEFAULT_CONFIG
+ end
+
+end
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/encode.rb b/actionmailer/lib/action_mailer/vendor/tmail/encode.rb
new file mode 100755
index 0000000000..8f33386f4d
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/encode.rb
@@ -0,0 +1,447 @@
+#
+# encode.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'nkf'
+require 'tmail/base64.rb'
+require 'tmail/stringio'
+require 'tmail/utils'
+
+
+module TMail
+
+ module StrategyInterface
+
+ def create_dest( obj )
+ case obj
+ when nil
+ StringOutput.new
+ when String
+ StringOutput.new(obj)
+ when IO, StringOutput
+ obj
+ else
+ raise TypeError, 'cannot handle this type of object for dest'
+ end
+ end
+ module_function :create_dest
+
+ def encoded( eol = "\r\n", charset = 'j', dest = nil )
+ accept_strategy Encoder, eol, charset, dest
+ end
+
+ def decoded( eol = "\n", charset = 'e', dest = nil )
+ accept_strategy Decoder, eol, charset, dest
+ end
+
+ alias to_s decoded
+
+ def accept_strategy( klass, eol, charset, dest = nil )
+ dest ||= ''
+ accept klass.new(create_dest(dest), charset, eol)
+ dest
+ end
+
+ end
+
+
+ ###
+ ### MIME B encoding decoder
+ ###
+
+ class Decoder
+
+ include TextUtils
+
+ encoded = '=\?(?:iso-2022-jp|euc-jp|shift_jis)\?[QB]\?[a-z0-9+/=]+\?='
+ ENCODED_WORDS = /#{encoded}(?:\s+#{encoded})*/i
+
+ OUTPUT_ENCODING = {
+ 'EUC' => 'e',
+ 'SJIS' => 's',
+ }
+
+ def self.decode( str, encoding = nil )
+ encoding ||= (OUTPUT_ENCODING[$KCODE] || 'j')
+ opt = '-m' + encoding
+ str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) }
+ end
+
+ def initialize( dest, encoding = nil, eol = "\n" )
+ @f = StrategyInterface.create_dest(dest)
+ @encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil
+ @eol = eol
+ end
+
+ def decode( str )
+ self.class.decode(str, @encoding)
+ end
+ private :decode
+
+ def terminate
+ end
+
+ def header_line( str )
+ @f << decode(str)
+ end
+
+ def header_name( nm )
+ @f << nm << ': '
+ end
+
+ def header_body( str )
+ @f << decode(str)
+ end
+
+ def space
+ @f << ' '
+ end
+
+ alias spc space
+
+ def lwsp( str )
+ @f << str
+ end
+
+ def meta( str )
+ @f << str
+ end
+
+ def text( str )
+ @f << decode(str)
+ end
+
+ def phrase( str )
+ @f << quote_phrase(decode(str))
+ end
+
+ def kv_pair( k, v )
+ @f << k << '=' << v
+ end
+
+ def puts( str = nil )
+ @f << str if str
+ @f << @eol
+ end
+
+ def write( str )
+ @f << str
+ end
+
+ end
+
+
+ ###
+ ### MIME B-encoding encoder
+ ###
+
+ #
+ # FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp).
+ #
+ class Encoder
+
+ include TextUtils
+
+ BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG)
+
+ def Encoder.encode( str )
+ e = new()
+ e.header_body str
+ e.terminate
+ e.dest.string
+ end
+
+ SPACER = "\t"
+ MAX_LINE_LEN = 70
+
+ OPTIONS = {
+ 'EUC' => '-Ej -m0',
+ 'SJIS' => '-Sj -m0',
+ 'UTF8' => nil, # FIXME
+ 'NONE' => nil
+ }
+
+ def initialize( dest = nil, encoding = nil, eol = "\r\n", limit = nil )
+ @f = StrategyInterface.create_dest(dest)
+ @opt = OPTIONS[$KCODE]
+ @eol = eol
+ reset
+ end
+
+ def normalize_encoding( str )
+ if @opt
+ then NKF.nkf(@opt, str)
+ else str
+ end
+ end
+
+ def reset
+ @text = ''
+ @lwsp = ''
+ @curlen = 0
+ end
+
+ def terminate
+ add_lwsp ''
+ reset
+ end
+
+ def dest
+ @f
+ end
+
+ def puts( str = nil )
+ @f << str if str
+ @f << @eol
+ end
+
+ def write( str )
+ @f << str
+ end
+
+ #
+ # add
+ #
+
+ def header_line( line )
+ scanadd line
+ end
+
+ def header_name( name )
+ add_text name.split(/-/).map {|i| i.capitalize }.join('-')
+ add_text ':'
+ add_lwsp ' '
+ end
+
+ def header_body( str )
+ scanadd normalize_encoding(str)
+ end
+
+ def space
+ add_lwsp ' '
+ end
+
+ alias spc space
+
+ def lwsp( str )
+ add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '')
+ end
+
+ def meta( str )
+ add_text str
+ end
+
+ def text( str )
+ scanadd normalize_encoding(str)
+ end
+
+ def phrase( str )
+ str = normalize_encoding(str)
+ if CONTROL_CHAR === str
+ scanadd str
+ else
+ add_text quote_phrase(str)
+ end
+ end
+
+ # FIXME: implement line folding
+ #
+ def kv_pair( k, v )
+ v = normalize_encoding(v)
+ if token_safe?(v)
+ add_text k + '=' + v
+ elsif not CONTROL_CHAR === v
+ add_text k + '=' + quote_token(v)
+ else
+ # apply RFC2231 encoding
+ kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v)
+ add_text kv
+ end
+ end
+
+ def encode_value( str )
+ str.gsub(TOKEN_UNSAFE) {|s| '%%%02x' % s[0] }
+ end
+
+ private
+
+ def scanadd( str, force = false )
+ types = ''
+ strs = []
+
+ until str.empty?
+ if m = /\A[^\e\t\r\n ]+/.match(str)
+ types << (force ? 'j' : 'a')
+ strs.push m[0]
+
+ elsif m = /\A[\t\r\n ]+/.match(str)
+ types << 's'
+ strs.push m[0]
+
+ elsif m = /\A\e../.match(str)
+ esc = m[0]
+ str = m.post_match
+ if esc != "\e(B" and m = /\A[^\e]+/.match(str)
+ types << 'j'
+ strs.push m[0]
+ end
+
+ else
+ raise 'TMail FATAL: encoder scan fail'
+ end
+ str = m.post_match
+ end
+
+ do_encode types, strs
+ end
+
+ def do_encode( types, strs )
+ #
+ # result : (A|E)(S(A|E))*
+ # E : W(SW)*
+ # W : (J|A)+ but must contain J # (J|A)*J(J|A)*
+ # A : <<A character string not to be encoded>>
+ # J : <<A character string to be encoded>>
+ # S : <<LWSP>>
+ #
+ # An encoding unit is `E'.
+ # Input (parameter `types') is (J|A)(J|A|S)*(J|A)
+ #
+ if BENCODE_DEBUG
+ puts
+ puts '-- do_encode ------------'
+ puts types.split(//).join(' ')
+ p strs
+ end
+
+ e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/
+
+ while m = e.match(types)
+ pre = m.pre_match
+ concat_A_S pre, strs[0, pre.size] unless pre.empty?
+ concat_E m[0], strs[m.begin(0) ... m.end(0)]
+ types = m.post_match
+ strs.slice! 0, m.end(0)
+ end
+ concat_A_S types, strs
+ end
+
+ def concat_A_S( types, strs )
+ i = 0
+ types.each_byte do |t|
+ case t
+ when ?a then add_text strs[i]
+ when ?s then add_lwsp strs[i]
+ else
+ raise "TMail FATAL: unknown flag: #{t.chr}"
+ end
+ i += 1
+ end
+ end
+
+ METHOD_ID = {
+ ?j => :extract_J,
+ ?e => :extract_E,
+ ?a => :extract_A,
+ ?s => :extract_S
+ }
+
+ def concat_E( types, strs )
+ if BENCODE_DEBUG
+ puts '---- concat_E'
+ puts "types=#{types.split(//).join(' ')}"
+ puts "strs =#{strs.inspect}"
+ end
+
+ flush() unless @text.empty?
+
+ chunk = ''
+ strs.each_with_index do |s,i|
+ mid = METHOD_ID[types[i]]
+ until s.empty?
+ unless c = __send__(mid, chunk.size, s)
+ add_with_encode chunk unless chunk.empty?
+ flush
+ chunk = ''
+ fold
+ c = __send__(mid, 0, s)
+ raise 'TMail FATAL: extract fail' unless c
+ end
+ chunk << c
+ end
+ end
+ add_with_encode chunk unless chunk.empty?
+ end
+
+ def extract_J( chunksize, str )
+ size = max_bytes(chunksize, str.size) - 6
+ size = (size % 2 == 0) ? (size) : (size - 1)
+ return nil if size <= 0
+ "\e$B#{str.slice!(0, size)}\e(B"
+ end
+
+ def extract_A( chunksize, str )
+ size = max_bytes(chunksize, str.size)
+ return nil if size <= 0
+ str.slice!(0, size)
+ end
+
+ alias extract_S extract_A
+
+ def max_bytes( chunksize, ssize )
+ (restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize
+ end
+
+ #
+ # free length buffer
+ #
+
+ def add_text( str )
+ @text << str
+ # puts '---- text -------------------------------------'
+ # puts "+ #{str.inspect}"
+ # puts "txt >>>#{@text.inspect}<<<"
+ end
+
+ def add_with_encode( str )
+ @text << "=?iso-2022-jp?B?#{Base64.encode(str)}?="
+ end
+
+ def add_lwsp( lwsp )
+ # puts '---- lwsp -------------------------------------'
+ # puts "+ #{lwsp.inspect}"
+ fold if restsize() <= 0
+ flush
+ @lwsp = lwsp
+ end
+
+ def flush
+ # puts '---- flush ----'
+ # puts "spc >>>#{@lwsp.inspect}<<<"
+ # puts "txt >>>#{@text.inspect}<<<"
+ @f << @lwsp << @text
+ @curlen += (@lwsp.size + @text.size)
+ @text = ''
+ @lwsp = ''
+ end
+
+ def fold
+ # puts '---- fold ----'
+ @f << @eol
+ @curlen = 0
+ @lwsp = SPACER
+ end
+
+ def restsize
+ MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size)
+ end
+
+ end
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/facade.rb b/actionmailer/lib/action_mailer/vendor/tmail/facade.rb
new file mode 100755
index 0000000000..4b8e2fbc07
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/facade.rb
@@ -0,0 +1,531 @@
+#
+# facade.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'tmail/utils'
+
+
+module TMail
+
+ class Mail
+
+ def header_string( name, default = nil )
+ h = @header[name.downcase] or return default
+ h.to_s
+ end
+
+ ###
+ ### attributes
+ ###
+
+ include TextUtils
+
+ def set_string_array_attr( key, strs )
+ strs.flatten!
+ if strs.empty?
+ @header.delete key.downcase
+ else
+ store key, strs.join(', ')
+ end
+ strs
+ end
+ private :set_string_array_attr
+
+ def set_string_attr( key, str )
+ if str
+ store key, str
+ else
+ @header.delete key.downcase
+ end
+ str
+ end
+ private :set_string_attr
+
+ def set_addrfield( name, arg )
+ if arg
+ h = HeaderField.internal_new(name, @config)
+ h.addrs.replace [arg].flatten
+ @header[name] = h
+ else
+ @header.delete name
+ end
+ arg
+ end
+ private :set_addrfield
+
+ def addrs2specs( addrs )
+ return nil unless addrs
+ list = addrs.map {|addr|
+ if addr.address_group?
+ then addr.map {|a| a.spec }
+ else addr.spec
+ end
+ }.flatten
+ return nil if list.empty?
+ list
+ end
+ private :addrs2specs
+
+
+ #
+ # date time
+ #
+
+ def date( default = nil )
+ if h = @header['date']
+ h.date
+ else
+ default
+ end
+ end
+
+ def date=( time )
+ if time
+ store 'Date', time2str(time)
+ else
+ @header.delete 'date'
+ end
+ time
+ end
+
+ def strftime( fmt, default = nil )
+ if t = date
+ t.strftime(fmt)
+ else
+ default
+ end
+ end
+
+
+ #
+ # destination
+ #
+
+ def to_addrs( default = nil )
+ if h = @header['to']
+ h.addrs
+ else
+ default
+ end
+ end
+
+ def cc_addrs( default = nil )
+ if h = @header['cc']
+ h.addrs
+ else
+ default
+ end
+ end
+
+ def bcc_addrs( default = nil )
+ if h = @header['bcc']
+ h.addrs
+ else
+ default
+ end
+ end
+
+ def to_addrs=( arg )
+ set_addrfield 'to', arg
+ end
+
+ def cc_addrs=( arg )
+ set_addrfield 'cc', arg
+ end
+
+ def bcc_addrs=( arg )
+ set_addrfield 'bcc', arg
+ end
+
+ def to( default = nil )
+ addrs2specs(to_addrs(nil)) || default
+ end
+
+ def cc( default = nil )
+ addrs2specs(cc_addrs(nil)) || default
+ end
+
+ def bcc( default = nil )
+ addrs2specs(bcc_addrs(nil)) || default
+ end
+
+ def to=( *strs )
+ set_string_array_attr 'To', strs
+ end
+
+ def cc=( *strs )
+ set_string_array_attr 'Cc', strs
+ end
+
+ def bcc=( *strs )
+ set_string_array_attr 'Bcc', strs
+ end
+
+
+ #
+ # originator
+ #
+
+ def from_addrs( default = nil )
+ if h = @header['from']
+ h.addrs
+ else
+ default
+ end
+ end
+
+ def from_addrs=( arg )
+ set_addrfield 'from', arg
+ end
+
+ def from( default = nil )
+ addrs2specs(from_addrs(nil)) || default
+ end
+
+ def from=( *strs )
+ set_string_array_attr 'From', strs
+ end
+
+ def friendly_from( default = nil )
+ h = @header['from']
+ a, = h.addrs
+ return default unless a
+ return a.phrase if a.phrase
+ return h.comments.join(' ') unless h.comments.empty?
+ a.spec
+ end
+
+
+ def reply_to_addrs( default = nil )
+ if h = @header['reply-to']
+ h.addrs
+ else
+ default
+ end
+ end
+
+ def reply_to_addrs=( arg )
+ set_addrfield 'reply-to', arg
+ end
+
+ def reply_to( default = nil )
+ addrs2specs(reply_to_addrs(nil)) || default
+ end
+
+ def reply_to=( *strs )
+ set_string_array_attr 'Reply-To', strs
+ end
+
+
+ def sender_addr( default = nil )
+ f = @header['sender'] or return default
+ f.addr or return default
+ end
+
+ def sender_addr=( addr )
+ if addr
+ h = HeaderField.internal_new('sender', @config)
+ h.addr = addr
+ @header['sender'] = h
+ else
+ @header.delete 'sender'
+ end
+ addr
+ end
+
+ def sender( default )
+ f = @header['sender'] or return default
+ a = f.addr or return default
+ a.spec
+ end
+
+ def sender=( str )
+ set_string_attr 'Sender', str
+ end
+
+
+ #
+ # subject
+ #
+
+ def subject( default = nil )
+ if h = @header['subject']
+ h.body
+ else
+ default
+ end
+ end
+
+ def subject=( str )
+ set_string_attr 'Subject', str
+ end
+
+
+ #
+ # identity & threading
+ #
+
+ def message_id( default = nil )
+ if h = @header['message-id']
+ h.id || default
+ else
+ default
+ end
+ end
+
+ def message_id=( str )
+ set_string_attr 'Message-Id', str
+ end
+
+ def in_reply_to( default = nil )
+ if h = @header['in-reply-to']
+ h.ids
+ else
+ default
+ end
+ end
+
+ def in_reply_to=( *idstrs )
+ set_string_array_attr 'In-Reply-To', idstrs
+ end
+
+ def references( default = nil )
+ if h = @header['references']
+ h.refs
+ else
+ default
+ end
+ end
+
+ def references=( *strs )
+ set_string_array_attr 'References', strs
+ end
+
+
+ #
+ # MIME headers
+ #
+
+ def mime_version( default = nil )
+ if h = @header['mime-version']
+ h.version || default
+ else
+ default
+ end
+ end
+
+ def mime_version=( m, opt = nil )
+ if opt
+ if h = @header['mime-version']
+ h.major = m
+ h.minor = opt
+ else
+ store 'Mime-Version', "#{m}.#{opt}"
+ end
+ else
+ store 'Mime-Version', m
+ end
+ m
+ end
+
+
+ def content_type( default = nil )
+ if h = @header['content-type']
+ h.content_type || default
+ else
+ default
+ end
+ end
+
+ def main_type( default = nil )
+ if h = @header['content-type']
+ h.main_type || default
+ else
+ default
+ end
+ end
+
+ def sub_type( default = nil )
+ if h = @header['content-type']
+ h.sub_type || default
+ else
+ default
+ end
+ end
+
+ def set_content_type( str, sub = nil, param = nil )
+ if sub
+ main, sub = str, sub
+ else
+ main, sub = str.split(%r</>, 2)
+ raise ArgumentError, "sub type missing: #{str.inspect}" unless sub
+ end
+ if h = @header['content-type']
+ h.main_type = main
+ h.sub_type = sub
+ h.params.clear
+ else
+ store 'Content-Type', "#{main}/#{sub}"
+ end
+ @header['content-type'].params.replace param if param
+
+ str
+ end
+
+ alias content_type= set_content_type
+
+ def type_param( name, default = nil )
+ if h = @header['content-type']
+ h[name] || default
+ else
+ default
+ end
+ end
+
+ def charset( default = nil )
+ if h = @header['content-type']
+ h['charset'] or default
+ else
+ default
+ end
+ end
+
+ def charset=( str )
+ if str
+ if h = @header[ 'content-type' ]
+ h['charset'] = str
+ else
+ store 'Content-Type', "text/plain; charset=#{str}"
+ end
+ end
+ str
+ end
+
+
+ def transfer_encoding( default = nil )
+ if h = @header['content-transfer-encoding']
+ h.encoding || default
+ else
+ default
+ end
+ end
+
+ def transfer_encoding=( str )
+ set_string_attr 'Content-Transfer-Encoding', str
+ end
+
+ alias encoding transfer_encoding
+ alias encoding= transfer_encoding=
+ alias content_transfer_encoding transfer_encoding
+ alias content_transfer_encoding= transfer_encoding=
+
+
+ def disposition( default = nil )
+ if h = @header['content-disposition']
+ h.disposition || default
+ else
+ default
+ end
+ end
+
+ alias content_disposition disposition
+
+ def set_disposition( str, params = nil )
+ if h = @header['content-disposition']
+ h.disposition = str
+ h.params.clear
+ else
+ h = store('Content-Disposition', str)
+ end
+ h.params.replace params if params
+ end
+
+ alias disposition= set_disposition
+ alias set_content_disposition set_disposition
+ alias content_disposition= set_disposition
+
+ def disposition_param( name, default = nil )
+ if h = @header['content-disposition']
+ h[name] || default
+ else
+ default
+ end
+ end
+
+ ###
+ ### utils
+ ###
+
+ def create_reply
+ mail = TMail::Mail.parse('')
+ mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '')
+ mail.to_addrs = reply_addresses([])
+ mail.in_reply_to = [message_id(nil)].compact
+ mail.references = references([]) + [message_id(nil)].compact
+ mail.mime_version = '1.0'
+ mail
+ end
+
+
+ def base64_encode
+ store 'Content-Transfer-Encoding', 'Base64'
+ self.body = Base64.folding_encode(self.body)
+ end
+
+ def base64_decode
+ if /base64/i === self.transfer_encoding('')
+ store 'Content-Transfer-Encoding', '8bit'
+ self.body = Base64.decode(self.body, @config.strict_base64decode?)
+ end
+ end
+
+
+ def destinations( default = nil )
+ ret = []
+ %w( to cc bcc ).each do |nm|
+ if h = @header[nm]
+ h.addrs.each {|i| ret.push i.address }
+ end
+ end
+ ret.empty? ? default : ret
+ end
+
+ def each_destination( &block )
+ destinations([]).each do |i|
+ if Address === i
+ yield i
+ else
+ i.each(&block)
+ end
+ end
+ end
+
+ alias each_dest each_destination
+
+
+ def reply_addresses( default = nil )
+ reply_to_addrs(nil) or from_addrs(nil) or default
+ end
+
+ def error_reply_addresses( default = nil )
+ if s = sender(nil)
+ [s]
+ else
+ from_addrs(default)
+ end
+ end
+
+
+ def multipart?
+ main_type('').downcase == 'multipart'
+ end
+
+ end # class Mail
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/header.rb b/actionmailer/lib/action_mailer/vendor/tmail/header.rb
new file mode 100755
index 0000000000..73639b5629
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/header.rb
@@ -0,0 +1,893 @@
+#
+# header.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'tmail/encode'
+require 'tmail/address'
+require 'tmail/parser'
+require 'tmail/config'
+require 'tmail/utils'
+
+
+module TMail
+
+ class HeaderField
+
+ include TextUtils
+
+ class << self
+
+ alias newobj new
+
+ def new( name, body, conf = DEFAULT_CONFIG )
+ klass = FNAME_TO_CLASS[name.downcase] || UnstructuredHeader
+ klass.newobj body, conf
+ end
+
+ def new_from_port( port, name, conf = DEFAULT_CONFIG )
+ re = Regep.new('\A(' + Regexp.quote(name) + '):', 'i')
+ str = nil
+ port.ropen {|f|
+ f.each do |line|
+ if m = re.match(line) then str = m.post_match.strip
+ elsif str and /\A[\t ]/ === line then str << ' ' << line.strip
+ elsif /\A-*\s*\z/ === line then break
+ elsif str then break
+ end
+ end
+ }
+ new(name, str, Config.to_config(conf))
+ end
+
+ def internal_new( name, conf )
+ FNAME_TO_CLASS[name].newobj('', conf, true)
+ end
+
+ end # class << self
+
+ def initialize( body, conf, intern = false )
+ @body = body
+ @config = conf
+
+ @illegal = false
+ @parsed = false
+ if intern
+ @parsed = true
+ parse_init
+ end
+ end
+
+ def inspect
+ "#<#{self.class} #{@body.inspect}>"
+ end
+
+ def illegal?
+ @illegal
+ end
+
+ def empty?
+ ensure_parsed
+ return true if @illegal
+ isempty?
+ end
+
+ private
+
+ def ensure_parsed
+ return if @parsed
+ @parsed = true
+ parse
+ end
+
+ # defabstract parse
+ # end
+
+ def clear_parse_status
+ @parsed = false
+ @illegal = false
+ end
+
+ public
+
+ def body
+ ensure_parsed
+ v = Decoder.new(s = '')
+ do_accept v
+ v.terminate
+ s
+ end
+
+ def body=( str )
+ @body = str
+ clear_parse_status
+ end
+
+ include StrategyInterface
+
+ def accept( strategy, dummy1 = nil, dummy2 = nil )
+ ensure_parsed
+ do_accept strategy
+ strategy.terminate
+ end
+
+ # abstract do_accept
+
+ end
+
+
+ class UnstructuredHeader < HeaderField
+
+ def body
+ ensure_parsed
+ @body
+ end
+
+ def body=( arg )
+ ensure_parsed
+ @body = arg
+ end
+
+ private
+
+ def parse_init
+ end
+
+ def parse
+ @body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, ''))
+ end
+
+ def isempty?
+ not @body
+ end
+
+ def do_accept( strategy )
+ strategy.text @body
+ end
+
+ end
+
+
+ class StructuredHeader < HeaderField
+
+ def comments
+ ensure_parsed
+ @comments
+ end
+
+ private
+
+ def parse
+ save = nil
+
+ begin
+ parse_init
+ do_parse
+ rescue SyntaxError
+ if not save and mime_encoded? @body
+ save = @body
+ @body = Decoder.decode(save)
+ retry
+ elsif save
+ @body = save
+ end
+
+ @illegal = true
+ raise if @config.strict_parse?
+ end
+ end
+
+ def parse_init
+ @comments = []
+ init
+ end
+
+ def do_parse
+ obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments)
+ set obj if obj
+ end
+
+ end
+
+
+ class DateTimeHeader < StructuredHeader
+
+ PARSE_TYPE = :DATETIME
+
+ def date
+ ensure_parsed
+ @date
+ end
+
+ def date=( arg )
+ ensure_parsed
+ @date = arg
+ end
+
+ private
+
+ def init
+ @date = nil
+ end
+
+ def set( t )
+ @date = t
+ end
+
+ def isempty?
+ not @date
+ end
+
+ def do_accept( strategy )
+ strategy.meta time2str(@date)
+ end
+
+ end
+
+
+ class AddressHeader < StructuredHeader
+
+ PARSE_TYPE = :MADDRESS
+
+ def addrs
+ ensure_parsed
+ @addrs
+ end
+
+ private
+
+ def init
+ @addrs = []
+ end
+
+ def set( a )
+ @addrs = a
+ end
+
+ def isempty?
+ @addrs.empty?
+ end
+
+ def do_accept( strategy )
+ first = true
+ @addrs.each do |a|
+ if first
+ first = false
+ else
+ strategy.meta ','
+ strategy.space
+ end
+ a.accept strategy
+ end
+
+ @comments.each do |c|
+ strategy.space
+ strategy.meta '('
+ strategy.text c
+ strategy.meta ')'
+ end
+ end
+
+ end
+
+
+ class ReturnPathHeader < AddressHeader
+
+ PARSE_TYPE = :RETPATH
+
+ def addr
+ addrs()[0]
+ end
+
+ def spec
+ a = addr() or return nil
+ a.spec
+ end
+
+ def routes
+ a = addr() or return nil
+ a.routes
+ end
+
+ private
+
+ def do_accept( strategy )
+ a = addr()
+
+ strategy.meta '<'
+ unless a.routes.empty?
+ strategy.meta a.routes.map {|i| '@' + i }.join(',')
+ strategy.meta ':'
+ end
+ spec = a.spec
+ strategy.meta spec if spec
+ strategy.meta '>'
+ end
+
+ end
+
+
+ class SingleAddressHeader < AddressHeader
+
+ def addr
+ addrs()[0]
+ end
+
+ private
+
+ def do_accept( strategy )
+ a = addr()
+ a.accept strategy
+ @comments.each do |c|
+ strategy.space
+ strategy.meta '('
+ strategy.text c
+ strategy.meta ')'
+ end
+ end
+
+ end
+
+
+ class MessageIdHeader < StructuredHeader
+
+ def id
+ ensure_parsed
+ @id
+ end
+
+ def id=( arg )
+ ensure_parsed
+ @id = arg
+ end
+
+ private
+
+ def init
+ @id = nil
+ end
+
+ def isempty?
+ not @id
+ end
+
+ def do_parse
+ @id = @body.slice(MESSAGE_ID) or
+ raise SyntaxError, "wrong Message-ID format: #{@body}"
+ end
+
+ def do_accept( strategy )
+ strategy.meta @id
+ end
+
+ end
+
+
+ class ReferencesHeader < StructuredHeader
+
+ def refs
+ ensure_parsed
+ @refs
+ end
+
+ def each_id
+ self.refs.each do |i|
+ yield i if MESSAGE_ID === i
+ end
+ end
+
+ def ids
+ ensure_parsed
+ @ids
+ end
+
+ def each_phrase
+ self.refs.each do |i|
+ yield i unless MESSAGE_ID === i
+ end
+ end
+
+ def phrases
+ ret = []
+ each_phrase {|i| ret.push i }
+ ret
+ end
+
+ private
+
+ def init
+ @refs = []
+ @ids = []
+ end
+
+ def isempty?
+ @ids.empty?
+ end
+
+ def do_parse
+ str = @body
+ while m = MESSAGE_ID.match(str)
+ pre = m.pre_match.strip
+ @refs.push pre unless pre.empty?
+ @refs.push s = m[0]
+ @ids.push s
+ str = m.post_match
+ end
+ str = str.strip
+ @refs.push str unless str.empty?
+ end
+
+ def do_accept( strategy )
+ first = true
+ @ids.each do |i|
+ if first
+ first = false
+ else
+ strategy.space
+ end
+ strategy.meta i
+ end
+ end
+
+ end
+
+
+ class ReceivedHeader < StructuredHeader
+
+ PARSE_TYPE = :RECEIVED
+
+ def from
+ ensure_parsed
+ @from
+ end
+
+ def from=( arg )
+ ensure_parsed
+ @from = arg
+ end
+
+ def by
+ ensure_parsed
+ @by
+ end
+
+ def by=( arg )
+ ensure_parsed
+ @by = arg
+ end
+
+ def via
+ ensure_parsed
+ @via
+ end
+
+ def via=( arg )
+ ensure_parsed
+ @via = arg
+ end
+
+ def with
+ ensure_parsed
+ @with
+ end
+
+ def id
+ ensure_parsed
+ @id
+ end
+
+ def id=( arg )
+ ensure_parsed
+ @id = arg
+ end
+
+ def _for
+ ensure_parsed
+ @_for
+ end
+
+ def _for=( arg )
+ ensure_parsed
+ @_for = arg
+ end
+
+ def date
+ ensure_parsed
+ @date
+ end
+
+ def date=( arg )
+ ensure_parsed
+ @date = arg
+ end
+
+ private
+
+ def init
+ @from = @by = @via = @with = @id = @_for = nil
+ @with = []
+ @date = nil
+ end
+
+ def set( args )
+ @from, @by, @via, @with, @id, @_for, @date = *args
+ end
+
+ def isempty?
+ @with.empty? and not (@from or @by or @via or @id or @_for or @date)
+ end
+
+ def do_accept( strategy )
+ list = []
+ list.push 'from ' + @from if @from
+ list.push 'by ' + @by if @by
+ list.push 'via ' + @via if @via
+ @with.each do |i|
+ list.push 'with ' + i
+ end
+ list.push 'id ' + @id if @id
+ list.push 'for <' + @_for + '>' if @_for
+
+ first = true
+ list.each do |i|
+ strategy.space unless first
+ strategy.meta i
+ first = false
+ end
+ if @date
+ strategy.meta ';'
+ strategy.space
+ strategy.meta time2str(@date)
+ end
+ end
+
+ end
+
+
+ class KeywordsHeader < StructuredHeader
+
+ PARSE_TYPE = :KEYWORDS
+
+ def keys
+ ensure_parsed
+ @keys
+ end
+
+ private
+
+ def init
+ @keys = []
+ end
+
+ def set( a )
+ @keys = a
+ end
+
+ def isempty?
+ @keys.empty?
+ end
+
+ def do_accept( strategy )
+ first = true
+ @keys.each do |i|
+ if first
+ first = false
+ else
+ strategy.meta ','
+ end
+ strategy.meta i
+ end
+ end
+
+ end
+
+
+ class EncryptedHeader < StructuredHeader
+
+ PARSE_TYPE = :ENCRYPTED
+
+ def encrypter
+ ensure_parsed
+ @encrypter
+ end
+
+ def encrypter=( arg )
+ ensure_parsed
+ @encrypter = arg
+ end
+
+ def keyword
+ ensure_parsed
+ @keyword
+ end
+
+ def keyword=( arg )
+ ensure_parsed
+ @keyword = arg
+ end
+
+ private
+
+ def init
+ @encrypter = nil
+ @keyword = nil
+ end
+
+ def set( args )
+ @encrypter, @keyword = args
+ end
+
+ def isempty?
+ not (@encrypter or @keyword)
+ end
+
+ def do_accept( strategy )
+ if @key
+ strategy.meta @encrypter + ','
+ strategy.space
+ strategy.meta @keyword
+ else
+ strategy.meta @encrypter
+ end
+ end
+
+ end
+
+
+ class MimeVersionHeader < StructuredHeader
+
+ PARSE_TYPE = :MIMEVERSION
+
+ def major
+ ensure_parsed
+ @major
+ end
+
+ def major=( arg )
+ ensure_parsed
+ @major = arg
+ end
+
+ def minor
+ ensure_parsed
+ @minor
+ end
+
+ def minor=( arg )
+ ensure_parsed
+ @minor = arg
+ end
+
+ def version
+ sprintf('%d.%d', major, minor)
+ end
+
+ private
+
+ def init
+ @major = nil
+ @minor = nil
+ end
+
+ def set( args )
+ @major, @minor = *args
+ end
+
+ def isempty?
+ not (@major or @minor)
+ end
+
+ def do_accept( strategy )
+ strategy.meta sprintf('%d.%d', @major, @minor)
+ end
+
+ end
+
+
+ class ContentTypeHeader < StructuredHeader
+
+ PARSE_TYPE = :CTYPE
+
+ def main_type
+ ensure_parsed
+ @main
+ end
+
+ def main_type=( arg )
+ ensure_parsed
+ @main = arg.downcase
+ end
+
+ def sub_type
+ ensure_parsed
+ @sub
+ end
+
+ def sub_type=( arg )
+ ensure_parsed
+ @sub = arg.downcase
+ end
+
+ def content_type
+ ensure_parsed
+ @sub ? sprintf('%s/%s', @main, @sub) : @main
+ end
+
+ def params
+ ensure_parsed
+ @params
+ end
+
+ def []( key )
+ ensure_parsed
+ @params and @params[key]
+ end
+
+ def []=( key, val )
+ ensure_parsed
+ (@params ||= {})[key] = val
+ end
+
+ private
+
+ def init
+ @main = @sub = @params = nil
+ end
+
+ def set( args )
+ @main, @sub, @params = *args
+ end
+
+ def isempty?
+ not (@main or @sub)
+ end
+
+ def do_accept( strategy )
+ if @sub
+ strategy.meta sprintf('%s/%s', @main, @sub)
+ else
+ strategy.meta @main
+ end
+ @params.each do |k,v|
+ strategy.meta ';'
+ strategy.space
+ strategy.kv_pair k, v
+ end
+ end
+
+ end
+
+
+ class ContentTransferEncodingHeader < StructuredHeader
+
+ PARSE_TYPE = :CENCODING
+
+ def encoding
+ ensure_parsed
+ @encoding
+ end
+
+ def encoding=( arg )
+ ensure_parsed
+ @encoding = arg
+ end
+
+ private
+
+ def init
+ @encoding = nil
+ end
+
+ def set( s )
+ @encoding = s
+ end
+
+ def isempty?
+ not @encoding
+ end
+
+ def do_accept( strategy )
+ strategy.meta @encoding.capitalize
+ end
+
+ end
+
+
+ class ContentDispositionHeader < StructuredHeader
+
+ PARSE_TYPE = :CDISPOSITION
+
+ def disposition
+ ensure_parsed
+ @disposition
+ end
+
+ def disposition=( str )
+ ensure_parsed
+ @disposition = str.downcase
+ end
+
+ def params
+ ensure_parsed
+ @params
+ end
+
+ def []( key )
+ ensure_parsed
+ @params and @params[key]
+ end
+
+ def []=( key, val )
+ ensure_parsed
+ (@params ||= {})[key] = val
+ end
+
+ private
+
+ def init
+ @disposition = @params = nil
+ end
+
+ def set( args )
+ @disposition, @params = *args
+ end
+
+ def isempty?
+ not @disposition and (not @params or @params.empty?)
+ end
+
+ def do_accept( strategy )
+ strategy.meta @disposition
+ @params.each do |k,v|
+ strategy.meta ';'
+ strategy.space
+ strategy.kv_pair k, v
+ end
+ end
+
+ end
+
+
+ class HeaderField # redefine
+
+ FNAME_TO_CLASS = {
+ 'date' => DateTimeHeader,
+ 'resent-date' => DateTimeHeader,
+ 'to' => AddressHeader,
+ 'cc' => AddressHeader,
+ 'bcc' => AddressHeader,
+ 'from' => AddressHeader,
+ 'reply-to' => AddressHeader,
+ 'resent-to' => AddressHeader,
+ 'resent-cc' => AddressHeader,
+ 'resent-bcc' => AddressHeader,
+ 'resent-from' => AddressHeader,
+ 'resent-reply-to' => AddressHeader,
+ 'sender' => SingleAddressHeader,
+ 'resent-sender' => SingleAddressHeader,
+ 'return-path' => ReturnPathHeader,
+ 'message-id' => MessageIdHeader,
+ 'resent-message-id' => MessageIdHeader,
+ 'in-reply-to' => ReferencesHeader,
+ 'received' => ReceivedHeader,
+ 'references' => ReferencesHeader,
+ 'keywords' => KeywordsHeader,
+ 'encrypted' => EncryptedHeader,
+ 'mime-version' => MimeVersionHeader,
+ 'content-type' => ContentTypeHeader,
+ 'content-transfer-encoding' => ContentTransferEncodingHeader,
+ 'content-disposition' => ContentDispositionHeader,
+ 'content-id' => MessageIdHeader,
+ 'subject' => UnstructuredHeader,
+ 'comments' => UnstructuredHeader,
+ 'content-description' => UnstructuredHeader
+ }
+
+ end
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/info.rb b/actionmailer/lib/action_mailer/vendor/tmail/info.rb
new file mode 100755
index 0000000000..918f232096
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/info.rb
@@ -0,0 +1,16 @@
+#
+# info.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+module TMail
+
+ Version = '0.10.7'
+ Copyright = 'Copyright (c) 1998-2002 Minero Aoki'
+
+end
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/loader.rb b/actionmailer/lib/action_mailer/vendor/tmail/loader.rb
new file mode 100755
index 0000000000..7907315401
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/loader.rb
@@ -0,0 +1 @@
+require 'tmail/mailbox'
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/mail.rb b/actionmailer/lib/action_mailer/vendor/tmail/mail.rb
new file mode 100755
index 0000000000..bfea9b6db4
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/mail.rb
@@ -0,0 +1,420 @@
+#
+# mail.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'tmail/facade'
+require 'tmail/encode'
+require 'tmail/header'
+require 'tmail/port'
+require 'tmail/config'
+require 'tmail/utils'
+require 'socket'
+
+
+module TMail
+
+ class Mail
+
+ class << self
+ def load( fname )
+ new(FilePort.new(fname))
+ end
+
+ alias load_from load
+ alias loadfrom load
+
+ def parse( str )
+ new(StringPort.new(str))
+ end
+ end
+
+ def initialize( port = nil, conf = DEFAULT_CONFIG )
+ @port = port || StringPort.new
+ @config = Config.to_config(conf)
+
+ @header = {}
+ @body_port = nil
+ @body_parsed = false
+ @epilogue = ''
+ @parts = []
+
+ @port.ropen {|f|
+ parse_header f
+ parse_body f unless @port.reproducible?
+ }
+ end
+
+ attr_reader :port
+
+ def inspect
+ "\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
+ end
+
+ #
+ # to_s interfaces
+ #
+
+ public
+
+ include StrategyInterface
+
+ def write_back( eol = "\n", charset = 'e' )
+ parse_body
+ @port.wopen {|stream| encoded eol, charset, stream }
+ end
+
+ def accept( strategy )
+ with_multipart_encoding(strategy) {
+ ordered_each do |name, field|
+ next if field.empty?
+ strategy.header_name canonical(name)
+ field.accept strategy
+ strategy.puts
+ end
+ strategy.puts
+ body_port().ropen {|r|
+ strategy.write r.read
+ }
+ }
+ end
+
+ private
+
+ def canonical( name )
+ name.split(/-/).map {|s| s.capitalize }.join('-')
+ end
+
+ def with_multipart_encoding( strategy )
+ if parts().empty? # DO NOT USE @parts
+ yield
+
+ else
+ bound = ::TMail.new_boundary
+ if @header.key? 'content-type'
+ @header['content-type'].params['boundary'] = bound
+ else
+ store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
+ end
+
+ yield
+
+ parts().each do |tm|
+ strategy.puts
+ strategy.puts '--' + bound
+ tm.accept strategy
+ end
+ strategy.puts
+ strategy.puts '--' + bound + '--'
+ strategy.write epilogue()
+ end
+ end
+
+ ###
+ ### header
+ ###
+
+ public
+
+ ALLOW_MULTIPLE = {
+ 'received' => true,
+ 'resent-date' => true,
+ 'resent-from' => true,
+ 'resent-sender' => true,
+ 'resent-to' => true,
+ 'resent-cc' => true,
+ 'resent-bcc' => true,
+ 'resent-message-id' => true,
+ 'comments' => true,
+ 'keywords' => true
+ }
+ USE_ARRAY = ALLOW_MULTIPLE
+
+ def header
+ @header.dup
+ end
+
+ def []( key )
+ @header[key.downcase]
+ end
+
+ alias fetch []
+
+ def []=( key, val )
+ dkey = key.downcase
+
+ if val.nil?
+ @header.delete dkey
+ return nil
+ end
+
+ case val
+ when String
+ header = new_hf(key, val)
+ when HeaderField
+ ;
+ when Array
+ ALLOW_MULTIPLE.include? dkey or
+ raise ArgumentError, "#{key}: Header must not be multiple"
+ @header[dkey] = val
+ return val
+ else
+ header = new_hf(key, val.to_s)
+ end
+ if ALLOW_MULTIPLE.include? dkey
+ (@header[dkey] ||= []).push header
+ else
+ @header[dkey] = header
+ end
+
+ val
+ end
+
+ alias store []=
+
+ def each_header
+ @header.each do |key, val|
+ [val].flatten.each {|v| yield key, v }
+ end
+ end
+
+ alias each_pair each_header
+
+ def each_header_name( &block )
+ @header.each_key(&block)
+ end
+
+ alias each_key each_header_name
+
+ def each_field( &block )
+ @header.values.flatten.each(&block)
+ end
+
+ alias each_value each_field
+
+ FIELD_ORDER = %w(
+ return-path received
+ resent-date resent-from resent-sender resent-to
+ resent-cc resent-bcc resent-message-id
+ date from sender reply-to to cc bcc
+ message-id in-reply-to references
+ subject comments keywords
+ mime-version content-type content-transfer-encoding
+ content-disposition content-description
+ )
+
+ def ordered_each
+ list = @header.keys
+ FIELD_ORDER.each do |name|
+ if list.delete(name)
+ [@header[name]].flatten.each {|v| yield name, v }
+ end
+ end
+ list.each do |name|
+ [@header[name]].flatten.each {|v| yield name, v }
+ end
+ end
+
+ def clear
+ @header.clear
+ end
+
+ def delete( key )
+ @header.delete key.downcase
+ end
+
+ def delete_if
+ @header.delete_if do |key,val|
+ if Array === val
+ val.delete_if {|v| yield key, v }
+ val.empty?
+ else
+ yield key, val
+ end
+ end
+ end
+
+ def keys
+ @header.keys
+ end
+
+ def key?( key )
+ @header.key? key.downcase
+ end
+
+ def values_at( *args )
+ args.map {|k| @header[k.downcase] }.flatten
+ end
+
+ alias indexes values_at
+ alias indices values_at
+
+ private
+
+ def parse_header( f )
+ name = field = nil
+ unixfrom = nil
+
+ while line = f.gets
+ case line
+ when /\A[ \t]/ # continue from prev line
+ raise SyntaxError, 'mail is began by space' unless field
+ field << ' ' << line.strip
+
+ when /\A([^\: \t]+):\s*/ # new header line
+ add_hf name, field if field
+ name = $1
+ field = $' #.strip
+
+ when /\A\-*\s*\z/ # end of header
+ add_hf name, field if field
+ name = field = nil
+ break
+
+ when /\AFrom (\S+)/
+ unixfrom = $1
+
+ else
+ raise SyntaxError, "wrong mail header: '#{line.inspect}'"
+ end
+ end
+ add_hf name, field if name
+
+ if unixfrom
+ add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
+ end
+ end
+
+ def add_hf( name, field )
+ key = name.downcase
+ field = new_hf(name, field)
+
+ if ALLOW_MULTIPLE.include? key
+ (@header[key] ||= []).push field
+ else
+ @header[key] = field
+ end
+ end
+
+ def new_hf( name, field )
+ HeaderField.new(name, field, @config)
+ end
+
+ ###
+ ### body
+ ###
+
+ public
+
+ def body_port
+ parse_body
+ @body_port
+ end
+
+ def each( &block )
+ body_port().ropen {|f| f.each(&block) }
+ end
+
+ def body
+ parse_body
+ @body_port.ropen {|f|
+ return f.read
+ }
+ end
+
+ def body=( str )
+ parse_body
+ @body_port.wopen {|f| f.write str }
+ str
+ end
+
+ alias preamble body
+ alias preamble= body=
+
+ def epilogue
+ parse_body
+ @epilogue.dup
+ end
+
+ def epilogue=( str )
+ parse_body
+ @epilogue = str
+ str
+ end
+
+ def parts
+ parse_body
+ @parts
+ end
+
+ def each_part( &block )
+ parts().each(&block)
+ end
+
+ private
+
+ def parse_body( f = nil )
+ return if @body_parsed
+ if f
+ parse_body_0 f
+ else
+ @port.ropen {|f|
+ skip_header f
+ parse_body_0 f
+ }
+ end
+ @body_parsed = true
+ end
+
+ def skip_header( f )
+ while line = f.gets
+ return if /\A[\r\n]*\z/ === line
+ end
+ end
+
+ def parse_body_0( f )
+ if multipart?
+ read_multipart f
+ else
+ @body_port = @config.new_body_port(self)
+ @body_port.wopen {|w|
+ w.write f.read
+ }
+ end
+ end
+
+ def read_multipart( src )
+ bound = @header['content-type'].params['boundary']
+ is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/
+ lastbound = "--#{bound}--"
+
+ ports = [ @config.new_preamble_port(self) ]
+ begin
+ f = ports.last.wopen
+ while line = src.gets
+ if is_sep === line
+ f.close
+ break if line.strip == lastbound
+ ports.push @config.new_part_port(self)
+ f = ports.last.wopen
+ else
+ f << line
+ end
+ end
+ @epilogue = (src.read || '')
+ ensure
+ f.close if f and not f.closed?
+ end
+
+ @body_port = ports.shift
+ @parts = ports.map {|p| self.class.new(p, @config) }
+ end
+
+ end # class Mail
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/mailbox.rb b/actionmailer/lib/action_mailer/vendor/tmail/mailbox.rb
new file mode 100755
index 0000000000..5c041c98a8
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/mailbox.rb
@@ -0,0 +1,414 @@
+#
+# mailbox.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'tmail/port'
+require 'socket'
+require 'mutex_m'
+
+
+unless [].respond_to?(:sort_by)
+module Enumerable#:nodoc:
+ def sort_by
+ map {|i| [yield(i), i] }.sort {|a,b| a.first <=> b.first }.map {|i| i[1] }
+ end
+end
+end
+
+
+module TMail
+
+ class MhMailbox
+
+ PORT_CLASS = MhPort
+
+ def initialize( dir )
+ edir = File.expand_path(dir)
+ raise ArgumentError, "not directory: #{dir}"\
+ unless FileTest.directory? edir
+ @dirname = edir
+ @last_file = nil
+ @last_atime = nil
+ end
+
+ def directory
+ @dirname
+ end
+
+ alias dirname directory
+
+ attr_accessor :last_atime
+
+ def inspect
+ "#<#{self.class} #{@dirname}>"
+ end
+
+ def close
+ end
+
+ def new_port
+ PORT_CLASS.new(next_file_name())
+ end
+
+ def each_port
+ mail_files().each do |path|
+ yield PORT_CLASS.new(path)
+ end
+ @last_atime = Time.now
+ end
+
+ alias each each_port
+
+ def reverse_each_port
+ mail_files().reverse_each do |path|
+ yield PORT_CLASS.new(path)
+ end
+ @last_atime = Time.now
+ end
+
+ alias reverse_each reverse_each_port
+
+ # old #each_mail returns Port
+ #def each_mail
+ # each_port do |port|
+ # yield Mail.new(port)
+ # end
+ #end
+
+ def each_new_port( mtime = nil, &block )
+ mtime ||= @last_atime
+ return each_port(&block) unless mtime
+ return unless File.mtime(@dirname) >= mtime
+
+ mail_files().each do |path|
+ yield PORT_CLASS.new(path) if File.mtime(path) > mtime
+ end
+ @last_atime = Time.now
+ end
+
+ private
+
+ def mail_files
+ Dir.entries(@dirname)\
+ .select {|s| /\A\d+\z/ === s }\
+ .map {|s| s.to_i }\
+ .sort\
+ .map {|i| "#{@dirname}/#{i}" }\
+ .select {|path| FileTest.file? path }
+ end
+
+ def next_file_name
+ unless n = @last_file
+ n = 0
+ Dir.entries(@dirname)\
+ .select {|s| /\A\d+\z/ === s }\
+ .map {|s| s.to_i }.sort\
+ .each do |i|
+ next unless FileTest.file? "#{@dirname}/#{i}"
+ n = i
+ end
+ end
+ begin
+ n += 1
+ end while FileTest.exist? "#{@dirname}/#{n}"
+ @last_file = n
+
+ "#{@dirname}/#{n}"
+ end
+
+ end # MhMailbox
+
+ MhLoader = MhMailbox
+
+
+ class UNIXMbox
+
+ def UNIXMbox.lock( fname )
+ begin
+ f = File.open(fname)
+ f.flock File::LOCK_EX
+ yield f
+ ensure
+ f.flock File::LOCK_UN
+ f.close if f and not f.closed?
+ end
+ end
+
+ class << self
+ alias newobj new
+ end
+
+ def UNIXMbox.new( fname, tmpdir = nil, readonly = false )
+ tmpdir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
+ newobj(fname, "#{tmpdir}/ruby_tmail_#{$$}_#{rand()}", readonly, false)
+ end
+
+ def UNIXMbox.static_new( fname, dir, readonly = false )
+ newobj(fname, dir, readonly, true)
+ end
+
+ def initialize( fname, mhdir, readonly, static )
+ @filename = fname
+ @readonly = readonly
+ @closed = false
+
+ Dir.mkdir mhdir
+ @real = MhMailbox.new(mhdir)
+ @finalizer = UNIXMbox.mkfinal(@real, @filename, !@readonly, !static)
+ ObjectSpace.define_finalizer self, @finalizer
+ end
+
+ def UNIXMbox.mkfinal( mh, mboxfile, writeback_p, cleanup_p )
+ lambda {
+ if writeback_p
+ lock(mboxfile) {|f|
+ mh.each_port do |port|
+ f.puts create_from_line(port)
+ port.ropen {|r|
+ f.puts r.read
+ }
+ end
+ }
+ end
+ if cleanup_p
+ Dir.foreach(mh.dirname) do |fname|
+ next if /\A\.\.?\z/ === fname
+ File.unlink "#{mh.dirname}/#{fname}"
+ end
+ Dir.rmdir mh.dirname
+ end
+ }
+ end
+
+ # make _From line
+ def UNIXMbox.create_from_line( port )
+ sprintf 'From %s %s',
+ fromaddr(), TextUtils.time2str(File.mtime(port.filename))
+ end
+
+ def UNIXMbox.fromaddr
+ h = HeaderField.new_from_port(port, 'Return-Path') ||
+ HeaderField.new_from_port(port, 'From') or return 'nobody'
+ a = h.addrs[0] or return 'nobody'
+ a.spec
+ end
+ private_class_method :fromaddr
+
+ def close
+ return if @closed
+
+ ObjectSpace.undefine_finalizer self
+ @finalizer.call
+ @finalizer = nil
+ @real = nil
+ @closed = true
+ @updated = nil
+ end
+
+ def each_port( &block )
+ close_check
+ update
+ @real.each_port(&block)
+ end
+
+ alias each each_port
+
+ def reverse_each_port( &block )
+ close_check
+ update
+ @real.reverse_each_port(&block)
+ end
+
+ alias reverse_each reverse_each_port
+
+ # old #each_mail returns Port
+ #def each_mail( &block )
+ # each_port do |port|
+ # yield Mail.new(port)
+ # end
+ #end
+
+ def each_new_port( mtime = nil )
+ close_check
+ update
+ @real.each_new_port(mtime) {|p| yield p }
+ end
+
+ def new_port
+ close_check
+ @real.new_port
+ end
+
+ private
+
+ def close_check
+ @closed and raise ArgumentError, 'accessing already closed mbox'
+ end
+
+ def update
+ return if FileTest.zero?(@filename)
+ return if @updated and File.mtime(@filename) < @updated
+ w = nil
+ port = nil
+ time = nil
+ UNIXMbox.lock(@filename) {|f|
+ begin
+ f.each do |line|
+ if /\AFrom / === line
+ w.close if w
+ File.utime time, time, port.filename if time
+
+ port = @real.new_port
+ w = port.wopen
+ time = fromline2time(line)
+ else
+ w.print line if w
+ end
+ end
+ ensure
+ if w and not w.closed?
+ w.close
+ File.utime time, time, port.filename if time
+ end
+ end
+ f.truncate(0) unless @readonly
+ @updated = Time.now
+ }
+ end
+
+ def fromline2time( line )
+ m = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/.match(line) \
+ or return nil
+ Time.local(m[6].to_i, m[1], m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i)
+ end
+
+ end # UNIXMbox
+
+ MboxLoader = UNIXMbox
+
+
+ class Maildir
+
+ extend Mutex_m
+
+ PORT_CLASS = MaildirPort
+
+ @seq = 0
+ def Maildir.unique_number
+ synchronize {
+ @seq += 1
+ return @seq
+ }
+ end
+
+ def initialize( dir = nil )
+ @dirname = dir || ENV['MAILDIR']
+ raise ArgumentError, "not directory: #{@dirname}"\
+ unless FileTest.directory? @dirname
+ @new = "#{@dirname}/new"
+ @tmp = "#{@dirname}/tmp"
+ @cur = "#{@dirname}/cur"
+ end
+
+ def directory
+ @dirname
+ end
+
+ def inspect
+ "#<#{self.class} #{@dirname}>"
+ end
+
+ def close
+ end
+
+ def each_port
+ mail_files(@cur).each do |path|
+ yield PORT_CLASS.new(path)
+ end
+ end
+
+ alias each each_port
+
+ def reverse_each_port
+ mail_files(@cur).reverse_each do |path|
+ yield PORT_CLASS.new(path)
+ end
+ end
+
+ alias reverse_each reverse_each_port
+
+ def new_port
+ fname = nil
+ tmpfname = nil
+ newfname = nil
+
+ begin
+ fname = "#{Time.now.to_i}.#{$$}_#{Maildir.unique_number}.#{Socket.gethostname}"
+
+ tmpfname = "#{@tmp}/#{fname}"
+ newfname = "#{@new}/#{fname}"
+ end while FileTest.exist? tmpfname
+
+ if block_given?
+ File.open(tmpfname, 'w') {|f| yield f }
+ File.rename tmpfname, newfname
+ PORT_CLASS.new(newfname)
+ else
+ File.open(tmpfname, 'w') {|f| f.write "\n\n" }
+ PORT_CLASS.new(tmpfname)
+ end
+ end
+
+ def each_new_port
+ mail_files(@new).each do |path|
+ dest = @cur + '/' + File.basename(path)
+ File.rename path, dest
+ yield PORT_CLASS.new(dest)
+ end
+
+ check_tmp
+ end
+
+ TOO_OLD = 60 * 60 * 36 # 36 hour
+
+ def check_tmp
+ old = Time.now.to_i - TOO_OLD
+
+ each_filename(@tmp) do |full, fname|
+ if FileTest.file? full and
+ File.stat(full).mtime.to_i < old
+ File.unlink full
+ end
+ end
+ end
+
+ private
+
+ def mail_files( dir )
+ Dir.entries(dir)\
+ .select {|s| s[0] != ?. }\
+ .sort_by {|s| s.slice(/\A\d+/).to_i }\
+ .map {|s| "#{dir}/#{s}" }\
+ .select {|path| FileTest.file? path }
+ end
+
+ def each_filename( dir )
+ Dir.foreach(dir) do |fname|
+ path = "#{dir}/#{fname}"
+ if fname[0] != ?. and FileTest.file? path
+ yield path, fname
+ end
+ end
+ end
+
+ end # Maildir
+
+ MaildirLoader = Maildir
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/mbox.rb b/actionmailer/lib/action_mailer/vendor/tmail/mbox.rb
new file mode 100755
index 0000000000..7907315401
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/mbox.rb
@@ -0,0 +1 @@
+require 'tmail/mailbox'
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/net.rb b/actionmailer/lib/action_mailer/vendor/tmail/net.rb
new file mode 100755
index 0000000000..0ef433b88a
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/net.rb
@@ -0,0 +1,261 @@
+#
+# net.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'nkf'
+
+
+module TMail
+
+ class Mail
+
+ def send_to( smtp )
+ do_send_to(smtp) do
+ ready_to_send
+ end
+ end
+
+ def send_text_to( smtp )
+ do_send_to(smtp) do
+ ready_to_send
+ mime_encode
+ end
+ end
+
+ def do_send_to( smtp )
+ from = from_address or raise ArgumentError, 'no from address'
+ (dests = destinations).empty? and raise ArgumentError, 'no receipient'
+ yield
+ send_to_0 smtp, from, dests
+ end
+ private :do_send_to
+
+ def send_to_0( smtp, from, to )
+ smtp.ready(from, to) do |f|
+ encoded "\r\n", 'j', f, ''
+ end
+ end
+
+ def ready_to_send
+ delete_no_send_fields
+ add_message_id
+ add_date
+ end
+
+ NOSEND_FIELDS = %w(
+ received
+ bcc
+ )
+
+ def delete_no_send_fields
+ NOSEND_FIELDS.each do |nm|
+ delete nm
+ end
+ delete_if {|n,v| v.empty? }
+ end
+
+ def add_message_id( fqdn = nil )
+ self.message_id = ::TMail::new_msgid(fqdn)
+ end
+
+ def add_date
+ self.date = Time.now
+ end
+
+ def mime_encode
+ if parts.empty?
+ mime_encode_singlepart
+ else
+ mime_encode_multipart true
+ end
+ end
+
+ def mime_encode_singlepart
+ self.mime_version = '1.0'
+ b = body
+ if NKF.guess(b) != NKF::BINARY
+ mime_encode_text b
+ else
+ mime_encode_binary b
+ end
+ end
+
+ def mime_encode_text( body )
+ self.body = NKF.nkf('-j -m0', body)
+ self.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
+ self.encoding = '7bit'
+ end
+
+ def mime_encode_binary( body )
+ self.body = [body].pack('m')
+ self.set_content_type 'application', 'octet-stream'
+ self.encoding = 'Base64'
+ end
+
+ def mime_encode_multipart( top = true )
+ self.mime_version = '1.0' if top
+ self.set_content_type 'multipart', 'mixed'
+ e = encoding(nil)
+ if e and not /\A(?:7bit|8bit|binary)\z/i === e
+ raise ArgumentError,
+ 'using C.T.Encoding with multipart mail is not permitted'
+ end
+ end
+
+ def create_empty_mail
+ self.class.new(StringPort.new(''), @config)
+ end
+
+ def create_reply
+ setup_reply create_empty_mail()
+ end
+
+ def setup_reply( m )
+ if tmp = reply_addresses(nil)
+ m.to_addrs = tmp
+ end
+
+ mid = message_id(nil)
+ tmp = references(nil) || []
+ tmp.push mid if mid
+ m.in_reply_to = [mid] if mid
+ m.references = tmp unless tmp.empty?
+ m.subject = 'Re: ' + subject('').sub(/\A(?:\s*re:)+/i, '')
+
+ m
+ end
+
+ def create_forward
+ setup_forward create_empty_mail()
+ end
+
+ def setup_forward( mail )
+ m = Mail.new(StringPort.new(''))
+ m.body = decoded
+ m.set_content_type 'message', 'rfc822'
+ m.encoding = encoding('7bit')
+ mail.parts.push m
+ end
+
+ end
+
+
+ class DeleteFields
+
+ NOSEND_FIELDS = %w(
+ received
+ bcc
+ )
+
+ def initialize( nosend = nil, delempty = true )
+ @no_send_fields = nosend || NOSEND_FIELDS.dup
+ @delete_empty_fields = delempty
+ end
+
+ attr :no_send_fields
+ attr :delete_empty_fields, true
+
+ def exec( mail )
+ @no_send_fields.each do |nm|
+ delete nm
+ end
+ delete_if {|n,v| v.empty? } if @delete_empty_fields
+ end
+
+ end
+
+
+ class AddMessageId
+
+ def initialize( fqdn = nil )
+ @fqdn = fqdn
+ end
+
+ attr :fqdn, true
+
+ def exec( mail )
+ mail.message_id = ::TMail::new_msgid(@fqdn)
+ end
+
+ end
+
+
+ class AddDate
+
+ def exec( mail )
+ mail.date = Time.now
+ end
+
+ end
+
+
+ class MimeEncodeAuto
+
+ def initialize( s = nil, m = nil )
+ @singlepart_composer = s || MimeEncodeSingle.new
+ @multipart_composer = m || MimeEncodeMulti.new
+ end
+
+ attr :singlepart_composer
+ attr :multipart_composer
+
+ def exec( mail )
+ if mail._builtin_multipart?
+ then @multipart_composer
+ else @singlepart_composer end.exec mail
+ end
+
+ end
+
+
+ class MimeEncodeSingle
+
+ def exec( mail )
+ mail.mime_version = '1.0'
+ b = mail.body
+ if NKF.guess(b) != NKF::BINARY
+ on_text b
+ else
+ on_binary b
+ end
+ end
+
+ def on_text( body )
+ mail.body = NKF.nkf('-j -m0', body)
+ mail.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
+ mail.encoding = '7bit'
+ end
+
+ def on_binary( body )
+ mail.body = [body].pack('m')
+ mail.set_content_type 'application', 'octet-stream'
+ mail.encoding = 'Base64'
+ end
+
+ end
+
+
+ class MimeEncodeMulti
+
+ def exec( mail, top = true )
+ mail.mime_version = '1.0' if top
+ mail.set_content_type 'multipart', 'mixed'
+ e = encoding(nil)
+ if e and not /\A(?:7bit|8bit|binary)\z/i === e
+ raise ArgumentError,
+ 'using C.T.Encoding with multipart mail is not permitted'
+ end
+ mail.parts.each do |m|
+ exec m, false if m._builtin_multipart?
+ end
+ end
+
+ end
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/obsolete.rb b/actionmailer/lib/action_mailer/vendor/tmail/obsolete.rb
new file mode 100755
index 0000000000..1088453127
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/obsolete.rb
@@ -0,0 +1,116 @@
+#
+# obsolete.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+module TMail
+
+ # mail.rb
+ class Mail
+ alias include? key?
+ alias has_key? key?
+
+ def values
+ ret = []
+ each_field {|v| ret.push v }
+ ret
+ end
+
+ def value?( val )
+ HeaderField === val or return false
+
+ [ @header[val.name.downcase] ].flatten.include? val
+ end
+
+ alias has_value? value?
+ end
+
+
+ # facade.rb
+ class Mail
+ def from_addr( default = nil )
+ addr, = from_addrs(nil)
+ addr || default
+ end
+
+ def from_address( default = nil )
+ if a = from_addr(nil)
+ a.spec
+ else
+ default
+ end
+ end
+
+ alias from_address= from_addrs=
+
+ def from_phrase( default = nil )
+ if a = from_addr(nil)
+ a.phrase
+ else
+ default
+ end
+ end
+
+ alias msgid message_id
+ alias msgid= message_id=
+
+ alias each_dest each_destination
+ end
+
+
+ # address.rb
+ class Address
+ alias route routes
+ alias addr spec
+
+ def spec=( str )
+ @local, @domain = str.split(/@/,2).map {|s| s.split(/\./) }
+ end
+
+ alias addr= spec=
+ alias address= spec=
+ end
+
+
+ # mbox.rb
+ class MhMailbox
+ alias new_mail new_port
+ alias each_mail each_port
+ alias each_newmail each_new_port
+ end
+ class UNIXMbox
+ alias new_mail new_port
+ alias each_mail each_port
+ alias each_newmail each_new_port
+ end
+ class Maildir
+ alias new_mail new_port
+ alias each_mail each_port
+ alias each_newmail each_new_port
+ end
+
+
+ # utils.rb
+ extend TextUtils
+
+ class << self
+ alias msgid? message_id?
+ alias boundary new_boundary
+ alias msgid new_message_id
+ alias new_msgid new_message_id
+ end
+
+ def Mail.boundary
+ ::TMail.new_boundary
+ end
+
+ def Mail.msgid
+ ::TMail.new_message_id
+ end
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/parser.rb b/actionmailer/lib/action_mailer/vendor/tmail/parser.rb
new file mode 100755
index 0000000000..c01b935891
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/parser.rb
@@ -0,0 +1,1503 @@
+#
+# DO NOT MODIFY!!!!
+# This file is automatically generated by racc 1.4.3
+# from racc grammer file "parser.y".
+#
+#
+# parser.rb: generated by racc (runtime embedded)
+#
+
+###### racc/parser.rb
+
+unless $".index 'racc/parser.rb'
+$".push 'racc/parser.rb'
+
+self.class.module_eval <<'..end /home/aamine/lib/ruby/racc/parser.rb modeval..idb76f2e220d', '/home/aamine/lib/ruby/racc/parser.rb', 1
+#
+# parser.rb
+#
+# Copyright (c) 1999-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the same terms of ruby.
+#
+# As a special exception, when this code is copied by Racc
+# into a Racc output file, you may use that output file
+# without restriction.
+#
+# $Id: parser.rb,v 1.1.1.1 2004/10/14 11:59:58 webster132 Exp $
+#
+
+unless defined? NotImplementedError
+ NotImplementedError = NotImplementError
+end
+
+
+module Racc
+ class ParseError < StandardError; end
+end
+unless defined?(::ParseError)
+ ParseError = Racc::ParseError
+end
+
+
+module Racc
+
+ unless defined? Racc_No_Extentions
+ Racc_No_Extentions = false
+ end
+
+ class Parser
+
+ Racc_Runtime_Version = '1.4.3'
+ Racc_Runtime_Revision = '$Revision: 1.1.1.1 $'.split(/\s+/)[1]
+
+ Racc_Runtime_Core_Version_R = '1.4.3'
+ Racc_Runtime_Core_Revision_R = '$Revision: 1.1.1.1 $'.split(/\s+/)[1]
+ begin
+ require 'racc/cparse'
+ # Racc_Runtime_Core_Version_C = (defined in extention)
+ Racc_Runtime_Core_Revision_C = Racc_Runtime_Core_Id_C.split(/\s+/)[2]
+ unless new.respond_to?(:_racc_do_parse_c, true)
+ raise LoadError, 'old cparse.so'
+ end
+ if Racc_No_Extentions
+ raise LoadError, 'selecting ruby version of racc runtime core'
+ end
+
+ Racc_Main_Parsing_Routine = :_racc_do_parse_c
+ Racc_YY_Parse_Method = :_racc_yyparse_c
+ Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_C
+ Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_C
+ Racc_Runtime_Type = 'c'
+ rescue LoadError
+ Racc_Main_Parsing_Routine = :_racc_do_parse_rb
+ Racc_YY_Parse_Method = :_racc_yyparse_rb
+ Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_R
+ Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_R
+ Racc_Runtime_Type = 'ruby'
+ end
+
+ def self.racc_runtime_type
+ Racc_Runtime_Type
+ end
+
+ private
+
+ def _racc_setup
+ @yydebug = false unless self.class::Racc_debug_parser
+ @yydebug = false unless defined? @yydebug
+ if @yydebug
+ @racc_debug_out = $stderr unless defined? @racc_debug_out
+ @racc_debug_out ||= $stderr
+ end
+ arg = self.class::Racc_arg
+ arg[13] = true if arg.size < 14
+ arg
+ end
+
+ def _racc_init_sysvars
+ @racc_state = [0]
+ @racc_tstack = []
+ @racc_vstack = []
+
+ @racc_t = nil
+ @racc_val = nil
+
+ @racc_read_next = true
+
+ @racc_user_yyerror = false
+ @racc_error_status = 0
+ end
+
+
+ ###
+ ### do_parse
+ ###
+
+ def do_parse
+ __send__ Racc_Main_Parsing_Routine, _racc_setup(), false
+ end
+
+ def next_token
+ raise NotImplementedError, "#{self.class}\#next_token is not defined"
+ end
+
+ def _racc_do_parse_rb( arg, in_debug )
+ action_table, action_check, action_default, action_pointer,
+ goto_table, goto_check, goto_default, goto_pointer,
+ nt_base, reduce_table, token_table, shift_n,
+ reduce_n, use_result, * = arg
+
+ _racc_init_sysvars
+ tok = act = i = nil
+ nerr = 0
+
+ catch(:racc_end_parse) {
+ while true
+ if i = action_pointer[@racc_state[-1]]
+ if @racc_read_next
+ if @racc_t != 0 # not EOF
+ tok, @racc_val = next_token()
+ unless tok # EOF
+ @racc_t = 0
+ else
+ @racc_t = (token_table[tok] or 1) # error token
+ end
+ racc_read_token(@racc_t, tok, @racc_val) if @yydebug
+ @racc_read_next = false
+ end
+ end
+ i += @racc_t
+ if i >= 0 and
+ act = action_table[i] and
+ action_check[i] == @racc_state[-1]
+ ;
+ else
+ act = action_default[@racc_state[-1]]
+ end
+ else
+ act = action_default[@racc_state[-1]]
+ end
+ while act = _racc_evalact(act, arg)
+ end
+ end
+ }
+ end
+
+
+ ###
+ ### yyparse
+ ###
+
+ def yyparse( recv, mid )
+ __send__ Racc_YY_Parse_Method, recv, mid, _racc_setup(), true
+ end
+
+ def _racc_yyparse_rb( recv, mid, arg, c_debug )
+ action_table, action_check, action_default, action_pointer,
+ goto_table, goto_check, goto_default, goto_pointer,
+ nt_base, reduce_table, token_table, shift_n,
+ reduce_n, use_result, * = arg
+
+ _racc_init_sysvars
+ tok = nil
+ act = nil
+ i = nil
+ nerr = 0
+
+
+ catch(:racc_end_parse) {
+ until i = action_pointer[@racc_state[-1]]
+ while act = _racc_evalact(action_default[@racc_state[-1]], arg)
+ end
+ end
+
+ recv.__send__(mid) do |tok, val|
+# $stderr.puts "rd: tok=#{tok}, val=#{val}"
+ unless tok
+ @racc_t = 0
+ else
+ @racc_t = (token_table[tok] or 1) # error token
+ end
+ @racc_val = val
+ @racc_read_next = false
+
+ i += @racc_t
+ if i >= 0 and
+ act = action_table[i] and
+ action_check[i] == @racc_state[-1]
+ ;
+# $stderr.puts "01: act=#{act}"
+ else
+ act = action_default[@racc_state[-1]]
+# $stderr.puts "02: act=#{act}"
+# $stderr.puts "curstate=#{@racc_state[-1]}"
+ end
+
+ while act = _racc_evalact(act, arg)
+ end
+
+ while not (i = action_pointer[@racc_state[-1]]) or
+ not @racc_read_next or
+ @racc_t == 0 # $
+ if i and i += @racc_t and
+ i >= 0 and
+ act = action_table[i] and
+ action_check[i] == @racc_state[-1]
+ ;
+# $stderr.puts "03: act=#{act}"
+ else
+# $stderr.puts "04: act=#{act}"
+ act = action_default[@racc_state[-1]]
+ end
+
+ while act = _racc_evalact(act, arg)
+ end
+ end
+ end
+ }
+ end
+
+
+ ###
+ ### common
+ ###
+
+ def _racc_evalact( act, arg )
+# $stderr.puts "ea: act=#{act}"
+ action_table, action_check, action_default, action_pointer,
+ goto_table, goto_check, goto_default, goto_pointer,
+ nt_base, reduce_table, token_table, shift_n,
+ reduce_n, use_result, * = arg
+nerr = 0 # tmp
+
+ if act > 0 and act < shift_n
+ #
+ # shift
+ #
+
+ if @racc_error_status > 0
+ @racc_error_status -= 1 unless @racc_t == 1 # error token
+ end
+
+ @racc_vstack.push @racc_val
+ @racc_state.push act
+ @racc_read_next = true
+
+ if @yydebug
+ @racc_tstack.push @racc_t
+ racc_shift @racc_t, @racc_tstack, @racc_vstack
+ end
+
+ elsif act < 0 and act > -reduce_n
+ #
+ # reduce
+ #
+
+ code = catch(:racc_jump) {
+ @racc_state.push _racc_do_reduce(arg, act)
+ false
+ }
+ if code
+ case code
+ when 1 # yyerror
+ @racc_user_yyerror = true # user_yyerror
+ return -reduce_n
+ when 2 # yyaccept
+ return shift_n
+ else
+ raise RuntimeError, '[Racc Bug] unknown jump code'
+ end
+ end
+
+ elsif act == shift_n
+ #
+ # accept
+ #
+
+ racc_accept if @yydebug
+ throw :racc_end_parse, @racc_vstack[0]
+
+ elsif act == -reduce_n
+ #
+ # error
+ #
+
+ case @racc_error_status
+ when 0
+ unless arg[21] # user_yyerror
+ nerr += 1
+ on_error @racc_t, @racc_val, @racc_vstack
+ end
+ when 3
+ if @racc_t == 0 # is $
+ throw :racc_end_parse, nil
+ end
+ @racc_read_next = true
+ end
+ @racc_user_yyerror = false
+ @racc_error_status = 3
+
+ while true
+ if i = action_pointer[@racc_state[-1]]
+ i += 1 # error token
+ if i >= 0 and
+ (act = action_table[i]) and
+ action_check[i] == @racc_state[-1]
+ break
+ end
+ end
+
+ throw :racc_end_parse, nil if @racc_state.size < 2
+ @racc_state.pop
+ @racc_vstack.pop
+ if @yydebug
+ @racc_tstack.pop
+ racc_e_pop @racc_state, @racc_tstack, @racc_vstack
+ end
+ end
+
+ return act
+
+ else
+ raise RuntimeError, "[Racc Bug] unknown action #{act.inspect}"
+ end
+
+ racc_next_state(@racc_state[-1], @racc_state) if @yydebug
+
+ nil
+ end
+
+ def _racc_do_reduce( arg, act )
+ action_table, action_check, action_default, action_pointer,
+ goto_table, goto_check, goto_default, goto_pointer,
+ nt_base, reduce_table, token_table, shift_n,
+ reduce_n, use_result, * = arg
+ state = @racc_state
+ vstack = @racc_vstack
+ tstack = @racc_tstack
+
+ i = act * -3
+ len = reduce_table[i]
+ reduce_to = reduce_table[i+1]
+ method_id = reduce_table[i+2]
+ void_array = []
+
+ tmp_t = tstack[-len, len] if @yydebug
+ tmp_v = vstack[-len, len]
+ tstack[-len, len] = void_array if @yydebug
+ vstack[-len, len] = void_array
+ state[-len, len] = void_array
+
+ # tstack must be updated AFTER method call
+ if use_result
+ vstack.push __send__(method_id, tmp_v, vstack, tmp_v[0])
+ else
+ vstack.push __send__(method_id, tmp_v, vstack)
+ end
+ tstack.push reduce_to
+
+ racc_reduce(tmp_t, reduce_to, tstack, vstack) if @yydebug
+
+ k1 = reduce_to - nt_base
+ if i = goto_pointer[k1]
+ i += state[-1]
+ if i >= 0 and (curstate = goto_table[i]) and goto_check[i] == k1
+ return curstate
+ end
+ end
+ goto_default[k1]
+ end
+
+ def on_error( t, val, vstack )
+ raise ParseError, sprintf("\nparse error on value %s (%s)",
+ val.inspect, token_to_str(t) || '?')
+ end
+
+ def yyerror
+ throw :racc_jump, 1
+ end
+
+ def yyaccept
+ throw :racc_jump, 2
+ end
+
+ def yyerrok
+ @racc_error_status = 0
+ end
+
+
+ # for debugging output
+
+ def racc_read_token( t, tok, val )
+ @racc_debug_out.print 'read '
+ @racc_debug_out.print tok.inspect, '(', racc_token2str(t), ') '
+ @racc_debug_out.puts val.inspect
+ @racc_debug_out.puts
+ end
+
+ def racc_shift( tok, tstack, vstack )
+ @racc_debug_out.puts "shift #{racc_token2str tok}"
+ racc_print_stacks tstack, vstack
+ @racc_debug_out.puts
+ end
+
+ def racc_reduce( toks, sim, tstack, vstack )
+ out = @racc_debug_out
+ out.print 'reduce '
+ if toks.empty?
+ out.print ' <none>'
+ else
+ toks.each {|t| out.print ' ', racc_token2str(t) }
+ end
+ out.puts " --> #{racc_token2str(sim)}"
+
+ racc_print_stacks tstack, vstack
+ @racc_debug_out.puts
+ end
+
+ def racc_accept
+ @racc_debug_out.puts 'accept'
+ @racc_debug_out.puts
+ end
+
+ def racc_e_pop( state, tstack, vstack )
+ @racc_debug_out.puts 'error recovering mode: pop token'
+ racc_print_states state
+ racc_print_stacks tstack, vstack
+ @racc_debug_out.puts
+ end
+
+ def racc_next_state( curstate, state )
+ @racc_debug_out.puts "goto #{curstate}"
+ racc_print_states state
+ @racc_debug_out.puts
+ end
+
+ def racc_print_stacks( t, v )
+ out = @racc_debug_out
+ out.print ' ['
+ t.each_index do |i|
+ out.print ' (', racc_token2str(t[i]), ' ', v[i].inspect, ')'
+ end
+ out.puts ' ]'
+ end
+
+ def racc_print_states( s )
+ out = @racc_debug_out
+ out.print ' ['
+ s.each {|st| out.print ' ', st }
+ out.puts ' ]'
+ end
+
+ def racc_token2str( tok )
+ self.class::Racc_token_to_s_table[tok] or
+ raise RuntimeError, "[Racc Bug] can't convert token #{tok} to string"
+ end
+
+ def token_to_str( t )
+ self.class::Racc_token_to_s_table[t]
+ end
+
+ end
+
+end
+..end /home/aamine/lib/ruby/racc/parser.rb modeval..idb76f2e220d
+end # end of racc/parser.rb
+
+
+#
+# parser.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'tmail/scanner'
+require 'tmail/utils'
+
+
+module TMail
+
+ class Parser < Racc::Parser
+
+module_eval <<'..end parser.y modeval..id43721faf1c', 'parser.y', 331
+
+ include TextUtils
+
+ def self.parse( ident, str, cmt = nil )
+ new.parse(ident, str, cmt)
+ end
+
+ MAILP_DEBUG = false
+
+ def initialize
+ self.debug = MAILP_DEBUG
+ end
+
+ def debug=( flag )
+ @yydebug = flag && Racc_debug_parser
+ @scanner_debug = flag
+ end
+
+ def debug
+ @yydebug
+ end
+
+ def parse( ident, str, comments = nil )
+ @scanner = Scanner.new(str, ident, comments)
+ @scanner.debug = @scanner_debug
+ @first = [ident, ident]
+ result = yyparse(self, :parse_in)
+ comments.map! {|c| to_kcode(c) } if comments
+ result
+ end
+
+ private
+
+ def parse_in( &block )
+ yield @first
+ @scanner.scan(&block)
+ end
+
+ def on_error( t, val, vstack )
+ raise SyntaxError, "parse error on token #{racc_token2str t}"
+ end
+
+..end parser.y modeval..id43721faf1c
+
+##### racc 1.4.3 generates ###
+
+racc_reduce_table = [
+ 0, 0, :racc_error,
+ 2, 35, :_reduce_1,
+ 2, 35, :_reduce_2,
+ 2, 35, :_reduce_3,
+ 2, 35, :_reduce_4,
+ 2, 35, :_reduce_5,
+ 2, 35, :_reduce_6,
+ 2, 35, :_reduce_7,
+ 2, 35, :_reduce_8,
+ 2, 35, :_reduce_9,
+ 2, 35, :_reduce_10,
+ 2, 35, :_reduce_11,
+ 2, 35, :_reduce_12,
+ 6, 36, :_reduce_13,
+ 0, 48, :_reduce_none,
+ 2, 48, :_reduce_none,
+ 3, 49, :_reduce_16,
+ 5, 49, :_reduce_17,
+ 1, 50, :_reduce_18,
+ 7, 37, :_reduce_19,
+ 0, 51, :_reduce_none,
+ 2, 51, :_reduce_21,
+ 0, 52, :_reduce_none,
+ 2, 52, :_reduce_23,
+ 1, 58, :_reduce_24,
+ 3, 58, :_reduce_25,
+ 2, 58, :_reduce_26,
+ 0, 53, :_reduce_none,
+ 2, 53, :_reduce_28,
+ 0, 54, :_reduce_29,
+ 3, 54, :_reduce_30,
+ 0, 55, :_reduce_none,
+ 2, 55, :_reduce_32,
+ 2, 55, :_reduce_33,
+ 0, 56, :_reduce_none,
+ 2, 56, :_reduce_35,
+ 1, 61, :_reduce_36,
+ 1, 61, :_reduce_37,
+ 0, 57, :_reduce_none,
+ 2, 57, :_reduce_39,
+ 1, 38, :_reduce_none,
+ 1, 38, :_reduce_none,
+ 3, 38, :_reduce_none,
+ 1, 46, :_reduce_none,
+ 1, 46, :_reduce_none,
+ 1, 46, :_reduce_none,
+ 1, 39, :_reduce_none,
+ 2, 39, :_reduce_47,
+ 1, 64, :_reduce_48,
+ 3, 64, :_reduce_49,
+ 1, 68, :_reduce_none,
+ 1, 68, :_reduce_none,
+ 1, 69, :_reduce_52,
+ 3, 69, :_reduce_53,
+ 1, 47, :_reduce_none,
+ 1, 47, :_reduce_none,
+ 2, 47, :_reduce_56,
+ 2, 67, :_reduce_none,
+ 3, 65, :_reduce_58,
+ 2, 65, :_reduce_59,
+ 1, 70, :_reduce_60,
+ 2, 70, :_reduce_61,
+ 4, 62, :_reduce_62,
+ 3, 62, :_reduce_63,
+ 2, 72, :_reduce_none,
+ 2, 73, :_reduce_65,
+ 4, 73, :_reduce_66,
+ 3, 63, :_reduce_67,
+ 1, 63, :_reduce_68,
+ 1, 74, :_reduce_none,
+ 2, 74, :_reduce_70,
+ 1, 71, :_reduce_71,
+ 3, 71, :_reduce_72,
+ 1, 59, :_reduce_73,
+ 3, 59, :_reduce_74,
+ 1, 76, :_reduce_75,
+ 2, 76, :_reduce_76,
+ 1, 75, :_reduce_none,
+ 1, 75, :_reduce_none,
+ 1, 75, :_reduce_none,
+ 1, 77, :_reduce_none,
+ 1, 77, :_reduce_none,
+ 1, 77, :_reduce_none,
+ 1, 66, :_reduce_none,
+ 2, 66, :_reduce_none,
+ 3, 60, :_reduce_85,
+ 1, 40, :_reduce_86,
+ 3, 40, :_reduce_87,
+ 1, 79, :_reduce_none,
+ 2, 79, :_reduce_89,
+ 1, 41, :_reduce_90,
+ 2, 41, :_reduce_91,
+ 3, 42, :_reduce_92,
+ 5, 43, :_reduce_93,
+ 3, 43, :_reduce_94,
+ 0, 80, :_reduce_95,
+ 5, 80, :_reduce_96,
+ 1, 82, :_reduce_none,
+ 1, 82, :_reduce_none,
+ 1, 44, :_reduce_99,
+ 3, 45, :_reduce_100,
+ 0, 81, :_reduce_none,
+ 1, 81, :_reduce_none,
+ 1, 78, :_reduce_none,
+ 1, 78, :_reduce_none,
+ 1, 78, :_reduce_none,
+ 1, 78, :_reduce_none,
+ 1, 78, :_reduce_none,
+ 1, 78, :_reduce_none,
+ 1, 78, :_reduce_none ]
+
+racc_reduce_n = 110
+
+racc_shift_n = 168
+
+racc_action_table = [
+ -70, -69, 23, 25, 146, 147, 29, 31, 105, 106,
+ 16, 17, 20, 22, 136, 27, -70, -69, 32, 101,
+ -70, -69, 154, 100, 113, 115, -70, -69, -70, 109,
+ 75, 23, 25, 101, 155, 29, 31, 142, 143, 16,
+ 17, 20, 22, 107, 27, 23, 25, 32, 98, 29,
+ 31, 96, 94, 16, 17, 20, 22, 78, 27, 23,
+ 25, 32, 112, 29, 31, 74, 91, 16, 17, 20,
+ 22, 88, 117, 92, 81, 32, 23, 25, 80, 123,
+ 29, 31, 100, 125, 16, 17, 20, 22, 126, 23,
+ 25, 109, 32, 29, 31, 91, 128, 16, 17, 20,
+ 22, 129, 27, 23, 25, 32, 101, 29, 31, 101,
+ 130, 16, 17, 20, 22, 79, 52, 23, 25, 32,
+ 78, 29, 31, 133, 78, 16, 17, 20, 22, 77,
+ 23, 25, 75, 32, 29, 31, 65, 62, 16, 17,
+ 20, 22, 139, 23, 25, 101, 32, 29, 31, 60,
+ 100, 16, 17, 20, 22, 44, 27, 101, 148, 32,
+ 23, 25, 120, 149, 29, 31, 152, 153, 16, 17,
+ 20, 22, 42, 27, 157, 159, 32, 23, 25, 120,
+ 40, 29, 31, 15, 164, 16, 17, 20, 22, 40,
+ 27, 23, 25, 32, 68, 29, 31, 166, 167, 16,
+ 17, 20, 22, nil, 27, 23, 25, 32, nil, 29,
+ 31, 74, nil, 16, 17, 20, 22, nil, 23, 25,
+ nil, 32, 29, 31, nil, nil, 16, 17, 20, 22,
+ nil, 23, 25, nil, 32, 29, 31, nil, nil, 16,
+ 17, 20, 22, nil, 23, 25, nil, 32, 29, 31,
+ nil, nil, 16, 17, 20, 22, nil, 23, 25, nil,
+ 32, 29, 31, nil, nil, 16, 17, 20, 22, nil,
+ 27, 23, 25, 32, nil, 29, 31, nil, nil, 16,
+ 17, 20, 22, nil, 23, 25, nil, 32, 29, 31,
+ nil, nil, 16, 17, 20, 22, nil, 23, 25, nil,
+ 32, 29, 31, nil, nil, 16, 17, 20, 22, nil,
+ 84, 25, nil, 32, 29, 31, nil, 87, 16, 17,
+ 20, 22, 4, 6, 7, 8, 9, 10, 11, 12,
+ 13, 1, 2, 3, 84, 25, nil, nil, 29, 31,
+ nil, 87, 16, 17, 20, 22, 84, 25, nil, nil,
+ 29, 31, nil, 87, 16, 17, 20, 22, 84, 25,
+ nil, nil, 29, 31, nil, 87, 16, 17, 20, 22,
+ 84, 25, nil, nil, 29, 31, nil, 87, 16, 17,
+ 20, 22, 84, 25, nil, nil, 29, 31, nil, 87,
+ 16, 17, 20, 22, 84, 25, nil, nil, 29, 31,
+ nil, 87, 16, 17, 20, 22 ]
+
+racc_action_check = [
+ 75, 28, 68, 68, 136, 136, 68, 68, 72, 72,
+ 68, 68, 68, 68, 126, 68, 75, 28, 68, 67,
+ 75, 28, 143, 66, 86, 86, 75, 28, 75, 75,
+ 28, 3, 3, 86, 143, 3, 3, 134, 134, 3,
+ 3, 3, 3, 73, 3, 152, 152, 3, 62, 152,
+ 152, 60, 56, 152, 152, 152, 152, 51, 152, 52,
+ 52, 152, 80, 52, 52, 52, 50, 52, 52, 52,
+ 52, 45, 89, 52, 42, 52, 71, 71, 41, 96,
+ 71, 71, 97, 98, 71, 71, 71, 71, 100, 7,
+ 7, 101, 71, 7, 7, 102, 104, 7, 7, 7,
+ 7, 105, 7, 8, 8, 7, 108, 8, 8, 111,
+ 112, 8, 8, 8, 8, 40, 8, 9, 9, 8,
+ 36, 9, 9, 117, 121, 9, 9, 9, 9, 33,
+ 10, 10, 70, 9, 10, 10, 13, 12, 10, 10,
+ 10, 10, 130, 2, 2, 131, 10, 2, 2, 11,
+ 135, 2, 2, 2, 2, 6, 2, 138, 139, 2,
+ 90, 90, 90, 140, 90, 90, 141, 142, 90, 90,
+ 90, 90, 5, 90, 148, 151, 90, 127, 127, 127,
+ 4, 127, 127, 1, 157, 127, 127, 127, 127, 159,
+ 127, 26, 26, 127, 26, 26, 26, 163, 164, 26,
+ 26, 26, 26, nil, 26, 27, 27, 26, nil, 27,
+ 27, 27, nil, 27, 27, 27, 27, nil, 155, 155,
+ nil, 27, 155, 155, nil, nil, 155, 155, 155, 155,
+ nil, 122, 122, nil, 155, 122, 122, nil, nil, 122,
+ 122, 122, 122, nil, 76, 76, nil, 122, 76, 76,
+ nil, nil, 76, 76, 76, 76, nil, 38, 38, nil,
+ 76, 38, 38, nil, nil, 38, 38, 38, 38, nil,
+ 38, 55, 55, 38, nil, 55, 55, nil, nil, 55,
+ 55, 55, 55, nil, 94, 94, nil, 55, 94, 94,
+ nil, nil, 94, 94, 94, 94, nil, 59, 59, nil,
+ 94, 59, 59, nil, nil, 59, 59, 59, 59, nil,
+ 114, 114, nil, 59, 114, 114, nil, 114, 114, 114,
+ 114, 114, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 77, 77, nil, nil, 77, 77,
+ nil, 77, 77, 77, 77, 77, 44, 44, nil, nil,
+ 44, 44, nil, 44, 44, 44, 44, 44, 113, 113,
+ nil, nil, 113, 113, nil, 113, 113, 113, 113, 113,
+ 88, 88, nil, nil, 88, 88, nil, 88, 88, 88,
+ 88, 88, 74, 74, nil, nil, 74, 74, nil, 74,
+ 74, 74, 74, 74, 129, 129, nil, nil, 129, 129,
+ nil, 129, 129, 129, 129, 129 ]
+
+racc_action_pointer = [
+ 320, 152, 129, 17, 165, 172, 137, 75, 89, 103,
+ 116, 135, 106, 105, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, 177, 191, 1, nil,
+ nil, nil, nil, 109, nil, nil, 94, nil, 243, nil,
+ 99, 64, 74, nil, 332, 52, nil, nil, nil, nil,
+ 50, 31, 45, nil, nil, 257, 36, nil, nil, 283,
+ 22, nil, 16, nil, nil, nil, -3, -10, -12, nil,
+ 103, 62, -8, 15, 368, 0, 230, 320, nil, nil,
+ 47, nil, nil, nil, nil, nil, 4, nil, 356, 50,
+ 146, nil, nil, nil, 270, nil, 65, 56, 52, nil,
+ 57, 62, 79, nil, 68, 81, nil, nil, 77, nil,
+ nil, 80, 96, 344, 296, nil, nil, 108, nil, nil,
+ nil, 98, 217, nil, nil, nil, -19, 163, nil, 380,
+ 128, 116, nil, nil, 14, 124, -26, nil, 128, 141,
+ 148, 141, 152, 7, nil, nil, nil, nil, 160, nil,
+ nil, 149, 31, nil, nil, 204, nil, 167, nil, 174,
+ nil, nil, nil, 169, 184, nil, nil, nil ]
+
+racc_action_default = [
+ -110, -110, -110, -110, -14, -110, -20, -110, -110, -110,
+ -110, -110, -110, -110, -10, -95, -106, -107, -77, -44,
+ -108, -11, -109, -79, -43, -103, -110, -110, -60, -104,
+ -55, -105, -78, -68, -54, -71, -45, -12, -110, -1,
+ -110, -110, -110, -2, -110, -22, -51, -48, -50, -3,
+ -40, -41, -110, -46, -4, -86, -5, -88, -6, -90,
+ -110, -7, -95, -8, -9, -99, -101, -61, -59, -56,
+ -69, -110, -110, -110, -110, -75, -110, -110, -57, -15,
+ -110, 168, -73, -80, -82, -21, -24, -81, -110, -27,
+ -110, -83, -47, -89, -110, -91, -110, -101, -110, -100,
+ -102, -75, -58, -52, -110, -110, -64, -63, -65, -76,
+ -72, -67, -110, -110, -110, -26, -23, -110, -29, -49,
+ -84, -42, -87, -92, -94, -95, -110, -110, -62, -110,
+ -110, -25, -74, -28, -31, -101, -110, -53, -66, -110,
+ -110, -34, -110, -110, -93, -96, -98, -97, -110, -18,
+ -13, -38, -110, -30, -33, -110, -32, -16, -19, -14,
+ -35, -36, -37, -110, -110, -39, -85, -17 ]
+
+racc_goto_table = [
+ 39, 67, 70, 73, 24, 37, 69, 66, 36, 38,
+ 57, 59, 55, 67, 108, 83, 90, 111, 69, 99,
+ 85, 49, 53, 76, 158, 134, 141, 70, 73, 151,
+ 118, 89, 45, 156, 160, 150, 140, 21, 14, 19,
+ 119, 102, 64, 63, 61, 83, 70, 104, 83, 58,
+ 124, 132, 56, 131, 97, 54, 93, 43, 5, 83,
+ 95, 145, 76, nil, 116, 76, nil, nil, 127, 138,
+ 103, nil, nil, nil, 38, nil, nil, 110, nil, nil,
+ nil, nil, nil, nil, 83, 83, nil, nil, 144, nil,
+ nil, nil, nil, nil, nil, 57, 121, 122, nil, nil,
+ 83, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, 135, nil, nil,
+ nil, nil, nil, 93, nil, nil, nil, 70, 162, 137,
+ 70, 163, 161, 38, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, 165 ]
+
+racc_goto_check = [
+ 2, 37, 37, 29, 13, 13, 28, 46, 31, 36,
+ 41, 41, 45, 37, 25, 44, 32, 25, 28, 47,
+ 24, 4, 4, 42, 23, 20, 21, 37, 29, 22,
+ 19, 18, 17, 26, 27, 16, 15, 12, 11, 33,
+ 34, 35, 10, 9, 8, 44, 37, 29, 44, 7,
+ 47, 43, 6, 25, 46, 5, 41, 3, 1, 44,
+ 41, 48, 42, nil, 24, 42, nil, nil, 32, 25,
+ 13, nil, nil, nil, 36, nil, nil, 41, nil, nil,
+ nil, nil, nil, nil, 44, 44, nil, nil, 47, nil,
+ nil, nil, nil, nil, nil, 41, 31, 45, nil, nil,
+ 44, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, 46, nil, nil,
+ nil, nil, nil, 41, nil, nil, nil, 37, 29, 13,
+ 37, 29, 28, 36, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, 2 ]
+
+racc_goto_pointer = [
+ nil, 58, -4, 51, 14, 47, 43, 39, 33, 31,
+ 29, 37, 35, 2, nil, -94, -105, 26, -14, -59,
+ -93, -108, -112, -127, -24, -60, -110, -118, -20, -24,
+ nil, 6, -34, 37, -50, -27, 6, -25, nil, nil,
+ nil, 1, -5, -63, -29, 3, -8, -47, -75 ]
+
+racc_goto_default = [
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, 48, 41, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, 86, nil, nil, 30, 34,
+ 50, 51, nil, 46, 47, nil, 26, 28, 71, 72,
+ 33, 35, 114, 82, 18, nil, nil, nil, nil ]
+
+racc_token_table = {
+ false => 0,
+ Object.new => 1,
+ :DATETIME => 2,
+ :RECEIVED => 3,
+ :MADDRESS => 4,
+ :RETPATH => 5,
+ :KEYWORDS => 6,
+ :ENCRYPTED => 7,
+ :MIMEVERSION => 8,
+ :CTYPE => 9,
+ :CENCODING => 10,
+ :CDISPOSITION => 11,
+ :ADDRESS => 12,
+ :MAILBOX => 13,
+ :DIGIT => 14,
+ :ATOM => 15,
+ "," => 16,
+ ":" => 17,
+ :FROM => 18,
+ :BY => 19,
+ "@" => 20,
+ :DOMLIT => 21,
+ :VIA => 22,
+ :WITH => 23,
+ :ID => 24,
+ :FOR => 25,
+ ";" => 26,
+ "<" => 27,
+ ">" => 28,
+ "." => 29,
+ :QUOTED => 30,
+ :TOKEN => 31,
+ "/" => 32,
+ "=" => 33 }
+
+racc_use_result_var = false
+
+racc_nt_base = 34
+
+Racc_arg = [
+ racc_action_table,
+ racc_action_check,
+ racc_action_default,
+ racc_action_pointer,
+ racc_goto_table,
+ racc_goto_check,
+ racc_goto_default,
+ racc_goto_pointer,
+ racc_nt_base,
+ racc_reduce_table,
+ racc_token_table,
+ racc_shift_n,
+ racc_reduce_n,
+ racc_use_result_var ]
+
+Racc_token_to_s_table = [
+'$end',
+'error',
+'DATETIME',
+'RECEIVED',
+'MADDRESS',
+'RETPATH',
+'KEYWORDS',
+'ENCRYPTED',
+'MIMEVERSION',
+'CTYPE',
+'CENCODING',
+'CDISPOSITION',
+'ADDRESS',
+'MAILBOX',
+'DIGIT',
+'ATOM',
+'","',
+'":"',
+'FROM',
+'BY',
+'"@"',
+'DOMLIT',
+'VIA',
+'WITH',
+'ID',
+'FOR',
+'";"',
+'"<"',
+'">"',
+'"."',
+'QUOTED',
+'TOKEN',
+'"/"',
+'"="',
+'$start',
+'content',
+'datetime',
+'received',
+'addrs_TOP',
+'retpath',
+'keys',
+'enc',
+'version',
+'ctype',
+'cencode',
+'cdisp',
+'addr_TOP',
+'mbox',
+'day',
+'hour',
+'zone',
+'from',
+'by',
+'via',
+'with',
+'id',
+'for',
+'received_datetime',
+'received_domain',
+'domain',
+'msgid',
+'received_addrspec',
+'routeaddr',
+'spec',
+'addrs',
+'group_bare',
+'commas',
+'group',
+'addr',
+'mboxes',
+'addr_phrase',
+'local_head',
+'routes',
+'at_domains',
+'local',
+'word',
+'dots',
+'domword',
+'atom',
+'phrase',
+'params',
+'opt_semicolon',
+'value']
+
+Racc_debug_parser = false
+
+##### racc system variables end #####
+
+ # reduce 0 omitted
+
+module_eval <<'.,.,', 'parser.y', 16
+ def _reduce_1( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 17
+ def _reduce_2( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 18
+ def _reduce_3( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 19
+ def _reduce_4( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 20
+ def _reduce_5( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 21
+ def _reduce_6( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 22
+ def _reduce_7( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 23
+ def _reduce_8( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 24
+ def _reduce_9( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 25
+ def _reduce_10( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 26
+ def _reduce_11( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 27
+ def _reduce_12( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 33
+ def _reduce_13( val, _values)
+ t = Time.gm(val[3].to_i, val[2], val[1].to_i, 0, 0, 0)
+ (t + val[4] - val[5]).localtime
+ end
+.,.,
+
+ # reduce 14 omitted
+
+ # reduce 15 omitted
+
+module_eval <<'.,.,', 'parser.y', 42
+ def _reduce_16( val, _values)
+ (val[0].to_i * 60 * 60) +
+ (val[2].to_i * 60)
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 47
+ def _reduce_17( val, _values)
+ (val[0].to_i * 60 * 60) +
+ (val[2].to_i * 60) +
+ (val[4].to_i)
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 54
+ def _reduce_18( val, _values)
+ timezone_string_to_unixtime(val[0])
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 59
+ def _reduce_19( val, _values)
+ val
+ end
+.,.,
+
+ # reduce 20 omitted
+
+module_eval <<'.,.,', 'parser.y', 65
+ def _reduce_21( val, _values)
+ val[1]
+ end
+.,.,
+
+ # reduce 22 omitted
+
+module_eval <<'.,.,', 'parser.y', 71
+ def _reduce_23( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 77
+ def _reduce_24( val, _values)
+ join_domain(val[0])
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 81
+ def _reduce_25( val, _values)
+ join_domain(val[2])
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 85
+ def _reduce_26( val, _values)
+ join_domain(val[0])
+ end
+.,.,
+
+ # reduce 27 omitted
+
+module_eval <<'.,.,', 'parser.y', 91
+ def _reduce_28( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 96
+ def _reduce_29( val, _values)
+ []
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 100
+ def _reduce_30( val, _values)
+ val[0].push val[2]
+ val[0]
+ end
+.,.,
+
+ # reduce 31 omitted
+
+module_eval <<'.,.,', 'parser.y', 107
+ def _reduce_32( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 111
+ def _reduce_33( val, _values)
+ val[1]
+ end
+.,.,
+
+ # reduce 34 omitted
+
+module_eval <<'.,.,', 'parser.y', 117
+ def _reduce_35( val, _values)
+ val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 123
+ def _reduce_36( val, _values)
+ val[0].spec
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 127
+ def _reduce_37( val, _values)
+ val[0].spec
+ end
+.,.,
+
+ # reduce 38 omitted
+
+module_eval <<'.,.,', 'parser.y', 134
+ def _reduce_39( val, _values)
+ val[1]
+ end
+.,.,
+
+ # reduce 40 omitted
+
+ # reduce 41 omitted
+
+ # reduce 42 omitted
+
+ # reduce 43 omitted
+
+ # reduce 44 omitted
+
+ # reduce 45 omitted
+
+ # reduce 46 omitted
+
+module_eval <<'.,.,', 'parser.y', 146
+ def _reduce_47( val, _values)
+ [ Address.new(nil, nil) ]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 148
+ def _reduce_48( val, _values)
+ val
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 149
+ def _reduce_49( val, _values)
+ val[0].push val[2]; val[0]
+ end
+.,.,
+
+ # reduce 50 omitted
+
+ # reduce 51 omitted
+
+module_eval <<'.,.,', 'parser.y', 156
+ def _reduce_52( val, _values)
+ val
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 160
+ def _reduce_53( val, _values)
+ val[0].push val[2]
+ val[0]
+ end
+.,.,
+
+ # reduce 54 omitted
+
+ # reduce 55 omitted
+
+module_eval <<'.,.,', 'parser.y', 168
+ def _reduce_56( val, _values)
+ val[1].phrase = Decoder.decode(val[0])
+ val[1]
+ end
+.,.,
+
+ # reduce 57 omitted
+
+module_eval <<'.,.,', 'parser.y', 176
+ def _reduce_58( val, _values)
+ AddressGroup.new(val[0], val[2])
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 178
+ def _reduce_59( val, _values)
+ AddressGroup.new(val[0], [])
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 181
+ def _reduce_60( val, _values)
+ val[0].join('.')
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 182
+ def _reduce_61( val, _values)
+ val[0] << ' ' << val[1].join('.')
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 186
+ def _reduce_62( val, _values)
+ val[2].routes.replace val[1]
+ val[2]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 191
+ def _reduce_63( val, _values)
+ val[1]
+ end
+.,.,
+
+ # reduce 64 omitted
+
+module_eval <<'.,.,', 'parser.y', 196
+ def _reduce_65( val, _values)
+ [ val[1].join('.') ]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 197
+ def _reduce_66( val, _values)
+ val[0].push val[3].join('.'); val[0]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 199
+ def _reduce_67( val, _values)
+ Address.new( val[0], val[2] )
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 200
+ def _reduce_68( val, _values)
+ Address.new( val[0], nil )
+ end
+.,.,
+
+ # reduce 69 omitted
+
+module_eval <<'.,.,', 'parser.y', 203
+ def _reduce_70( val, _values)
+ val[0].push ''; val[0]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 206
+ def _reduce_71( val, _values)
+ val
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 209
+ def _reduce_72( val, _values)
+ val[1].times do
+ val[0].push ''
+ end
+ val[0].push val[2]
+ val[0]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 217
+ def _reduce_73( val, _values)
+ val
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 220
+ def _reduce_74( val, _values)
+ val[1].times do
+ val[0].push ''
+ end
+ val[0].push val[2]
+ val[0]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 227
+ def _reduce_75( val, _values)
+ 0
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 228
+ def _reduce_76( val, _values)
+ 1
+ end
+.,.,
+
+ # reduce 77 omitted
+
+ # reduce 78 omitted
+
+ # reduce 79 omitted
+
+ # reduce 80 omitted
+
+ # reduce 81 omitted
+
+ # reduce 82 omitted
+
+ # reduce 83 omitted
+
+ # reduce 84 omitted
+
+module_eval <<'.,.,', 'parser.y', 243
+ def _reduce_85( val, _values)
+ val[1] = val[1].spec
+ val.join('')
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 247
+ def _reduce_86( val, _values)
+ val
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 248
+ def _reduce_87( val, _values)
+ val[0].push val[2]; val[0]
+ end
+.,.,
+
+ # reduce 88 omitted
+
+module_eval <<'.,.,', 'parser.y', 251
+ def _reduce_89( val, _values)
+ val[0] << ' ' << val[1]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 255
+ def _reduce_90( val, _values)
+ val.push nil
+ val
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 260
+ def _reduce_91( val, _values)
+ val
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 265
+ def _reduce_92( val, _values)
+ [ val[0].to_i, val[2].to_i ]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 270
+ def _reduce_93( val, _values)
+ [ val[0].downcase, val[2].downcase, decode_params(val[3]) ]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 274
+ def _reduce_94( val, _values)
+ [ val[0].downcase, nil, decode_params(val[1]) ]
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 279
+ def _reduce_95( val, _values)
+ {}
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 283
+ def _reduce_96( val, _values)
+ val[0][ val[2].downcase ] = val[4]
+ val[0]
+ end
+.,.,
+
+ # reduce 97 omitted
+
+ # reduce 98 omitted
+
+module_eval <<'.,.,', 'parser.y', 292
+ def _reduce_99( val, _values)
+ val[0].downcase
+ end
+.,.,
+
+module_eval <<'.,.,', 'parser.y', 297
+ def _reduce_100( val, _values)
+ [ val[0].downcase, decode_params(val[1]) ]
+ end
+.,.,
+
+ # reduce 101 omitted
+
+ # reduce 102 omitted
+
+ # reduce 103 omitted
+
+ # reduce 104 omitted
+
+ # reduce 105 omitted
+
+ # reduce 106 omitted
+
+ # reduce 107 omitted
+
+ # reduce 108 omitted
+
+ # reduce 109 omitted
+
+ def _reduce_none( val, _values)
+ val[0]
+ end
+
+ end # class Parser
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/port.rb b/actionmailer/lib/action_mailer/vendor/tmail/port.rb
new file mode 100755
index 0000000000..982aee8ba1
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/port.rb
@@ -0,0 +1,358 @@
+#
+# port.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'tmail/stringio'
+
+
+module TMail
+
+ class Port
+ def reproducible?
+ false
+ end
+ end
+
+
+ ###
+ ### FilePort
+ ###
+
+ class FilePort < Port
+
+ def initialize( fname )
+ @filename = File.expand_path(fname)
+ super()
+ end
+
+ attr_reader :filename
+
+ alias ident filename
+
+ def ==( other )
+ other.respond_to?(:filename) and @filename == other.filename
+ end
+
+ alias eql? ==
+
+ def hash
+ @filename.hash
+ end
+
+ def inspect
+ "#<#{self.class}:#{@filename}>"
+ end
+
+ def reproducible?
+ true
+ end
+
+ def size
+ File.size @filename
+ end
+
+
+ def ropen( &block )
+ File.open(@filename, &block)
+ end
+
+ def wopen( &block )
+ File.open(@filename, 'w', &block)
+ end
+
+ def aopen( &block )
+ File.open(@filename, 'a', &block)
+ end
+
+
+ def read_all
+ ropen {|f|
+ return f.read
+ }
+ end
+
+
+ def remove
+ File.unlink @filename
+ end
+
+ def move_to( port )
+ begin
+ File.link @filename, port.filename
+ rescue Errno::EXDEV
+ copy_to port
+ end
+ File.unlink @filename
+ end
+
+ alias mv move_to
+
+ def copy_to( port )
+ if FilePort === port
+ copy_file @filename, port.filename
+ else
+ File.open(@filename) {|r|
+ port.wopen {|w|
+ while s = r.sysread(4096)
+ w.write << s
+ end
+ } }
+ end
+ end
+
+ alias cp copy_to
+
+ private
+
+ # from fileutils.rb
+ def copy_file( src, dest )
+ st = r = w = nil
+
+ File.open(src, 'rb') {|r|
+ File.open(dest, 'wb') {|w|
+ st = r.stat
+ begin
+ while true
+ w.write r.sysread(st.blksize)
+ end
+ rescue EOFError
+ end
+ } }
+ end
+
+ end
+
+
+ module MailFlags
+
+ def seen=( b )
+ set_status 'S', b
+ end
+
+ def seen?
+ get_status 'S'
+ end
+
+ def replied=( b )
+ set_status 'R', b
+ end
+
+ def replied?
+ get_status 'R'
+ end
+
+ def flagged=( b )
+ set_status 'F', b
+ end
+
+ def flagged?
+ get_status 'F'
+ end
+
+ private
+
+ def procinfostr( str, tag, true_p )
+ a = str.upcase.split(//)
+ a.push true_p ? tag : nil
+ a.delete tag unless true_p
+ a.compact.sort.join('').squeeze
+ end
+
+ end
+
+
+ class MhPort < FilePort
+
+ include MailFlags
+
+ private
+
+ def set_status( tag, flag )
+ begin
+ tmpfile = @filename + '.tmailtmp.' + $$.to_s
+ File.open(tmpfile, 'w') {|f|
+ write_status f, tag, flag
+ }
+ File.unlink @filename
+ File.link tmpfile, @filename
+ ensure
+ File.unlink tmpfile
+ end
+ end
+
+ def write_status( f, tag, flag )
+ stat = ''
+ File.open(@filename) {|r|
+ while line = r.gets
+ if line.strip.empty?
+ break
+ elsif m = /\AX-TMail-Status:/i.match(line)
+ stat = m.post_match.strip
+ else
+ f.print line
+ end
+ end
+
+ s = procinfostr(stat, tag, flag)
+ f.puts 'X-TMail-Status: ' + s unless s.empty?
+ f.puts
+
+ while s = r.read(2048)
+ f.write s
+ end
+ }
+ end
+
+ def get_status( tag )
+ File.foreach(@filename) {|line|
+ return false if line.strip.empty?
+ if m = /\AX-TMail-Status:/i.match(line)
+ return m.post_match.strip.include?(tag[0])
+ end
+ }
+ false
+ end
+
+ end
+
+
+ class MaildirPort < FilePort
+
+ def move_to_new
+ new = replace_dir(@filename, 'new')
+ File.rename @filename, new
+ @filename = new
+ end
+
+ def move_to_cur
+ new = replace_dir(@filename, 'cur')
+ File.rename @filename, new
+ @filename = new
+ end
+
+ def replace_dir( path, dir )
+ "#{File.dirname File.dirname(path)}/#{dir}/#{File.basename path}"
+ end
+ private :replace_dir
+
+
+ include MailFlags
+
+ private
+
+ MAIL_FILE = /\A(\d+\.[\d_]+\.[^:]+)(?:\:(\d),(\w+)?)?\z/
+
+ def set_status( tag, flag )
+ if m = MAIL_FILE.match(File.basename(@filename))
+ s, uniq, type, info, = m.to_a
+ return if type and type != '2' # do not change anything
+ newname = File.dirname(@filename) + '/' +
+ uniq + ':2,' + procinfostr(info.to_s, tag, flag)
+ else
+ newname = @filename + ':2,' + tag
+ end
+
+ File.link @filename, newname
+ File.unlink @filename
+ @filename = newname
+ end
+
+ def get_status( tag )
+ m = MAIL_FILE.match(File.basename(@filename)) or return false
+ m[2] == '2' and m[3].to_s.include?(tag[0])
+ end
+
+ end
+
+
+ ###
+ ### StringPort
+ ###
+
+ class StringPort < Port
+
+ def initialize( str = '' )
+ @buffer = str
+ super()
+ end
+
+ def string
+ @buffer
+ end
+
+ def to_s
+ @buffer.dup
+ end
+
+ alias read_all to_s
+
+ def size
+ @buffer.size
+ end
+
+ def ==( other )
+ StringPort === other and @buffer.equal? other.string
+ end
+
+ alias eql? ==
+
+ def hash
+ @buffer.id.hash
+ end
+
+ def inspect
+ "#<#{self.class}:id=#{sprintf '0x%x', @buffer.id}>"
+ end
+
+ def reproducible?
+ true
+ end
+
+ def ropen( &block )
+ @buffer or raise Errno::ENOENT, "#{inspect} is already removed"
+ StringInput.open(@buffer, &block)
+ end
+
+ def wopen( &block )
+ @buffer = ''
+ StringOutput.new(@buffer, &block)
+ end
+
+ def aopen( &block )
+ @buffer ||= ''
+ StringOutput.new(@buffer, &block)
+ end
+
+ def remove
+ @buffer = nil
+ end
+
+ alias rm remove
+
+ def copy_to( port )
+ port.wopen {|f|
+ f.write @buffer
+ }
+ end
+
+ alias cp copy_to
+
+ def move_to( port )
+ if StringPort === port
+ str = @buffer
+ port.instance_eval { @buffer = str }
+ else
+ copy_to port
+ end
+ remove
+ end
+
+ end
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/scanner.rb b/actionmailer/lib/action_mailer/vendor/tmail/scanner.rb
new file mode 100755
index 0000000000..b602466d49
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/scanner.rb
@@ -0,0 +1,22 @@
+#
+# scanner.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'tmail/utils'
+
+module TMail
+ require 'tmail/scanner_r.rb'
+ begin
+ raise LoadError, 'Turn off Ruby extention by user choice' if ENV['NORUBYEXT']
+ require 'tmail/scanner_c.so'
+ Scanner = Scanner_C
+ rescue LoadError
+ Scanner = Scanner_R
+ end
+end
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb b/actionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb
new file mode 100755
index 0000000000..fb90f9b5e9
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb
@@ -0,0 +1,244 @@
+#
+# scanner_r.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+require 'tmail/config'
+
+
+module TMail
+
+ class Scanner_R
+
+ Version = '0.10.7'
+ Version.freeze
+
+ MIME_HEADERS = {
+ :CTYPE => true,
+ :CENCODING => true,
+ :CDISPOSITION => true
+ }
+
+ alnum = 'a-zA-Z0-9'
+ atomsyms = %q[ _#!$%&`'*+-{|}~^/=? ].strip
+ tokensyms = %q[ _#!$%&`'*+-{|}~^. ].strip
+
+ atomchars = alnum + Regexp.quote(atomsyms)
+ tokenchars = alnum + Regexp.quote(tokensyms)
+ iso2022str = '\e(?!\(B)..(?:[^\e]+|\e(?!\(B)..)*\e\(B'
+
+ eucstr = '(?:[\xa1-\xfe][\xa1-\xfe])+'
+ sjisstr = '(?:[\x81-\x9f\xe0-\xef][\x40-\x7e\x80-\xfc])+'
+ utf8str = '(?:[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf])+'
+
+ quoted_with_iso2022 = /\A(?:[^\\\e"]+|#{iso2022str})+/n
+ domlit_with_iso2022 = /\A(?:[^\\\e\]]+|#{iso2022str})+/n
+ comment_with_iso2022 = /\A(?:[^\\\e()]+|#{iso2022str})+/n
+
+ quoted_without_iso2022 = /\A[^\\"]+/n
+ domlit_without_iso2022 = /\A[^\\\]]+/n
+ comment_without_iso2022 = /\A[^\\()]+/n
+
+ PATTERN_TABLE = {}
+ PATTERN_TABLE['EUC'] =
+ [
+ /\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+/n,
+ /\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+/n,
+ quoted_with_iso2022,
+ domlit_with_iso2022,
+ comment_with_iso2022
+ ]
+ PATTERN_TABLE['SJIS'] =
+ [
+ /\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+/n,
+ /\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+/n,
+ quoted_with_iso2022,
+ domlit_with_iso2022,
+ comment_with_iso2022
+ ]
+ PATTERN_TABLE['UTF8'] =
+ [
+ /\A(?:[#{atomchars}]+|#{utf8str})+/n,
+ /\A(?:[#{tokenchars}]+|#{utf8str})+/n,
+ quoted_without_iso2022,
+ domlit_without_iso2022,
+ comment_without_iso2022
+ ]
+ PATTERN_TABLE['NONE'] =
+ [
+ /\A[#{atomchars}]+/n,
+ /\A[#{tokenchars}]+/n,
+ quoted_without_iso2022,
+ domlit_without_iso2022,
+ comment_without_iso2022
+ ]
+
+
+ def initialize( str, scantype, comments )
+ init_scanner str
+ @comments = comments || []
+ @debug = false
+
+ # fix scanner mode
+ @received = (scantype == :RECEIVED)
+ @is_mime_header = MIME_HEADERS[scantype]
+
+ atom, token, @quoted_re, @domlit_re, @comment_re = PATTERN_TABLE[$KCODE]
+ @word_re = (MIME_HEADERS[scantype] ? token : atom)
+ end
+
+ attr_accessor :debug
+
+ def scan( &block )
+ if @debug
+ scan_main do |arr|
+ s, v = arr
+ printf "%7d %-10s %s\n",
+ rest_size(),
+ s.respond_to?(:id2name) ? s.id2name : s.inspect,
+ v.inspect
+ yield arr
+ end
+ else
+ scan_main(&block)
+ end
+ end
+
+ private
+
+ RECV_TOKEN = {
+ 'from' => :FROM,
+ 'by' => :BY,
+ 'via' => :VIA,
+ 'with' => :WITH,
+ 'id' => :ID,
+ 'for' => :FOR
+ }
+
+ def scan_main
+ until eof?
+ if skip(/\A[\n\r\t ]+/n) # LWSP
+ break if eof?
+ end
+
+ if s = readstr(@word_re)
+ if @is_mime_header
+ yield :TOKEN, s
+ else
+ # atom
+ if /\A\d+\z/ === s
+ yield :DIGIT, s
+ elsif @received
+ yield RECV_TOKEN[s.downcase] || :ATOM, s
+ else
+ yield :ATOM, s
+ end
+ end
+
+ elsif skip(/\A"/)
+ yield :QUOTED, scan_quoted_word()
+
+ elsif skip(/\A\[/)
+ yield :DOMLIT, scan_domain_literal()
+
+ elsif skip(/\A\(/)
+ @comments.push scan_comment()
+
+ else
+ c = readchar()
+ yield c, c
+ end
+ end
+
+ yield false, '$'
+ end
+
+ def scan_quoted_word
+ scan_qstr(@quoted_re, /\A"/, 'quoted-word')
+ end
+
+ def scan_domain_literal
+ '[' + scan_qstr(@domlit_re, /\A\]/, 'domain-literal') + ']'
+ end
+
+ def scan_qstr( pattern, terminal, type )
+ result = ''
+ until eof?
+ if s = readstr(pattern) then result << s
+ elsif skip(terminal) then return result
+ elsif skip(/\A\\/) then result << readchar()
+ else
+ raise "TMail FATAL: not match in #{type}"
+ end
+ end
+ scan_error! "found unterminated #{type}"
+ end
+
+ def scan_comment
+ result = ''
+ nest = 1
+ content = @comment_re
+
+ until eof?
+ if s = readstr(content) then result << s
+ elsif skip(/\A\)/) then nest -= 1
+ return result if nest == 0
+ result << ')'
+ elsif skip(/\A\(/) then nest += 1
+ result << '('
+ elsif skip(/\A\\/) then result << readchar()
+ else
+ raise 'TMail FATAL: not match in comment'
+ end
+ end
+ scan_error! 'found unterminated comment'
+ end
+
+ # string scanner
+
+ def init_scanner( str )
+ @src = str
+ end
+
+ def eof?
+ @src.empty?
+ end
+
+ def rest_size
+ @src.size
+ end
+
+ def readstr( re )
+ if m = re.match(@src)
+ @src = m.post_match
+ m[0]
+ else
+ nil
+ end
+ end
+
+ def readchar
+ readstr(/\A./)
+ end
+
+ def skip( re )
+ if m = re.match(@src)
+ @src = m.post_match
+ true
+ else
+ false
+ end
+ end
+
+ def scan_error!( msg )
+ raise SyntaxError, msg
+ end
+
+ end
+
+end # module TMail
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/stringio.rb b/actionmailer/lib/action_mailer/vendor/tmail/stringio.rb
new file mode 100755
index 0000000000..24cf8a3a7c
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/stringio.rb
@@ -0,0 +1,260 @@
+#
+# stringio.rb
+#
+# Copyright (c) 1999-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+# Id: stringio.rb,v 1.10 2003/04/27 22:02:14 aamine Exp
+#
+
+class StringInput#:nodoc:
+
+ include Enumerable
+
+ class << self
+
+ def new( str )
+ if block_given?
+ begin
+ f = super
+ yield f
+ ensure
+ f.close if f
+ end
+ else
+ super
+ end
+ end
+
+ alias open new
+
+ end
+
+ def initialize( str )
+ @src = str
+ @pos = 0
+ @closed = false
+ @lineno = 0
+ end
+
+ attr_reader :lineno
+
+ def string
+ @src
+ end
+
+ def inspect
+ "#<#{self.class}:#{@closed ? 'closed' : 'open'},src=#{@src[0,30].inspect}>"
+ end
+
+ def close
+ stream_check!
+ @pos = nil
+ @closed = true
+ end
+
+ def closed?
+ @closed
+ end
+
+ def pos
+ stream_check!
+ [@pos, @src.size].min
+ end
+
+ alias tell pos
+
+ def seek( offset, whence = IO::SEEK_SET )
+ stream_check!
+ case whence
+ when IO::SEEK_SET
+ @pos = offset
+ when IO::SEEK_CUR
+ @pos += offset
+ when IO::SEEK_END
+ @pos = @src.size - offset
+ else
+ raise ArgumentError, "unknown seek flag: #{whence}"
+ end
+ @pos = 0 if @pos < 0
+ @pos = [@pos, @src.size + 1].min
+ offset
+ end
+
+ def rewind
+ stream_check!
+ @pos = 0
+ end
+
+ def eof?
+ stream_check!
+ @pos > @src.size
+ end
+
+ def each( &block )
+ stream_check!
+ begin
+ @src.each(&block)
+ ensure
+ @pos = 0
+ end
+ end
+
+ def gets
+ stream_check!
+ if idx = @src.index(?\n, @pos)
+ idx += 1 # "\n".size
+ line = @src[ @pos ... idx ]
+ @pos = idx
+ @pos += 1 if @pos == @src.size
+ else
+ line = @src[ @pos .. -1 ]
+ @pos = @src.size + 1
+ end
+ @lineno += 1
+
+ line
+ end
+
+ def getc
+ stream_check!
+ ch = @src[@pos]
+ @pos += 1
+ @pos += 1 if @pos == @src.size
+ ch
+ end
+
+ def read( len = nil )
+ stream_check!
+ return read_all unless len
+ str = @src[@pos, len]
+ @pos += len
+ @pos += 1 if @pos == @src.size
+ str
+ end
+
+ alias sysread read
+
+ def read_all
+ stream_check!
+ return nil if eof?
+ rest = @src[@pos ... @src.size]
+ @pos = @src.size + 1
+ rest
+ end
+
+ def stream_check!
+ @closed and raise IOError, 'closed stream'
+ end
+
+end
+
+
+class StringOutput#:nodoc:
+
+ class << self
+
+ def new( str = '' )
+ if block_given?
+ begin
+ f = super
+ yield f
+ ensure
+ f.close if f
+ end
+ else
+ super
+ end
+ end
+
+ alias open new
+
+ end
+
+ def initialize( str = '' )
+ @dest = str
+ @closed = false
+ end
+
+ def close
+ @closed = true
+ end
+
+ def closed?
+ @closed
+ end
+
+ def string
+ @dest
+ end
+
+ alias value string
+ alias to_str string
+
+ def size
+ @dest.size
+ end
+
+ alias pos size
+
+ def inspect
+ "#<#{self.class}:#{@dest ? 'open' : 'closed'},#{id}>"
+ end
+
+ def print( *args )
+ stream_check!
+ raise ArgumentError, 'wrong # of argument (0 for >1)' if args.empty?
+ args.each do |s|
+ raise ArgumentError, 'nil not allowed' if s.nil?
+ @dest << s.to_s
+ end
+ nil
+ end
+
+ def puts( *args )
+ stream_check!
+ args.each do |str|
+ @dest << (s = str.to_s)
+ @dest << "\n" unless s[-1] == ?\n
+ end
+ @dest << "\n" if args.empty?
+ nil
+ end
+
+ def putc( ch )
+ stream_check!
+ @dest << ch.chr
+ nil
+ end
+
+ def printf( *args )
+ stream_check!
+ @dest << sprintf(*args)
+ nil
+ end
+
+ def write( str )
+ stream_check!
+ s = str.to_s
+ @dest << s
+ s.size
+ end
+
+ alias syswrite write
+
+ def <<( str )
+ stream_check!
+ @dest << str.to_s
+ self
+ end
+
+ private
+
+ def stream_check!
+ @closed and raise IOError, 'closed stream'
+ end
+
+end
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/tmail.rb b/actionmailer/lib/action_mailer/vendor/tmail/tmail.rb
new file mode 100755
index 0000000000..57ed3cc581
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/tmail.rb
@@ -0,0 +1 @@
+require 'tmail'
diff --git a/actionmailer/lib/action_mailer/vendor/tmail/utils.rb b/actionmailer/lib/action_mailer/vendor/tmail/utils.rb
new file mode 100755
index 0000000000..4f603b96b9
--- /dev/null
+++ b/actionmailer/lib/action_mailer/vendor/tmail/utils.rb
@@ -0,0 +1,215 @@
+#
+# utils.rb
+#
+# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2 or later.
+#
+
+module TMail
+
+ class SyntaxError < StandardError; end
+
+
+ def TMail.new_boundary
+ 'mimepart_' + random_tag
+ end
+
+ def TMail.new_message_id( fqdn = nil )
+ fqdn ||= ::Socket.gethostname
+ "<#{random_tag()}@#{fqdn}.tmail>"
+ end
+
+ def TMail.random_tag
+ @uniq += 1
+ t = Time.now
+ sprintf('%x%x_%x%x%d%x',
+ t.to_i, t.tv_usec,
+ $$, Thread.current.id, @uniq, rand(255))
+ end
+ private_class_method :random_tag
+
+ @uniq = 0
+
+
+ module TextUtils
+
+ aspecial = '()<>[]:;.@\\,"'
+ tspecial = '()<>[];:@\\,"/?='
+ lwsp = " \t\r\n"
+ control = '\x00-\x1f\x7f-\xff'
+
+ ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{lwsp}]/n
+ PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n
+ TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{lwsp}]/n
+ CONTROL_CHAR = /[#{control}]/n
+
+ def atom_safe?( str )
+ not ATOM_UNSAFE === str
+ end
+
+ def quote_atom( str )
+ (ATOM_UNSAFE === str) ? dquote(str) : str
+ end
+
+ def quote_phrase( str )
+ (PHRASE_UNSAFE === str) ? dquote(str) : str
+ end
+
+ def token_safe?( str )
+ not TOKEN_UNSAFE === str
+ end
+
+ def quote_token( str )
+ (TOKEN_UNSAFE === str) ? dquote(str) : str
+ end
+
+ def dquote( str )
+ '"' + str.gsub(/["\\]/n) {|s| '\\' + s } + '"'
+ end
+ private :dquote
+
+
+ def join_domain( arr )
+ arr.map {|i|
+ if /\A\[.*\]\z/ === i
+ i
+ else
+ quote_atom(i)
+ end
+ }.join('.')
+ end
+
+
+ ZONESTR_TABLE = {
+ 'jst' => 9 * 60,
+ 'eet' => 2 * 60,
+ 'bst' => 1 * 60,
+ 'met' => 1 * 60,
+ 'gmt' => 0,
+ 'utc' => 0,
+ 'ut' => 0,
+ 'nst' => -(3 * 60 + 30),
+ 'ast' => -4 * 60,
+ 'edt' => -4 * 60,
+ 'est' => -5 * 60,
+ 'cdt' => -5 * 60,
+ 'cst' => -6 * 60,
+ 'mdt' => -6 * 60,
+ 'mst' => -7 * 60,
+ 'pdt' => -7 * 60,
+ 'pst' => -8 * 60,
+ 'a' => -1 * 60,
+ 'b' => -2 * 60,
+ 'c' => -3 * 60,
+ 'd' => -4 * 60,
+ 'e' => -5 * 60,
+ 'f' => -6 * 60,
+ 'g' => -7 * 60,
+ 'h' => -8 * 60,
+ 'i' => -9 * 60,
+ # j not use
+ 'k' => -10 * 60,
+ 'l' => -11 * 60,
+ 'm' => -12 * 60,
+ 'n' => 1 * 60,
+ 'o' => 2 * 60,
+ 'p' => 3 * 60,
+ 'q' => 4 * 60,
+ 'r' => 5 * 60,
+ 's' => 6 * 60,
+ 't' => 7 * 60,
+ 'u' => 8 * 60,
+ 'v' => 9 * 60,
+ 'w' => 10 * 60,
+ 'x' => 11 * 60,
+ 'y' => 12 * 60,
+ 'z' => 0 * 60
+ }
+
+ def timezone_string_to_unixtime( str )
+ if m = /([\+\-])(\d\d?)(\d\d)/.match(str)
+ sec = (m[2].to_i * 60 + m[3].to_i) * 60
+ m[1] == '-' ? -sec : sec
+ else
+ min = ZONESTR_TABLE[str.downcase] or
+ raise SyntaxError, "wrong timezone format '#{str}'"
+ min * 60
+ end
+ end
+
+
+ WDAY = %w( Sun Mon Tue Wed Thu Fri Sat TMailBUG )
+ MONTH = %w( TMailBUG Jan Feb Mar Apr May Jun
+ Jul Aug Sep Oct Nov Dec TMailBUG )
+
+ def time2str( tm )
+ # [ruby-list:7928]
+ gmt = Time.at(tm.to_i)
+ gmt.gmtime
+ offset = tm.to_i - Time.local(*gmt.to_a[0,6].reverse).to_i
+
+ # DO NOT USE strftime: setlocale() breaks it
+ sprintf '%s, %s %s %d %02d:%02d:%02d %+.2d%.2d',
+ WDAY[tm.wday], tm.mday, MONTH[tm.month],
+ tm.year, tm.hour, tm.min, tm.sec,
+ *(offset / 60).divmod(60)
+ end
+
+
+ MESSAGE_ID = /<[^\@>]+\@[^>\@]+>/
+
+ def message_id?( str )
+ MESSAGE_ID === str
+ end
+
+
+ MIME_ENCODED = /=\?[^\s?=]+\?[QB]\?[^\s?=]+\?=/i
+
+ def mime_encoded?( str )
+ MIME_ENCODED === str
+ end
+
+
+ def decode_params( hash )
+ new = Hash.new
+ encoded = nil
+ hash.each do |key, value|
+ if m = /\*(?:(\d+)\*)?\z/.match(key)
+ ((encoded ||= {})[m.pre_match] ||= [])[(m[1] || 0).to_i] = value
+ else
+ new[key] = to_kcode(value)
+ end
+ end
+ if encoded
+ encoded.each do |key, strings|
+ new[key] = decode_RFC2231(strings.join(''))
+ end
+ end
+
+ new
+ end
+
+ NKF_FLAGS = {
+ 'EUC' => '-e -m',
+ 'SJIS' => '-s -m'
+ }
+
+ def to_kcode( str )
+ flag = NKF_FLAGS[$KCODE] or return str
+ NKF.nkf(flag, str)
+ end
+
+ RFC2231_ENCODED = /\A(?:iso-2022-jp|euc-jp|shift_jis|us-ascii)?'[a-z]*'/in
+
+ def decode_RFC2231( str )
+ m = RFC2231_ENCODED.match(str) or return str
+ NKF.nkf(NKF_FLAGS[$KCODE],
+ m.post_match.gsub(/%[\da-f]{2}/in) {|s| s[1,2].hex.chr })
+ end
+
+ end
+
+end
diff --git a/actionmailer/test/fixtures/templates/signed_up.rhtml b/actionmailer/test/fixtures/templates/signed_up.rhtml
new file mode 100644
index 0000000000..a85d5fa442
--- /dev/null
+++ b/actionmailer/test/fixtures/templates/signed_up.rhtml
@@ -0,0 +1,3 @@
+Hello there,
+
+Mr. <%= @recipient %> \ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/signed_up.rhtml b/actionmailer/test/fixtures/test_mailer/signed_up.rhtml
new file mode 100644
index 0000000000..a85d5fa442
--- /dev/null
+++ b/actionmailer/test/fixtures/test_mailer/signed_up.rhtml
@@ -0,0 +1,3 @@
+Hello there,
+
+Mr. <%= @recipient %> \ No newline at end of file
diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb
new file mode 100755
index 0000000000..adc5be2fe9
--- /dev/null
+++ b/actionmailer/test/mail_service_test.rb
@@ -0,0 +1,92 @@
+$:.unshift(File.dirname(__FILE__) + "/../lib/")
+
+require 'test/unit'
+require 'action_mailer'
+
+class TestMailer < ActionMailer::Base
+ def signed_up(recipient)
+ @recipients = recipient
+ @subject = "[Signed up] Welcome #{recipient}"
+ @from = "system@loudthinking.com"
+ @sent_on = Time.local(2004, 12, 12)
+ @body["recipient"] = recipient
+ end
+
+ def cancelled_account(recipient)
+ @recipients = recipient
+ @subject = "[Cancelled] Goodbye #{recipient}"
+ @from = "system@loudthinking.com"
+ @sent_on = Time.local(2004, 12, 12)
+ @body = "Goodbye, Mr. #{recipient}"
+ end
+end
+
+TestMailer.template_root = File.dirname(__FILE__) + "/fixtures"
+
+class ActionMailerTest < Test::Unit::TestCase
+ def setup
+ ActionMailer::Base.delivery_method = :test
+ ActionMailer::Base.perform_deliveries = true
+ ActionMailer::Base.deliveries = []
+
+ @recipient = 'test@localhost'
+ end
+
+ def test_signed_up
+ expected = TMail::Mail.new
+ expected.to = @recipient
+ expected.subject = "[Signed up] Welcome #{@recipient}"
+ expected.body = "Hello there, \n\nMr. #{@recipient}"
+ expected.from = "system@loudthinking.com"
+ expected.date = Time.local(2004, 12, 12)
+
+ created = nil
+ assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }
+ assert_not_nil created
+ assert_equal expected.encoded, created.encoded
+
+ assert_nothing_raised { TestMailer.deliver_signed_up(@recipient) }
+ assert_not_nil ActionMailer::Base.deliveries.first
+ assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
+ end
+
+ def test_cancelled_account
+ expected = TMail::Mail.new
+ expected.to = @recipient
+ expected.subject = "[Cancelled] Goodbye #{@recipient}"
+ expected.body = "Goodbye, Mr. #{@recipient}"
+ expected.from = "system@loudthinking.com"
+ expected.date = Time.local(2004, 12, 12)
+
+ created = nil
+ assert_nothing_raised { created = TestMailer.create_cancelled_account(@recipient) }
+ assert_not_nil created
+ assert_equal expected.encoded, created.encoded
+
+ assert_nothing_raised { TestMailer.deliver_cancelled_account(@recipient) }
+ assert_not_nil ActionMailer::Base.deliveries.first
+ assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
+ end
+
+ def test_instances_are_nil
+ assert_nil ActionMailer::Base.new
+ assert_nil TestMailer.new
+ end
+
+ def test_deliveries_array
+ assert_not_nil ActionMailer::Base.deliveries
+ assert_equal 0, ActionMailer::Base.deliveries.size
+ TestMailer.deliver_signed_up(@recipient)
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ assert_not_nil ActionMailer::Base.deliveries.first
+ end
+
+ def test_perform_deliveries_flag
+ ActionMailer::Base.perform_deliveries = false
+ TestMailer.deliver_signed_up(@recipient)
+ assert_equal 0, ActionMailer::Base.deliveries.size
+ ActionMailer::Base.perform_deliveries = true
+ TestMailer.deliver_signed_up(@recipient)
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+end
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
new file mode 100644
index 0000000000..a422af3f0e
--- /dev/null
+++ b/actionpack/CHANGELOG
@@ -0,0 +1,738 @@
+*CVS*
+
+* Upgraded to Builder 1.2.1
+
+* Added :module as an alias for :controller_prefix to url_for and friends, so you can do redirect_to(:module => "shop", :controller => "purchases")
+ and go to /shop/purchases/
+
+* Added support for controllers in modules through @params["module"].
+
+* Added reloading for dependencies under cached environments like FastCGI and mod_ruby. This makes it possible to use those environments for development.
+ This is turned on by default, but can be turned off with ActionController::Base.reload_dependencies = false in production environments.
+
+ NOTE: This will only have an effect if you use the new model, service, and observer class methods to mark dependencies. All libraries loaded through
+ require will be "forever" cached. You can, however, use ActionController::Base.load_or_require("library") to get this behavior outside of the new
+ dependency style.
+
+* Added that controllers will automatically require their own helper if possible. So instead of doing:
+
+ class MsgController < AbstractApplicationController
+ helper :msg
+ end
+
+ ...you can just do:
+
+ class MsgController < AbstractApplicationController
+ end
+
+* Added dependencies_on(layer) to query the dependencies of a controller. Examples:
+
+ MsgController.dependencies_on(:model) # => [ :post, :comment, :attachment ]
+ MsgController.dependencies_on(:service) # => [ :notification_service ]
+ MsgController.dependencies_on(:observer) # => [ :comment_observer ]
+
+* Added a new dependency model with the class methods model, service, and observer. Example:
+
+ class MsgController < AbstractApplicationController
+ model :post, :comment, :attachment
+ service :notification_service
+ observer :comment_observer
+ end
+
+ These new "keywords" remove the need for explicitly calling 'require' in most cases. The observer method even instantiates the
+ observer as well as requiring it.
+
+* Fixed that link_to would escape & in the url again after url_for already had done so
+
+*0.9.5* (28)
+
+* Added helper_method to designate that a given private or protected method you should available as a helper in the view. [bitsweat]
+
+* Fixed assert_rendered_file so it actually verifies if that was the rendered file [htonl]
+
+* Added the option for sharing partial spacer templates just like partials themselves [radsaq]
+
+* Fixed that Russia was named twice in country_select [alexey]
+
+* Fixed request_origin to use remote_ip instead of remote_addr [bitsweat]
+
+* Fixed link_to breakage when nil was passed for html_options [alexey]
+
+* Fixed redirect_to on a virtual server setup with apache with a port other than the default where it would forget the port number [seanohalpin]
+
+* Fixed that auto-loading webrick on Windows would cause file uploads to fail [bitsweat]
+
+* Fixed issues with sending files on WEBrick by setting the proper binmode [bitsweat]
+
+* Added send_data as an alternative to send_file when the stream is not read off the filesystem but from a database or generated live [bitsweat]
+
+* Added a new way to include helpers that doesn't require the include hack and can go without the explicit require. [bitsweat]
+
+ Before:
+
+ module WeblogHelper
+ def self.append_features(controller) #:nodoc:
+ controller.ancestors.include?(ActionController::Base) ? controller.add_template_helper(self) : super
+ end
+ end
+
+ require 'weblog_helper'
+ class WeblogController < ActionController::Base
+ include WeblogHelper
+ end
+
+ After:
+
+ module WeblogHelper
+ end
+
+ class WeblogController < ActionController::Base
+ helper :weblog
+ end
+
+* Added a default content-type of "text/xml" to .rxml renders [Ryan Platte]
+
+* Fixed that when /controller/index was requested by the browser, url_for would generates wrong URLs [Ryan Platte]
+
+* Fixed a bug that would share cookies between users when using FastCGI and mod_ruby [The Robot Co-op]
+
+* Added an optional third hash parameter to the process method in functional tests that takes the session data to be used [alexey]
+
+* Added UrlHelper#mail_to to make it easier to create mailto: style ahrefs
+
+* Added better error messages for layouts declared with the .rhtml extension (which they shouldn't) [geech]
+
+* Added another case to DateHelper#distance_in_minutes to return "less than a minute" instead of "0 minutes" and "1 minute" instead of "1 minutes"
+
+* Added a hidden field to checkboxes generated with FormHelper#check_box that will make sure that the unchecked value (usually 0)
+ is sent even if the checkbox is not checked. This relieves the controller from doing custom checking if the the checkbox wasn't
+ checked. BEWARE: This might conflict with your run-on-the-mill work-around code. [Tobias Luetke]
+
+* Fixed error_message_on to just use the first if more than one error had been added [marcel]
+
+* Fixed that URL rewriting with /controller/ was working but /controller was not and that you couldn't use :id on index [geech]
+
+* Fixed a bug with link_to where the :confirm option wouldn't be picked up if the link was a straight url instead of an option hash
+
+* Changed scaffolding of forms to use <label> tags instead of <b> to please W3C [evl]
+
+* Added DateHelper#distance_of_time_in_words_to_now(from_time) that works like distance_of_time_in_words,
+ but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
+
+* Added assert_flash_equal(expected, key, message), assert_session_equal(expected, key, message),
+ assert_assigned_equal(expected, key, message) to test the contents of flash, session, and template assigns.
+
+* Improved the failure report on assert_success when the action triggered a redirection [alexey].
+
+* Added "markdown" to accompany "textilize" as a TextHelper method for converting text to HTML using the Markdown syntax.
+ BlueCloth must be installed in order for this method to become available.
+
+* Made sure that an active session exists before we attempt to delete it [Samuel]
+
+* Changed link_to with Javascript confirmation to use onclick instead of onClick for XHTML validity [Scott Barron]
+
+
+*0.9.0 (43)*
+
+* Added support for Builder-based templates for files with the .rxml extension. These new templates are an alternative to ERb that
+ are especially useful for generating XML content, such as this RSS example from Basecamp:
+
+ xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
+ xml.channel do
+ xml.title(@feed_title)
+ xml.link(@url)
+ xml.description "Basecamp: Recent items"
+ xml.language "en-us"
+ xml.ttl "40"
+
+ for item in @recent_items
+ xml.item do
+ xml.title(item_title(item))
+ xml.description(item_description(item)) if item_description(item)
+ xml.pubDate(item_pubDate(item))
+ xml.guid(@person.firm.account.url + @recent_items.url(item))
+ xml.link(@person.firm.account.url + @recent_items.url(item))
+
+ xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
+ end
+ end
+ end
+ end
+
+ ...which will generate something like:
+
+ <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <channel>
+ <title>Web Site Redesign</title>
+ <link>http://www.basecamphq.com/clients/travelcenter/1/</link>
+ <description>Basecamp: Recent items</description>
+ <language>en-us</language>
+ <ttl>40</ttl>
+ <item>
+ <title>Post: don't you know</title>
+ <description>&amp;lt;p&amp;gt;deeper and down&amp;lt;/p&amp;gt;</description>
+ <pubDate>Fri, 20 Aug 2004 21:13:50 CEST</pubDate>
+ <guid>http://www.basecamphq.com/clients/travelcenter/1/msg/assets/96976/comments</guid>
+ <link>http://www.basecamphq.com/clients/travelcenter/1/msg/assets/96976/comments</link>
+ <dc:creator>David H. Heinemeier</dc:creator>
+ </item>
+ <item>
+ <title>Milestone completed: Design Comp 2</title>
+ <pubDate>Mon, 9 Aug 2004 14:42:06 CEST</pubDate>
+ <guid>http://www.basecamphq.com/clients/travelcenter/1/milestones/#49</guid>
+ <link>http://www.basecamphq.com/clients/travelcenter/1/milestones/#49</link>
+ </item>
+ </channel>
+ </rss>
+
+ The "xml" local variable is automatically available in .rxml templates. You construct the template by calling a method with the name
+ of the tag you want. Options for the tag can be specified as a hash parameter to that method.
+
+ Builder-based templates can be mixed and matched with the regular ERb ones. The only thing that differentiates them is the extension.
+ No new methods have been added to the public interface to handle them.
+
+ Action Pack ships with a version of Builder, but it will use the RubyGems version if you have one installed.
+
+ Read more about Builder on: http://onestepback.org/index.cgi/Tech/Ruby/StayingSimple.rdoc
+
+ [Builder is created by Jim Weirich]
+
+* Added much improved support for functional testing [what-a-day].
+
+ # Old style
+ def test_failing_authenticate
+ @request.request_uri = "/login/authenticate"
+ @request.action = "authenticate"
+ @request.request_parameters["user_name"] = "nop"
+ @request.request_parameters["password"] = ""
+
+ response = LoginController.process_test(@request)
+
+ assert_equal "The username and/or password you entered is invalid.", response.session["flash"]["alert"]
+ assert_equal "http://37signals.basecamp.com/login/", response.headers["location"]
+ end
+
+ # New style
+ def test_failing_authenticate
+ process :authenticate, "user_name" => "nop", "password" => ""
+ assert_flash_has 'alert'
+ assert_redirected_to :action => "index"
+ end
+
+ See a full example on http://codepaste.org/view/paste/334
+
+* Increased performance by up to 100% with a revised cookie class that fixes the performance problems with the
+ default one that ships with 1.8.1 and below. It replaces the inheritance on SimpleDelegator with DelegateClass(Array)
+ following the suggestion from Matz on:
+ http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14
+
+* Added caching for compiled ERb templates. On Basecamp, it gave between 8.5% and 71% increase in performance [Andreas Schwarz].
+
+* Added implicit counter variable to render_collection_of_partials [Marcel]. From the docs:
+
+ <%= render_collection_of_partials "ad", @advertisements %>
+
+ This will render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. An iteration counter
+ will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the
+ example above, the template would be fed +ad_counter+.
+
+* Fixed problems with two sessions being maintained on reset_session that would particularly screw up ActiveRecordStore.
+
+* Fixed reset_session to start an entirely new session instead of merely deleting the old. So you can now safely access @session
+ after calling reset_ression and expect it to work.
+
+* Added @request.get?, @request.post?, @request.put?, @request.delete? as convenience query methods for @request.method [geech]
+
+* Added @request.method that'll return a symbol representing the HTTP method, such as :get, :post, :put, :delete [geech]
+
+* Changed @request.remote_ip and @request.host to work properly even when a proxy is in front of the application [geech]
+
+* Added JavaScript confirm feature to link_to. Documentation:
+
+ The html_options have a special feature for creating javascript confirm alerts where if you pass
+ :confirm => 'Are you sure?', the link will be guarded with a JS popup asking that question.
+ If the user accepts, the link is processed, otherwise not.
+
+* Added link_to_unless_current as a UrlHelper method [Sam Stephenson]. Documentation:
+
+ Creates a link tag of the given +name+ using an URL created by the set of +options+, unless the current
+ controller, action, and id are the same as the link's, in which case only the name is returned (or the
+ given block is yielded, if one exists). This is useful for creating link bars where you don't want to link
+ to the page currently being viewed.
+
+* Fixed that UrlRewriter (the driver for url_for, link_to, etc) would blow up when the anchor was an integer [alexey]
+
+* Added that layouts defined with no directory defaults to layouts. So layout "weblog/standard" will use
+ weblog/standard (as always), but layout "standard" will use layouts/standard.
+
+* Fixed that partials (or any template starting with an underscore) was publically viewable [Marten]
+
+* Added HTML escaping to text_area helper.
+
+* Added :overwrite_params to url_for and friends to keep the parameters as they were passed to the current action and only overwrite a subset.
+ The regular :params will clear the slate so you need to manually add in existing parameters if you want to reuse them. [raphinou]
+
+* Fixed scaffolding problem with composite named objects [Moo Jester]
+
+* Added the possibility for shared partials. Example:
+
+ <%= render_partial "advertisement/ad", ad %>
+
+ This will render the partial "advertisement/_ad.rhtml" regardless of which controller this is being called from.
+
+ [Jacob Fugal]
+
+* Fixed crash when encountering forms that have empty-named fields [James Prudente]
+
+* Added check_box form helper method now accepts true/false as well as 1/0 [what-a-day]
+
+* Fixed the lacking creation of all directories with install.rb [Dave Steinberg]
+
+* Fixed that date_select returns valid XHTML selected options [Andreas Schwarz]
+
+* Fixed referencing an action with the same name as a controller in url_for [what-a-day]
+
+* Fixed the destructive nature of Base#attributes= on the argument [Kevin Watt]
+
+* Changed ActionControllerError to decent from StandardError instead of Exception. It can now be caught by a generic rescue.
+
+* Added SessionRestoreError that is raised when a session being restored holds objects where there is no class available.
+
+* Added block as option for inline filters. So what used to be written as:
+
+ before_filter Proc { |controller| return false if controller.params["stop_action"] }
+
+ ...can now be as:
+
+ before_filter { |controller| return false if controller.params["stop_action"] }
+
+ [Jeremy Kemper]
+
+* Made the following methods public (was protected): url_for, controller_class_name, controller_name, action_name
+ This makes it easier to write filters without cheating around the encapsulation with send.
+
+* ActionController::Base#reset_session now sticks even if you access @session afterwards [Kent Sibilev]
+
+* Improved the exception logging so the log file gets almost as much as in-browser debugging.
+
+* Changed base class setup from AbstractTemplate/ERbTemplate to ActionView::Base. This change should be harmless unless you were
+ accessing Action View directly in which case you now need to reference the Base class.\
+
+* Added that render_collection_of_partials returns nil if the collection is empty. This makes showing a “no items†message easier.
+ For example: <%= render_collection_of_partials("message", @messages) || "No messages found." %> [Sam Stephenson]
+
+* Added :month_before_year as an option to date_select to get the month select before the year. Especially useful for credit card forms.
+
+* Added :add_month_numbers to select_month to get options like "3 - March".
+
+* Removed Base.has_active_layout? as it couldn't answer the question without the instance. Use Base#active_layout instead.
+
+* Removed redundant call to update on ActionController::Base#close_session [Andreas Schwarz]
+
+* Fixed that DRb Store accidently started its own server (instead of just client) [Andreas]
+
+* Fixed strip_links so it now works across multiple lines [Chad Fowler]
+
+* Fixed the TemplateError exception to show the proper trace on to_s (useful for unit test debugging)
+
+* Implemented class inheritable attributes without eval [Caio Chassot]
+
+* Made TextHelper#concat accept binding as it would otherwise not work
+
+* The FormOptionsHelper will now call to_s on the keys and values used to generate options
+
+
+*0.8.5*
+
+* Introduced passing of locally scoped variables between templates:
+
+ You can pass local variables to sub templates by using a hash of with the variable
+ names as keys and the objects as values:
+
+ <%= render "shared/header", { "headline" => "Welcome", "person" => person } %>
+
+ These can now be accessed in shared/header with:
+
+ Headline: <%= headline %>
+ First name: <%= person.first_name %>
+
+* Introduced the concept of partials as a certain type of sub templates:
+
+ There's also a convenience method for rendering sub templates within the current
+ controller that depends on a single object (we call this kind of sub templates for
+ partials). It relies on the fact that partials should follow the naming convention
+ of being prefixed with an underscore -- as to separate them from regular templates
+ that could be rendered on their own. In the template for Advertiser#buy, we could have:
+
+ <% for ad in @advertisements %>
+ <%= render_partial "ad", ad %>
+ <% end %>
+
+ This would render "advertiser/_ad.rhtml" and pass the local variable +ad+
+ for the template to display.
+
+ == Rendering a collection of partials
+
+ The example of partial use describes a familar pattern where a template needs
+ to iterate over a array and render a sub template for each of the elements.
+ This pattern has been implemented as a single method that accepts an array and
+ renders a partial by the same name of as the elements contained within. So the
+ three-lined example in "Using partials" can be rewritten with a single line:
+
+ <%= render_collection_of_partials "ad", @advertisements %>
+
+ So this will render "advertiser/_ad.rhtml" and pass the local variable +ad+ for
+ the template to display.
+
+* Improved send_file by allowing a wide range of options to be applied [Jeremy Kemper]:
+
+ Sends the file by streaming it 4096 bytes at a time. This way the
+ whole file doesn't need to be read into memory at once. This makes
+ it feasible to send even large files.
+
+ Be careful to sanitize the path parameter if it coming from a web
+ page. send_file(@params['path'] allows a malicious user to
+ download any file on your server.
+
+ Options:
+ * <tt>:filename</tt> - specifies the filename the browser will see.
+ Defaults to File.basename(path).
+ * <tt>:type</tt> - specifies an HTTP content type.
+ Defaults to 'application/octet-stream'.
+ * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
+ Valid values are 'inline' and 'attachment' (default).
+ * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream
+ the file. Defaults to 4096.
+
+ The default Content-Type and Content-Disposition headers are
+ set to download arbitrary binary files in as many browsers as
+ possible. IE versions 4, 5, 5.5, and 6 are all known to have
+ a variety of quirks (especially when downloading over SSL).
+
+ Simple download:
+ send_file '/path/to.zip'
+
+ Show a JPEG in browser:
+ send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
+
+ Read about the other Content-* HTTP headers if you'd like to
+ provide the user with more information (such as Content-Description).
+ http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
+
+ Also be aware that the document may be cached by proxies and browsers.
+ The Pragma and Cache-Control headers declare how the file may be cached
+ by intermediaries. They default to require clients to validate with
+ the server before releasing cached responses. See
+ http://www.mnot.net/cache_docs/ for an overview of web caching and
+ http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
+ for the Cache-Control header spec.
+
+* Added pluralize method to the TextHelper that makes it easy to get strings like "1 message", "3 messages"
+
+* Added proper escaping for the rescues [Andreas Schwartz]
+
+* Added proper escaping for the option and collection tags [Andreas Schwartz]
+
+* Fixed NaN errors on benchmarking [Jim Weirich]
+
+* Fixed query string parsing for URLs that use the escaped versions of & or ; as part of a key or value
+
+* Fixed bug with custom Content-Type headers being in addition to rather than instead of the default header.
+ (This bug didn't matter with neither CGI or mod_ruby, but FCGI exploded on it) [With help from Ara T. Howard]
+
+
+*0.8.0*
+
+* Added select, collection_select, and country_select to make it easier for Active Records to set attributes through
+ drop-down lists of options. Example:
+
+ <%= select "person", "gender", %w( Male Female ) %>
+
+ ...would give the following:
+
+ <select name="person[gender]" id="person_gender"><option>Male</option><option>Female</option></select>
+
+* Added an option for getting multiple values on a single form name into an array instead of having the last one overwrite.
+ This is especially useful for groups of checkboxes, which can now be written as:
+
+ <input type="checkbox" name="rights[]" value="CREATE" />
+ <input type="checkbox" name="rights[]" value="UPDATE" />
+ <input type="checkbox" name="rights[]" value="DELETE" />
+
+ ...and retrieved in the controller action with:
+
+ @params["rights"] # => [ "CREATE", "UPDATE", "DELETE" ]
+
+ The old behavior (where the last one wins, "DELETE" in the example) is still available. Just don't add "[]" to the
+ end of the name. [Scott Baron]
+
+* Added send_file which uses the new render_text block acceptance to make it feasible to send large files.
+ The files is sent with a bunch of voodoo HTTP headers required to get arbitrary files to download as
+ expected in as many browsers as possible (eg, IE hacks). Example:
+
+ def play_movie
+ send_file "/movies/that_movie.avi"
+ end
+
+ [Jeremy Kemper]
+
+* render_text now accepts a block for deferred rendering. Useful for streaming large files, displaying
+ a “please wait†message during a complex search, etc. Streaming example:
+
+ render_text do |response|
+ File.open(path, 'rb') do |file|
+ while buf = file.read(1024)
+ print buf
+ end
+ end
+ end
+
+ [Jeremy Kemper]
+
+* Added a new Tag Helper that can generate generic tags programmatically insted of through HTML. Example:
+
+ tag("br", "clear" => "all") => <br clear="all" />
+
+ ...that's usually not terribly interesting (unless you have a lot of options already in a hash), but it
+ gives way for more specific tags, like the new form tag:
+
+ form_tag({ :controller => "weblog", :action => "update" }, { :multipart => "true", "style" => "width: 200px"}) =>
+ <form action="/weblog/update" enctype="multipart/formdata" style="width: 200px">
+
+ There's even a "pretty" version for people who don't like to open tags in code and close them in HTML:
+
+ <%= start_form_tag :action => "update" %>
+ # all the input fields
+ <%= end_form_tag %>
+
+ (end_form_tag just returns "</form>")
+
+* The selected parameter in options_for_select may now also an array of values to be selected when
+ using a multiple select. Example:
+
+ options_for_select([ "VISA", "Mastercard", "Discover" ], ["VISA", "Discover"]) =>
+ <option selected>VISA</option>\n<option>Mastercard</option>\n<option selected>Discover</option>
+
+ [Scott Baron]
+
+* Changed the URL rewriter so controller_prefix and action_prefix can be used in isolation. You can now do:
+
+ url_for(:controller_prefix => "clients")
+
+ ...or:
+
+ url_for(:action_prefix => "category/messages")
+
+ Neither would have worked in isolation before (:controller_prefix required a :controller and :action_prefix required an :action)
+
+* Started process of a cleaner separation between Action Controller and ERb-based Action Views by introducing an
+ abstract base class for views. And Amita adapter could be fitted in more easily now.
+
+* The date helper methods date_select and datetime_select now also use the field error wrapping
+ (div with class fieldWithErrors by default).
+
+* The date helper methods date_select and datetime_select can now discard selects
+
+* Added option on AbstractTemplate to specify a different field error wrapping. Example:
+
+ ActionView::AbstractTemplate.field_error_proc = Proc.new do |html, instance|
+ "<p>#{instance.method_name + instance.error_message}</p><div style='background-color: red'>#{html}</div>"
+ end
+
+ ...would give the following on a Post#title (text field) error:
+
+ <p>Title can't be empty</p>
+ <div style='background-color: red'>
+ <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
+ </div>
+
+* The UrlHelper methods url_for and link_to will now by default only return paths, not complete URIs.
+ That should make it easier to fit a Rails application behind a proxy or load-balancer.
+ You can overwrite this by passing :only_path => false as part of the options. [Suggested by U235]
+
+* Fixed bug with having your own layout for use with scaffolding [Kevin Radloff]
+
+* Fixed bug where redirect_to_path didn't append the port on non-standard ports [dhawkins]
+
+* Scaffolding plays nicely with single-table inheritance (LoadErrors are caught) [Jeremy Kemper]
+
+* Scaffolding plays nice with plural models like Category/categories [Jeremy Kemper]
+
+* Fixed missing suffix appending in scaffolding [Kevin Radloff]
+
+
+*0.7.9*
+
+* The "form" method now present boolean fields from PostgreSQL as drop-down menu. [Scott]
+
+* Scaffolding now automatically attempts to require the class that's being scaffolded.
+
+* Scaffolding will use the current active layout, instead of its own, if one has been specified. Example:
+
+ class WeblogController < ActionController::Base
+ layout "layouts/weblog"
+ scaffold :post
+ end
+
+ [Suggested by Scott]
+
+* Changed url_for (and all the that drives, like redirect_to, link_to, link_for) so you can pass it a symbol instead of a hash.
+ This symbol is a method reference which is then called to calculate the url. Example:
+
+ class WeblogController < ActionController::Base
+ def update
+ # do some update
+ redirect_to :dashboard_url
+ end
+
+ protected
+ def dashboard_url
+ if @project.active?
+ url_for :controller => "project", :action => "dashboard"
+ else
+ url_for :controller => "account", :action => "dashboard"
+ end
+ end
+ end
+
+* Added default_url_options to specialize behavior for all url_for (and friends) calls:
+
+ Overwrite to implement a number of default options that all url_for-based methods will use.
+ The default options should come in form of a hash, just like the one you would use for
+ url_for directly. Example:
+
+ def default_url_options(options)
+ { :controller_prefix => @project.active? ? "projects/" : "accounts/" }
+ end
+
+ As you can infer from the example, this is mostly useful for situations where you want to
+ centralize dynamic dissions about the urls as they stem from the business domain. Please note
+ that any individual url_for call can always override the defaults set by this method.
+
+
+* Changed url_for so that an "id" passed in the :params is not treated special. You need to use the dedicated :id to get
+ the special auto path-params treatment. Considering the url http://localhost:81/friends/list
+
+ url_for(:action => "show", :params => { "id" => 5 })
+ ...used to give http://localhost:81/friends/show/5
+ ......now gives http://localhost:81/friends/show?id=5
+
+ If you want the automated id behavior, do:
+
+ url_for(:action => "show", :id => 5 )
+ ....which gives http://localhost:81/friends/show/5
+
+
+* Fixed problem with anchor being inserted before path parameters with url_for (and friends)
+
+
+*0.7.8*
+
+* Fixed session bug where you couldn't store any objects that didn't exist in the standard library
+ (such as Active Record objects).
+
+* Added reset_session method for Action Controller objects to clear out all objects in the session.
+
+* Fixed that exceptions raised during filters are now also caught by the default rescues
+
+* Added new around_filter for doing before and after filtering with a single object [Florian Weber]:
+
+ class WeblogController < ActionController::Base
+ around_filter BenchmarkingFilter.new
+
+ # Before this action is performed, BenchmarkingFilter#before(controller) is executed
+ def index
+ end
+ # After this action has been performed, BenchmarkingFilter#after(controller) is executed
+ end
+
+ class BenchmarkingFilter
+ def initialize
+ @runtime
+ end
+
+ def before
+ start_timer
+ end
+
+ def after
+ stop_timer
+ report_result
+ end
+ end
+
+* Added the options for specifying a different name and id for the form helper methods than what is guessed [Florian Weber]:
+
+ text_field "post", "title"
+ ...just gives: <input id="post_title" name="post[title]" size="30" type="text" value="" />
+
+ text_field "post", "title", "id" => "title_for_post", "name" => "first_post_title"
+ ...can now give: <input id="title_for_post" name="first_post_title" size="30" type="text" value="" />
+
+* Added DebugHelper with a single "debug" method for doing pretty dumps of objects in the view
+ (now used in the default rescues to better present the contents of session and template variables)
+
+* Added note to log about the templates rendered within layouts (before just the layout was shown)
+
+* Fixed redirects on https setups [Andreas]
+
+* Fixed scaffolding problem on the edit action when using :suffix => true [Scott]
+
+* Fixed scaffolding problem where implementing list.rhtml wouldn't work for the index action
+
+* URLs generated now uses &amp; instead of just & so pages using it can validate with W3C [Spotted by Andreas]
+
+
+*0.7.7*
+
+* Fixed bug in CGI extension that prevented multipart forms from working
+
+
+*0.7.6*
+
+* Included ERB::Util so all templates can easily escape HTML content with <%=h @person.content %>
+
+* All requests are now considered local by default, so everyone will be exposed to detailed debugging screens on errors.
+ When the application is ready to go public, set ActionController::Base.consider_all_requests_local to false,
+ and implement the protected method local_request? in the controller to determine when debugging screens should be shown.
+
+* Fixed three bugs with the url_for/redirect_to/link_to handling. Considering the url http://localhost:81/friends/show/1
+
+ url_for(:action => "list")
+ ...used to give http://localhost:81/friends/list/1
+ ......now gives http://localhost:81/friends/list
+
+ url_for(:controller => "friends", :action => "destroy", :id => 5)
+ ...used to give http://localhost:81/friends/destroy
+ ......now gives http://localhost:81/friends/destroy/5
+
+ Considering the url http://localhost:81/teachers/show/t
+
+ url_for(:action => "list", :id => 5)
+ ...used to give http://localhost:81/5eachers/list/t
+ ......now gives http://localhost:81/teachers/list/5
+
+ [Reported by David Morton & Radsaq]
+
+* Logs exception to logfile in addition to showing them for local requests
+
+* Protects the eruby load behind a begin/rescue block. eRuby is not required to run ActionController.
+
+* Fixed install.rb to also install clean_logger and the templates
+
+* Added ActiveRecordStore as a session option. Read more in lib/action_controller/session/active_record_store.rb [Tim Bates]
+
+* Change license to MIT License (and included license file in package)
+
+* Application error page now returns status code 500 instead of 200
+
+* Fixed using Procs as layout handlers [Florian Weber]
+
+* Fixed bug with using redirects ports other than 80
+
+* Added index method that calls list on scaffolding
+
+
+*0.7.5*
+
+* First public release \ No newline at end of file
diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE
new file mode 100644
index 0000000000..26f55e7799
--- /dev/null
+++ b/actionpack/MIT-LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2004 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/actionpack/README b/actionpack/README
new file mode 100755
index 0000000000..d22ca0a701
--- /dev/null
+++ b/actionpack/README
@@ -0,0 +1,418 @@
+= Action Pack -- On rails from request to response
+
+Action Pack splits the response to a web request into a controller part
+(performing the logic) and a view part (rendering a template). This two-step
+approach is known as an action, which will normally create, read, update, or
+delete (CRUD for short) some sort of model part (often backed by a database)
+before choosing either to render a template or redirecting to another action.
+
+Action Pack implements these actions as public methods on Action Controllers
+and uses Action Views to implement the template rendering. Action Controllers
+are then responsible for handling all the actions relating to a certain part
+of an application. This grouping usually consists of actions for lists and for
+CRUDs revolving around a single (or a few) model objects. So ContactController
+would be responsible for listing contacts, creating, deleting, and updating
+contacts. A WeblogController could be responsible for both posts and comments.
+
+Action View templates are written using embedded Ruby in tags mingled in with
+the HTML. To avoid cluttering the templates with code, a bunch of helper
+classes provide common behavior for forms, dates, and strings. And it's easy
+to add specific helpers to keep the separation as the application evolves.
+
+Note: Some of the features, such as scaffolding and form building, are tied to
+ActiveRecord[http://activerecord.rubyonrails.org] (an object-relational
+mapping package), but that doesn't mean that Action Pack depends on Active
+Record. Action Pack is an independent package that can be used with any sort
+of backend (Instiki[http://www.instiki.org], which is based on an older version
+of Action Pack, uses Madeleine for example). Read more about the role Action
+Pack can play when used together with Active Record on
+http://www.rubyonrails.org.
+
+A short rundown of the major features:
+
+* Actions grouped in controller as methods instead of separate command objects
+ and can therefore helper share methods.
+
+ BlogController < ActionController::Base
+ def display
+ @customer = find_customer
+ end
+
+ def update
+ @customer = find_customer
+ @customer.attributes = @params["customer"]
+ @customer.save ?
+ redirect_to(:action => "display") :
+ render("customer/edit")
+ end
+
+ private
+ def find_customer() Customer.find(@params["id"]) end
+ end
+
+ Learn more in link:classes/ActionController/Base.html
+
+
+* Embedded Ruby for templates (no new "easy" template language)
+
+ <% for post in @posts %>
+ Title: <%= post.title %>
+ <% end %>
+
+ All post titles: <%= @post.collect{ |p| p.title }.join ", " %>
+
+ <% unless @person.is_client? %>
+ Not for clients to see...
+ <% end %>
+
+ Learn more in link:classes/ActionView.html
+
+
+* Builder-based templates (great for XML content, like RSS)
+
+ xml.rss("version" => "2.0") do
+ xml.channel do
+ xml.title(@feed_title)
+ xml.link(@url)
+ xml.description "Basecamp: Recent items"
+ xml.language "en-us"
+ xml.ttl "40"
+
+ for item in @recent_items
+ xml.item do
+ xml.title(item_title(item))
+ xml.description(item_description(item))
+ xml.pubDate(item_pubDate(item))
+ xml.guid(@recent_items.url(item))
+ xml.link(@recent_items.url(item))
+ end
+ end
+ end
+ end
+
+
+* Filters for pre and post processing of the response (as methods, procs, and classes)
+
+ class WeblogController < ActionController::Base
+ before_filter :authenticate, :cache, :audit
+ after_filter { |c| c.response.body = GZip::compress(c.response.body) }
+ after_filter LocalizeFilter
+
+ def list
+ # Before this action is run, the user will be authenticated, the cache
+ # will be examined to see if a valid copy of the results already
+ # exist, and the action will be logged for auditing.
+
+ # After this action has run, the output will first be localized then
+ # compressed to minimize bandwith usage
+ end
+
+ private
+ def authenticate
+ # Implement the filter will full access to both request and response
+ end
+ end
+
+ Learn more in link:classes/ActionController/Filters/ClassMethods.html
+
+
+* Helpers for forms, dates, action links, and text
+
+ <%= text_field "post", "title", "size" => 30 %>
+ <%= html_date_select(Date.today) %>
+ <%= link_to "New post", :controller => "post", :action => "new" %>
+ <%= truncate(post.title, 25) %>
+
+ Learn more in link:classes/ActionView/Helpers.html
+
+
+* Layout sharing for template reuse (think simple version of Struts
+ Tiles[http://jakarta.apache.org/struts/userGuide/dev_tiles.html])
+
+ class WeblogController < ActionController::Base
+ layout "weblog_layout"
+
+ def hello_world
+ end
+ end
+
+ Layout file (called weblog_layout):
+ <html><body><%= @content_for_layout %></body></html>
+
+ Template for hello_world action:
+ <h1>Hello world</h1>
+
+ Result of running hello_world action:
+ <html><body><h1>Hello world</h1></body></html>
+
+ Learn more in link:classes/ActionController/Layout.html
+
+
+* Advanced redirection that makes pretty urls easy
+
+ RewriteRule ^/library/books/([A-Z]+)([0-9]+)/([-_a-zA-Z0-9]+)$ \
+ /books_controller.cgi?action=$3&type=$1&code=$2 [QSA] [L]
+
+ Accessing /library/books/ISBN/0743536703/show calls BooksController#show
+
+ From that URL, you can rewrite the redirect in a number of ways:
+
+ redirect_to(:action => "edit") =>
+ /library/books/ISBN/0743536703/edit
+
+ redirect_to(:path_params => { "type" => "XTC", "code" => "12354345" }) =>
+ /library/books/XTC/12354345/show
+
+ redirect_to(:controller_prefix => "admin", :controller => "accounts") =>
+ /admin/accounts/
+
+ Learn more in link:classes/ActionController/Base.html
+
+
+* Easy testing of both controller and template result through TestRequest/Response
+
+ class LoginControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = LoginController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_failing_authenticate
+ process :authenticate, "user_name" => "nop", "password" => ""
+ assert_flash_has 'alert'
+ assert_redirected_to :action => "index"
+ end
+ end
+
+ Learn more in link:classes/ActionController/TestRequest.html
+
+
+* Automated benchmarking and integrated logging
+
+ Processing WeblogController#index (for 127.0.0.1 at Fri May 28 00:41:55)
+ Parameters: {"action"=>"index", "controller"=>"weblog"}
+ Rendering weblog/index (200 OK)
+ Completed in 0.029281 (34 reqs/sec)
+
+ If Active Record is used as the model, you'll have the database debugging
+ as well:
+
+ Processing WeblogController#create (for 127.0.0.1 at Sat Jun 19 14:04:23)
+ Params: {"controller"=>"weblog", "action"=>"create",
+ "post"=>{"title"=>"this is good"} }
+ SQL (0.000627) INSERT INTO posts (title) VALUES('this is good')
+ Redirected to http://test/weblog/display/5
+ Completed in 0.221764 (4 reqs/sec) | DB: 0.059920 (27%)
+
+ You specify a logger through a class method, such as:
+
+ ActionController::Base.logger = Logger.new("Application Log")
+ ActionController::Base.logger = Log4r::Logger.new("Application Log")
+
+
+* Powerful debugging mechanism for local requests
+
+ All exceptions raised on actions performed on the request of a local user
+ will be presented with a tailored debugging screen that includes exception
+ message, stack trace, request parameters, session contents, and the
+ half-finished response.
+
+ Learn more in link:classes/ActionController/Rescue.html
+
+
+* Scaffolding for Action Record model objects
+
+ require 'account' # must be an Active Record class
+ class AccountController < ActionController::Base
+ scaffold :account
+ end
+
+ The AccountController now has the full CRUD range of actions and default
+ templates: list, show, destroy, new, create, edit, update
+
+ Learn more in link:classes/ActionController/Scaffolding/ClassMethods.html
+
+
+* Form building for Active Record model objects
+
+ The post object has a title (varchar), content (text), and
+ written_on (date)
+
+ <%= form "post" %>
+
+ ...will generate something like (the selects will have more options of
+ course):
+
+ <form action="create" method="POST">
+ <p>
+ <b>Title:</b><br/>
+ <input type="text" name="post[title]" value="<%= @post.title %>" />
+ </p>
+ <p>
+ <b>Content:</b><br/>
+ <textarea name="post[content]"><%= @post.title %></textarea>
+ </p>
+ <p>
+ <b>Written on:</b><br/>
+ <select name='post[written_on(3i)]'><option>18</option></select>
+ <select name='post[written_on(2i)]'><option value='7'>July</option></select>
+ <select name='post[written_on(1i)]'><option>2004</option></select>
+ </p>
+
+ <input type="submit" value="Create">
+ </form>
+
+ This form generates a @params["post"] array that can be used directly in a save action:
+
+ class WeblogController < ActionController::Base
+ def save
+ post = Post.create(@params["post"])
+ redirect_to :action => "display", :path_params => { "id" => post.id }
+ end
+ end
+
+ Learn more in link:classes/ActionView/Helpers/ActiveRecordHelper.html
+
+
+* Automated mapping of URLs to controller/action pairs through Apache's
+ mod_rewrite
+
+ Requesting /blog/display/5 will call BlogController#display and
+ make 5 available as an instance variable through @params["id"]
+
+
+* Runs on top of CGI, FCGI, and mod_ruby
+
+ See the address_book_controller example for all three forms
+
+
+== Simple example
+
+This example will implement a simple weblog system using inline templates and
+an Active Record model. The first thing we need to do is setup an .htaccess to
+interpret pretty URLs into something the controller can use. Let's use the
+simplest form for starters:
+
+ RewriteRule ^weblog/([-_a-zA-Z0-9]+)/([0-9]+)$ \
+ /weblog_controller.cgi?action=$2&id=$3 [QSA]
+ RewriteRule ^weblog/([-_a-zA-Z0-9]+)$ \
+ /weblog_controller.cgi?action=$2 [QSA]
+ RewriteRule ^weblog/$ \
+ /weblog_controller.cgi?action=index [QSA]
+
+Now we'll be able to access URLs like weblog/display/5 and have
+WeblogController#display called with { "id" => 5 } in the @params array
+available for the action. So let's build that WeblogController with just a few
+methods:
+
+ require 'action_controller'
+ require 'post'
+ class WeblogController < ActionController::Base
+ layout "weblog/layout"
+
+ def index
+ @posts = Post.find_all
+ end
+
+ def display
+ @post = Post.find(@params["id"])
+ end
+
+ def new
+ @post = Post.new
+ end
+
+ def create
+ @post = Post.create(@params["post"])
+ @post.save
+ redirect_to :action => "display", :id => @post.id
+ end
+ end
+
+ WeblogController::Base.template_root = File.dirname(__FILE__)
+ WeblogController.process_cgi if $0 == __FILE__
+
+The last two lines are responsible for telling ActionController where the
+template files are located and actually running the controller on a new
+request from the web-server (like to be Apache).
+
+And the templates look like this:
+
+ weblog/layout.rhtml:
+ <html><body>
+ <%= @content_for_layout %>
+ </body></html>
+
+ weblog/index.rhtml:
+ <% for post in @posts %>
+ <p><%= link_to(post.title, :action => "display", :id => post.id %></p>
+ <% end %>
+
+ weblog/display.rhtml:
+ <p>
+ <b><%= post.title %></b><br/>
+ <b><%= post.content %></b>
+ </p>
+
+ weblog/new.rhtml:
+ <%= form "post" %>
+
+This simple setup will list all the posts in the system on the index page,
+which is called by accessing /weblog/. It uses the form builder for the Active
+Record model to make the new screen, which in turns hand everything over to
+the create action (that's the default target for the form builder when given a
+new model). After creating the post, it'll redirect to the display page using
+an URL such as /weblog/display/5 (where 5 is the id of the post.
+
+
+== Examples
+
+Action Pack ships with three examples that all demonstrate an increasingly
+detailed view of the possibilities. First is blog_controller that is just a
+single file for the whole MVC (but still split into separate parts). Second is
+the debate_controller that uses separate template files and multiple screens.
+Third is the address_book_controller that uses the layout feature to separate
+template casing from content.
+
+Please note that you might need to change the "shebang" line to
+#!/usr/local/env ruby, if your Ruby is not placed in /usr/local/bin/ruby
+
+
+== Download
+
+The latest version of Action Pack can be found at
+
+* http://rubyforge.org/project/showfiles.php?group_id=249
+
+Documentation can be found at
+
+* http://actionpack.rubyonrails.org
+
+
+== Installation
+
+You can install Action Pack with the following command.
+
+ % [sudo] ruby install.rb
+
+from its distribution directory.
+
+
+== License
+
+Action Pack is released under the same license as Ruby.
+
+
+== Support
+
+The Action Pack homepage is http://actionpack.rubyonrails.org. You can find
+the Action Pack RubyForge page at http://rubyforge.org/projects/actionpack.
+And as Jim from Rake says:
+
+ Feel free to submit commits or feature requests. If you send a patch,
+ remember to update the corresponding unit tests. If fact, I prefer
+ new feature to be submitted in the form of new unit tests.
+
+For other information, feel free to ask on the ruby-talk mailing list (which
+is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com. \ No newline at end of file
diff --git a/actionpack/RUNNING_UNIT_TESTS b/actionpack/RUNNING_UNIT_TESTS
new file mode 100644
index 0000000000..c27ee02d67
--- /dev/null
+++ b/actionpack/RUNNING_UNIT_TESTS
@@ -0,0 +1,25 @@
+== Running with Rake
+
+The easiest way to run the unit tests is through Rake. The default task runs
+the entire test suite for all classes. For more information, checkout the
+full array of rake tasks with "rake -T"
+
+Rake can be found at http://rake.rubyforge.org
+
+== Running by hand
+
+If you only want to run a single test suite, or don't want to bother with Rake,
+you can do so with something like:
+
+ ruby controller/base_test.rb
+
+== Dependency on ActiveRecord and database setup
+
+Test cases in test/controller/active_record_assertions.rb depend on having
+activerecord installed and configured in a particular way. See comment in the
+test file itself for details. If ActiveRecord is not in
+actionpack/../activerecord directory, these tests are skipped. If activerecord
+is installed, but not configured as expected, the tests will fail.
+
+Other tests are runnable from a fresh copy of actionpack without any configuration.
+
diff --git a/actionpack/Rakefile b/actionpack/Rakefile
new file mode 100755
index 0000000000..fb9f3c9bfb
--- /dev/null
+++ b/actionpack/Rakefile
@@ -0,0 +1,105 @@
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+
+PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
+PKG_NAME = 'actionpack'
+PKG_VERSION = '0.9.5' + PKG_BUILD
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+
+desc "Default Task"
+task :default => [ :test ]
+
+# Run the unit tests
+
+Rake::TestTask.new { |t|
+ t.libs << "test"
+ t.pattern = 'test/*/*_test.rb'
+ t.verbose = true
+}
+
+
+# Genereate the RDoc documentation
+
+Rake::RDocTask.new { |rdoc|
+ rdoc.rdoc_dir = 'doc'
+ rdoc.title = "Action Pack -- On rails from request to response"
+ rdoc.options << '--line-numbers --inline-source --main README'
+ rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+}
+
+
+# Create compressed packages
+
+
+dist_dirs = [ "lib", "test", "examples" ]
+
+spec = Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.summary = "Web-flow and rendering framework putting the VC in MVC."
+ s.description = %q{Eases web-request routing, handling, and response as a half-way front, half-way page controller. Implemented with specific emphasis on enabling easy unit/integration testing that doesn't require a browser.}
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.rubyforge_project = "actionpack"
+ s.homepage = "http://actionpack.rubyonrails.org"
+
+ s.has_rdoc = true
+ s.requirements << 'none'
+ s.require_path = 'lib'
+ s.autorequire = 'action_controller'
+
+ s.files = [ "rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG", "MIT-LICENSE", "examples/.htaccess" ]
+ dist_dirs.each do |dir|
+ s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "CVS" ) }
+ end
+ s.files.delete "examples/benchmark.rb"
+ s.files.delete "examples/benchmark_with_ar.fcgi"
+end
+
+Rake::GemPackageTask.new(spec) do |p|
+ p.gem_spec = spec
+ p.need_tar = true
+ p.need_zip = true
+end
+
+
+# Publish beta gem
+desc "Publish the API documentation"
+task :pgem => [:package] do
+ Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
+ `ssh davidhh@one.textdrive.com './gemupdate.sh'`
+end
+
+# Publish documentation
+desc "Publish the API documentation"
+task :pdoc => [:rdoc] do
+ Rake::SshDirPublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/ap", "doc").upload
+end
+
+
+desc "Count lines in the main rake file"
+task :lines do
+ lines = 0
+ codelines = 0
+ Dir.foreach("lib/action_controller") { |file_name|
+ next unless file_name =~ /.*rb/
+
+ f = File.open("lib/action_controller/" + file_name)
+
+ while line = f.gets
+ lines += 1
+ next if line =~ /^\s*$/
+ next if line =~ /^\s*#/
+ codelines += 1
+ end
+ }
+ puts "Lines #{lines}, LOC #{codelines}"
+end \ No newline at end of file
diff --git a/actionpack/examples/.htaccess b/actionpack/examples/.htaccess
new file mode 100644
index 0000000000..fb59fa105e
--- /dev/null
+++ b/actionpack/examples/.htaccess
@@ -0,0 +1,24 @@
+<IfModule mod_ruby.c>
+ RubyRequire apache/ruby-run
+ RubySafeLevel 0
+
+ <Files *.rbx>
+ SetHandler ruby-object
+ RubyHandler Apache::RubyRun.instance
+ </Files>
+</IfModule>
+
+
+RewriteEngine On
+RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.fcgi?action=$2&id=$3 [QSA]
+RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.fcgi?action=$2 [QSA]
+RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/$ /$1_controller.fcgi?action=index [QSA]
+
+RewriteRule ^modruby/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.rbx?action=$2&id=$3 [QSA]
+RewriteRule ^modruby/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.rbx?action=$2 [QSA]
+RewriteRule ^modruby/([-_a-zA-Z0-9]+)/$ /$1_controller.rbx?action=index [QSA]
+
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.cgi?action=$2&id=$3 [QSA]
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.cgi?action=$2 [QSA]
+RewriteRule ^([-_a-zA-Z0-9]+)/$ /$1_controller.cgi?action=index [QSA]
+
diff --git a/actionpack/examples/address_book/index.rhtml b/actionpack/examples/address_book/index.rhtml
new file mode 100644
index 0000000000..217d39075c
--- /dev/null
+++ b/actionpack/examples/address_book/index.rhtml
@@ -0,0 +1,33 @@
+<h1>Address Book</h1>
+
+<% if @people.empty? %>
+ <p>No people in the address book yet</p>
+<% else %>
+ <table>
+ <tr><th>Name</th><th>Email Address</th><th>Phone Number</th></tr>
+ <% for person in @people %>
+ <tr><td><%= person.name %></td><td><%= person.email_address %></td><td><%= person.phone_number %></td></tr>
+ <% end %>
+ </table>
+<% end %>
+
+<form action="create_person">
+ <p>
+ Name:<br />
+ <input type="text" name="person[name]">
+ </p>
+
+ <p>
+ Email address:<br />
+ <input type="text" name="person[email_address]">
+ </p>
+
+ <p>
+ Phone number:<br />
+ <input type="text" name="person[phone_number]">
+ </p>
+
+ <p>
+ <input type="submit" value="Create Person">
+ </p>
+</form> \ No newline at end of file
diff --git a/actionpack/examples/address_book/layout.rhtml b/actionpack/examples/address_book/layout.rhtml
new file mode 100644
index 0000000000..931e141c01
--- /dev/null
+++ b/actionpack/examples/address_book/layout.rhtml
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title><%= @title || "Untitled" %></title>
+</head>
+<body>
+<%= @content_for_layout %>
+</body>
+</html> \ No newline at end of file
diff --git a/actionpack/examples/address_book_controller.cgi b/actionpack/examples/address_book_controller.cgi
new file mode 100755
index 0000000000..2e15467285
--- /dev/null
+++ b/actionpack/examples/address_book_controller.cgi
@@ -0,0 +1,9 @@
+#!/usr/local/bin/ruby
+
+require "address_book_controller"
+
+begin
+ AddressBookController.process_cgi(CGI.new)
+rescue => e
+ CGI.new.out { "#{e.class}: #{e.message}" }
+end \ No newline at end of file
diff --git a/actionpack/examples/address_book_controller.fcgi b/actionpack/examples/address_book_controller.fcgi
new file mode 100755
index 0000000000..39947b4444
--- /dev/null
+++ b/actionpack/examples/address_book_controller.fcgi
@@ -0,0 +1,6 @@
+#!/usr/local/bin/ruby
+
+require "address_book_controller"
+require "fcgi"
+
+FCGI.each_cgi { |cgi| AddressBookController.process_cgi(cgi) } \ No newline at end of file
diff --git a/actionpack/examples/address_book_controller.rb b/actionpack/examples/address_book_controller.rb
new file mode 100644
index 0000000000..01d498e1bc
--- /dev/null
+++ b/actionpack/examples/address_book_controller.rb
@@ -0,0 +1,52 @@
+$:.unshift(File.dirname(__FILE__) + "/../lib")
+
+require "action_controller"
+require "action_controller/test_process"
+
+Person = Struct.new("Person", :id, :name, :email_address, :phone_number)
+
+class AddressBookService
+ attr_reader :people
+
+ def initialize() @people = [] end
+ def create_person(data) people.unshift(Person.new(next_person_id, data["name"], data["email_address"], data["phone_number"])) end
+ def find_person(topic_id) people.select { |person| person.id == person.to_i }.first end
+ def next_person_id() people.first.id + 1 end
+end
+
+class AddressBookController < ActionController::Base
+ layout "address_book/layout"
+
+ before_filter :initialize_session_storage
+
+ # Could also have used a proc
+ # before_filter proc { |c| c.instance_variable_set("@address_book", c.session["address_book"] ||= AddressBookService.new) }
+
+ def index
+ @title = "Address Book"
+ @people = @address_book.people
+ end
+
+ def person
+ @person = @address_book.find_person(@params["id"])
+ end
+
+ def create_person
+ @address_book.create_person(@params["person"])
+ redirect_to :action => "index"
+ end
+
+ private
+ def initialize_session_storage
+ @address_book = @session["address_book"] ||= AddressBookService.new
+ end
+end
+
+ActionController::Base.template_root = File.dirname(__FILE__)
+# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir
+
+begin
+ AddressBookController.process_cgi(CGI.new) if $0 == __FILE__
+rescue => e
+ CGI.new.out { "#{e.class}: #{e.message}" }
+end \ No newline at end of file
diff --git a/actionpack/examples/address_book_controller.rbx b/actionpack/examples/address_book_controller.rbx
new file mode 100644
index 0000000000..8c04eeccc8
--- /dev/null
+++ b/actionpack/examples/address_book_controller.rbx
@@ -0,0 +1,4 @@
+#!/usr/local/bin/ruby
+
+require "address_book_controller"
+AddressBookController.process_cgi(CGI.new) \ No newline at end of file
diff --git a/actionpack/examples/benchmark.rb b/actionpack/examples/benchmark.rb
new file mode 100644
index 0000000000..1e10a0c962
--- /dev/null
+++ b/actionpack/examples/benchmark.rb
@@ -0,0 +1,52 @@
+$:.unshift(File.dirname(__FILE__) + "/../lib")
+
+require "action_controller"
+require 'action_controller/test_process'
+
+Person = Struct.new("Person", :name, :address, :age)
+
+class BenchmarkController < ActionController::Base
+ def message
+ render_text "hello world"
+ end
+
+ def list
+ @people = [ Person.new("David"), Person.new("Mary") ]
+ render_template "hello: <% for person in @people %>Name: <%= person.name %><% end %>"
+ end
+
+ def form_helper
+ @person = Person.new "david", "hyacintvej", 24
+ render_template(
+ "<% person = Person.new 'Mary', 'hyacintvej', 22 %> " +
+ "change the name <%= text_field 'person', 'name' %> and <%= text_field 'person', 'address' %> and <%= text_field 'person', 'age' %>"
+ )
+ end
+end
+
+#ActionController::Base.template_root = File.dirname(__FILE__)
+
+require "benchmark"
+
+RUNS = ARGV[0] ? ARGV[0].to_i : 50
+
+require "profile" if ARGV[1]
+
+runtime = Benchmark.measure {
+ RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "list" })) }
+}
+
+puts "List: #{RUNS / runtime.real}"
+
+
+runtime = Benchmark.measure {
+ RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "message" })) }
+}
+
+puts "Message: #{RUNS / runtime.real}"
+
+runtime = Benchmark.measure {
+ RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "form_helper" })) }
+}
+
+puts "Form helper: #{RUNS / runtime.real}"
diff --git a/actionpack/examples/benchmark_with_ar.fcgi b/actionpack/examples/benchmark_with_ar.fcgi
new file mode 100755
index 0000000000..b9de370e24
--- /dev/null
+++ b/actionpack/examples/benchmark_with_ar.fcgi
@@ -0,0 +1,89 @@
+#!/usr/local/bin/ruby
+
+begin
+
+$:.unshift(File.dirname(__FILE__) + "/../lib")
+$:.unshift(File.dirname(__FILE__) + "/../../../edge/activerecord/lib")
+
+require 'fcgi'
+require 'action_controller'
+require 'action_controller/test_process'
+
+require 'active_record'
+
+class Post < ActiveRecord::Base; end
+
+ActiveRecord::Base.establish_connection(:adapter => "mysql", :database => "basecamp")
+
+SESSION_OPTIONS = { "database_manager" => CGI::Session::MemoryStore }
+
+class TestController < ActionController::Base
+ def index
+ render_template <<-EOT
+ <% for post in Post.find_all(nil,nil,100) %>
+ <%= post.title %>
+ <% end %>
+ EOT
+ end
+
+ def show_one
+ render_template <<-EOT
+ <%= Post.find_first.title %>
+ EOT
+ end
+
+ def text
+ render_text "hello world"
+ end
+
+ def erb_text
+ render_template "hello <%= 'world' %>"
+ end
+
+ def erb_loop
+ render_template <<-EOT
+ <% for post in 1..100 %>
+ <%= post %>
+ <% end %>
+ EOT
+ end
+
+ def rescue_action(e) puts e.message + e.backtrace.join("\n") end
+end
+
+if ARGV.empty? && ENV["REQUEST_URI"]
+ FCGI.each_cgi do |cgi|
+ TestController.process(ActionController::CgiRequest.new(cgi, SESSION_OPTIONS), ActionController::CgiResponse.new(cgi)).out
+ end
+else
+ if ARGV.empty?
+ cgi = CGI.new
+ end
+
+ require 'benchmark'
+ require 'profile' if ARGV[2] == "profile"
+
+ RUNS = ARGV[1] ? ARGV[1].to_i : 50
+
+ runtime = Benchmark::measure {
+ RUNS.times {
+ if ARGV.empty?
+ TestController.process(ActionController::CgiRequest.new(cgi, SESSION_OPTIONS), ActionController::CgiResponse.new(cgi))
+ else
+ response = TestController.process_test(
+ ActionController::TestRequest.new({"action" => ARGV[0]})
+ )
+ puts(response.body) if ARGV[2] == "show"
+ end
+ }
+ }
+
+ puts "Runs: #{RUNS}"
+ puts "Avg. runtime: #{runtime.real / RUNS}"
+ puts "Requests/second: #{RUNS / runtime.real}"
+end
+
+rescue Exception => e
+ # CGI.new.out { "<pre>" + e.message + e.backtrace.join("\n") + "</pre>" }
+ $stderr << e.message + e.backtrace.join("\n")
+end \ No newline at end of file
diff --git a/actionpack/examples/blog_controller.cgi b/actionpack/examples/blog_controller.cgi
new file mode 100755
index 0000000000..e64fe85f0c
--- /dev/null
+++ b/actionpack/examples/blog_controller.cgi
@@ -0,0 +1,53 @@
+#!/usr/local/bin/ruby
+
+$:.unshift(File.dirname(__FILE__) + "/../lib")
+
+require "action_controller"
+
+Post = Struct.new("Post", :title, :body)
+
+class BlogController < ActionController::Base
+ before_filter :initialize_session_storage
+
+ def index
+ @posts = @session["posts"]
+
+ render_template <<-"EOF"
+ <html><body>
+ <%= @flash["alert"] %>
+ <h1>Posts</h1>
+ <% @posts.each do |post| %>
+ <p><b><%= post.title %></b><br /><%= post.body %></p>
+ <% end %>
+
+ <h1>Create post</h1>
+ <form action="create">
+ Title: <input type="text" name="post[title]"><br>
+ Body: <textarea name="post[body]"></textarea><br>
+ <input type="submit" value="save">
+ </form>
+
+ </body></html>
+ EOF
+ end
+
+ def create
+ @session["posts"].unshift(Post.new(@params["post"]["title"], @params["post"]["body"]))
+ flash["alert"] = "New post added!"
+ redirect_to :action => "index"
+ end
+
+ private
+ def initialize_session_storage
+ @session["posts"] = [] if @session["posts"].nil?
+ end
+end
+
+ActionController::Base.template_root = File.dirname(__FILE__)
+# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir
+
+begin
+ BlogController.process_cgi(CGI.new) if $0 == __FILE__
+rescue => e
+ CGI.new.out { "#{e.class}: #{e.message}" }
+end \ No newline at end of file
diff --git a/actionpack/examples/debate/index.rhtml b/actionpack/examples/debate/index.rhtml
new file mode 100644
index 0000000000..ddaa87da57
--- /dev/null
+++ b/actionpack/examples/debate/index.rhtml
@@ -0,0 +1,14 @@
+<html>
+<body>
+<h1>Topics</h1>
+
+<%= link_to "New topic", :action => "new_topic" %>
+
+<ul>
+<% for topic in @topics %>
+ <li><%= link_to "#{topic.title} (#{topic.replies.length} replies)", :action => "topic", :path_params => { "id" => topic.id } %></li>
+<% end %>
+</ul>
+
+</body>
+</html> \ No newline at end of file
diff --git a/actionpack/examples/debate/new_topic.rhtml b/actionpack/examples/debate/new_topic.rhtml
new file mode 100644
index 0000000000..f52a69cc31
--- /dev/null
+++ b/actionpack/examples/debate/new_topic.rhtml
@@ -0,0 +1,22 @@
+<html>
+<body>
+<h1>New topic</h1>
+
+<form action="<%= url_for(:action => "create_topic") %>" method="post">
+ <p>
+ Title:<br>
+ <input type="text" name="topic[title]">
+ </p>
+
+ <p>
+ Body:<br>
+ <textarea name="topic[body]" style="width: 200px; height: 200px"></textarea>
+ </p>
+
+ <p>
+ <input type="submit" value="Create topic">
+ </p>
+</form>
+
+</body>
+</html> \ No newline at end of file
diff --git a/actionpack/examples/debate/topic.rhtml b/actionpack/examples/debate/topic.rhtml
new file mode 100644
index 0000000000..e247c00f0d
--- /dev/null
+++ b/actionpack/examples/debate/topic.rhtml
@@ -0,0 +1,32 @@
+<html>
+<body>
+<h1><%= @topic.title %></h1>
+
+<p><%= @topic.body %></p>
+
+<%= link_to "Back to topics", :action => "index" %>
+
+<% unless @topic.replies.empty? %>
+ <h2>Replies</h2>
+ <ol>
+ <% for reply in @topic.replies %>
+ <li><%= reply.body %></li>
+ <% end %>
+ </ol>
+<% end %>
+
+<h2>Reply to this topic</h2>
+
+<form action="<%= url_for(:action => "create_reply") %>" method="post">
+ <input type="hidden" name="reply[topic_id]" value="<%= @topic.id %>">
+ <p>
+ <textarea name="reply[body]" style="width: 200px; height: 200px"></textarea>
+ </p>
+
+ <p>
+ <input type="submit" value="Create reply">
+ </p>
+</form>
+
+</body>
+</html> \ No newline at end of file
diff --git a/actionpack/examples/debate_controller.cgi b/actionpack/examples/debate_controller.cgi
new file mode 100755
index 0000000000..b82ac6259d
--- /dev/null
+++ b/actionpack/examples/debate_controller.cgi
@@ -0,0 +1,57 @@
+#!/usr/local/bin/ruby
+
+$:.unshift(File.dirname(__FILE__) + "/../lib")
+
+require "action_controller"
+
+Topic = Struct.new("Topic", :id, :title, :body, :replies)
+Reply = Struct.new("Reply", :body)
+
+class DebateService
+ attr_reader :topics
+
+ def initialize() @topics = [] end
+ def create_topic(data) topics.unshift(Topic.new(next_topic_id, data["title"], data["body"], [])) end
+ def create_reply(data) find_topic(data["topic_id"]).replies << Reply.new(data["body"]) end
+ def find_topic(topic_id) topics.select { |topic| topic.id == topic_id.to_i }.first end
+ def next_topic_id() topics.first.id + 1 end
+end
+
+class DebateController < ActionController::Base
+ before_filter :initialize_session_storage
+
+ def index
+ @topics = @debate.topics
+ end
+
+ def topic
+ @topic = @debate.find_topic(@params["id"])
+ end
+
+ # def new_topic() end <-- This is not needed as the template doesn't require any assigns
+
+ def create_topic
+ @debate.create_topic(@params["topic"])
+ redirect_to :action => "index"
+ end
+
+ def create_reply
+ @debate.create_reply(@params["reply"])
+ redirect_to :action => "topic", :path_params => { "id" => @params["reply"]["topic_id"] }
+ end
+
+ private
+ def initialize_session_storage
+ @session["debate"] = DebateService.new if @session["debate"].nil?
+ @debate = @session["debate"]
+ end
+end
+
+ActionController::Base.template_root = File.dirname(__FILE__)
+# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir
+
+begin
+ DebateController.process_cgi(CGI.new) if $0 == __FILE__
+rescue => e
+ CGI.new.out { "#{e.class}: #{e.message}" }
+end \ No newline at end of file
diff --git a/actionpack/install.rb b/actionpack/install.rb
new file mode 100644
index 0000000000..758c476a70
--- /dev/null
+++ b/actionpack/install.rb
@@ -0,0 +1,97 @@
+require 'rbconfig'
+require 'find'
+require 'ftools'
+
+include Config
+
+# this was adapted from rdoc's install.rb by ways of Log4r
+
+$sitedir = CONFIG["sitelibdir"]
+unless $sitedir
+ version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
+ $libdir = File.join(CONFIG["libdir"], "ruby", version)
+ $sitedir = $:.find {|x| x =~ /site_ruby/ }
+ if !$sitedir
+ $sitedir = File.join($libdir, "site_ruby")
+ elsif $sitedir !~ Regexp.quote(version)
+ $sitedir = File.join($sitedir, version)
+ end
+end
+
+makedirs = %w{ action_controller/assertions action_controller/cgi_ext
+ action_controller/session action_controller/support
+ action_controller/templates action_controller/templates/rescues
+ action_controller/templates/scaffolds
+ action_view/helpers action_view/vendor action_view/vendor/builder
+}
+
+
+makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))}
+
+# deprecated files that should be removed
+# deprecated = %w{ }
+
+# files to install in library path
+files = %w-
+ action_controller.rb
+ action_controller/assertions/action_pack_assertions.rb
+ action_controller/assertions/active_record_assertions.rb
+ action_controller/base.rb
+ action_controller/benchmarking.rb
+ action_controller/cgi_ext/cgi_ext.rb
+ action_controller/cgi_ext/cgi_methods.rb
+ action_controller/cgi_process.rb
+ action_controller/filters.rb
+ action_controller/flash.rb
+ action_controller/helpers.rb
+ action_controller/layout.rb
+ action_controller/request.rb
+ action_controller/rescue.rb
+ action_controller/response.rb
+ action_controller/scaffolding.rb
+ action_controller/session/active_record_store.rb
+ action_controller/session/drb_server.rb
+ action_controller/session/drb_store.rb
+ action_controller/support/class_inheritable_attributes.rb
+ action_controller/support/class_attribute_accessors.rb
+ action_controller/support/clean_logger.rb
+ action_controller/support/cookie_performance_fix.rb
+ action_controller/support/inflector.rb
+ action_controller/templates/rescues/_request_and_response.rhtml
+ action_controller/templates/rescues/diagnostics.rhtml
+ action_controller/templates/rescues/layout.rhtml
+ action_controller/templates/rescues/missing_template.rhtml
+ action_controller/templates/rescues/template_error.rhtml
+ action_controller/templates/rescues/unknown_action.rhtml
+ action_controller/templates/scaffolds/edit.rhtml
+ action_controller/templates/scaffolds/layout.rhtml
+ action_controller/templates/scaffolds/list.rhtml
+ action_controller/templates/scaffolds/new.rhtml
+ action_controller/templates/scaffolds/show.rhtml
+ action_controller/test_process.rb
+ action_controller/url_rewriter.rb
+ action_view.rb
+ action_view/base.rb
+ action_view/helpers/active_record_helper.rb
+ action_view/helpers/date_helper.rb
+ action_view/helpers/debug_helper.rb
+ action_view/helpers/form_helper.rb
+ action_view/helpers/form_options_helper.rb
+ action_view/helpers/text_helper.rb
+ action_view/helpers/tag_helper.rb
+ action_view/helpers/url_helper.rb
+ action_view/partials.rb
+ action_view/template_error.rb
+ action_view/vendor/builder.rb
+ action_view/vendor/builder/blankslate.rb
+ action_view/vendor/builder/xmlbase.rb
+ action_view/vendor/builder/xmlevents.rb
+ action_view/vendor/builder/xmlmarkup.rb
+-
+
+# the acual gruntwork
+Dir.chdir("lib")
+# File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
+files.each {|f|
+ File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
+}
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
new file mode 100755
index 0000000000..6445940d78
--- /dev/null
+++ b/actionpack/lib/action_controller.rb
@@ -0,0 +1,51 @@
+#--
+# Copyright (c) 2004 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+$:.unshift(File.dirname(__FILE__))
+
+require 'action_controller/support/clean_logger'
+
+require 'action_controller/base'
+require 'action_controller/rescue'
+require 'action_controller/benchmarking'
+require 'action_controller/filters'
+require 'action_controller/layout'
+require 'action_controller/flash'
+require 'action_controller/scaffolding'
+require 'action_controller/helpers'
+require 'action_controller/dependencies'
+require 'action_controller/cgi_process'
+
+ActionController::Base.class_eval do
+ include ActionController::Filters
+ include ActionController::Layout
+ include ActionController::Flash
+ include ActionController::Benchmarking
+ include ActionController::Rescue
+ include ActionController::Scaffolding
+ include ActionController::Helpers
+ include ActionController::Dependencies
+end
+
+require 'action_view'
+ActionController::Base.template_class = ActionView::Base \ No newline at end of file
diff --git a/actionpack/lib/action_controller/assertions/action_pack_assertions.rb b/actionpack/lib/action_controller/assertions/action_pack_assertions.rb
new file mode 100644
index 0000000000..2cfbcbc938
--- /dev/null
+++ b/actionpack/lib/action_controller/assertions/action_pack_assertions.rb
@@ -0,0 +1,199 @@
+require 'test/unit'
+require 'test/unit/assertions'
+require 'rexml/document'
+
+module Test #:nodoc:
+ module Unit #:nodoc:
+ # Adds a wealth of assertions to do functional testing of Action Controllers.
+ module Assertions
+ # -- basic assertions ---------------------------------------------------
+
+ # ensure that the web request has been serviced correctly
+ def assert_success(message=nil)
+ response = acquire_assertion_target
+ if response.success?
+ # to count the assertion
+ assert_block("") { true }
+ else
+ if response.redirect?
+ msg = build_message(message, "Response unexpectedly redirect to <?>", response.redirect_url)
+ else
+ msg = build_message(message, "unsuccessful request (response code = <?>)",
+ response.response_code)
+ end
+ assert_block(msg) { false }
+ end
+ end
+
+ # ensure the request was rendered with the appropriate template file
+ def assert_rendered_file(expected=nil, message=nil)
+ response = acquire_assertion_target
+ rendered = expected ? response.rendered_file(!expected.include?('/')) : response.rendered_file
+ msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered)
+ assert_block(msg) do
+ if expected.nil?
+ response.rendered_with_file?
+ else
+ expected == rendered
+ end
+ end
+ end
+
+ # -- session assertions -------------------------------------------------
+
+ # ensure that the session has an object with the specified name
+ def assert_session_has(key=nil, message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> is not in the session <?>", key, response.session)
+ assert_block(msg) { response.has_session_object?(key) }
+ end
+
+ # ensure that the session has no object with the specified name
+ def assert_session_has_no(key=nil, message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> is in the session <?>", key, response.session)
+ assert_block(msg) { !response.has_session_object?(key) }
+ end
+
+ def assert_session_equal(expected = nil, key = nil, message = nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> expected in session['?'] but was <?>", expected, key, response.session[key])
+ assert_block(msg) { expected == response.session[key] }
+ end
+
+ # -- flash assertions ---------------------------------------------------
+
+ # ensure that the flash has an object with the specified name
+ def assert_flash_has(key=nil, message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> is not in the flash <?>", key, response.flash)
+ assert_block(msg) { response.has_flash_object?(key) }
+ end
+
+ # ensure that the flash has no object with the specified name
+ def assert_flash_has_no(key=nil, message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> is in the flash <?>", key, response.flash)
+ assert_block(msg) { !response.has_flash_object?(key) }
+ end
+
+ # ensure the flash exists
+ def assert_flash_exists(message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "the flash does not exist <?>", response.session['flash'] )
+ assert_block(msg) { response.has_flash? }
+ end
+
+ # ensure the flash does not exist
+ def assert_flash_not_exists(message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "the flash exists <?>", response.flash)
+ assert_block(msg) { !response.has_flash? }
+ end
+
+ # ensure the flash is empty but existant
+ def assert_flash_empty(message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "the flash is not empty <?>", response.flash)
+ assert_block(msg) { !response.has_flash_with_contents? }
+ end
+
+ # ensure the flash is not empty
+ def assert_flash_not_empty(message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "the flash is empty")
+ assert_block(msg) { response.has_flash_with_contents? }
+ end
+
+ def assert_flash_equal(expected = nil, key = nil, message = nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> expected in flash['?'] but was <?>", expected, key, response.flash[key])
+ assert_block(msg) { expected == response.flash[key] }
+ end
+
+ # -- redirection assertions ---------------------------------------------
+
+ # ensure we have be redirected
+ def assert_redirect(message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "response is not a redirection (response code is <?>)", response.response_code)
+ assert_block(msg) { response.redirect? }
+ end
+
+ def assert_redirected_to(options = {}, message=nil)
+ assert_redirect(message)
+ response = acquire_assertion_target
+
+ msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>)", response.redirected_to)
+ assert_block(msg) do
+ if options.is_a?(Symbol)
+ response.redirected_to == options
+ else
+ options.keys.all? { |k| options[k] == response.redirected_to[k] }
+ end
+ end
+ end
+
+ # ensure our redirection url is an exact match
+ def assert_redirect_url(url=nil, message=nil)
+ assert_redirect(message)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> is not the redirected location <?>", url, response.redirect_url)
+ assert_block(msg) { response.redirect_url == url }
+ end
+
+ # ensure our redirection url matches a pattern
+ def assert_redirect_url_match(pattern=nil, message=nil)
+ assert_redirect(message)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> was not found in the location: <?>", pattern, response.redirect_url)
+ assert_block(msg) { response.redirect_url_match?(pattern) }
+ end
+
+ # -- template assertions ------------------------------------------------
+
+ # ensure that a template object with the given name exists
+ def assert_template_has(key=nil, message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> is not a template object", key )
+ assert_block(msg) { response.has_template_object?(key) }
+ end
+
+ # ensure that a template object with the given name does not exist
+ def assert_template_has_no(key=nil,message=nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> is a template object <?>", key, response.template_objects[key])
+ assert_block(msg) { !response.has_template_object?(key) }
+ end
+
+ # ensures that the object assigned to the template on +key+ is equal to +expected+ object.
+ def assert_assigned_equal(expected = nil, key = nil, message = nil)
+ response = acquire_assertion_target
+ msg = build_message(message, "<?> expected in assigns['?'] but was <?>", expected, key, response.template.assigns[key.to_s])
+ assert_block(msg) { expected == response.template.assigns[key.to_s] }
+ end
+
+ # Asserts that the template returns the +expected+ string or array based on the XPath +expression+.
+ # This will only work if the template rendered a valid XML document.
+ def assert_template_xpath_match(expression=nil, expected=nil, message=nil)
+ response = acquire_assertion_target
+ xml, matches = REXML::Document.new(response.body), []
+ xml.elements.each(expression) { |e| matches << e.text }
+ matches = matches.first if matches.length < 2
+
+ msg = build_message(message, "<?> found <?>, not <?>", expression, matches, expected)
+ assert_block(msg) { matches == expected }
+ end
+
+ # -- helper functions ---------------------------------------------------
+
+ # get the TestResponse object that these assertions depend upon
+ def acquire_assertion_target
+ target = ActionController::TestResponse.assertion_target
+ assert_block( "Unable to acquire the TestResponse.assertion_target. Please set this before calling this assertion." ) { !target.nil? }
+ target
+ end
+
+ end # Assertions
+ end # Unit
+end # Test
diff --git a/actionpack/lib/action_controller/assertions/active_record_assertions.rb b/actionpack/lib/action_controller/assertions/active_record_assertions.rb
new file mode 100644
index 0000000000..9167eae53e
--- /dev/null
+++ b/actionpack/lib/action_controller/assertions/active_record_assertions.rb
@@ -0,0 +1,65 @@
+require 'test/unit'
+require 'test/unit/assertions'
+# active_record is assumed to be loaded by this point
+
+module Test #:nodoc:
+ module Unit #:nodoc:
+ module Assertions
+ # Assert the template object with the given name is an Active Record descendant and is valid.
+ def assert_valid_record(key = nil, message = nil)
+ record = find_record_in_template(key)
+ msg = build_message(message, "Active Record is invalid <?>)", record.errors.full_messages)
+ assert_block(msg) { record.valid? }
+ end
+
+ # Assert the template object with the given name is an Active Record descendant and is invalid.
+ def assert_invalid_record(key = nil, message = nil)
+ record = find_record_in_template(key)
+ msg = build_message(message, "Active Record is valid)")
+ assert_block(msg) { !record.valid? }
+ end
+
+ # Assert the template object with the given name is an Active Record descendant and the specified column(s) are valid.
+ def assert_valid_column_on_record(key = nil, columns = "", message = nil)
+ record = find_record_in_template(key)
+ record.validate
+
+ cols = glue_columns(columns)
+ cols.delete_if { |col| !record.errors.invalid?(col) }
+ msg = build_message(message, "Active Record has invalid columns <?>)", cols.join(",") )
+ assert_block(msg) { cols.empty? }
+ end
+
+ # Assert the template object with the given name is an Active Record descendant and the specified column(s) are invalid.
+ def assert_invalid_column_on_record(key = nil, columns = "", message = nil)
+ record = find_record_in_template(key)
+ record.validate
+
+ cols = glue_columns(columns)
+ cols.delete_if { |col| record.errors.invalid?(col) }
+ msg = build_message(message, "Active Record has valid columns <?>)", cols.join(",") )
+ assert_block(msg) { cols.empty? }
+ end
+
+ private
+ def glue_columns(columns)
+ cols = []
+ cols << columns if columns.class == String
+ cols += columns if columns.class == Array
+ cols
+ end
+
+ def find_record_in_template(key = nil)
+ response = acquire_assertion_target
+
+ assert_template_has(key)
+ record = response.template_objects[key]
+
+ assert_not_nil(record)
+ assert_kind_of ActiveRecord::Base, record
+
+ return record
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
new file mode 100755
index 0000000000..0cbcbbf688
--- /dev/null
+++ b/actionpack/lib/action_controller/base.rb
@@ -0,0 +1,689 @@
+require 'action_controller/request'
+require 'action_controller/response'
+require 'action_controller/url_rewriter'
+require 'action_controller/support/class_attribute_accessors'
+require 'action_controller/support/class_inheritable_attributes'
+require 'action_controller/support/inflector'
+
+module ActionController #:nodoc:
+ class ActionControllerError < StandardError #:nodoc:
+ end
+ class SessionRestoreError < ActionControllerError #:nodoc:
+ end
+ class MissingTemplate < ActionControllerError #:nodoc:
+ end
+ class UnknownAction < ActionControllerError #:nodoc:
+ end
+
+ # Action Controllers are made up of one or more actions that performs its purpose and then either renders a template or
+ # redirects to another action. An action is defined as a public method on the controller, which will automatically be
+ # made accessible to the web-server through a mod_rewrite mapping. A sample controller could look like this:
+ #
+ # class GuestBookController < ActionController::Base
+ # def index
+ # @entries = Entry.find_all
+ # end
+ #
+ # def sign
+ # Entry.create(@params["entry"])
+ # redirect_to :action => "index"
+ # end
+ # end
+ #
+ # GuestBookController.template_root = "templates/"
+ # GuestBookController.process_cgi
+ #
+ # All actions assume that you want to render a template matching the name of the action at the end of the performance
+ # unless you tell it otherwise. The index action complies with this assumption, so after populating the @entries instance
+ # variable, the GuestBookController will render "templates/guestbook/index.rhtml".
+ #
+ # Unlike index, the sign action isn't interested in rendering a template. So after performing its main purpose (creating a
+ # new entry in the guest book), it sheds the rendering assumption and initiates a redirect instead. This redirect works by
+ # returning an external "302 Moved" HTTP response that takes the user to the index action.
+ #
+ # The index and sign represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect.
+ # Most actions are variations of these themes.
+ #
+ # Also note that it's the final call to <tt>process_cgi</tt> that actually initiates the action performance. It will extract
+ # request and response objects from the CGI
+ #
+ # == Requests
+ #
+ # Requests are processed by the Action Controller framework by extracting the value of the "action" key in the request parameters.
+ # This value should hold the name of the action to be performed. Once the action has been identified, the remaining
+ # request parameters, the session (if one is available), and the full request with all the http headers are made available to
+ # the action through instance variables. Then the action is performed.
+ #
+ # The full request object is available in @request and is primarily used to query for http headers. These queries are made by
+ # accessing the environment hash, like this:
+ #
+ # def hello_ip
+ # location = @request.env["REMOTE_ADDRESS"]
+ # render_text "Hello stranger from #{location}"
+ # end
+ #
+ # == Parameters
+ #
+ # All request parameters whether they come from a GET or POST request, or from the URL, are available through the @params hash.
+ # So an action that was performed through /weblog/list?category=All&limit=5 will include { "category" => "All", "limit" => 5 }
+ # in @params.
+ #
+ # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
+ #
+ # <input type="text" name="post[name]" value="david">
+ # <input type="text" name="post[address]" value="hyacintvej">
+ #
+ # A request stemming from a form holding these inputs will include { "post" # => { "name" => "david", "address" => "hyacintvej" } }.
+ # If the address input had been named "post[address][street]", the @params would have included
+ # { "post" => { "address" => { "street" => "hyacintvej" } } }. There's no limit to the depth of the nesting.
+ #
+ # == Sessions
+ #
+ # Sessions allows you to store objects in memory between requests. This is useful for objects that are not yet ready to be persisted,
+ # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such
+ # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely
+ # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at.
+ #
+ # You can place objects in the session by using the <tt>@session</tt> hash:
+ #
+ # @session["person"] = Person.authenticate(user_name, password)
+ #
+ # And retrieved again through the same hash:
+ #
+ # Hello #{@session["person"]}
+ #
+ # Any object can be placed in the session (as long as it can be Marshalled). But remember that 1000 active sessions each storing a
+ # 50kb object could lead to a 50MB memory overhead. In other words, think carefully about size and caching before resorting to the use
+ # of the session.
+ #
+ # == Responses
+ #
+ # Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
+ # object is generated automatically through the use of renders and redirects, so it's normally nothing you'll need to be concerned about.
+ #
+ # == Renders
+ #
+ # Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
+ # of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured.
+ # The controller passes objects to the view by assigning instance variables:
+ #
+ # def show
+ # @post = Post.find(@params["id"])
+ # end
+ #
+ # Which are then automatically available to the view:
+ #
+ # Title: <%= @post.title %>
+ #
+ # You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use
+ # the manual rendering methods:
+ #
+ # def search
+ # @results = Search.find(@params["query"])
+ # case @results
+ # when 0 then render "weblog/no_results"
+ # when 1 then render_action "show"
+ # when 2..10 then render_action "show_many"
+ # end
+ # end
+ #
+ # Read more about writing ERb and Builder templates in link:classes/ActionView/Base.html.
+ #
+ # == Redirects
+ #
+ # Redirecting is what actions that update the model do when they're done. The <tt>save_post</tt> method shouldn't be responsible for also
+ # showing the post once it's saved -- that's the job for <tt>show_post</tt>. So once <tt>save_post</tt> has completed its business, it'll
+ # redirect to <tt>show_post</tt>. All redirects are external, which means that when the user refreshes his browser, it's not going to save
+ # the post again, but rather just show it one more time.
+ #
+ # This sounds fairly simple, but the redirection is complicated by the quest for a phenomenon known as "pretty urls". Instead of accepting
+ # the dreadful beings that is "weblog_controller?action=show&post_id=5", Action Controller goes out of its way to represent the former as
+ # "/weblog/show/5". And this is even the simple case. As an example of a more advanced pretty url consider
+ # "/library/books/ISBN/0743536703/show", which can be mapped to books_controller?action=show&type=ISBN&id=0743536703.
+ #
+ # Redirects work by rewriting the URL of the current action. So if the show action was called by "/library/books/ISBN/0743536703/show",
+ # we can redirect to an edit action simply by doing <tt>redirect_to(:action => "edit")</tt>, which could throw the user to
+ # "/library/books/ISBN/0743536703/edit". Naturally, you'll need to setup the .htaccess (or other means of URL rewriting for the web server)
+ # to point to the proper controller and action in the first place, but once you have, it can be rewritten with ease.
+ #
+ # Let's consider a bunch of examples on how to go from "/library/books/ISBN/0743536703/edit" to somewhere else:
+ #
+ # redirect_to(:action => "show", :action_prefix => "XTC/123") =>
+ # "http://www.singlefile.com/library/books/XTC/123/show"
+ #
+ # redirect_to(:path_params => {"type" => "EXBC"}) =>
+ # "http://www.singlefile.com/library/books/EXBC/0743536703/show"
+ #
+ # redirect_to(:controller => "settings") =>
+ # "http://www.singlefile.com/library/settings/"
+ #
+ # For more examples of redirecting options, have a look at the unit test in test/controller/url_test.rb. It's very readable and will give
+ # you an excellent understanding of the different options and what they do.
+ #
+ # == Environments
+ #
+ # Action Controller works out of the box with CGI, FastCGI, and mod_ruby. CGI and mod_ruby controllers are triggered just the same using:
+ #
+ # WeblogController.process_cgi
+ #
+ # FastCGI controllers are triggered using:
+ #
+ # FCGI.each_cgi{ |cgi| WeblogController.process_cgi(cgi) }
+ class Base
+ include ClassInheritableAttributes
+
+ DEFAULT_RENDER_STATUS_CODE = "200 OK"
+
+ DEFAULT_SEND_FILE_OPTIONS = {
+ :type => 'application/octet_stream',
+ :disposition => 'attachment',
+ :stream => true,
+ :buffer_size => 4096
+ }
+
+
+ # Determines whether the view has access to controller internals @request, @response, @session, and @template.
+ # By default, it does.
+ @@view_controller_internals = true
+ cattr_accessor :view_controller_internals
+
+ # All requests are considered local by default, so everyone will be exposed to detailed debugging screens on errors.
+ # When the application is ready to go public, this should be set to false, and the protected method <tt>local_request?</tt>
+ # should instead be implemented in the controller to determine when debugging screens should be shown.
+ @@consider_all_requests_local = true
+ cattr_accessor :consider_all_requests_local
+
+ # When turned on (which is default), all dependencies are included using "load". This mean that any change is instant in cached
+ # environments like mod_ruby or FastCGI. When set to false, "require" is used, which is faster but requires server restart to
+ # be effective.
+ @@reload_dependencies = true
+ cattr_accessor :reload_dependencies
+
+ # Template root determines the base from which template references will be made. So a call to render("test/template")
+ # will be converted to "#{template_root}/test/template.rhtml".
+ cattr_accessor :template_root
+
+ # The logger is used for generating information on the action run-time (including benchmarking) if available.
+ # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
+ cattr_accessor :logger
+
+ # Determines which template class should be used by ActionController.
+ cattr_accessor :template_class
+
+ # Turn on +ignore_missing_templates+ if you want to unit test actions without making the associated templates.
+ cattr_accessor :ignore_missing_templates
+
+ # Holds the request object that's primarily used to get environment variables through access like
+ # <tt>@request.env["REQUEST_URI"]</tt>.
+ attr_accessor :request
+
+ # Holds a hash of all the GET, POST, and Url parameters passed to the action. Accessed like <tt>@params["post_id"]</tt>
+ # to get the post_id. No type casts are made, so all values are returned as strings.
+ attr_accessor :params
+
+ # Holds the response object that's primarily used to set additional HTTP headers through access like
+ # <tt>@response.headers["Cache-Control"] = "no-cache"</tt>. Can also be used to access the final body HTML after a template
+ # has been rendered through @response.body -- useful for <tt>after_filter</tt>s that wants to manipulate the output,
+ # such as a OutputCompressionFilter.
+ attr_accessor :response
+
+ # Holds a hash of objects in the session. Accessed like <tt>@session["person"]</tt> to get the object tied to the "person"
+ # key. The session will hold any type of object as values, but the key should be a string.
+ attr_accessor :session
+
+ # Holds a hash of header names and values. Accessed like <tt>@headers["Cache-Control"]</tt> to get the value of the Cache-Control
+ # directive. Values should always be specified as strings.
+ attr_accessor :headers
+
+ # Holds a hash of cookie names and values. Accessed like <tt>@cookies["user_name"]</tt> to get the value of the user_name cookie.
+ # This hash is read-only. You set new cookies using the cookie method.
+ attr_accessor :cookies
+
+ # Holds the hash of variables that are passed on to the template class to be made available to the view. This hash
+ # is generated by taking a snapshot of all the instance variables in the current scope just before a template is rendered.
+ attr_accessor :assigns
+
+ class << self
+ # Factory for the standard create, process loop where the controller is discarded after processing.
+ def process(request, response) #:nodoc:
+ new.process(request, response)
+ end
+
+ # Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController".
+ def controller_class_name
+ Inflector.demodulize(name)
+ end
+
+ # Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat".
+ def controller_name
+ Inflector.underscore(controller_class_name.sub(/Controller/, ""))
+ end
+
+ # Loads the <tt>file_name</tt> if reload_dependencies is true or requires if it's false.
+ def require_or_load(file_name)
+ reload_dependencies ? load("#{file_name}.rb") : require(file_name)
+ end
+ end
+
+ public
+ # Extracts the action_name from the request parameters and performs that action.
+ def process(request, response) #:nodoc:
+ initialize_template_class(response)
+ assign_shortcuts(request, response)
+ initialize_current_url
+
+ log_processing unless logger.nil?
+ perform_action
+ close_session
+
+ return @response
+ end
+
+ # Returns an URL that has been rewritten according to the hash of +options+ (for doing a complete redirect, use redirect_to). The
+ # valid keys in options are specified below with an example going from "/library/books/ISBN/0743536703/show" (mapped to
+ # books_controller?action=show&type=ISBN&id=0743536703):
+ #
+ # .---> controller .--> action
+ # /library/books/ISBN/0743536703/show
+ # '------> '--------------> action_prefix
+ # controller_prefix
+ #
+ # * <tt>:controller_prefix</tt> - specifies the string before the controller name, which would be "/library" for the example.
+ # Called with "/shop" gives "/shop/books/ISBN/0743536703/show".
+ # * <tt>:controller</tt> - specifies a new controller and clears out everything after the controller name (including the action,
+ # the pre- and suffix, and all params), so called with "settings" gives "/library/settings/".
+ # * <tt>:action_prefix</tt> - specifies the string between the controller name and the action name, which would
+ # be "/ISBN/0743536703" for the example. Called with "/XTC/123/" gives "/library/books/XTC/123/show".
+ # * <tt>:action</tt> - specifies a new action, so called with "edit" gives "/library/books/ISBN/0743536703/edit"
+ # * <tt>:action_suffix</tt> - specifies the string after the action name, which would be empty for the example.
+ # Called with "/detailed" gives "/library/books/ISBN/0743536703/detailed".
+ # * <tt>:path_params</tt> - specifies a hash that contains keys mapping to the request parameter names. In the example,
+ # { "type" => "ISBN", "id" => "0743536703" } would be the path_params. It serves as another way of replacing part of
+ # the action_prefix or action_suffix. So passing { "type" => "XTC" } would give "/library/books/XTC/0743536703/show".
+ # * <tt>:id</tt> - shortcut where ":id => 5" can be used instead of specifying :path_params => { "id" => 5 }.
+ # Called with "123" gives "/library/books/ISBN/123/show".
+ # * <tt>:params</tt> - specifies a hash that represents the regular request parameters, such as { "cat" => 1,
+ # "origin" => "there"} that would give "?cat=1&origin=there". Called with { "temporary" => 1 } in the example would give
+ # "/library/books/ISBN/0743536703/show?temporary=1"
+ # * <tt>:anchor</tt> - specifies the anchor name to be appended to the path. Called with "x14" would give
+ # "/library/books/ISBN/0743536703/show#x14"
+ # * <tt>:only_path</tt> - if true, returns the absolute URL (omitting the protocol, host name, and port).
+ #
+ # Naturally, you can combine multiple options in a single redirect. Examples:
+ #
+ # redirect_to(:controller_prefix => "/shop", :controller => "settings")
+ # redirect_to(:action => "edit", :id => 3425)
+ # redirect_to(:action => "edit", :path_params => { "type" => "XTC" }, :params => { "temp" => 1})
+ # redirect_to(:action => "publish", :action_prefix => "/published", :anchor => "x14")
+ #
+ # Instead of passing an options hash, you can also pass a method reference in the form of a symbol. Consider this example:
+ #
+ # class WeblogController < ActionController::Base
+ # def update
+ # # do some update
+ # redirect_to :dashboard_url
+ # end
+ #
+ # protected
+ # def dashboard_url
+ # url_for :controller => (@project.active? ? "project" : "account"), :action => "dashboard"
+ # end
+ # end
+ def url_for(options = {}, *parameters_for_method_reference) #:doc:
+ case options
+ when String then options
+ when Symbol then send(options, *parameters_for_method_reference)
+ when Hash then @url.rewrite(rewrite_options(options))
+ end
+ end
+
+ def module_name
+ @params["module"]
+ end
+
+ # Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController".
+ def controller_class_name
+ self.class.controller_class_name
+ end
+
+ # Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat".
+ def controller_name
+ self.class.controller_name
+ end
+
+ # Returns the name of the action this controller is processing.
+ def action_name
+ @params["action"] || "index"
+ end
+
+ protected
+ # Renders the template specified by <tt>template_name</tt>, which defaults to the name of the current controller and action.
+ # So calling +render+ in WeblogController#show will attempt to render "#{template_root}/weblog/show.rhtml" or
+ # "#{template_root}/weblog/show.rxml" (in that order). The template_root is set on the ActionController::Base class and is
+ # shared by all controllers. It's also possible to pass a status code using the second parameter. This defaults to "200 OK",
+ # but can be changed, such as by calling <tt>render("weblog/error", "500 Error")</tt>.
+ def render(template_name = nil, status = nil) #:doc:
+ render_file(template_name || default_template_name, status, true)
+ end
+
+ # Works like render, but instead of requiring a full template name, you can get by with specifying the action name. So calling
+ # <tt>render_action "show_many"</tt> in WeblogController#display will render "#{template_root}/weblog/show_many.rhtml" or
+ # "#{template_root}/weblog/show_many.rxml".
+ def render_action(action_name, status = nil) #:doc:
+ render default_template_name(action_name), status
+ end
+
+ # Works like render, but disregards the template_root and requires a full path to the template that needs to be rendered. Can be
+ # used like <tt>render_file "/Users/david/Code/Ruby/template"</tt> to render "/Users/david/Code/Ruby/template.rhtml" or
+ # "/Users/david/Code/Ruby/template.rxml".
+ def render_file(template_path, status = nil, use_full_path = false) #:doc:
+ assert_existance_of_template_file(template_path) if use_full_path
+ logger.info("Rendering #{template_path} (#{status || DEFAULT_RENDER_STATUS_CODE})") unless logger.nil?
+
+ add_variables_to_assigns
+ render_text(@template.render_file(template_path, use_full_path), status)
+ end
+
+ # Renders the +template+ string, which is useful for rendering short templates you don't want to bother having a file for. So
+ # you'd call <tt>render_template "Hello, <%= @user.name %>"</tt> to greet the current user. Or if you want to render as Builder
+ # template, you could do <tt>render_template "xml.h1 @user.name", nil, "rxml"</tt>.
+ def render_template(template, status = nil, type = "rhtml") #:doc:
+ add_variables_to_assigns
+ render_text(@template.render_template(type, template), status)
+ end
+
+ # Renders the +text+ string without parsing it through any template engine. Useful for rendering static information as it's
+ # considerably faster than rendering through the template engine.
+ # Use block for response body if provided (useful for deferred rendering or streaming output).
+ def render_text(text = nil, status = nil, &block) #:doc:
+ add_variables_to_assigns
+ @response.headers["Status"] = status || DEFAULT_RENDER_STATUS_CODE
+ @response.body = block_given? ? block : text
+ @performed_render = true
+ end
+
+ # Sends the file by streaming it 4096 bytes at a time. This way the
+ # whole file doesn't need to be read into memory at once. This makes
+ # it feasible to send even large files.
+ #
+ # Be careful to sanitize the path parameter if it coming from a web
+ # page. send_file(@params['path']) allows a malicious user to
+ # download any file on your server.
+ #
+ # Options:
+ # * <tt>:filename</tt> - suggests a filename for the browser to use.
+ # Defaults to File.basename(path).
+ # * <tt>:type</tt> - specifies an HTTP content type.
+ # Defaults to 'application/octet-stream'.
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
+ # Valid values are 'inline' and 'attachment' (default).
+ # * <tt>:streaming</tt> - whether to send the file to the user agent as it is read (true)
+ # or to read the entire file before sending (false). Defaults to true.
+ # * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
+ # Defaults to 4096.
+ #
+ # The default Content-Type and Content-Disposition headers are
+ # set to download arbitrary binary files in as many browsers as
+ # possible. IE versions 4, 5, 5.5, and 6 are all known to have
+ # a variety of quirks (especially when downloading over SSL).
+ #
+ # Simple download:
+ # send_file '/path/to.zip'
+ #
+ # Show a JPEG in browser:
+ # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
+ #
+ # Read about the other Content-* HTTP headers if you'd like to
+ # provide the user with more information (such as Content-Description).
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
+ #
+ # Also be aware that the document may be cached by proxies and browsers.
+ # The Pragma and Cache-Control headers declare how the file may be cached
+ # by intermediaries. They default to require clients to validate with
+ # the server before releasing cached responses. See
+ # http://www.mnot.net/cache_docs/ for an overview of web caching and
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
+ # for the Cache-Control header spec.
+ def send_file(path, options = {})
+ raise MissingFile unless File.file?(path) and File.readable?(path)
+
+ options[:length] ||= File.size(path)
+ options[:filename] ||= File.basename(path)
+ send_file_headers! options
+
+ if options[:stream]
+ render_text do
+ logger.info "Streaming file #{path}" unless logger.nil?
+ len = options[:buffer_size] || 4096
+ File.open(path, 'rb') do |file|
+ begin
+ while true
+ $stdout.syswrite file.sysread(len)
+ end
+ rescue EOFError
+ end
+ end
+ end
+ else
+ logger.info "Sending file #{path}" unless logger.nil?
+ File.open(path, 'rb') { |file| render_text file.read }
+ end
+ end
+
+ # Send binary data to the user as a file download. May set content type, apparent file name,
+ # and specify whether to show data inline or download as an attachment.
+ #
+ # Options:
+ # * <tt>:filename</tt> - Suggests a filename for the browser to use.
+ # * <tt>:type</tt> - specifies an HTTP content type.
+ # Defaults to 'application/octet-stream'.
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
+ # Valid values are 'inline' and 'attachment' (default).
+ #
+ # Generic data download:
+ # send_data buffer
+ #
+ # Download a dynamically-generated tarball:
+ # send_data generate_tgz('dir'), :filename => 'dir.tgz'
+ #
+ # Display an image Active Record in the browser:
+ # send_data image.data, :type => image.content_type, :disposition => 'inline'
+ #
+ # See +send_file+ for more information on HTTP Content-* headers and caching.
+ def send_data(data, options = {})
+ logger.info "Sending data #{options[:filename]}" unless logger.nil?
+ send_file_headers! options.merge(:length => data.size)
+ render_text data
+ end
+
+ def rewrite_options(options)
+ if defaults = default_url_options(options)
+ defaults.merge(options)
+ else
+ options
+ end
+ end
+
+ # Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in
+ # the form of a hash, just like the one you would use for url_for directly. Example:
+ #
+ # def default_url_options(options)
+ # { :controller_prefix => @project.active? ? "projects/" : "accounts/" }
+ # end
+ #
+ # As you can infer from the example, this is mostly useful for situations where you want to centralize dynamic decisions about the
+ # urls as they stem from the business domain. Please note that any individual url_for call can always override the defaults set
+ # by this method.
+ def default_url_options(options) #:doc:
+ end
+
+ # Redirects the browser to an URL that has been rewritten according to the hash of +options+ using a "302 Moved" HTTP header.
+ # See url_for for a description of the valid options.
+ def redirect_to(options = {}, *parameters_for_method_reference) #:doc:
+ if parameters_for_method_reference.empty?
+ @response.redirected_to = options
+ redirect_to_url(url_for(options))
+ else
+ @response.redirected_to, @response.redirected_to_method_params = options, parameters_for_method_reference
+ redirect_to_url(url_for(options, *parameters_for_method_reference))
+ end
+ end
+
+ # Redirects the browser to the specified <tt>path</tt> within the current host (specified with a leading /). Used to sidestep
+ # the URL rewriting and go directly to a known path. Example: <tt>redirect_to_path "/images/screenshot.jpg"</tt>.
+ def redirect_to_path(path) #:doc:
+ redirect_to_url(@request.protocol + @request.host_with_port + path)
+ end
+
+ # Redirects the browser to the specified <tt>url</tt>. Used to redirect outside of the current application. Example:
+ # <tt>redirect_to_url "http://www.rubyonrails.org"</tt>.
+ def redirect_to_url(url) #:doc:
+ logger.info("Redirected to #{url}") unless logger.nil?
+ @response.redirect(url)
+ @performed_redirect = true
+ end
+
+ # Creates a new cookie that is sent along-side the next render or redirect command. API is the same as for CGI::Cookie.
+ # Examples:
+ #
+ # cookie("name", "value1", "value2", ...)
+ # cookie("name" => "name", "value" => "value")
+ # cookie('name' => 'name',
+ # 'value' => ['value1', 'value2', ...],
+ # 'path' => 'path', # optional
+ # 'domain' => 'domain', # optional
+ # 'expires' => Time.now, # optional
+ # 'secure' => true # optional
+ # )
+ def cookie(*options) #:doc:
+ @response.headers["cookie"] << CGI::Cookie.new(*options)
+ end
+
+ # Resets the session by clearsing out all the objects stored within and initializing a new session object.
+ def reset_session #:doc:
+ @request.reset_session
+ @session = @request.session
+ @response.session = @session
+ end
+
+ private
+ def initialize_template_class(response)
+ begin
+ response.template = template_class.new(template_root, {}, self)
+ rescue
+ raise "You must assign a template class through ActionController.template_class= before processing a request"
+ end
+
+ @performed_render = @performed_redirect = false
+ end
+
+ def assign_shortcuts(request, response)
+ @request, @params, @cookies = request, request.parameters, request.cookies
+
+ @response = response
+ @response.session = request.session
+
+ @session = @response.session
+ @template = @response.template
+ @assigns = @response.template.assigns
+ @headers = @response.headers
+ end
+
+ def initialize_current_url
+ @url = UrlRewriter.new(@request, controller_name, action_name)
+ end
+
+ def log_processing
+ logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin})"
+ logger.info " Parameters: #{@params.inspect}"
+ end
+
+ def perform_action
+ if action_methods.include?(action_name)
+ send(action_name)
+ render unless @performed_render || @performed_redirect
+ elsif template_exists? && template_public?
+ render
+ else
+ raise UnknownAction, "No action responded to #{action_name}", caller
+ end
+ end
+
+ def action_methods
+ action_controller_classes = self.class.ancestors.reject{ |a| [Object, Kernel].include?(a) }
+ action_controller_classes.inject([]) { |action_methods, klass| action_methods + klass.instance_methods(false) }
+ end
+
+ def add_variables_to_assigns
+ add_instance_variables_to_assigns
+ add_class_variables_to_assigns if view_controller_internals
+ end
+
+ def add_instance_variables_to_assigns
+ protected_variables_cache = protected_instance_variables
+ instance_variables.each do |var|
+ next if protected_variables_cache.include?(var)
+ @assigns[var[1..-1]] = instance_variable_get(var)
+ end
+ end
+
+ def add_class_variables_to_assigns
+ %w( template_root logger template_class ignore_missing_templates ).each do |cvar|
+ @assigns[cvar] = self.send(cvar)
+ end
+ end
+
+ def protected_instance_variables
+ if view_controller_internals
+ [ "@assigns", "@performed_redirect", "@performed_render" ]
+ else
+ [ "@assigns", "@performed_redirect", "@performed_render", "@request", "@response", "@session", "@cookies", "@template" ]
+ end
+ end
+
+ def request_origin
+ "#{@request.remote_ip} at #{Time.now.to_s}"
+ end
+
+ def close_session
+ @session.close unless @session.nil? || Hash === @session
+ end
+
+ def template_exists?(template_name = default_template_name)
+ @template.file_exists?(template_name)
+ end
+
+ def template_public?(template_name = default_template_name)
+ @template.file_public?(template_name)
+ end
+
+ def assert_existance_of_template_file(template_name)
+ unless template_exists?(template_name) || ignore_missing_templates
+ full_template_path = @template.send(:full_template_path, template_name, 'rhtml')
+ template_type = (template_name =~ /layouts/i) ? 'layout' : 'template'
+ raise(MissingTemplate, "Missing #{template_type} #{full_template_path}")
+ end
+ end
+
+ def send_file_headers!(options)
+ options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options))
+ [:length, :type, :disposition].each do |arg|
+ raise ArgumentError, ":#{arg} option required" if options[arg].nil?
+ end
+
+ disposition = options[:disposition] || 'attachment'
+ disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
+
+ @headers.update(
+ 'Content-Length' => options[:length],
+ 'Content-Type' => options[:type],
+ 'Content-Disposition' => disposition,
+ 'Content-Transfer-Encoding' => 'binary'
+ );
+ end
+
+ def default_template_name(default_action_name = action_name)
+ module_name ? "#{module_name}/#{controller_name}/#{default_action_name}" : "#{controller_name}/#{default_action_name}"
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/benchmarking.rb b/actionpack/lib/action_controller/benchmarking.rb
new file mode 100644
index 0000000000..e6ff65e150
--- /dev/null
+++ b/actionpack/lib/action_controller/benchmarking.rb
@@ -0,0 +1,49 @@
+require 'benchmark'
+
+module ActionController #:nodoc:
+ # The benchmarking module times the performance of actions and reports to the logger. If the Active Record
+ # package has been included, a separate timing section for database calls will be added as well.
+ module Benchmarking #:nodoc:
+ def self.append_features(base)
+ super
+ base.class_eval {
+ alias_method :perform_action_without_benchmark, :perform_action
+ alias_method :perform_action, :perform_action_with_benchmark
+
+ alias_method :render_without_benchmark, :render
+ alias_method :render, :render_with_benchmark
+ }
+ end
+
+ def render_with_benchmark(template_name = default_template_name, status = "200 OK")
+ if logger.nil?
+ render_without_benchmark(template_name, status)
+ else
+ @rendering_runtime = Benchmark::measure{ render_without_benchmark(template_name, status) }.real
+ end
+ end
+
+ def perform_action_with_benchmark
+ if logger.nil?
+ perform_action_without_benchmark
+ else
+ runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max
+ log_message = "Completed in #{sprintf("%4f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
+ log_message << rendering_runtime(runtime) if @rendering_runtime
+ log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
+ logger.info(log_message)
+ end
+ end
+
+ private
+ def rendering_runtime(runtime)
+ " | Rendering: #{sprintf("%f", @rendering_runtime)} (#{sprintf("%d", (@rendering_runtime / runtime) * 100)}%)"
+ end
+
+ def active_record_runtime(runtime)
+ db_runtime = ActiveRecord::Base.connection.reset_runtime
+ db_percentage = (db_runtime / runtime) * 100
+ " | DB: #{sprintf("%f", db_runtime)} (#{sprintf("%d", db_percentage)}%)"
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/cgi_ext/cgi_ext.rb b/actionpack/lib/action_controller/cgi_ext/cgi_ext.rb
new file mode 100755
index 0000000000..371ead695b
--- /dev/null
+++ b/actionpack/lib/action_controller/cgi_ext/cgi_ext.rb
@@ -0,0 +1,43 @@
+require 'cgi'
+require 'cgi/session'
+require 'cgi/session/pstore'
+require 'action_controller/cgi_ext/cgi_methods'
+
+# Wrapper around the CGIMethods that have been secluded to allow testing without
+# an instatiated CGI object
+class CGI #:nodoc:
+ class << self
+ alias :escapeHTML_fail_on_nil :escapeHTML
+
+ def escapeHTML(string)
+ escapeHTML_fail_on_nil(string) unless string.nil?
+ end
+ end
+
+ # Returns a parameter hash including values from both the request (POST/GET)
+ # and the query string with the latter taking precedence.
+ def parameters
+ request_parameters.update(query_parameters)
+ end
+
+ def query_parameters
+ CGIMethods.parse_query_parameters(query_string)
+ end
+
+ def request_parameters
+ CGIMethods.parse_request_parameters(params)
+ end
+
+ def redirect(where)
+ header({
+ "Status" => "302 Moved",
+ "location" => "#{where}"
+ })
+ end
+
+ def session(parameters = nil)
+ parameters = {} if parameters.nil?
+ parameters['database_manager'] = CGI::Session::PStore
+ CGI::Session.new(self, parameters)
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb
new file mode 100755
index 0000000000..261490580c
--- /dev/null
+++ b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb
@@ -0,0 +1,91 @@
+require 'cgi'
+
+# Static methods for parsing the query and request parameters that can be used in
+# a CGI extension class or testing in isolation.
+class CGIMethods #:nodoc:
+ public
+ # Returns a hash with the pairs from the query string. The implicit hash construction that is done in
+ # parse_request_params is not done here.
+ def CGIMethods.parse_query_parameters(query_string)
+ parsed_params = {}
+
+ query_string.split(/[&;]/).each { |p|
+ k, v = p.split('=')
+
+ k = CGI.unescape(k) unless k.nil?
+ v = CGI.unescape(v) unless v.nil?
+
+ if k =~ /(.*)\[\]$/
+ if parsed_params.has_key? $1
+ parsed_params[$1] << v
+ else
+ parsed_params[$1] = [v]
+ end
+ else
+ parsed_params[k] = v.nil? ? nil : v
+ end
+ }
+
+ return parsed_params
+ end
+
+ # Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" /
+ # "Somewhere cool!" are translated into a full hash hierarchy, like
+ # { "customer" => { "address" => { "street" => "Somewhere cool!" } } }
+ def CGIMethods.parse_request_parameters(params)
+ parsed_params = {}
+
+ for key, value in params
+ value = [value] if key =~ /.*\[\]$/
+ CGIMethods.build_deep_hash(
+ CGIMethods.get_typed_value(value[0]),
+ parsed_params,
+ CGIMethods.get_levels(key)
+ )
+ end
+
+ return parsed_params
+ end
+
+ private
+ def CGIMethods.get_typed_value(value)
+ if value.respond_to?(:content_type) && !value.content_type.empty?
+ # Uploaded file
+ value
+ elsif value.respond_to?(:read)
+ # Value as part of a multipart request
+ value.read
+ elsif value.class == Array
+ value
+ else
+ # Standard value (not a multipart request)
+ value.to_s
+ end
+ end
+
+ def CGIMethods.get_levels(key_string)
+ return [] if key_string.nil? or key_string.empty?
+
+ levels = []
+ main, existance = /(\w+)(\[)?.?/.match(key_string).captures
+ levels << main
+
+ unless existance.nil?
+ hash_part = key_string.sub(/\w+\[/, "")
+ hash_part.slice!(-1, 1)
+ levels += hash_part.split(/\]\[/)
+ end
+
+ levels
+ end
+
+ def CGIMethods.build_deep_hash(value, hash, levels)
+ if levels.length == 0
+ value;
+ elsif hash.nil?
+ { levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) }
+ else
+ hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) })
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/cgi_process.rb b/actionpack/lib/action_controller/cgi_process.rb
new file mode 100644
index 0000000000..e69a0b2f8d
--- /dev/null
+++ b/actionpack/lib/action_controller/cgi_process.rb
@@ -0,0 +1,124 @@
+require 'action_controller/cgi_ext/cgi_ext'
+require 'action_controller/support/cookie_performance_fix'
+require 'action_controller/session/drb_store'
+require 'action_controller/session/active_record_store'
+
+module ActionController #:nodoc:
+ class Base
+ # Process a request extracted from an CGI object and return a response. Pass false as <tt>session_options</tt> to disable
+ # sessions (large performance increase if sessions are not needed). The <tt>session_options</tt> are the same as for CGI::Session:
+ #
+ # * <tt>:database_manager</tt> - standard options are CGI::Session::FileStore, CGI::Session::MemoryStore, and CGI::Session::PStore
+ # (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
+ # lib/action_controller/session.
+ # * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'.
+ # * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ parameter
+ # of the request, or automatically generated for a new session.
+ # * <tt>:new_session</tt> - if true, force creation of a new session. If not set, a new session is only created if none currently
+ # exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set,
+ # an ArgumentError is raised.
+ # * <tt>:session_expires</tt> - the time the current session expires, as a +Time+ object. If not set, the session will continue
+ # indefinitely.
+ # * <tt>:session_domain</tt> - the hostname domain for which this session is valid. If not set, defaults to the hostname of the
+ # server.
+ # * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS.
+ # * <tt>:session_path</tt> - the path for which this session applies. Defaults to the directory of the CGI script.
+ def self.process_cgi(cgi = CGI.new, session_options = {})
+ new.process_cgi(cgi, session_options)
+ end
+
+ def process_cgi(cgi, session_options = {}) #:nodoc:
+ process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out
+ end
+ end
+
+ class CgiRequest < AbstractRequest #:nodoc:
+ attr_accessor :cgi
+
+ DEFAULT_SESSION_OPTIONS =
+ { "database_manager" => CGI::Session::PStore, "prefix" => "ruby_sess.", "session_path" => "/" }
+
+ def initialize(cgi, session_options = {})
+ @cgi = cgi
+ @session_options = session_options
+ super()
+ end
+
+ def query_parameters
+ @cgi.query_string ? CGIMethods.parse_query_parameters(@cgi.query_string) : {}
+ end
+
+ def request_parameters
+ CGIMethods.parse_request_parameters(@cgi.params)
+ end
+
+ def env
+ @cgi.send(:env_table)
+ end
+
+ def cookies
+ @cgi.cookies.freeze
+ end
+
+ def host
+ env["HTTP_X_FORWARDED_HOST"] || @cgi.host.split(":").first
+ end
+
+ def session
+ return @session unless @session.nil?
+ begin
+ @session = (@session_options == false ? {} : CGI::Session.new(@cgi, DEFAULT_SESSION_OPTIONS.merge(@session_options)))
+ @session["__valid_session"]
+ return @session
+ rescue ArgumentError => e
+ @session.delete if @session
+ raise(
+ ActionController::SessionRestoreError,
+ "Session contained objects where the class definition wasn't available. " +
+ "Remember to require classes for all objects kept in the session. " +
+ "The session has been deleted."
+ )
+ end
+ end
+
+ def reset_session
+ @session.delete
+ @session = (@session_options == false ? {} : new_session)
+ end
+
+ def method_missing(method_id, *arguments)
+ @cgi.send(method_id, *arguments) rescue super
+ end
+
+ private
+ def new_session
+ CGI::Session.new(@cgi, DEFAULT_SESSION_OPTIONS.merge(@session_options).merge("new_session" => true))
+ end
+ end
+
+ class CgiResponse < AbstractResponse #:nodoc:
+ def initialize(cgi)
+ @cgi = cgi
+ super()
+ end
+
+ def out
+ convert_content_type!(@headers)
+ $stdout.binmode if $stdout.respond_to?(:binmode)
+ print @cgi.header(@headers)
+ if @body.respond_to?(:call)
+ @body.call(self)
+ else
+ print @body
+ end
+ end
+
+ private
+ def convert_content_type!(headers)
+ if headers["Content-Type"]
+ headers["type"] = headers["Content-Type"]
+ headers.delete "Content-Type"
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/dependencies.rb b/actionpack/lib/action_controller/dependencies.rb
new file mode 100644
index 0000000000..6f092500d1
--- /dev/null
+++ b/actionpack/lib/action_controller/dependencies.rb
@@ -0,0 +1,49 @@
+module ActionController #:nodoc:
+ module Dependencies #:nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def model(*models)
+ require_dependencies(:model, models)
+ depend_on(:model, models)
+ end
+
+ def service(*services)
+ require_dependencies(:service, services)
+ depend_on(:service, services)
+ end
+
+ def observer(*observers)
+ require_dependencies(:observer, observers)
+ depend_on(:observer, observers)
+ instantiate_observers(observers)
+ end
+
+ def dependencies_on(layer) # :nodoc:
+ read_inheritable_attribute("#{layer}_dependencies")
+ end
+
+ def depend_on(layer, dependencies)
+ write_inheritable_array("#{layer}_dependencies", dependencies)
+ end
+
+ private
+ def instantiate_observers(observers)
+ observers.flatten.each { |observer| Object.const_get(Inflector.classify(observer.to_s)).instance }
+ end
+
+ def require_dependencies(layer, dependencies)
+ dependencies.flatten.each do |dependency|
+ begin
+ require_or_load(dependency.to_s)
+ rescue LoadError
+ raise LoadError, "Missing #{layer} #{dependency}.rb"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/filters.rb b/actionpack/lib/action_controller/filters.rb
new file mode 100644
index 0000000000..bd5c545dfb
--- /dev/null
+++ b/actionpack/lib/action_controller/filters.rb
@@ -0,0 +1,279 @@
+module ActionController #:nodoc:
+ module Filters #:nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ base.class_eval { include ActionController::Filters::InstanceMethods }
+ end
+
+ # Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
+ # authentication, caching, or auditing before the intended action is performed. Or to do localization or output
+ # compression after the action has been performed.
+ #
+ # Filters have access to the request, response, and all the instance variables set by other filters in the chain
+ # or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <tt>before_filter</tt>
+ # to halt the processing before the intended action is processed by returning false. This is especially useful for
+ # filters like authentication where you're not interested in allowing the action to be performed if the proper
+ # credentials are not in order.
+ #
+ # == Filter inheritance
+ #
+ # Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without
+ # affecting the superclass. For example:
+ #
+ # class BankController < ActionController::Base
+ # before_filter :audit
+ #
+ # private
+ # def audit
+ # # record the action and parameters in an audit log
+ # end
+ # end
+ #
+ # class VaultController < BankController
+ # before_filter :verify_credentials
+ #
+ # private
+ # def verify_credentials
+ # # make sure the user is allowed into the vault
+ # end
+ # end
+ #
+ # Now any actions performed on the BankController will have the audit method called before. On the VaultController,
+ # first the audit method is called, then the verify_credentials method. If the audit method returns false, then
+ # verify_credentials and the intended action is never called.
+ #
+ # == Filter types
+ #
+ # A filter can take one of three forms: method reference (symbol), external class, or inline method (proc). The first
+ # is the most common and works by referencing a protected or private method somewhere in the inheritance hierarchy of
+ # the controller by use of a symbol. In the bank example above, both BankController and VaultController use this form.
+ #
+ # Using an external class makes for more easily reused generic filters, such as output compression. External filter classes
+ # are implemented by having a static +filter+ method on any class and then passing this class to the filter method. Example:
+ #
+ # class OutputCompressionFilter
+ # def self.filter(controller)
+ # controller.response.body = compress(controller.response.body)
+ # end
+ # end
+ #
+ # class NewspaperController < ActionController::Base
+ # after_filter OutputCompressionFilter
+ # end
+ #
+ # The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can
+ # manipulate them as it sees fit.
+ #
+ # The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation.
+ # Or just as a quick test. It works like this:
+ #
+ # class WeblogController < ActionController::Base
+ # before_filter { |controller| return false if controller.params["stop_action"] }
+ # end
+ #
+ # As you can see, the block expects to be passed the controller after it has assigned the request to the internal variables.
+ # This means that the block has access to both the request and response objects complete with convenience methods for params,
+ # session, template, and assigns. Note: The inline method doesn't strictly has to be a block. Any object that responds to call
+ # and returns 1 or -1 on arity will do (such as a Proc or an Method object).
+ #
+ # == Filter chain ordering
+ #
+ # Using <tt>before_filter</tt> and <tt>after_filter</tt> appends the specified filters to the existing chain. That's usually
+ # just fine, but some times you care more about the order in which the filters are executed. When that's the case, you
+ # can use <tt>prepend_before_filter</tt> and <tt>prepend_after_filter</tt>. Filters added by these methods will be put at the
+ # beginning of their respective chain and executed before the rest. For example:
+ #
+ # class ShoppingController
+ # before_filter :verify_open_shop
+ #
+ # class CheckoutController
+ # prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock
+ #
+ # The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt>
+ # <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop
+ # is open or not.
+ #
+ # You may pass multiple filter arguments of each type as well as a filter block.
+ # If a block is given, it is treated as the last argument.
+ #
+ # == Around filters
+ #
+ # In addition to the individual before and after filters, it's also possible to specify that a single object should handle
+ # both the before and after call. That's especially usefuly when you need to keep state active between the before and after,
+ # such as the example of a benchmark filter below:
+ #
+ # class WeblogController < ActionController::Base
+ # around_filter BenchmarkingFilter.new
+ #
+ # # Before this action is performed, BenchmarkingFilter#before(controller) is executed
+ # def index
+ # end
+ # # After this action has been performed, BenchmarkingFilter#after(controller) is executed
+ # end
+ #
+ # class BenchmarkingFilter
+ # def initialize
+ # @runtime
+ # end
+ #
+ # def before
+ # start_timer
+ # end
+ #
+ # def after
+ # stop_timer
+ # report_result
+ # end
+ # end
+ module ClassMethods
+ # The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions
+ # on this controller are performed.
+ def append_before_filter(*filters, &block)
+ filters << block if block_given?
+ append_filter_to_chain("before", filters)
+ end
+
+ # The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions
+ # on this controller are performed.
+ def prepend_before_filter(*filters, &block)
+ filters << block if block_given?
+ prepend_filter_to_chain("before", filters)
+ end
+
+ # Short-hand for append_before_filter since that's the most common of the two.
+ alias :before_filter :append_before_filter
+
+ # The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions
+ # on this controller are performed.
+ def append_after_filter(*filters, &block)
+ filters << block if block_given?
+ append_filter_to_chain("after", filters)
+ end
+
+ # The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions
+ # on this controller are performed.
+ def prepend_after_filter(*filters, &block)
+ filters << block if block_given?
+ prepend_filter_to_chain("after", filters)
+ end
+
+ # Short-hand for append_after_filter since that's the most common of the two.
+ alias :after_filter :append_after_filter
+
+ # The passed <tt>filters</tt> will have their +before+ method appended to the array of filters that's run both before actions
+ # on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all
+ # respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like:
+ #
+ # B#before
+ # A#before
+ # A#after
+ # B#after
+ def append_around_filter(filters)
+ for filter in [filters].flatten
+ ensure_filter_responds_to_before_and_after(filter)
+ append_before_filter { |c| filter.before(c) }
+ prepend_after_filter { |c| filter.after(c) }
+ end
+ end
+
+ # The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions
+ # on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all
+ # respond to both +before+ and +after+. So if you do prepend_around_filter A.new, B.new, the callstack will look like:
+ #
+ # A#before
+ # B#before
+ # B#after
+ # A#after
+ def prepend_around_filter(filters)
+ for filter in [filters].flatten
+ ensure_filter_responds_to_before_and_after(filter)
+ prepend_before_filter { |c| filter.before(c) }
+ append_after_filter { |c| filter.after(c) }
+ end
+ end
+
+ # Short-hand for append_around_filter since that's the most common of the two.
+ alias :around_filter :append_around_filter
+
+ # Returns all the before filters for this class and all its ancestors.
+ def before_filters #:nodoc:
+ read_inheritable_attribute("before_filters")
+ end
+
+ # Returns all the after filters for this class and all its ancestors.
+ def after_filters #:nodoc:
+ read_inheritable_attribute("after_filters")
+ end
+
+ private
+ def append_filter_to_chain(condition, filters)
+ write_inheritable_array("#{condition}_filters", filters)
+ end
+
+ def prepend_filter_to_chain(condition, filters)
+ write_inheritable_attribute("#{condition}_filters", filters + read_inheritable_attribute("#{condition}_filters"))
+ end
+
+ def ensure_filter_responds_to_before_and_after(filter)
+ unless filter.respond_to?(:before) && filter.respond_to?(:after)
+ raise ActionControllerError, "Filter object must respond to both before and after"
+ end
+ end
+ end
+
+ module InstanceMethods # :nodoc:
+ def self.append_features(base)
+ super
+ base.class_eval {
+ alias_method :perform_action_without_filters, :perform_action
+ alias_method :perform_action, :perform_action_with_filters
+ }
+ end
+
+ def perform_action_with_filters
+ return if before_action == false
+ perform_action_without_filters
+ after_action
+ end
+
+ # Calls all the defined before-filter filters, which are added by using "before_filter :method".
+ # If any of the filters return false, no more filters will be executed and the action is aborted.
+ def before_action #:doc:
+ call_filters(self.class.before_filters)
+ end
+
+ # Calls all the defined after-filter filters, which are added by using "after_filter :method".
+ # If any of the filters return false, no more filters will be executed.
+ def after_action #:doc:
+ call_filters(self.class.after_filters)
+ end
+
+ private
+ def call_filters(filters)
+ filters.each do |filter|
+ if Symbol === filter
+ if self.send(filter) == false then return false end
+ elsif filter_block?(filter)
+ if filter.call(self) == false then return false end
+ elsif filter_class?(filter)
+ if filter.filter(self) == false then return false end
+ else
+ raise(
+ ActionControllerError,
+ "Filters need to be either a symbol, proc/method, or class implementing a static filter method"
+ )
+ end
+ end
+ end
+
+ def filter_block?(filter)
+ filter.respond_to?("call") && (filter.arity == 1 || filter.arity == -1)
+ end
+
+ def filter_class?(filter)
+ filter.respond_to?("filter")
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/flash.rb b/actionpack/lib/action_controller/flash.rb
new file mode 100644
index 0000000000..220ed8c77a
--- /dev/null
+++ b/actionpack/lib/action_controller/flash.rb
@@ -0,0 +1,65 @@
+module ActionController #:nodoc:
+ # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
+ # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action
+ # that sets <tt>flash["notice"] = "Succesfully created"</tt> before redirecting to a display action that can then expose
+ # the flash to its template. Actually, that exposure is automatically done. Example:
+ #
+ # class WeblogController < ActionController::Base
+ # def create
+ # # save post
+ # flash["notice"] = "Succesfully created post"
+ # redirect_to :action => "display", :params => { "id" => post.id }
+ # end
+ #
+ # def display
+ # # doesn't need to assign the flash notice to the template, that's done automatically
+ # end
+ # end
+ #
+ # display.rhtml
+ # <% if @flash["notice"] %><div class="notice"><%= @flash["notice"] %></div><% end %>
+ #
+ # This example just places a string in the flash, but you can put any object in there. And of course, you can put as many
+ # as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
+ module Flash
+ def self.append_features(base) #:nodoc:
+ super
+ base.before_filter(:fire_flash)
+ base.after_filter(:clear_flash)
+ end
+
+ protected
+ # Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or
+ # <tt>flash["notice"] = "hello"</tt> to put a new one.
+ def flash #:doc:
+ if @session["flash"].nil?
+ @session["flash"] = {}
+ @session["flashes"] ||= 0
+ end
+ @session["flash"]
+ end
+
+ # Can be called by any action that would like to keep the current content of the flash around for one more action.
+ def keep_flash #:doc:
+ @session["flashes"] = 0
+ end
+
+ private
+ # Records that the contents of @session["flash"] was flashed to the action
+ def fire_flash
+ if @session["flash"]
+ @session["flashes"] += 1 unless @session["flash"].empty?
+ @assigns["flash"] = @session["flash"]
+ else
+ @assigns["flash"] = {}
+ end
+ end
+
+ def clear_flash
+ if @session["flash"] && (@session["flashes"].nil? || @session["flashes"] >= 1)
+ @session["flash"] = {}
+ @session["flashes"] = 0
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/helpers.rb b/actionpack/lib/action_controller/helpers.rb
new file mode 100644
index 0000000000..9c88582288
--- /dev/null
+++ b/actionpack/lib/action_controller/helpers.rb
@@ -0,0 +1,100 @@
+module ActionController #:nodoc:
+ module Helpers #:nodoc:
+ def self.append_features(base)
+ super
+ base.class_eval { class << self; alias_method :inherited_without_helper, :inherited; end }
+ base.extend(ClassMethods)
+ end
+
+ # The template helpers serves to relieve the templates from including the same inline code again and again. It's a
+ # set of standardized methods for working with forms (FormHelper), dates (DateHelper), texts (TextHelper), and
+ # Active Records (ActiveRecordHelper) that's available to all templates by default.
+ #
+ # It's also really easy to make your own helpers and it's much encouraged to keep the template files free
+ # from complicated logic. It's even encouraged to bundle common compositions of methods from other helpers
+ # (often the common helpers) as they're used by the specific application.
+ #
+ # module MyHelper
+ # def hello_world() "hello world" end
+ # end
+ #
+ # MyHelper can now be included in a controller, like this:
+ #
+ # class MyController < ActionController::Base
+ # helper :my_helper
+ # end
+ #
+ # ...and, same as above, used in any template rendered from MyController, like this:
+ #
+ # Let's hear what the helper has to say: <tt><%= hello_world %></tt>
+ module ClassMethods
+ # Makes all the (instance) methods in the helper module available to templates rendered through this controller.
+ # See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules
+ # available to the templates.
+ def add_template_helper(helper_module)
+ template_class.class_eval "include #{helper_module}"
+ end
+
+ # Declare a helper. If you use this method in your controller, you don't
+ # have to do the +self.append_features+ incantation in your helper class.
+ # helper :foo
+ # requires 'foo_helper' and includes FooHelper in the template class.
+ # helper FooHelper
+ # includes FooHelper in the template class.
+ # helper { def foo() "#{bar} is the very best" end }
+ # evaluates the block in the template class, adding method #foo.
+ # helper(:three, BlindHelper) { def mice() 'mice' end }
+ # does all three.
+ def helper(*args, &block)
+ args.flatten.each do |arg|
+ case arg
+ when Module
+ add_template_helper(arg)
+ when String, Symbol
+ file_name = Inflector.underscore(arg.to_s.downcase) + '_helper'
+ class_name = Inflector.camelize(file_name)
+ begin
+ require_or_load(file_name)
+ rescue LoadError
+ raise LoadError, "Missing helper file helpers/#{file_name}.rb"
+ end
+ raise ArgumentError, "Missing #{class_name} module in helpers/#{file_name}.rb" unless Object.const_defined?(class_name)
+ add_template_helper(Object.const_get(class_name))
+ else
+ raise ArgumentError, 'helper expects String, Symbol, or Module argument'
+ end
+ end
+
+ # Evaluate block in template class if given.
+ template_class.module_eval(&block) if block_given?
+ end
+
+ # Declare a controller method as a helper. For example,
+ # helper_method :link_to
+ # def link_to(name, options) ... end
+ # makes the link_to controller method available in the view.
+ def helper_method(*methods)
+ template_class.controller_delegate(*methods)
+ end
+
+ # Declare a controller attribute as a helper. For example,
+ # helper_attr :name
+ # attr_accessor :name
+ # makes the name and name= controller methods available in the view.
+ # The is a convenience wrapper for helper_method.
+ def helper_attr(*attrs)
+ attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
+ end
+
+ private
+ def inherited(child)
+ inherited_without_helper(child)
+ begin
+ child.helper(child.controller_name)
+ rescue LoadError
+ # No default helper available for this controller
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/layout.rb b/actionpack/lib/action_controller/layout.rb
new file mode 100644
index 0000000000..7ae25ddabd
--- /dev/null
+++ b/actionpack/lib/action_controller/layout.rb
@@ -0,0 +1,149 @@
+module ActionController #:nodoc:
+ module Layout #:nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ base.class_eval do
+ alias_method :render_without_layout, :render
+ alias_method :render, :render_with_layout
+ end
+ end
+
+ # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
+ # repeated setups. The inclusion pattern has pages that look like this:
+ #
+ # <%= render "shared/header" %>
+ # Hello World
+ # <%= render "shared/footer" %>
+ #
+ # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
+ # and if you ever want to change the structure of these two includes, you'll have to change all the templates.
+ #
+ # With layouts, you can flip it around and have the common structure know where to insert changing content. This means
+ # that the header and footer is only mentioned in one place, like this:
+ #
+ # <!-- The header part of this layout -->
+ # <%= @content_for_layout %>
+ # <!-- The footer part of this layout -->
+ #
+ # And then you have content pages that look like this:
+ #
+ # hello world
+ #
+ # Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout,
+ # like this:
+ #
+ # <!-- The header part of this layout -->
+ # hello world
+ # <!-- The footer part of this layout -->
+ #
+ # == Accessing shared variables
+ #
+ # Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
+ # references that won't materialize before rendering time:
+ #
+ # <h1><%= @page_title %></h1>
+ # <%= @content_for_layout %>
+ #
+ # ...and content pages that fulfill these references _at_ rendering time:
+ #
+ # <% @page_title = "Welcome" %>
+ # Off-world colonies offers you a chance to start a new life
+ #
+ # The result after rendering is:
+ #
+ # <h1>Welcome</h1>
+ # Off-world colonies offers you a chance to start a new life
+ #
+ # == Inheritance for layouts
+ #
+ # Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
+ #
+ # class BankController < ActionController::Base
+ # layout "layouts/bank_standard"
+ #
+ # class InformationController < BankController
+ #
+ # class VaultController < BankController
+ # layout :access_level_layout
+ #
+ # class EmployeeController < BankController
+ # layout nil
+ #
+ # The InformationController uses "layouts/bank_standard" inherited from the BankController, the VaultController overwrites
+ # and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all.
+ #
+ # == Types of layouts
+ #
+ # Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
+ # you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
+ # be done either by specifying a method reference as a symbol or using an inline method (as a proc).
+ #
+ # The method reference is the preferred approach to variable layouts and is used like this:
+ #
+ # class WeblogController < ActionController::Base
+ # layout :writers_and_readers
+ #
+ # def index
+ # # fetching posts
+ # end
+ #
+ # private
+ # def writers_and_readers
+ # logged_in? ? "writer_layout" : "reader_layout"
+ # end
+ #
+ # Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
+ # is logged in or not.
+ #
+ # If you want to use an inline method, such as a proc, do something like this:
+ #
+ # class WeblogController < ActionController::Base
+ # layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
+ #
+ # Of course, the most common way of specifying a layout is still just as a plain template path:
+ #
+ # class WeblogController < ActionController::Base
+ # layout "layouts/weblog_standard"
+ #
+ # == Avoiding the use of a layout
+ #
+ # If you have a layout that by default is applied to all the actions of a controller, you still have the option to rendering
+ # a given action without a layout. Just use the method <tt>render_without_layout</tt>, which works just like Base.render --
+ # it just doesn't apply any layouts.
+ module ClassMethods
+ # If a layout is specified, all actions rendered through render and render_action will have their result assigned
+ # to <tt>@content_for_layout</tt>, which can then be used by the layout to insert their contents with
+ # <tt><%= @content_for_layout %></tt>. This layout can itself depend on instance variables assigned during action
+ # performance and have access to them as any normal template would.
+ def layout(template_name)
+ write_inheritable_attribute "layout", template_name
+ end
+ end
+
+ # Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method
+ # is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method
+ # object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return
+ # weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard.
+ def active_layout(passed_layout = nil)
+ layout = passed_layout || self.class.read_inheritable_attribute("layout")
+ active_layout = case layout
+ when Symbol then send(layout)
+ when Proc then layout.call(self)
+ when String then layout
+ end
+ active_layout.include?("/") ? active_layout : "layouts/#{active_layout}" if active_layout
+ end
+
+ def render_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc:
+ if layout ||= active_layout
+ add_variables_to_assigns
+ logger.info("Rendering #{template_name} within #{layout}") unless logger.nil?
+ @content_for_layout = @template.render_file(template_name, true)
+ render_without_layout(layout, status)
+ else
+ render_without_layout(template_name, status)
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb
new file mode 100755
index 0000000000..1085066ea0
--- /dev/null
+++ b/actionpack/lib/action_controller/request.rb
@@ -0,0 +1,99 @@
+module ActionController
+ # These methods are available in both the production and test Request objects.
+ class AbstractRequest
+ # Returns both GET and POST parameters in a single hash.
+ def parameters
+ @parameters ||= request_parameters.update(query_parameters)
+ end
+
+ def method
+ env['REQUEST_METHOD'].downcase.intern
+ end
+
+ def get?
+ method == :get
+ end
+
+ def post?
+ method == :post
+ end
+
+ def put?
+ method == :put
+ end
+
+ def delete?
+ method == :delete
+ end
+
+ # Determine originating IP address. REMOTE_ADDR is the standard
+ # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
+ # HTTP_X_FORWARDED_FOR are set by proxies so check for these before
+ # falling back to REMOTE_ADDR. HTTP_X_FORWARDED_FOR may be a comma-
+ # delimited list in the case of multiple chained proxies; the first is
+ # the originating IP.
+ def remote_ip
+ if env['HTTP_CLIENT_IP']
+ env['HTTP_CLIENT_IP']
+ elsif env['HTTP_X_FORWARDED_FOR']
+ remote_ip = env['HTTP_X_FORWARDED_FOR'].split(',').reject { |ip|
+ ip =~ /^unknown$|^(10|172\.16|192\.168)\./i
+ }.first
+
+ remote_ip ? remote_ip.strip : env['REMOTE_ADDR']
+ else
+ env['REMOTE_ADDR']
+ end
+ end
+
+ def request_uri
+ env["REQUEST_URI"]
+ end
+
+ def protocol
+ port == 443 ? "https://" : "http://"
+ end
+
+ def path
+ request_uri ? request_uri.split("?").first : ""
+ end
+
+ def port
+ env["SERVER_PORT"].to_i
+ end
+
+ def host_with_port
+ if env['HTTP_HOST']
+ env['HTTP_HOST']
+ elsif (protocol == "http://" && port == 80) || (protocol == "https://" && port == 443)
+ host
+ else
+ host + ":#{port}"
+ end
+ end
+
+ #--
+ # Must be implemented in the concrete request
+ #++
+ def query_parameters
+ end
+
+ def request_parameters
+ end
+
+ def env
+ end
+
+ def host
+ end
+
+ def cookies
+ end
+
+ def session
+ end
+
+ def reset_session
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/rescue.rb b/actionpack/lib/action_controller/rescue.rb
new file mode 100644
index 0000000000..c0933b2666
--- /dev/null
+++ b/actionpack/lib/action_controller/rescue.rb
@@ -0,0 +1,94 @@
+module ActionController #:nodoc:
+ # Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view
+ # (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view
+ # is already implemented by the Action Controller, but the public view should be tailored to your specific application. So too
+ # could the decision on whether something is a public or a developer request.
+ #
+ # You can tailor the rescuing behavior and appearance by overwriting the following two stub methods.
+ module Rescue
+ def self.append_features(base) #:nodoc:
+ super
+ base.class_eval do
+ alias_method :perform_action_without_rescue, :perform_action
+ alias_method :perform_action, :perform_action_with_rescue
+ end
+ end
+
+ protected
+ # Exception handler called when the performance of an action raises an exception.
+ def rescue_action(exception)
+ log_error(exception) unless logger.nil?
+
+ if consider_all_requests_local || local_request?
+ rescue_action_locally(exception)
+ else
+ rescue_action_in_public(exception)
+ end
+ end
+
+ # Overwrite to implement custom logging of errors. By default logs as fatal.
+ def log_error(exception) #:doc:
+ if ActionView::TemplateError === exception
+ logger.fatal(exception.to_s)
+ else
+ logger.fatal(
+ "\n\n#{exception.class} (#{exception.message}):\n " +
+ clean_backtrace(exception).join("\n ") +
+ "\n\n"
+ )
+ end
+ end
+
+ # Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>).
+ def rescue_action_in_public(exception) #:doc:
+ render_text "<html><body><h1>Application error (Rails)</h1></body></html>"
+ end
+
+ # Overwrite to expand the meaning of a local request in order to show local rescues on other occurances than
+ # the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging
+ # remotely.
+ def local_request? #:doc:
+ @request.remote_addr == "127.0.0.1"
+ end
+
+ # Renders a detailed diagnostics screen on action exceptions.
+ def rescue_action_locally(exception)
+ @exception = exception
+ @rescues_path = File.dirname(__FILE__) + "/templates/rescues/"
+ add_variables_to_assigns
+ @contents = @template.render_file(template_path_for_local_rescue(exception), false)
+
+ @headers["Content-Type"] = "text/html"
+ render_file(rescues_path("layout"), "500 Internal Error")
+ end
+
+ private
+ def perform_action_with_rescue #:nodoc:
+ begin
+ perform_action_without_rescue
+ rescue Exception => exception
+ rescue_action(exception)
+ end
+ end
+
+ def rescues_path(template_name)
+ File.dirname(__FILE__) + "/templates/rescues/#{template_name}.rhtml"
+ end
+
+ def template_path_for_local_rescue(exception)
+ rescues_path(
+ case exception
+ when MissingTemplate then "missing_template"
+ when UnknownAction then "unknown_action"
+ when ActionView::TemplateError then "template_error"
+ else "diagnostics"
+ end
+ )
+ end
+
+ def clean_backtrace(exception)
+ base_dir = File.expand_path(File.dirname(__FILE__) + "/../../../../")
+ exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/public/../config/environments/../../", "").gsub("/public/../", "") }
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb
new file mode 100755
index 0000000000..836dd5ffef
--- /dev/null
+++ b/actionpack/lib/action_controller/response.rb
@@ -0,0 +1,15 @@
+module ActionController
+ class AbstractResponse #:nodoc:
+ DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
+ attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params
+
+ def initialize
+ @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
+ end
+
+ def redirect(to_url)
+ @headers["Status"] = "302 Moved"
+ @headers["location"] = to_url
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/scaffolding.rb b/actionpack/lib/action_controller/scaffolding.rb
new file mode 100644
index 0000000000..49b35b37df
--- /dev/null
+++ b/actionpack/lib/action_controller/scaffolding.rb
@@ -0,0 +1,183 @@
+module ActionController
+ module Scaffolding # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ # Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions
+ # for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come
+ # with both controller logic and default templates that through introspection already know which fields to display
+ # and which input types to use. Example:
+ #
+ # class WeblogController < ActionController::Base
+ # scaffold :entry
+ # end
+ #
+ # This tiny piece of code will add all of the following methods to the controller:
+ #
+ # class WeblogController < ActionController::Base
+ # def index
+ # list
+ # end
+ #
+ # def list
+ # @entries = Entry.find_all
+ # render_scaffold "list"
+ # end
+ #
+ # def show
+ # @entry = Entry.find(@params["id"])
+ # render_scaffold
+ # end
+ #
+ # def destroy
+ # Entry.find(@params["id"]).destroy
+ # redirect_to :action => "list"
+ # end
+ #
+ # def new
+ # @entry = Entry.new
+ # render_scaffold
+ # end
+ #
+ # def create
+ # @entry = Entry.new(@params["entry"])
+ # if @entry.save
+ # flash["notice"] = "Entry was succesfully created"
+ # redirect_to :action => "list"
+ # else
+ # render "entry/new"
+ # end
+ # end
+ #
+ # def edit
+ # @entry = Entry.find(@params["id"])
+ # render_scaffold
+ # end
+ #
+ # def update
+ # @entry = Entry.find(@params["entry"]["id"])
+ # @entry.attributes = @params["entry"]
+ #
+ # if @entry.save
+ # flash["notice"] = "Entry was succesfully updated"
+ # redirect_to :action => "show/" + @entry.id.to_s
+ # else
+ # render "entry/edit"
+ # end
+ # end
+ # end
+ #
+ # The <tt>render_scaffold</tt> method will first check to see if you've made your own template (like "weblog/show.rhtml" for
+ # the show action) and if not, then render the generic template for that action. This gives you the possibility of using the
+ # scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template
+ # and one action at a time while relying on the rest of the scaffolded templates and actions.
+ module ClassMethods
+ # Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless
+ # one is specifically provide through <tt>options[:class_name]</tt>. So <tt>scaffold :post</tt> would use Post as the class
+ # and @post/@posts for the instance variables.
+ #
+ # It's possible to use more than one scaffold in a single controller by specifying <tt>options[:suffix] = true</tt>. This will
+ # make <tt>scaffold :post, :suffix => true</tt> use method names like list_post, show_post, and create_post
+ # instead of just list, show, and post. If suffix is used, then no index method is added.
+ def scaffold(model_id, options = {})
+ validate_options([ :class_name, :suffix ], options.keys)
+
+ require "#{model_id.id2name}" rescue logger.warn "Couldn't auto-require #{model_id.id2name}.rb" unless logger.nil?
+
+ singular_name = model_id.id2name
+ class_name = options[:class_name] || Inflector.camelize(singular_name)
+ plural_name = Inflector.pluralize(singular_name)
+ suffix = options[:suffix] ? "_#{singular_name}" : ""
+
+ unless options[:suffix]
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def index
+ list
+ end
+ end_eval
+ end
+
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def list#{suffix}
+ @#{plural_name} = #{class_name}.find_all
+ render#{suffix}_scaffold "list#{suffix}"
+ end
+
+ def show#{suffix}
+ @#{singular_name} = #{class_name}.find(@params["id"])
+ render#{suffix}_scaffold
+ end
+
+ def destroy#{suffix}
+ #{class_name}.find(@params["id"]).destroy
+ redirect_to :action => "list#{suffix}"
+ end
+
+ def new#{suffix}
+ @#{singular_name} = #{class_name}.new
+ render#{suffix}_scaffold
+ end
+
+ def create#{suffix}
+ @#{singular_name} = #{class_name}.new(@params["#{singular_name}"])
+ if @#{singular_name}.save
+ flash["notice"] = "#{class_name} was succesfully created"
+ redirect_to :action => "list#{suffix}"
+ else
+ render "#{singular_name}/new#{suffix}"
+ end
+ end
+
+ def edit#{suffix}
+ @#{singular_name} = #{class_name}.find(@params["id"])
+ render#{suffix}_scaffold
+ end
+
+ def update#{suffix}
+ @#{singular_name} = #{class_name}.find(@params["#{singular_name}"]["id"])
+ @#{singular_name}.attributes = @params["#{singular_name}"]
+
+ if @#{singular_name}.save
+ flash["notice"] = "#{class_name} was succesfully updated"
+ redirect_to :action => "show#{suffix}/" + @#{singular_name}.id.to_s
+ else
+ render "#{singular_name}/edit#{suffix}"
+ end
+ end
+
+ private
+ def render#{suffix}_scaffold(action = caller_method_name(caller))
+ if template_exists?("\#{controller_name}/\#{action}")
+ render_action(action)
+ else
+ @scaffold_class = #{class_name}
+ @scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}"
+ @scaffold_suffix = "#{suffix}"
+ add_instance_variables_to_assigns
+
+ @content_for_layout = @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false)
+ self.active_layout ? render_file(self.active_layout, "200 OK", true) : render_file(scaffold_path("layout"))
+ end
+ end
+
+ def scaffold_path(template_name)
+ File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
+ end
+
+ def caller_method_name(caller)
+ caller.first.scan(/`(.*)'/).first.first # ' ruby-mode
+ end
+ end_eval
+ end
+
+ private
+ # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
+ def validate_options(valid_option_keys, supplied_option_keys)
+ unknown_option_keys = supplied_option_keys - valid_option_keys
+ raise(ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/session/active_record_store.rb b/actionpack/lib/action_controller/session/active_record_store.rb
new file mode 100644
index 0000000000..c144f62e35
--- /dev/null
+++ b/actionpack/lib/action_controller/session/active_record_store.rb
@@ -0,0 +1,72 @@
+begin
+
+require 'active_record'
+require 'cgi'
+require 'cgi/session'
+
+# Contributed by Tim Bates
+class CGI
+ class Session
+ # ActiveRecord database based session storage class.
+ #
+ # Implements session storage in a database using the ActiveRecord ORM library. Assumes that the database
+ # has a table called +sessions+ with columns +id+ (numeric, primary key), +sessid+ and +data+ (text).
+ # The session data is stored in the +data+ column in YAML format; the user is responsible for ensuring that
+ # only data that can be YAMLized is stored in the session.
+ class ActiveRecordStore
+ # The ActiveRecord class which corresponds to the database table.
+ class Session < ActiveRecord::Base
+ serialize :data
+ # Isn't this class definition beautiful?
+ end
+
+ # Create a new ActiveRecordStore instance. This constructor is used internally by CGI::Session.
+ # The user does not generally need to call it directly.
+ #
+ # +session+ is the session for which this instance is being created.
+ #
+ # +option+ is currently ignored as no options are recognized.
+ #
+ # This session's ActiveRecord database row will be created if it does not exist, or opened if it does.
+ def initialize(session, option=nil)
+ @session = Session.find_first(["sessid = '%s'", session.session_id])
+ if @session
+ @data = @session.data
+ else
+ @session = Session.new("sessid" => session.session_id, "data" => {})
+ end
+ end
+
+ # Update and close the session's ActiveRecord object.
+ def close
+ return unless @session
+ update
+ @session = nil
+ end
+
+ # Close and destroy the session's ActiveRecord object.
+ def delete
+ return unless @session
+ @session.destroy
+ @session = nil
+ end
+
+ # Restore session state from the session's ActiveRecord object.
+ def restore
+ return unless @session
+ @data = @session.data
+ end
+
+ # Save session state in the session's ActiveRecord object.
+ def update
+ return unless @session
+ @session.data = @data
+ @session.save
+ end
+ end #ActiveRecordStore
+ end #Session
+end #CGI
+
+rescue LoadError
+ # Couldn't load Active Record, so don't make this store available
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/session/drb_server.rb b/actionpack/lib/action_controller/session/drb_server.rb
new file mode 100644
index 0000000000..6005b8b2b3
--- /dev/null
+++ b/actionpack/lib/action_controller/session/drb_server.rb
@@ -0,0 +1,9 @@
+#!/usr/local/bin/ruby -w
+
+# This is a really simple session storage daemon, basically just a hash,
+# which is enabled for DRb access.
+
+require 'drb'
+
+DRb.start_service('druby://127.0.0.1:9192', Hash.new)
+DRb.thread.join \ No newline at end of file
diff --git a/actionpack/lib/action_controller/session/drb_store.rb b/actionpack/lib/action_controller/session/drb_store.rb
new file mode 100644
index 0000000000..8ea23e8fff
--- /dev/null
+++ b/actionpack/lib/action_controller/session/drb_store.rb
@@ -0,0 +1,31 @@
+require 'cgi'
+require 'cgi/session'
+require 'drb'
+
+class CGI #:nodoc:all
+ class Session
+ class DRbStore
+ @@session_data = DRbObject.new(nil, 'druby://localhost:9192')
+
+ def initialize(session, option=nil)
+ @session_id = session.session_id
+ end
+
+ def restore
+ @h = @@session_data[@session_id] || {}
+ end
+
+ def update
+ @@session_data[@session_id] = @h
+ end
+
+ def close
+ update
+ end
+
+ def delete
+ @@session_data.delete(@session_id)
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/support/class_attribute_accessors.rb b/actionpack/lib/action_controller/support/class_attribute_accessors.rb
new file mode 100644
index 0000000000..786dcf98cb
--- /dev/null
+++ b/actionpack/lib/action_controller/support/class_attribute_accessors.rb
@@ -0,0 +1,57 @@
+# Extends the class object with class and instance accessors for class attributes,
+# just like the native attr* accessors for instance attributes.
+class Class # :nodoc:
+ def cattr_reader(*syms)
+ syms.each do |sym|
+ class_eval <<-EOS
+ if ! defined? @@#{sym.id2name}
+ @@#{sym.id2name} = nil
+ end
+
+ def self.#{sym.id2name}
+ @@#{sym}
+ end
+
+ def #{sym.id2name}
+ @@#{sym}
+ end
+
+ def call_#{sym.id2name}
+ case @@#{sym.id2name}
+ when Symbol then send(@@#{sym})
+ when Proc then @@#{sym}.call(self)
+ when String then @@#{sym}
+ else nil
+ end
+ end
+ EOS
+ end
+ end
+
+ def cattr_writer(*syms)
+ syms.each do |sym|
+ class_eval <<-EOS
+ if ! defined? @@#{sym.id2name}
+ @@#{sym.id2name} = nil
+ end
+
+ def self.#{sym.id2name}=(obj)
+ @@#{sym.id2name} = obj
+ end
+
+ def self.set_#{sym.id2name}(obj)
+ @@#{sym.id2name} = obj
+ end
+
+ def #{sym.id2name}=(obj)
+ @@#{sym} = obj
+ end
+ EOS
+ end
+ end
+
+ def cattr_accessor(*syms)
+ cattr_reader(*syms)
+ cattr_writer(*syms)
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/support/class_inheritable_attributes.rb b/actionpack/lib/action_controller/support/class_inheritable_attributes.rb
new file mode 100644
index 0000000000..7f061fdf1b
--- /dev/null
+++ b/actionpack/lib/action_controller/support/class_inheritable_attributes.rb
@@ -0,0 +1,37 @@
+# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
+# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
+# to, for example, an array without those additions being shared with either their parent, siblings, or
+# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
+module ClassInheritableAttributes # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods # :nodoc:
+ @@classes ||= {}
+
+ def inheritable_attributes
+ @@classes[self] ||= {}
+ end
+
+ def write_inheritable_attribute(key, value)
+ inheritable_attributes[key] = value
+ end
+
+ def write_inheritable_array(key, elements)
+ write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
+ write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
+ end
+
+ def read_inheritable_attribute(key)
+ inheritable_attributes[key]
+ end
+
+ private
+ def inherited(child)
+ @@classes[child] = inheritable_attributes.dup
+ end
+
+ end
+end
diff --git a/actionpack/lib/action_controller/support/clean_logger.rb b/actionpack/lib/action_controller/support/clean_logger.rb
new file mode 100644
index 0000000000..1a36562892
--- /dev/null
+++ b/actionpack/lib/action_controller/support/clean_logger.rb
@@ -0,0 +1,10 @@
+require 'logger'
+
+class Logger #:nodoc:
+ private
+ remove_const "Format"
+ Format = "%s\n"
+ def format_message(severity, timestamp, msg, progname)
+ Format % [msg]
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/support/cookie_performance_fix.rb b/actionpack/lib/action_controller/support/cookie_performance_fix.rb
new file mode 100644
index 0000000000..225cea1905
--- /dev/null
+++ b/actionpack/lib/action_controller/support/cookie_performance_fix.rb
@@ -0,0 +1,121 @@
+CGI.module_eval { remove_const "Cookie" }
+
+class CGI #:nodoc:
+ # This is a cookie class that fixes the performance problems with the default one that ships with 1.8.1 and below.
+ # It replaces the inheritance on SimpleDelegator with DelegateClass(Array) following the suggestion from Matz on
+ # http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14
+ class Cookie < DelegateClass(Array)
+ # Create a new CGI::Cookie object.
+ #
+ # The contents of the cookie can be specified as a +name+ and one
+ # or more +value+ arguments. Alternatively, the contents can
+ # be specified as a single hash argument. The possible keywords of
+ # this hash are as follows:
+ #
+ # name:: the name of the cookie. Required.
+ # value:: the cookie's value or list of values.
+ # path:: the path for which this cookie applies. Defaults to the
+ # base directory of the CGI script.
+ # domain:: the domain for which this cookie applies.
+ # expires:: the time at which this cookie expires, as a +Time+ object.
+ # secure:: whether this cookie is a secure cookie or not (default to
+ # false). Secure cookies are only transmitted to HTTPS
+ # servers.
+ #
+ # These keywords correspond to attributes of the cookie object.
+ def initialize(name = "", *value)
+ options = if name.kind_of?(String)
+ { "name" => name, "value" => value }
+ else
+ name
+ end
+ unless options.has_key?("name")
+ raise ArgumentError, "`name' required"
+ end
+
+ @name = options["name"]
+ @value = Array(options["value"])
+ # simple support for IE
+ if options["path"]
+ @path = options["path"]
+ else
+ %r|^(.*/)|.match(ENV["SCRIPT_NAME"])
+ @path = ($1 or "")
+ end
+ @domain = options["domain"]
+ @expires = options["expires"]
+ @secure = options["secure"] == true ? true : false
+
+ super(@value)
+ end
+
+ def __setobj__(obj)
+ @_dc_obj = obj
+ end
+
+ attr_accessor("name", "value", "path", "domain", "expires")
+ attr_reader("secure")
+
+ # Set whether the Cookie is a secure cookie or not.
+ #
+ # +val+ must be a boolean.
+ def secure=(val)
+ @secure = val if val == true or val == false
+ @secure
+ end
+
+ # Convert the Cookie to its string representation.
+ def to_s
+ buf = ""
+ buf += @name + '='
+
+ if @value.kind_of?(String)
+ buf += CGI::escape(@value)
+ else
+ buf += @value.collect{|v| CGI::escape(v) }.join("&")
+ end
+
+ if @domain
+ buf += '; domain=' + @domain
+ end
+
+ if @path
+ buf += '; path=' + @path
+ end
+
+ if @expires
+ buf += '; expires=' + CGI::rfc1123_date(@expires)
+ end
+
+ if @secure == true
+ buf += '; secure'
+ end
+
+ buf
+ end
+
+ # Parse a raw cookie string into a hash of cookie-name=>Cookie
+ # pairs.
+ #
+ # cookies = CGI::Cookie::parse("raw_cookie_string")
+ # # { "name1" => cookie1, "name2" => cookie2, ... }
+ #
+ def self.parse(raw_cookie)
+ cookies = Hash.new([])
+ return cookies unless raw_cookie
+
+ raw_cookie.split(/; /).each do |pairs|
+ name, values = pairs.split('=',2)
+ next unless name and values
+ name = CGI::unescape(name)
+ values ||= ""
+ values = values.split('&').collect{|v| CGI::unescape(v) }
+ unless cookies.has_key?(name)
+ cookies[name] = new({ "name" => name, "value" => values })
+ end
+ end
+
+ cookies
+ end
+ end # class Cookie
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/support/inflector.rb b/actionpack/lib/action_controller/support/inflector.rb
new file mode 100644
index 0000000000..05ff4fede9
--- /dev/null
+++ b/actionpack/lib/action_controller/support/inflector.rb
@@ -0,0 +1,78 @@
+# The Inflector transforms words from singular to plural, class names to table names, modulized class names to ones without,
+# and class names to foreign keys.
+module Inflector
+ extend self
+
+ def pluralize(word)
+ result = word.dup
+ plural_rules.each do |(rule, replacement)|
+ break if result.gsub!(rule, replacement)
+ end
+ return result
+ end
+
+ def singularize(word)
+ result = word.dup
+ singular_rules.each do |(rule, replacement)|
+ break if result.gsub!(rule, replacement)
+ end
+ return result
+ end
+
+ def camelize(lower_case_and_underscored_word)
+ lower_case_and_underscored_word.gsub(/(^|_)(.)/){$2.upcase}
+ end
+
+ def underscore(camel_cased_word)
+ camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
+ end
+
+ def demodulize(class_name_in_module)
+ class_name_in_module.gsub(/^.*::/, '')
+ end
+
+ def tableize(class_name)
+ pluralize(underscore(class_name))
+ end
+
+ def classify(table_name)
+ camelize(singularize(table_name))
+ end
+
+ def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
+ Inflector.underscore(Inflector.demodulize(class_name)) +
+ (separate_class_name_and_id_with_underscore ? "_id" : "id")
+ end
+
+ private
+ def plural_rules #:doc:
+ [
+ [/(x|ch|ss)$/, '\1es'], # search, switch, fix, box, process, address
+ [/([^aeiouy]|qu)y$/, '\1ies'], # query, ability, agency
+ [/(?:([^f])fe|([lr])f)$/, '\1\2ves'], # half, safe, wife
+ [/sis$/, 'ses'], # basis, diagnosis
+ [/([ti])um$/, '\1a'], # datum, medium
+ [/person$/, 'people'], # person, salesperson
+ [/man$/, 'men'], # man, woman, spokesman
+ [/child$/, 'children'], # child
+ [/s$/, 's'], # no change (compatibility)
+ [/$/, 's']
+ ]
+ end
+
+ def singular_rules #:doc:
+ [
+ [/(x|ch|ss)es$/, '\1'],
+ [/([^aeiouy]|qu)ies$/, '\1y'],
+ [/([lr])ves$/, '\1f'],
+ [/([^f])ves$/, '\1fe'],
+ [/(analy|ba|diagno|parenthe|progno|synop|the)ses$/, '\1sis'],
+ [/([ti])a$/, '\1um'],
+ [/people$/, 'person'],
+ [/men$/, 'man'],
+ [/status$/, 'status'],
+ [/children$/, 'child'],
+ [/s$/, '']
+ ]
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml b/actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml
new file mode 100644
index 0000000000..f1b4a2a1dd
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml
@@ -0,0 +1,28 @@
+<%
+ base_dir = File.expand_path(File.dirname(__FILE__))
+
+ request_parameters_without_action = @request.parameters.clone
+ request_parameters_without_action.delete("action")
+ request_parameters_without_action.delete("controller")
+
+ request_dump = request_parameters_without_action.inspect.gsub(/,/, ",\n")
+ session_dump = @request.session.instance_variable_get("@data").inspect.gsub(/,/, ",\n")
+ response_dump = @response.inspect.gsub(/,/, ",\n")
+
+ template_assigns = @response.template.instance_variable_get("@assigns")
+ %w( response exception template session request template_root template_class url ignore_missing_templates logger cookies headers params ).each { |t| template_assigns.delete(t) }
+ template_dump = template_assigns.inspect.gsub(/,/, ",\n")
+%>
+
+<h2 style="margin-top: 30px">Request</h2>
+<p><b>Parameters</b>: <%=h request_dump == "{}" ? "None" : request_dump %></p>
+
+<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p>
+<div id="session_dump" style="display:none"><%= debug(@request.session.instance_variable_get("@data")) %></div>
+
+
+<h2 style="margin-top: 30px">Response</h2>
+<b>Headers</b>: <%=h @response.headers.inspect.gsub(/,/, ",\n") %><br/>
+
+<p><a href="#" onclick="document.getElementById('template_dump').style.display='block'; return false;">Show template parameters</a></p>
+<div id="template_dump" style="display:none"><%= debug(template_assigns) %></div>
diff --git a/actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml b/actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml
new file mode 100644
index 0000000000..4eb1ed0439
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml
@@ -0,0 +1,22 @@
+<%
+ base_dir = File.expand_path(File.dirname(__FILE__))
+
+ clean_backtrace = @exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/../config/environments/../../", "") }
+ app_trace = clean_backtrace.reject { |line| line[0..6] == "vendor/" || line.include?("dispatch.cgi") }
+ framework_trace = clean_backtrace - app_trace
+%>
+
+<h1>
+ <%=h @exception.class.to_s %> in
+ <%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %>
+</h1>
+<p><%=h @exception.message %></p>
+
+<% unless app_trace.empty? %><pre><code><%=h app_trace.collect { |line| line.gsub("/../", "") }.join("\n") %></code></pre><% end %>
+
+<% unless framework_trace.empty? %>
+ <a href="#" onclick="document.getElementById('framework_trace').style.display='block'; return false;">Show framework trace</a>
+ <pre id="framework_trace" style="display:none"><code><%=h framework_trace.join("\n") %></code></pre>
+<% end %>
+
+<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %>
diff --git a/actionpack/lib/action_controller/templates/rescues/layout.rhtml b/actionpack/lib/action_controller/templates/rescues/layout.rhtml
new file mode 100644
index 0000000000..d38f3e67f9
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/rescues/layout.rhtml
@@ -0,0 +1,29 @@
+<html>
+<head>
+ <title>Action Controller: Exception caught</title>
+ <style>
+ body { background-color: #fff; color: #333; }
+
+ body, p, ol, ul, td {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ }
+
+ pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+ }
+
+ a { color: #000; }
+ a:visited { color: #666; }
+ a:hover { color: #fff; background-color:#000; }
+ </style>
+</head>
+<body>
+
+<%= @contents %>
+
+</body>
+</html> \ No newline at end of file
diff --git a/actionpack/lib/action_controller/templates/rescues/missing_template.rhtml b/actionpack/lib/action_controller/templates/rescues/missing_template.rhtml
new file mode 100644
index 0000000000..dbfdf76947
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/rescues/missing_template.rhtml
@@ -0,0 +1,2 @@
+<h1>Template is missing</h1>
+<p><%=h @exception.message %></p>
diff --git a/actionpack/lib/action_controller/templates/rescues/template_error.rhtml b/actionpack/lib/action_controller/templates/rescues/template_error.rhtml
new file mode 100644
index 0000000000..326fd0b057
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/rescues/template_error.rhtml
@@ -0,0 +1,26 @@
+<%
+ base_dir = File.expand_path(File.dirname(__FILE__))
+
+ framework_trace = @exception.original_exception.backtrace.collect do |line|
+ line.gsub(base_dir, "").gsub("/../config/environments/../../", "")
+ end
+%>
+
+<h1>
+ <%=h @exception.original_exception.class.to_s %> in
+ <%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %>
+</h1>
+
+<p>
+ Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised
+ <u><%=h @exception.message %></u>
+</p>
+
+<pre><code><%=h @exception.source_extract %></code></pre>
+
+<p><%=h @exception.sub_template_message %></p>
+
+<a href="#" onclick="document.getElementById('framework_trace').style.display='block'">Show template trace</a>
+<pre id="framework_trace" style="display:none"><code><%=h framework_trace.join("\n") %></code></pre>
+
+<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %>
diff --git a/actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml b/actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml
new file mode 100644
index 0000000000..683379da10
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml
@@ -0,0 +1,2 @@
+<h1>Unknown action</h1>
+<p><%=h @exception.message %></p>
diff --git a/actionpack/lib/action_controller/templates/scaffolds/edit.rhtml b/actionpack/lib/action_controller/templates/scaffolds/edit.rhtml
new file mode 100644
index 0000000000..1c7f4d9770
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/scaffolds/edit.rhtml
@@ -0,0 +1,6 @@
+<h1>Editing <%= @scaffold_singular_name %></h1>
+
+<%= form(@scaffold_singular_name, :action => "../update" + @scaffold_suffix) %>
+
+<%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}").id %> |
+<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
diff --git a/actionpack/lib/action_controller/templates/scaffolds/layout.rhtml b/actionpack/lib/action_controller/templates/scaffolds/layout.rhtml
new file mode 100644
index 0000000000..511054abe8
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/scaffolds/layout.rhtml
@@ -0,0 +1,29 @@
+<html>
+<head>
+ <title>Scaffolding</title>
+ <style>
+ body { background-color: #fff; color: #333; }
+
+ body, p, ol, ul, td {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ }
+
+ pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+ }
+
+ a { color: #000; }
+ a:visited { color: #666; }
+ a:hover { color: #fff; background-color:#000; }
+ </style>
+</head>
+<body>
+
+<%= @content_for_layout %>
+
+</body>
+</html> \ No newline at end of file
diff --git a/actionpack/lib/action_controller/templates/scaffolds/list.rhtml b/actionpack/lib/action_controller/templates/scaffolds/list.rhtml
new file mode 100644
index 0000000000..33af7079b2
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/scaffolds/list.rhtml
@@ -0,0 +1,24 @@
+<h1>Listing <%= @scaffold_plural_name %></h1>
+
+<table>
+ <tr>
+ <% for column in @scaffold_class.content_columns %>
+ <th><%= column.human_name %></th>
+ <% end %>
+ </tr>
+
+<% for entry in instance_variable_get("@#{@scaffold_plural_name}") %>
+ <tr>
+ <% for column in @scaffold_class.content_columns %>
+ <td><%= entry.send(column.name) %></td>
+ <% end %>
+ <td><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry.id %></td>
+ <td><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry.id %></td>
+ <td><%= link_to "Destroy", :action => "destroy#{@scaffold_suffix}", :id => entry.id %></td>
+ </tr>
+<% end %>
+</table>
+
+<br />
+
+<%= link_to "New #{@scaffold_singular_name}", :action => "new#{@scaffold_suffix}" %>
diff --git a/actionpack/lib/action_controller/templates/scaffolds/new.rhtml b/actionpack/lib/action_controller/templates/scaffolds/new.rhtml
new file mode 100644
index 0000000000..02f52e72f5
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/scaffolds/new.rhtml
@@ -0,0 +1,5 @@
+<h1>New <%= @scaffold_singular_name %></h1>
+
+<%= form(@scaffold_singular_name, :action => "create" + @scaffold_suffix) %>
+
+<%= link_to "Back", :action => "list#{@scaffold_suffix}" %> \ No newline at end of file
diff --git a/actionpack/lib/action_controller/templates/scaffolds/show.rhtml b/actionpack/lib/action_controller/templates/scaffolds/show.rhtml
new file mode 100644
index 0000000000..10c46342fd
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/scaffolds/show.rhtml
@@ -0,0 +1,9 @@
+<% for column in @scaffold_class.content_columns %>
+ <p>
+ <b><%= column.human_name %>:</b>
+ <%= instance_variable_get("@#{@scaffold_singular_name}").send(column.name) %>
+ </p>
+<% end %>
+
+<%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}").id %> |
+<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb
new file mode 100644
index 0000000000..969f2573c5
--- /dev/null
+++ b/actionpack/lib/action_controller/test_process.rb
@@ -0,0 +1,195 @@
+require File.dirname(__FILE__) + '/assertions/action_pack_assertions'
+require File.dirname(__FILE__) + '/assertions/active_record_assertions'
+
+module ActionController #:nodoc:
+ class Base
+ # Process a test request called with a +TestRequest+ object.
+ def self.process_test(request)
+ new.process_test(request)
+ end
+
+ def process_test(request) #:nodoc:
+ process(request, TestResponse.new)
+ end
+ end
+
+ class TestRequest < AbstractRequest #:nodoc:
+ attr_writer :cookies
+ attr_accessor :query_parameters, :request_parameters, :session, :env
+ attr_accessor :host, :path, :request_uri, :remote_addr
+
+ def initialize(query_parameters = nil, request_parameters = nil, session = nil)
+ @query_parameters = query_parameters || {}
+ @request_parameters = request_parameters || {}
+ @session = session || TestSession.new
+
+ initialize_containers
+ initialize_default_values
+
+ super()
+ end
+
+ def reset_session
+ @session = {}
+ end
+
+ def cookies
+ @cookies.freeze
+ end
+
+ def action=(action_name)
+ @query_parameters.update({ "action" => action_name })
+ @parameters = nil
+ end
+
+ def request_uri=(uri)
+ @request_uri = uri
+ @path = uri.split("?").first
+ end
+
+ private
+ def initialize_containers
+ @env, @cookies = {}, {}
+ end
+
+ def initialize_default_values
+ @host = "test.host"
+ @request_uri = "/"
+ @remote_addr = "127.0.0.1"
+ @env["SERVER_PORT"] = 80
+ end
+ end
+
+ class TestResponse < AbstractResponse #:nodoc:
+ # the class attribute ties a TestResponse to the assertions
+ class << self
+ attr_accessor :assertion_target
+ end
+
+ # initializer
+ def initialize
+ TestResponse.assertion_target=self# if TestResponse.assertion_target.nil?
+ super()
+ end
+
+ # the response code of the request
+ def response_code
+ headers['Status'][0,3].to_i rescue 0
+ end
+
+ # was the response successful?
+ def success?
+ response_code == 200
+ end
+
+ # was the URL not found?
+ def missing?
+ response_code == 404
+ end
+
+ # were we redirected?
+ def redirect?
+ (300..399).include?(response_code)
+ end
+
+ # was there a server-side error?
+ def server_error?
+ (500..599).include?(response_code)
+ end
+
+ # returns the redirection location or nil
+ def redirect_url
+ redirect? ? headers['location'] : nil
+ end
+
+ # does the redirect location match this regexp pattern?
+ def redirect_url_match?( pattern )
+ return false if redirect_url.nil?
+ p = Regexp.new(pattern) if pattern.class == String
+ p = pattern if pattern.class == Regexp
+ return false if p.nil?
+ p.match(redirect_url) != nil
+ end
+
+ # returns the template path of the file which was used to
+ # render this response (or nil)
+ def rendered_file(with_controller=false)
+ unless template.first_render.nil?
+ unless with_controller
+ template.first_render
+ else
+ template.first_render.split('/').last || template.first_render
+ end
+ end
+ end
+
+ # was this template rendered by a file?
+ def rendered_with_file?
+ !rendered_file.nil?
+ end
+
+ # a shortcut to the flash (or an empty hash if no flash.. hey! that rhymes!)
+ def flash
+ session['flash'] || {}
+ end
+
+ # do we have a flash?
+ def has_flash?
+ !session['flash'].nil?
+ end
+
+ # do we have a flash that has contents?
+ def has_flash_with_contents?
+ !flash.empty?
+ end
+
+ # does the specified flash object exist?
+ def has_flash_object?(name=nil)
+ !flash[name].nil?
+ end
+
+ # does the specified object exist in the session?
+ def has_session_object?(name=nil)
+ !session[name].nil?
+ end
+
+ # a shortcut to the template.assigns
+ def template_objects
+ template.assigns || {}
+ end
+
+ # does the specified template object exist?
+ def has_template_object?(name=nil)
+ !template_objects[name].nil?
+ end
+end
+
+ class TestSession #:nodoc:
+ def initialize(attributes = {})
+ @attributes = attributes
+ end
+
+ def [](key)
+ @attributes[key]
+ end
+
+ def []=(key, value)
+ @attributes[key] = value
+ end
+
+ def update() end
+ def close() end
+ def delete() @attributes = {} end
+ end
+end
+
+class Test::Unit::TestCase #:nodoc:
+ private
+ # execute the request and set/volley the response
+ def process(action, parameters = nil, session = nil)
+ @request.action = action.to_s
+ @request.parameters.update(parameters) unless parameters.nil?
+ @request.session = ActionController::TestSession.new(session) unless session.nil?
+ @controller.process(@request, @response)
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb
new file mode 100644
index 0000000000..78638da39e
--- /dev/null
+++ b/actionpack/lib/action_controller/url_rewriter.rb
@@ -0,0 +1,170 @@
+module ActionController
+ # Rewrites urls for Base.redirect_to and Base.url_for in the controller.
+ class UrlRewriter #:nodoc:
+ VALID_OPTIONS = [:action, :action_prefix, :action_suffix, :module, :controller, :controller_prefix, :anchor, :params, :path_params, :id, :only_path, :overwrite_params ]
+
+ def initialize(request, controller, action)
+ @request, @controller, @action = request, controller, action
+ @rewritten_path = @request.path ? @request.path.dup : ""
+ end
+
+ def rewrite(options = {})
+ validate_options(VALID_OPTIONS, options.keys)
+
+ rewrite_url(
+ rewrite_path(@rewritten_path, options),
+ options
+ )
+ end
+
+ def to_s
+ to_str
+ end
+
+ def to_str
+ "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@controller}, #{@action}, #{@request.parameters.inspect}"
+ end
+
+ private
+ def validate_options(valid_option_keys, supplied_option_keys)
+ unknown_option_keys = supplied_option_keys - valid_option_keys
+ raise(ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
+ end
+
+ def rewrite_url(path, options)
+ rewritten_url = ""
+ rewritten_url << @request.protocol unless options[:only_path]
+ rewritten_url << @request.host_with_port unless options[:only_path]
+
+ rewritten_url << path
+ rewritten_url << build_query_string(new_parameters(options)) if options[:params] || options[:overwrite_params]
+ rewritten_url << "##{options[:anchor]}" if options[:anchor]
+ return rewritten_url
+ end
+
+ def rewrite_path(path, options)
+ include_id_in_path_params(options)
+
+ path = rewrite_action(path, options) if options[:action] || options[:action_prefix]
+ path = rewrite_path_params(path, options) if options[:path_params]
+ path = rewrite_controller(path, options) if options[:controller] || options[:controller_prefix]
+ return path
+ end
+
+ def rewrite_path_params(path, options)
+ index_action = options[:action] == 'index' || options[:action].nil? && @action == 'index'
+ id_only = options[:path_params].size == 1 && options[:path_params]['id']
+
+ if index_action && id_only
+ path += '/' unless path[-1..-1] == '/'
+ path += "index/#{options[:path_params]['id']}"
+ path
+ else
+ options[:path_params].inject(path) do |path, pair|
+ if options[:action].nil? && @request.parameters[pair.first]
+ path.sub(/\b#{@request.parameters[pair.first]}\b/, pair.last.to_s)
+ else
+ path += "/#{pair.last}"
+ end
+ end
+ end
+ end
+
+ def rewrite_action(path, options)
+ # This regex assumes that "index" actions won't be included in the URL
+ all, controller_prefix, action_prefix, action_suffix =
+ /^\/(.*)#{@controller}\/(.*)#{@action == "index" ? "" : @action}(.*)/.match(path).to_a
+
+ if @action == "index"
+ if action_prefix == "index"
+ # we broke the parsing assumption that this would be excluded, so
+ # don't tell action_name about our little boo-boo
+ path = path.sub(action_prefix, action_name(options, nil))
+ elsif action_prefix && !action_prefix.empty?
+ path = path.sub(action_prefix, action_name(options, action_prefix))
+ else
+ path = path.sub(%r(#{@controller}/?), @controller + "/" + action_name(options)) # " ruby-mode
+ end
+ else
+ path = path.sub((action_prefix || "") + @action + (action_suffix || ""), action_name(options, action_prefix))
+ end
+
+ if options[:controller_prefix] && !options[:controller]
+ ensure_slash_suffix(options, :controller_prefix)
+ if controller_prefix
+ path = path.sub(controller_prefix, options[:controller_prefix])
+ else
+ path = options[:controller_prefix] + path
+ end
+ end
+
+ return path
+ end
+
+ def rewrite_controller(path, options)
+ all, controller_prefix = /^\/(.*?)#{@controller}/.match(path).to_a
+ path = "/"
+ path << controller_name(options, controller_prefix)
+ path << action_name(options) if options[:action]
+ path << path_params_in_list(options) if options[:path_params]
+ return path
+ end
+
+ def action_name(options, action_prefix = nil, action_suffix = nil)
+ ensure_slash_suffix(options, :action_prefix)
+ ensure_slash_prefix(options, :action_suffix)
+
+ prefix = options[:action_prefix] || action_prefix || ""
+ suffix = options[:action] == "index" ? "" : (options[:action_suffix] || action_suffix || "")
+ name = (options[:action] == "index" ? "" : options[:action]) || ""
+
+ return prefix + name + suffix
+ end
+
+ def controller_name(options, controller_prefix)
+ options[:controller_prefix] = "#{options[:module]}/#{options[:controller_prefix]}" if options[:module]
+ ensure_slash_suffix(options, :controller_prefix)
+ controller_name = options[:controller_prefix] || controller_prefix || ""
+ controller_name << (options[:controller] + "/") if options[:controller]
+ return controller_name
+ end
+
+ def path_params_in_list(options)
+ options[:path_params].inject("") { |path, pair| path += "/#{pair.last}" }
+ end
+
+ def ensure_slash_suffix(options, key)
+ options[key] = options[key] + "/" if options[key] && !options[key].empty? && options[key][-1..-1] != "/"
+ end
+
+ def ensure_slash_prefix(options, key)
+ options[key] = "/" + options[key] if options[key] && !options[key].empty? && options[key][0..1] != "/"
+ end
+
+ def include_id_in_path_params(options)
+ options[:path_params] = (options[:path_params] || {}).merge({"id" => options[:id]}) if options[:id]
+ end
+
+ def new_parameters(options)
+ parameters = options[:params] || existing_parameters
+ parameters.update(options[:overwrite_params]) if options[:overwrite_params]
+ parameters.reject { |key,value| value.nil? }
+ end
+
+ def existing_parameters
+ @request.parameters.reject { |key, value| %w( id action controller).include?(key) }
+ end
+
+ # Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll
+ # be added as a path element instead of a regular parameter pair.
+ def build_query_string(hash)
+ elements = []
+ query_string = ""
+
+ hash.each { |key, value| elements << "#{CGI.escape(key)}=#{CGI.escape(value.to_s)}" }
+ unless elements.empty? then query_string << ("?" + elements.join("&")) end
+
+ return query_string
+ end
+ end
+end
diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb
new file mode 100644
index 0000000000..c39765d436
--- /dev/null
+++ b/actionpack/lib/action_view.rb
@@ -0,0 +1,49 @@
+#--
+# Copyright (c) 2004 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+begin
+ require 'rubygems'
+ require 'builder'
+rescue LoadError
+ # RubyGems is not available, use included Builder
+ $:.unshift(File.dirname(__FILE__) + "/action_view/vendor")
+ require 'action_view/vendor/builder'
+ensure
+ # Temporary patch until it's in Builder 1.2.2
+ class BlankSlate
+ class << self
+ def hide(name)
+ undef_method name if instance_methods.include?(name) and name !~ /^(__|instance_eval)/
+ end
+ end
+ end
+end
+
+require 'action_view/base'
+require 'action_view/partials'
+
+ActionView::Base.class_eval do
+ include ActionView::Partials
+end
+
+ActionView::Base.load_helpers(File.dirname(__FILE__) + "/action_view/helpers/") \ No newline at end of file
diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb
new file mode 100644
index 0000000000..84c8040760
--- /dev/null
+++ b/actionpack/lib/action_view/base.rb
@@ -0,0 +1,264 @@
+require 'erb'
+
+module ActionView #:nodoc:
+ class ActionViewError < StandardError #:nodoc:
+ end
+
+ # Action View templates can be written in two ways. If the template file has a +.rhtml+ extension then it uses a mixture of ERb
+ # (included in Ruby) and HTML. If the template file has a +.rxml+ extension then Jim Weirich's Builder::XmlMarkup library is used.
+ #
+ # = ERb
+ #
+ # You trigger ERb by using embeddings such as <% %> and <%= %>. The difference is whether you want output or not. Consider the
+ # following loop for names:
+ #
+ # <b>Names of all the people</b>
+ # <% for person in @people %>
+ # Name: <%= person.name %><br/>
+ # <% end %>
+ #
+ # The loop is setup in regular embedding tags (<% %>) and the name is written using the output embedding tag (<%= %>). Note that this
+ # is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong:
+ #
+ # Hi, Mr. <% puts "Frodo" %>
+ #
+ # (If you absolutely must write from within a function, you can use the TextHelper#concat)
+ #
+ # == Using sub templates
+ #
+ # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The
+ # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts):
+ #
+ # <%= render "shared/header" %>
+ # Something really specific and terrific
+ # <%= render "shared/footer" %>
+ #
+ # As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the
+ # result of the rendering. The output embedding writes it to the current template.
+ #
+ # But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance
+ # variables defined in using the regular embedding tags. Like this:
+ #
+ # <% @page_title = "A Wonderful Hello" %>
+ # <%= render "shared/header" %>
+ #
+ # Now the header can pick up on the @page_title variable and use it for outputting a title tag:
+ #
+ # <title><%= @page_title %></title>
+ #
+ # == Passing local variables to sub templates
+ #
+ # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values:
+ #
+ # <%= render "shared/header", { "headline" => "Welcome", "person" => person } %>
+ #
+ # These can now be accessed in shared/header with:
+ #
+ # Headline: <%= headline %>
+ # First name: <%= person.first_name %>
+ #
+ # == Template caching
+ #
+ # The parsing of ERb templates are cached by default, but the reading of them are not. This means that the application by default
+ # will reflect changes to the templates immediatly. If you'd like to sacrifice that immediacy for the speed gain given by also
+ # caching the loading of templates (reading from the file systen), you can turn that on with
+ # <tt>ActionView::Base.cache_template_loading = true</tt>.
+ #
+ # == Builder
+ #
+ # Builder templates are a more programatic alternative to ERb. They are especially useful for generating XML content. An +XmlMarkup+ object
+ # named +xml+ is automatically made available to templates with a +.rxml+ extension.
+ #
+ # Here are some basic examples:
+ #
+ # xml.em("emphasized") # => <em>emphasized</em>
+ # xml.em { xml.b("emp & bold") } # => <em><b>emph &amp; bold</b></em>
+ # xml.a("A Link", "href"=>"http://onestepback.org") # => <a href="http://onestepback.org">A Link</a>
+ # xm.target("name"=>"compile", "option"=>"fast") # => <target option="fast" name="compile"\>
+ # # NOTE: order of attributes is not specified.
+ #
+ # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following:
+ #
+ # xml.div {
+ # xml.h1(@person.name)
+ # xml.p(@person.bio)
+ # }
+ #
+ # would produce something like:
+ #
+ # <div>
+ # <h1>David Heinemeier Hansson</h1>
+ # <p>A product of Danish Design during the Winter of '79...</p>
+ # </div>
+ #
+ # A full-length RSS example actually used on Basecamp:
+ #
+ # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
+ # xml.channel do
+ # xml.title(@feed_title)
+ # xml.link(@url)
+ # xml.description "Basecamp: Recent items"
+ # xml.language "en-us"
+ # xml.ttl "40"
+ #
+ # for item in @recent_items
+ # xml.item do
+ # xml.title(item_title(item))
+ # xml.description(item_description(item)) if item_description(item)
+ # xml.pubDate(item_pubDate(item))
+ # xml.guid(@person.firm.account.url + @recent_items.url(item))
+ # xml.link(@person.firm.account.url + @recent_items.url(item))
+ #
+ # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
+ # end
+ # end
+ # end
+ # end
+ #
+ # More builder documentation can be found at http://builder.rubyforge.org.
+ class Base
+ include ERB::Util
+
+ attr_reader :first_render
+ attr_accessor :base_path, :assigns, :template_extension
+ attr_accessor :controller
+
+ # Turn on to cache the reading of templates from the file system. Doing so means that you have to restart the server
+ # when changing templates, but that rendering will be faster.
+ @@cache_template_loading = false
+ cattr_accessor :cache_template_loading
+
+ @@compiled_erb_templates = {}
+ @@loaded_templates = {}
+
+ def self.load_helpers(helper_dir)#:nodoc:
+ Dir.foreach(helper_dir) do |helper_file|
+ next unless helper_file =~ /_helper.rb$/
+ require helper_dir + helper_file
+ helper_module_name = helper_file.capitalize.gsub(/_([a-z])/) { |m| $1.capitalize }[0..-4]
+
+ class_eval("include ActionView::Helpers::#{helper_module_name}") if Helpers.const_defined?(helper_module_name)
+ end
+ end
+
+ def self.controller_delegate(*methods)
+ methods.flatten.each do |method|
+ class_eval <<-end_eval
+ def #{method}(*args, &block)
+ controller.send(%(#{method}), *args, &block)
+ end
+ end_eval
+ end
+ end
+
+ def initialize(base_path = nil, assigns_for_first_render = {}, controller = nil)#:nodoc:
+ @base_path, @assigns = base_path, assigns_for_first_render
+ @controller = controller
+ end
+
+ # Renders the template present at <tt>template_path</tt>. If <tt>use_full_path</tt> is set to true,
+ # it's relative to the template_root, otherwise it's absolute. The hash in <tt>local_assigns</tt>
+ # is made available as local variables.
+ def render_file(template_path, use_full_path = true, local_assigns = {})
+ @first_render = template_path if @first_render.nil?
+
+ if use_full_path
+ template_extension = pick_template_extension(template_path)
+ template_file_name = full_template_path(template_path, template_extension)
+ else
+ template_file_name = template_path
+ template_extension = template_path.split(".").last
+ end
+
+ template_source = read_template_file(template_file_name)
+
+ begin
+ render_template(template_extension, template_source, local_assigns)
+ rescue Exception => e
+ if TemplateError === e
+ e.sub_template_of(template_file_name)
+ raise e
+ else
+ raise TemplateError.new(@base_path, template_file_name, @assigns, template_source, e)
+ end
+ end
+ end
+
+ # Renders the template present at <tt>template_path</tt> (relative to the template_root).
+ # The hash in <tt>local_assigns</tt> is made available as local variables.
+ def render(template_path, local_assigns = {})
+ render_file(template_path, true, local_assigns)
+ end
+
+ # Renders the +template+ which is given as a string as either rhtml or rxml depending on <tt>template_extension</tt>.
+ # The hash in <tt>local_assigns</tt> is made available as local variables.
+ def render_template(template_extension, template, local_assigns = {})
+ b = binding
+ local_assigns.each { |key, value| eval "#{key} = local_assigns[\"#{key}\"]", b }
+ @assigns.each { |key, value| instance_variable_set "@#{key}", value }
+ xml = Builder::XmlMarkup.new(:indent => 2)
+
+ send(pick_rendering_method(template_extension), template, binding)
+ end
+
+ def pick_template_extension(template_path)#:nodoc:
+ if erb_template_exists?(template_path)
+ "rhtml"
+ elsif builder_template_exists?(template_path)
+ "rxml"
+ else
+ raise ActionViewError, "No rhtml or rxml template found for #{template_path}"
+ end
+ end
+
+ def pick_rendering_method(template_extension)#:nodoc:
+ (template_extension == "rxml" ? "rxml" : "rhtml") + "_render"
+ end
+
+ def erb_template_exists?(template_path)#:nodoc:
+ template_exists?(template_path, "rhtml")
+ end
+
+ def builder_template_exists?(template_path)#:nodoc:
+ template_exists?(template_path, "rxml")
+ end
+
+ def file_exists?(template_path)#:nodoc:
+ erb_template_exists?(template_path) || builder_template_exists?(template_path)
+ end
+
+ # Returns true is the file may be rendered implicitly.
+ def file_public?(template_path)#:nodoc:
+ template_path.split("/").last[0,1] != "_"
+ end
+
+ private
+ def full_template_path(template_path, extension)
+ "#{@base_path}/#{template_path}.#{extension}"
+ end
+
+ def template_exists?(template_path, extension)
+ FileTest.exists?(full_template_path(template_path, extension))
+ end
+
+ def read_template_file(template_path)
+ unless cache_template_loading && @@loaded_templates[template_path]
+ @@loaded_templates[template_path] = File.read(template_path)
+ end
+
+ @@loaded_templates[template_path]
+ end
+
+ def rhtml_render(template, binding)
+ @@compiled_erb_templates[template] ||= ERB.new(template)
+ @@compiled_erb_templates[template].result(binding)
+ end
+
+ def rxml_render(template, binding)
+ @controller.headers["Content-Type"] ||= 'text/xml'
+ eval(template, binding)
+ end
+ end
+end
+
+require 'action_view/template_error'
diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb
new file mode 100644
index 0000000000..b02b807fe1
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/active_record_helper.rb
@@ -0,0 +1,171 @@
+require 'cgi'
+require File.dirname(__FILE__) + '/form_helper'
+
+module ActionView
+ class Base
+ @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>" }
+ cattr_accessor :field_error_proc
+ end
+
+ module Helpers
+ # The Active Record Helper makes it easier to create forms for records kept in instance variables. The most far-reaching is the form
+ # method that creates a complete form for all the basic content types of the record (not associations or aggregations, though). This
+ # is a great of making the record quickly available for editing, but likely to prove lacklusters for a complicated real-world form.
+ # In that case, it's better to use the input method and the specialized form methods in link:classes/ActionView/Helpers/FormHelper.html
+ module ActiveRecordHelper
+ # Returns a default input tag for the type of object returned by the method. Example
+ # (title is a VARCHAR column and holds "Hello World"):
+ # input("post", "title") =>
+ # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
+ def input(record_name, method)
+ InstanceTag.new(record_name, method, self).to_tag
+ end
+
+ # Returns an entire form with input tags and everything for a specified Active Record object. Example
+ # (post is a new record that has a title using VARCHAR and a body using TEXT):
+ # form("post") =>
+ # <form action='create' method='POST'>
+ # <p>
+ # <label for="post_title">Title</label><br />
+ # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
+ # </p>
+ # <p>
+ # <label for="post_body">Body</label><br />
+ # <textarea cols="40" id="post_body" name="post[body]" rows="20" wrap="virtual">
+ # Back to the hill and over it again!
+ # </textarea>
+ # </p>
+ # <input type='submit' value='Create' />
+ # </form>
+ #
+ # It's possible to specialize the form builder by using a different action name and by supplying another
+ # block renderer. Example (entry is a new record that has a message attribute using VARCHAR):
+ #
+ # form("entry", :action => "sign", :input_block =>
+ # Proc.new { |record, column| "#{column.human_name}: #{input(record, column.name)}<br />" }) =>
+ #
+ # <form action='sign' method='POST'>
+ # Message:
+ # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /><br />
+ # <input type='submit' value='Sign' />
+ # </form>
+ def form(record_name, options = {})
+ record = instance_eval("@#{record_name}")
+ action = options[:action] || (record.new_record? ? "create" : "update")
+ id_field = record.new_record? ? "" : InstanceTag.new(record_name, "id", self).to_input_field_tag("hidden")
+
+ "<form action='#{action}' method='POST'>" +
+ id_field + all_input_tags(record, record_name, options) +
+ "<input type='submit' value='#{action.gsub(/[^A-Za-z]/, "").capitalize}' />" +
+ "</form>"
+ end
+
+ # Returns a string containing the error message attached to the +method+ on the +object+, if one exists.
+ # This error message is wrapped in a DIV tag, which can be specialized to include both a +prepend_text+ and +append_text+
+ # to properly introduce the error and a +css_class+ to style it accordingly. Examples (post has an error message
+ # "can't be empty" on the title attribute):
+ #
+ # <%= error_message_on "post", "title" %> =>
+ # <div class="formError">can't be empty</div>
+ #
+ # <%= error_message_on "post", "title", "Title simply ", " (or it won't work)", "inputError" %> =>
+ # <div class="inputError">Title simply can't be empty (or it won't work)</div>
+ def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError")
+ if errors = instance_eval("@#{object}").errors.on(method)
+ "<div class=\"#{css_class}\">#{prepend_text + (errors.is_a?(Array) ? errors.first : errors) + append_text}</div>"
+ end
+ end
+
+ def error_messages_for(object_name)
+ object = instance_eval("@#{object_name}")
+ unless object.errors.empty?
+ "<div id=\"errorExplanation\">" +
+ "<h2>#{object.errors.count} error#{"s" unless object.errors.count == 1} prohibited this #{object_name.gsub("_", " ")} from being saved</h2>" +
+ "<p>There were problems with the following fields (marked in red below):</p>" +
+ "<ul>#{object.errors.full_messages.collect { |msg| "<li>#{msg}</li>"}}</ul>" +
+ "</div>"
+ end
+ end
+
+ private
+ def all_input_tags(record, record_name, options)
+ input_block = options[:input_block] || default_input_block
+ record.class.content_columns.collect{ |column| input_block.call(record_name, column) }.join("\n")
+ end
+
+ def default_input_block
+ Proc.new { |record, column| "<p><label for=\"#{record}_#{column.name}\">#{column.human_name}</label><br />#{input(record, column.name)}</p>" }
+ end
+ end
+
+ class InstanceTag #:nodoc:
+ def to_tag(options = {})
+ case column_type
+ when :string
+ field_type = @method_name.include?("password") ? "password" : "text"
+ to_input_field_tag(field_type, options)
+ when :text
+ to_text_area_tag(options)
+ when :integer, :float
+ to_input_field_tag("text", options)
+ when :date
+ to_date_select_tag(options)
+ when :datetime
+ to_datetime_select_tag(options)
+ when :boolean
+ to_boolean_select_tag(options)
+ end
+ end
+
+ alias_method :tag_without_error_wrapping, :tag
+
+ def tag(name, options)
+ if object.respond_to?("errors") && object.errors.respond_to?("on")
+ error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name))
+ else
+ tag_without_error_wrapping(name, options)
+ end
+ end
+
+ alias_method :content_tag_without_error_wrapping, :content_tag
+
+ def content_tag(name, value, options)
+ if object.respond_to?("errors") && object.errors.respond_to?("on")
+ error_wrapping(content_tag_without_error_wrapping(name, value, options), object.errors.on(@method_name))
+ else
+ content_tag_without_error_wrapping(name, value, options)
+ end
+ end
+
+ alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag
+ def to_date_select_tag(options = {})
+ if object.respond_to?("errors") && object.errors.respond_to?("on")
+ error_wrapping(to_date_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
+ else
+ to_date_select_tag_without_error_wrapping(options)
+ end
+ end
+
+ alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag
+ def to_datetime_select_tag(options = {})
+ if object.respond_to?("errors") && object.errors.respond_to?("on")
+ error_wrapping(to_datetime_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
+ else
+ to_datetime_select_tag_without_error_wrapping(options)
+ end
+ end
+
+ def error_wrapping(html_tag, has_error)
+ has_error ? Base.field_error_proc.call(html_tag, self) : html_tag
+ end
+
+ def error_message
+ object.errors.on(@method_name)
+ end
+
+ def column_type
+ object.send("column_for_attribute", @method_name).type
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb
new file mode 100755
index 0000000000..5526c3eef4
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/date_helper.rb
@@ -0,0 +1,230 @@
+require "date"
+
+module ActionView
+ module Helpers
+ # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods
+ # share a number of common options that are as follows:
+ #
+ # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give
+ # birthday[month] instead of date[month] if passed to the select_month method.
+ # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date.
+ # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true, the select_month
+ # method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of "date[month]".
+ module DateHelper
+ DEFAULT_PREFIX = "date" unless const_defined?("DEFAULT_PREFIX")
+
+ # Reports the approximate distance in time between to Time objects. For example, if the distance is 47 minutes, it'll return
+ # "about 1 hour". See the source for the complete wording list.
+ def distance_of_time_in_words(from_time, to_time)
+ distance_in_minutes = ((to_time - from_time) / 60).round
+
+ case distance_in_minutes
+ when 0 then "less than a minute"
+ when 1 then "1 minute"
+ when 2..45 then "#{distance_in_minutes} minutes"
+ when 46..90 then "about 1 hour"
+ when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
+ when 1441..2880 then "1 day"
+ else "#{(distance_in_minutes / 1440).round} days"
+ end
+ end
+
+ # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
+ def distance_of_time_in_words_to_now(from_time)
+ distance_of_time_in_words(from_time, Time.now)
+ end
+
+ # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by
+ # +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash,
+ # which both accepts all the keys that each of the individual select builders does (like :use_month_numbers for select_month) and a range
+ # of discard options. The discard options are <tt>:discard_month</tt> and <tt>:discard_day</tt>. Set to true, they'll drop the respective
+ # select. Discarding the month select will also automatically discard the day select.
+ #
+ # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. Additionally, you can get the
+ # month select before the year by setting :month_before_year to true in the options. This is especially useful for credit card forms.
+ # Examples:
+ #
+ # date_select("post", "written_on")
+ # date_select("post", "written_on", :start_year => 1995)
+ # date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true,
+ # :discard_day => true, :include_blank => true)
+ #
+ # The selects are prepared for multi-parameter assignment to an Active Record object.
+ def date_select(object, method, options = {})
+ InstanceTag.new(object, method, self).to_date_select_tag(options)
+ end
+
+ # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based
+ # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples:
+ #
+ # datetime_select("post", "written_on")
+ # datetime_select("post", "written_on", :start_year => 1995)
+ #
+ # The selects are prepared for multi-parameter assignment to an Active Record object.
+ def datetime_select(object, method, options = {})
+ InstanceTag.new(object, method, self).to_datetime_select_tag(options)
+ end
+
+ # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
+ def select_date(date = Date.today, options = {})
+ select_year(date, options) + select_month(date, options) + select_day(date, options)
+ end
+
+ # Returns a set of html select-tags (one for year, month, day, hour, and minute) preselected the +datetime+.
+ def select_datetime(datetime = Time.now, options = {})
+ select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) +
+ select_hour(datetime, options) + select_minute(datetime, options)
+ end
+
+ # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
+ # The <tt>minute</tt> can also be substituted for a minute number.
+ def select_minute(datetime, options = {})
+ minute_options = []
+
+ 0.upto(59) do |minute|
+ minute_options << ((datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute ?
+ "<option selected=\"selected\">#{leading_zero_on_single_digits(minute)}</option>\n" :
+ "<option>#{leading_zero_on_single_digits(minute)}</option>\n"
+ )
+ end
+
+ select_html("minute", minute_options, options[:prefix], options[:include_blank], options[:discard_type])
+ end
+
+ # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
+ # The <tt>hour</tt> can also be substituted for a hour number.
+ def select_hour(datetime, options = {})
+ hour_options = []
+
+ 0.upto(23) do |hour|
+ hour_options << ((datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour ?
+ "<option selected=\"selected\">#{leading_zero_on_single_digits(hour)}</option>\n" :
+ "<option>#{leading_zero_on_single_digits(hour)}</option>\n"
+ )
+ end
+
+ select_html("hour", hour_options, options[:prefix], options[:include_blank], options[:discard_type])
+ end
+
+ # Returns a select tag with options for each of the days 1 through 31 with the current day selected.
+ # The <tt>date</tt> can also be substituted for a hour number.
+ def select_day(date, options = {})
+ day_options = []
+
+ 1.upto(31) do |day|
+ day_options << ((date.kind_of?(Fixnum) ? date : date.day) == day ?
+ "<option selected=\"selected\">#{day}</option>\n" :
+ "<option>#{day}</option>\n"
+ )
+ end
+
+ select_html("day", day_options, options[:prefix], options[:include_blank], options[:discard_type])
+ end
+
+ # Returns a select tag with options for each of the months January through December with the current month selected.
+ # The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values
+ # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names --
+ # set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names,
+ # set the <tt>:add_month_numbers</tt> key in +options+ to true. Examples:
+ #
+ # select_month(Date.today) # Will use keys like "January", "March"
+ # select_month(Date.today, :use_month_numbers => true) # Will use keys like "1", "3"
+ # select_month(Date.today, :add_month_numbers => true) # Will use keys like "1 - January", "3 - March"
+ def select_month(date, options = {})
+ month_options = []
+
+ 1.upto(12) do |month_number|
+ month_name = if options[:use_month_numbers]
+ month_number
+ elsif options[:add_month_numbers]
+ month_number.to_s + " - " + Date::MONTHNAMES[month_number]
+ else
+ Date::MONTHNAMES[month_number]
+ end
+
+ month_options << ((date.kind_of?(Fixnum) ? date : date.month) == month_number ?
+ "<option value='#{month_number}' selected=\"selected\">#{month_name}</option>\n" :
+ "<option value='#{month_number}'>#{month_name}</option>\n"
+ )
+ end
+
+ select_html("month", month_options, options[:prefix], options[:include_blank], options[:discard_type])
+ end
+
+ # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius
+ # can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the +options+. The <tt>date</tt> can also be substituted
+ # for a year given as a number. Example:
+ #
+ # select_year(Date.today, :start_year => 1992, :end_year => 2007)
+ def select_year(date, options = {})
+ year_options = []
+ unless date.kind_of?(Fixnum) then default_start_year, default_end_year = date.year - 5, date.year + 5 end
+
+ (options[:start_year] || default_start_year).upto(options[:end_year] || default_end_year) do |year|
+ year_options << ((date.kind_of?(Fixnum) ? date : date.year) == year ?
+ "<option selected=\"selected\">#{year}</option>\n" :
+ "<option>#{year}</option>\n"
+ )
+ end
+
+ select_html("year", year_options, options[:prefix], options[:include_blank], options[:discard_type])
+ end
+
+ private
+ def select_html(type, options, prefix = nil, include_blank = false, discard_type = false)
+ select_html = "<select name='#{prefix || DEFAULT_PREFIX}"
+ select_html << "[#{type}]" unless discard_type
+ select_html << "'>\n"
+ select_html << "<option></option>\n" if include_blank
+ select_html << options.to_s
+ select_html << "</select>\n"
+
+ return select_html
+ end
+
+ def leading_zero_on_single_digits(number)
+ number > 9 ? number : "0#{number}"
+ end
+ end
+
+ class InstanceTag #:nodoc:
+ include DateHelper
+
+ def to_date_select_tag(options = {})
+ defaults = { :discard_type => true }
+ options = defaults.merge(options)
+ options_with_prefix = Proc.new { |position| options.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) }
+ date = options[:include_blank] ? (value || 0) : (value || Date.today)
+
+ date_select = ""
+
+ if options[:month_before_year]
+ date_select << select_month(date, options_with_prefix.call(2)) unless options[:discard_month]
+ date_select << select_year(date, options_with_prefix.call(1))
+ else
+ date_select << select_year(date, options_with_prefix.call(1))
+ date_select << select_month(date, options_with_prefix.call(2)) unless options[:discard_month]
+ end
+
+ date_select << select_day(date, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month]
+
+ return date_select
+ end
+
+ def to_datetime_select_tag(options = {})
+ defaults = { :discard_type => true }
+ options = defaults.merge(options)
+ options_with_prefix = Proc.new { |position| options.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) }
+ datetime = options[:include_blank] ? (value || 0) : (value || Time.now)
+
+ datetime_select = select_year(datetime, options_with_prefix.call(1))
+ datetime_select << select_month(datetime, options_with_prefix.call(2)) unless options[:discard_month]
+ datetime_select << select_day(datetime, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month]
+ datetime_select << " &mdash; " + select_hour(datetime, options_with_prefix.call(4)) unless options[:discard_hour]
+ datetime_select << " : " + select_minute(datetime, options_with_prefix.call(5)) unless options[:discard_minute] || options[:discard_hour]
+
+ return datetime_select
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb
new file mode 100644
index 0000000000..8baea6f450
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/debug_helper.rb
@@ -0,0 +1,17 @@
+module ActionView
+ module Helpers
+ # Provides a set of methods for making it easier to locate problems.
+ module DebugHelper
+ # Returns a <pre>-tag set with the +object+ dumped by YAML. Very readable way to inspect an object.
+ def debug(object)
+ begin
+ Marshal::dump(object)
+ "<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", "&nbsp; ")}</pre>"
+ rescue Object => e
+ # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
+ "<code class='debug_dump'>#{h(object.inspect)}</code>"
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb
new file mode 100644
index 0000000000..389aa302a9
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/form_helper.rb
@@ -0,0 +1,182 @@
+require 'cgi'
+require File.dirname(__FILE__) + '/date_helper'
+require File.dirname(__FILE__) + '/tag_helper'
+
+module ActionView
+ module Helpers
+ # Provides a set of methods for working with forms and especially forms related to objects assigned to the template.
+ # The following is an example of a complete form for a person object that works for both creates and updates built
+ # with all the form helpers. The <tt>@person</tt> object was assigned by an action on the controller:
+ # <form action="save_person" method="post">
+ # Name:
+ # <%= text_field "person", "name", "size" => 20 %>
+ #
+ # Password:
+ # <%= password_field "person", "password", "maxsize" => 20 %>
+ #
+ # Single?:
+ # <%= check_box "person", "single" %>
+ #
+ # Description:
+ # <%= text_area "person", "description", "cols" => 20 %>
+ #
+ # <input type="submit" value="Save">
+ # </form>
+ #
+ # ...is compiled to:
+ #
+ # <form action="save_person" method="post">
+ # Name:
+ # <input type="text" id="person_name" name="person[name]"
+ # size="20" value="<%= @person.name %>" />
+ #
+ # Password:
+ # <input type="password" id="person_password" name="person[password]"
+ # size="20" maxsize="20" value="<%= @person.password %>" />
+ #
+ # Single?:
+ # <input type="checkbox" id="person_single" name="person[single] value="1" />
+ #
+ # Description:
+ # <textarea cols="20" rows="40" id="person_description" name="person[description]">
+ # <%= @person.description %>
+ # </textarea>
+ #
+ # <input type="submit" value="Save">
+ # </form>
+ #
+ # There's also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html,
+ # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html
+ module FormHelper
+ # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+.
+ #
+ # Examples (call, result):
+ # text_field("post", "title", "size" => 20)
+ # <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" />
+ def text_field(object, method, options = {})
+ InstanceTag.new(object, method, self).to_input_field_tag("text", options)
+ end
+
+ # Works just like text_field, but returns a input tag of the "password" type instead.
+ def password_field(object, method, options = {})
+ InstanceTag.new(object, method, self).to_input_field_tag("password", options)
+ end
+
+ # Works just like text_field, but returns a input tag of the "hidden" type instead.
+ def hidden_field(object, method, options = {})
+ InstanceTag.new(object, method, self).to_input_field_tag("hidden", options)
+ end
+
+ # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
+ # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+.
+ #
+ # Example (call, result):
+ # text_area("post", "body", "cols" => 20, "rows" => 40)
+ # <textarea cols="20" rows="40" id="post_body" name="post[body]">
+ # #{@post.body}
+ # </textarea>
+ def text_area(object, method, options = {})
+ InstanceTag.new(object, method, self).to_text_area_tag(options)
+ end
+
+ # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that
+ # integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a
+ # hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+
+ # is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything.
+ # We work around this problem by adding a hidden value with the same name as the checkbox.
+ #
+ # Example (call, result). Imagine that @post.validated? returns 1:
+ # check_box("post", "validated")
+ # <input type="checkbox" id="post_validate" name="post[validated] value="1" checked="checked" /><input name="post[validated]" type="hidden" value="0" />
+ #
+ # Example (call, result). Imagine that @puppy.gooddog returns no:
+ # check_box("puppy", "gooddog", {}, "yes", "no")
+ # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog] value="yes" /><input name="puppy[gooddog]" type="hidden" value="no" />
+ def check_box(object, method, options = {}, checked_value = "1", unchecked_value = "0")
+ InstanceTag.new(object, method, self).to_check_box_tag(options, checked_value, unchecked_value)
+ end
+ end
+
+ class InstanceTag #:nodoc:
+ include Helpers::TagHelper
+
+ attr_reader :method_name, :object_name
+
+ DEFAULT_FIELD_OPTIONS = { "size" => 30 } unless const_defined?("DEFAULT_FIELD_OPTIONS")
+ DEFAULT_TEXT_AREA_OPTIONS = { "wrap" => "virtual", "cols" => 40, "rows" => 20 } unless const_defined?("DEFAULT_TEXT_AREA_OPTIONS")
+
+ def initialize(object_name, method_name, template_object, local_binding = nil)
+ @object_name, @method_name = object_name, method_name
+ @template_object, @local_binding = template_object, local_binding
+ end
+
+ def to_input_field_tag(field_type, options = {})
+ html_options = DEFAULT_FIELD_OPTIONS.merge(options)
+ html_options.merge!({ "size" => options["maxlength"]}) if options["maxlength"] && !options["size"]
+ html_options.merge!({ "type" => field_type, "value" => value.to_s })
+ add_default_name_and_id(html_options)
+ tag("input", html_options)
+ end
+
+ def to_text_area_tag(options = {})
+ options = DEFAULT_TEXT_AREA_OPTIONS.merge(options)
+ add_default_name_and_id(options)
+ content_tag("textarea", html_escape(value), options)
+ end
+
+ def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
+ options.merge!({"checked" => "checked"}) if !value.nil? && ((value.is_a?(TrueClass) || value.is_a?(FalseClass)) ? value : value.to_i > 0)
+ options.merge!({ "type" => "checkbox", "value" => checked_value })
+ add_default_name_and_id(options)
+ tag("input", options) << tag("input", ({ "name" => options['name'], "type" => "hidden", "value" => unchecked_value }))
+ end
+
+ def to_date_tag()
+ defaults = { "discard_type" => true }
+ date = value || Date.today
+ options = Proc.new { |position| defaults.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) }
+
+ html_day_select(date, options.call(3)) +
+ html_month_select(date, options.call(2)) +
+ html_year_select(date, options.call(1))
+ end
+
+ def to_boolean_select_tag(options = {})
+ add_default_name_and_id(options)
+ tag_text = "<select"
+ tag_text << tag_options(options)
+ tag_text << "><option value=\"false\""
+ tag_text << " selected" if value == false
+ tag_text << ">False</option><option value=\"true\""
+ tag_text << " selected" if value
+ tag_text << ">True</option></select>"
+ end
+
+ def object
+ @template_object.instance_variable_get "@#{@object_name}"
+ end
+
+ def value
+ object.send(@method_name) unless object.nil?
+ end
+
+ private
+ def add_default_name_and_id(options)
+ options['name'] = tag_name unless options.has_key? "name"
+ options['id'] = tag_id unless options.has_key? "id"
+ end
+
+ def tag_name
+ "#{@object_name}[#{@method_name}]"
+ end
+
+ def tag_id
+ "#{@object_name}_#{@method_name}"
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb
new file mode 100644
index 0000000000..ca3798ede6
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/form_options_helper.rb
@@ -0,0 +1,212 @@
+require 'cgi'
+require 'erb'
+require File.dirname(__FILE__) + '/form_helper'
+
+module ActionView
+ module Helpers
+ # Provides a number of methods for turning different kinds of containers into a set of option tags. Neither of the methods provide
+ # the actual select tag, so you'll need to construct that in HTML manually.
+ module FormOptionsHelper
+ include ERB::Util
+
+ def select(object, method, choices, options = {}, html_options = {})
+ InstanceTag.new(object, method, self).to_select_tag(choices, options, html_options)
+ end
+
+ def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
+ InstanceTag.new(object, method, self).to_collection_select_tag(collection, value_method, text_method, options, html_options)
+ end
+
+ def country_select(object, method, priority_countries = nil, options = {}, html_options = {})
+ InstanceTag.new(object, method, self).to_country_select_tag(priority_countries, options, html_options)
+ end
+
+ # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
+ # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
+ # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
+ # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +Selected+
+ # may also be an array of values to be selected when using a multiple select.
+ #
+ # Examples (call, result):
+ # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
+ # <option value="$">Dollar</option>\n<option value="DKK">Kroner</option>
+ #
+ # options_for_select([ "VISA", "Mastercard" ], "Mastercard")
+ # <option>VISA</option>\n<option selected="selected">Mastercard</option>
+ #
+ # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
+ # <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option>
+ #
+ # options_for_select([ "VISA", "Mastercard", "Discover" ], ["VISA", "Discover"])
+ # <option selected="selected">VISA</option>\n<option>Mastercard</option>\n<option selected="selected">Discover</option>
+ def options_for_select(container, selected = nil)
+ container = container.to_a if Hash === container
+
+ options_for_select = container.inject([]) do |options, element|
+ if element.respond_to?(:first) && element.respond_to?(:last)
+ is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element.last) : element.last == selected) )
+ if is_selected
+ options << "<option value=\"#{html_escape(element.last.to_s)}\" selected=\"selected\">#{html_escape(element.first.to_s)}</option>"
+ else
+ options << "<option value=\"#{html_escape(element.last.to_s)}\">#{html_escape(element.first.to_s)}</option>"
+ end
+ else
+ is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element) : element == selected) )
+ options << ((is_selected) ? "<option selected=\"selected\">#{html_escape(element.to_s)}</option>" : "<option>#{html_escape(element.to_s)}</option>")
+ end
+ end
+
+ options_for_select.join("\n")
+ end
+
+ # Returns a string of option tags that has been compiled by iterating over the +collection+ and assigning the
+ # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
+ # If +selected_value+ is specified, the element returning a match on +value_method+ will get the selected option tag.
+ #
+ # Example (call, result). Imagine a loop iterating over each +person+ in <tt>@project.people</tt> to generate a input tag:
+ # options_from_collection_for_select(@project.people, "id", "name")
+ # <option value="#{person.id}">#{person.name}</option>
+ def options_from_collection_for_select(collection, value_method, text_method, selected_value = nil)
+ options_for_select(
+ collection.inject([]) { |options, object| options << [ object.send(text_method), object.send(value_method) ] },
+ selected_value
+ )
+ end
+
+ # Returns a string of option tags, like options_from_collection_for_select, but surrounds them by <optgroup> tags.
+ #
+ # An array of group objects are passed. Each group should return an array of options when calling group_method
+ # Each group should should return its name when calling group_label_method.
+ #
+ # html_option_groups_from_collection(@continents, "countries", "contient_name", "country_id", "country_name", @selected_country.id)
+ #
+ # Could become:
+ # <optgroup label="Africa">
+ # <select>Egypt</select>
+ # <select>Rwanda</select>
+ # ...
+ # </optgroup>
+ # <optgroup label="Asia">
+ # <select>China</select>
+ # <select>India</select>
+ # <select>Japan</select>
+ # ...
+ # </optgroup>
+ #
+ # with objects of the following classes:
+ # class Continent
+ # def initialize(p_name, p_countries) @continent_name = p_name; @countries = p_countries; end
+ # def continent_name() @continent_name; end
+ # def countries() @countries; end
+ # end
+ # class Country
+ # def initialize(id, name) @id = id; @name = name end
+ # def country_id() @id; end
+ # def country_name() @name; end
+ # end
+ def option_groups_from_collection_for_select(collection, group_method, group_label_method,
+ option_key_method, option_value_method, selected_key = nil)
+ collection.inject("") do |options_for_select, group|
+ group_label_string = eval("group.#{group_label_method}")
+ options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">"
+ options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key)
+ options_for_select += '</optgroup>'
+ end
+ end
+
+ # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to
+ # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so
+ # that they will be listed above the rest of the (long) list.
+ def country_options_for_select(selected = nil, priority_countries = nil)
+ country_options = ""
+
+ if priority_countries
+ country_options += options_for_select(priority_countries, selected)
+ country_options += "<option>-------------</option>\n"
+ end
+
+ if priority_countries && priority_countries.include?(selected)
+ country_options += options_for_select(COUNTRIES - priority_countries, selected)
+ else
+ country_options += options_for_select(COUNTRIES, selected)
+ end
+
+ return country_options
+ end
+
+
+ private
+ # All the countries included in the country_options output.
+ COUNTRIES = [ "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla",
+ "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia",
+ "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus",
+ "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina",
+ "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory",
+ "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burma", "Burundi", "Cambodia",
+ "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic",
+ "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia",
+ "Comoros", "Congo", "Congo, the Democratic Republic of the", "Cook Islands",
+ "Costa Rica", "Cote d'Ivoire", "Croatia", "Cyprus", "Czech Republic", "Denmark",
+ "Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt",
+ "El Salvador", "England", "Equatorial Guinea", "Eritrea", "Espana", "Estonia",
+ "Ethiopia", "Falkland Islands", "Faroe Islands", "Fiji", "Finland", "France",
+ "French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia",
+ "Georgia", "Germany", "Ghana", "Gibraltar", "Great Britain", "Greece", "Greenland",
+ "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana",
+ "Haiti", "Heard and Mc Donald Islands", "Honduras", "Hong Kong", "Hungary", "Iceland",
+ "India", "Indonesia", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan",
+ "Kazakhstan", "Kenya", "Kiribati", "Korea, Republic of", "Korea (South)", "Kuwait",
+ "Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho",
+ "Liberia", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia",
+ "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands",
+ "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico",
+ "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia",
+ "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal",
+ "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua",
+ "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Ireland",
+ "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Panama",
+ "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland",
+ "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russia", "Rwanda",
+ "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines",
+ "Samoa (Independent)", "San Marino", "Sao Tome and Principe", "Saudi Arabia",
+ "Scotland", "Senegal", "Seychelles", "Sierra Leone", "Singapore", "Slovakia",
+ "Slovenia", "Solomon Islands", "Somalia", "South Africa",
+ "South Georgia and the South Sandwich Islands", "South Korea", "Spain", "Sri Lanka",
+ "St. Helena", "St. Pierre and Miquelon", "Suriname", "Svalbard and Jan Mayen Islands",
+ "Swaziland", "Sweden", "Switzerland", "Taiwan", "Tajikistan", "Tanzania", "Thailand",
+ "Togo", "Tokelau", "Tonga", "Trinidad", "Trinidad and Tobago", "Tunisia", "Turkey",
+ "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine",
+ "United Arab Emirates", "United Kingdom", "United States",
+ "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu",
+ "Vatican City State (Holy See)", "Venezuela", "Viet Nam", "Virgin Islands (British)",
+ "Virgin Islands (U.S.)", "Wales", "Wallis and Futuna Islands", "Western Sahara",
+ "Yemen", "Zambia", "Zimbabwe" ] unless const_defined?("COUNTRIES")
+ end
+
+ class InstanceTag #:nodoc:
+ include FormOptionsHelper
+
+ def to_select_tag(choices, options, html_options)
+ add_default_name_and_id(html_options)
+ content_tag("select", add_blank_option(options_for_select(choices, value), options[:include_blank]), html_options)
+ end
+
+ def to_collection_select_tag(collection, value_method, text_method, options, html_options)
+ add_default_name_and_id(html_options)
+ content_tag(
+ "select", add_blank_option(options_from_collection_for_select(collection, value_method, text_method, value), options[:include_blank]), html_options
+ )
+ end
+
+ def to_country_select_tag(priority_countries, options, html_options)
+ add_default_name_and_id(html_options)
+ content_tag("select", add_blank_option(country_options_for_select(value, priority_countries), options[:include_blank]), html_options)
+ end
+
+ private
+ def add_blank_option(option_tags, add_blank)
+ add_blank ? "<option></option>\n" + option_tags : option_tags
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb
new file mode 100644
index 0000000000..90084c7a8d
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/tag_helper.rb
@@ -0,0 +1,59 @@
+require 'cgi'
+
+module ActionView
+ module Helpers
+ # This is poor man's Builder for the rare cases where you need to programmatically make tags but can't use Builder.
+ module TagHelper
+ include ERB::Util
+
+ # Examples:
+ # * tag("br") => <br />
+ # * tag("input", { "type" => "text"}) => <input type="text" />
+ def tag(name, options = {}, open = false)
+ "<#{name + tag_options(options)}" + (open ? ">" : " />")
+ end
+
+ # Examples:
+ # * content_tag("p", "Hello world!") => <p>Hello world!</p>
+ # * content_tag("div", content_tag("p", "Hello world!"), "class" => "strong") =>
+ # <div class="strong"><p>Hello world!</p></div>
+ def content_tag(name, content, options = {})
+ "<#{name + tag_options(options)}>#{content}</#{name}>"
+ end
+
+ # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
+ # ActionController::Base#url_for.
+ def form_tag(url_for_options, options = {}, *parameters_for_url)
+ html_options = { "method" => "POST" }.merge(options)
+
+ if html_options[:multipart]
+ html_options["enctype"] = "multipart/form-data"
+ html_options.delete(:multipart)
+ end
+
+ html_options["action"] = url_for(url_for_options, *parameters_for_url)
+
+ tag("form", html_options, true)
+ end
+
+ alias_method :start_form_tag, :form_tag
+
+ # Outputs "</form>"
+ def end_form_tag
+ "</form>"
+ end
+
+
+ private
+ def tag_options(options)
+ if options.empty?
+ ""
+ else
+ " " + options.collect { |pair|
+ "#{pair.first}=\"#{html_escape(pair.last)}\""
+ }.sort.join(" ")
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb
new file mode 100644
index 0000000000..7e05e468b8
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/text_helper.rb
@@ -0,0 +1,111 @@
+module ActionView
+ module Helpers #:nodoc:
+ # Provides a set of methods for working with text strings that can help unburden the level of inline Ruby code in the
+ # templates. In the example below we iterate over a collection of posts provided to the template and prints each title
+ # after making sure it doesn't run longer than 20 characters:
+ # <% for post in @posts %>
+ # Title: <%= truncate(post.title, 20) %>
+ # <% end %>
+ module TextHelper
+ # The regular puts and print are outlawed in eRuby. It's recommended to use the <%= "hello" %> form instead of print "hello".
+ # If you absolutely must use a method-based output, you can use concat. It's use like this <% concat "hello", binding %>. Notice that
+ # it doesn't have an equal sign in front. Using <%= concat "hello" %> would result in a double hello.
+ def concat(string, binding)
+ eval("_erbout", binding).concat(string)
+ end
+
+ # Truncates +text+ to the length of +length+ and replaces the last three characters with the +truncate_string+
+ # if the +text+ is longer than +length+.
+ def truncate(text, length = 30, truncate_string = "...")
+ if text.nil? then return end
+ if text.length > length then text[0..(length - 3)] + truncate_string else text end
+ end
+
+ # Highlights the +phrase+ where it is found in the +text+ by surrounding it like
+ # <strong class="highlight">I'm a highlight phrase</strong>. The highlighter can be specialized by
+ # passing +highlighter+ as single-quoted string with \1 where the phrase is supposed to be inserted.
+ # N.B.: The +phrase+ is sanitized to include only letters, digits, and spaces before use.
+ def highlight(text, phrase, highlighter = '<strong class="highlight">\1</strong>')
+ if text.nil? || phrase.nil? then return end
+ text.gsub(/(#{escape_regexp(phrase)})/i, highlighter) unless text.nil?
+ end
+
+ # Extracts an excerpt from the +text+ surrounding the +phrase+ with a number of characters on each side determined
+ # by +radius+. If the phrase isn't found, nil is returned. Ex:
+ # excerpt("hello my world", "my", 3) => "...lo my wo..."
+ def excerpt(text, phrase, radius = 100, excerpt_string = "...")
+ if text.nil? || phrase.nil? then return end
+ phrase = escape_regexp(phrase)
+
+ if found_pos = text =~ /(#{phrase})/i
+ start_pos = [ found_pos - radius, 0 ].max
+ end_pos = [ found_pos + phrase.length + radius, text.length ].min
+
+ prefix = start_pos > 0 ? excerpt_string : ""
+ postfix = end_pos < text.length ? excerpt_string : ""
+
+ prefix + text[start_pos..end_pos].strip + postfix
+ else
+ nil
+ end
+ end
+
+ # Attempts to pluralize the +singular+ word unless +count+ is 1. See source for pluralization rules.
+ def pluralize(count, singular, plural = nil)
+ "#{count} " + if count == 1
+ singular
+ elsif plural
+ plural
+ elsif Object.const_defined?("Inflector")
+ Inflector.pluralize(singular)
+ else
+ singular + "s"
+ end
+ end
+
+ begin
+ require "redcloth"
+
+ # Returns the text with all the Textile codes turned into HTML-tags.
+ # <i>This method is only available if RedCloth can be required</i>.
+ def textilize(text)
+ RedCloth.new(text).to_html
+ end
+
+ # Returns the text with all the Textile codes turned into HTML-tags, but without the regular bounding <p> tag.
+ # <i>This method is only available if RedCloth can be required</i>.
+ def textilize_without_paragraph(text)
+ textiled = textilize(text)
+ if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end
+ if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end
+ return textiled
+ end
+ rescue LoadError
+ # We can't really help what's not there
+ end
+
+ begin
+ require "bluecloth"
+
+ # Returns the text with all the Markdown codes turned into HTML-tags.
+ # <i>This method is only available if BlueCloth can be required</i>.
+ def markdown(text)
+ BlueCloth.new(text).to_html
+ end
+ rescue LoadError
+ # We can't really help what's not there
+ end
+
+ # Turns all links into words, like "<a href="something">else</a>" to "else".
+ def strip_links(text)
+ text.gsub(/<a.*>(.*)<\/a>/m, '\1')
+ end
+
+ private
+ # Returns a version of the text that's safe to use in a regular expression without triggering engine features.
+ def escape_regexp(text)
+ text.gsub(/([\\|?+*\/\)\(])/) { |m| "\\#{$1}" }
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb
new file mode 100644
index 0000000000..feda33d7c1
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/url_helper.rb
@@ -0,0 +1,78 @@
+module ActionView
+ module Helpers
+ # Provides a set of methods for making easy links and getting urls that depend on the controller and action. This means that
+ # you can use the same format for links in the views that you do in the controller. The different methods are even named
+ # synchronously, so link_to uses that same url as is generated by url_for, which again is the same url used for
+ # redirection in redirect_to.
+ module UrlHelper
+ # Returns the URL for the set of +options+ provided. See the valid options in link:classes/ActionController/Base.html#M000021
+ def url_for(options = {}, *parameters_for_method_reference)
+ if Hash === options then options = { :only_path => true }.merge(options) end
+ @controller.send(:url_for, options, *parameters_for_method_reference)
+ end
+
+ # Creates a link tag of the given +name+ using an URL created by the set of +options+. See the valid options in
+ # link:classes/ActionController/Base.html#M000021. It's also possible to pass a string instead of an options hash to
+ # get a link tag that just points without consideration. The html_options have a special feature for creating javascript
+ # confirm alerts where if you pass :confirm => 'Are you sure?', the link will be guarded with a JS popup asking that question.
+ # If the user accepts, the link is processed, otherwise not.
+ def link_to(name, options = {}, html_options = {}, *parameters_for_method_reference)
+ convert_confirm_option_to_javascript!(html_options) unless html_options.nil?
+ if options.is_a?(String)
+ content_tag "a", name, (html_options || {}).merge({ "href" => options })
+ else
+ content_tag("a", name, (html_options || {}).merge({ "href" => url_for(options, *parameters_for_method_reference) }))
+ end
+ end
+
+ # Creates a link tag of the given +name+ using an URL created by the set of +options+, unless the current
+ # controller, action, and id are the same as the link's, in which case only the name is returned (or the
+ # given block is yielded, if one exists). This is useful for creating link bars where you don't want to link
+ # to the page currently being viewed.
+ def link_to_unless_current(name, options = {}, html_options = {}, *parameters_for_method_reference)
+ assume_current_url_options!(options)
+
+ if destination_equal_to_current(options)
+ block_given? ?
+ yield(name, options, html_options, *parameters_for_method_reference) :
+ html_escape(name)
+ else
+ link_to name, options, html_options, *parameters_for_method_reference
+ end
+ end
+
+ # Creates a link tag for starting an email to the specified <tt>email_address</tt>, which is also used as the name of the
+ # link unless +name+ is specified. Additional HTML options, such as class or id, can be passed in the <tt>html_options</tt> hash.
+ def mail_to(email_address, name = nil, html_options = {})
+ content_tag "a", name || email_address, html_options.merge({ "href" => "mailto:#{email_address}" })
+ end
+
+ private
+ def destination_equal_to_current(options)
+ params_without_location = @params.reject { |key, value| %w( controller action id ).include?(key) }
+
+ options[:action] == @params['action'] &&
+ options[:id] == @params['id'] &&
+ options[:controller] == @params['controller'] &&
+ (options.has_key?(:params) ? params_without_location == options[:params] : true)
+ end
+
+ def assume_current_url_options!(options)
+ if options[:controller].nil?
+ options[:controller] = @params['controller']
+ if options[:action].nil?
+ options[:action] = @params['action']
+ if options[:id].nil? then options[:id] ||= @params['id'] end
+ end
+ end
+ end
+
+ def convert_confirm_option_to_javascript!(html_options)
+ if html_options.include?(:confirm)
+ html_options["onclick"] = "return confirm('#{html_options[:confirm]}');"
+ html_options.delete(:confirm)
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb
new file mode 100644
index 0000000000..96bde4c6d3
--- /dev/null
+++ b/actionpack/lib/action_view/partials.rb
@@ -0,0 +1,64 @@
+module ActionView
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a single object
+ # (we call this kind of sub templates for partials). It relies on the fact that partials should follow the naming convention of being
+ # prefixed with an underscore -- as to separate them from regular templates that could be rendered on their own. In the template for
+ # Advertiser#buy, we could have:
+ #
+ # <% for ad in @advertisements %>
+ # <%= render_partial "ad", ad %>
+ # <% end %>
+ #
+ # This would render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display.
+ #
+ # == Rendering a collection of partials
+ #
+ # The example of partial use describes a familar pattern where a template needs to iterate over an array and render a sub
+ # template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders
+ # a partial by the same name as the elements contained within. So the three-lined example in "Using partials" can be rewritten
+ # with a single line:
+ #
+ # <%= render_collection_of_partials "ad", @advertisements %>
+ #
+ # This will render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. An iteration counter
+ # will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the
+ # example above, the template would be fed +ad_counter+.
+ #
+ # == Rendering shared partials
+ #
+ # Two controllers can share a set of partials and render them like this:
+ #
+ # <%= render_partial "advertisement/ad", ad %>
+ #
+ # This will render the partial "advertisement/_ad.rhtml" regardless of which controller this is being called from.
+ module Partials
+ def render_partial(partial_path, object = nil, local_assigns = {})
+ path, partial_name = partial_pieces(partial_path)
+ object ||= controller.instance_variable_get("@#{partial_name}")
+ render("#{path}/_#{partial_name}", { partial_name => object }.merge(local_assigns))
+ end
+
+ def render_collection_of_partials(partial_name, collection, partial_spacer_template = nil)
+ collection_of_partials = Array.new
+ collection.each_with_index do |element, counter|
+ collection_of_partials.push(render_partial(partial_name, element, "#{partial_name.split("/").last}_counter" => counter))
+ end
+
+ return nil if collection_of_partials.empty?
+ if partial_spacer_template
+ spacer_path, spacer_name = partial_pieces(partial_spacer_template)
+ collection_of_partials.join(render("#{spacer_path}/_#{spacer_name}"))
+ else
+ collection_of_partials
+ end
+ end
+
+ private
+ def partial_pieces(partial_path)
+ if partial_path.include?('/')
+ return File.dirname(partial_path), File.basename(partial_path)
+ else
+ return controller.send(:controller_name), partial_path
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template_error.rb b/actionpack/lib/action_view/template_error.rb
new file mode 100644
index 0000000000..ab05b3303f
--- /dev/null
+++ b/actionpack/lib/action_view/template_error.rb
@@ -0,0 +1,84 @@
+module ActionView
+ # The TemplateError exception is raised when the compilation of the template fails. This exception then gathers a
+ # bunch of intimate details and uses it to report a very precise exception message.
+ class TemplateError < ActionViewError #:nodoc:
+ SOURCE_CODE_RADIUS = 3
+
+ attr_reader :original_exception
+
+ def initialize(base_path, file_name, assigns, source, original_exception)
+ @base_path, @file_name, @assigns, @source, @original_exception =
+ base_path, file_name, assigns, source, original_exception
+ end
+
+ def message
+ if original_exception.message.include?("(eval):")
+ original_exception.message.scan(/\(eval\):(?:[0-9]*):in `.*'(.*)/).first.first
+ else
+ original_exception.message
+ end
+ end
+
+ def sub_template_message
+ if @sub_templates
+ "Trace of template inclusion: " +
+ @sub_templates.collect { |template| strip_base_path(template) }.join(", ")
+ else
+ ""
+ end
+ end
+
+ def source_extract
+ source_code = IO.readlines(@file_name)
+
+ start_on_line = [ line_number - SOURCE_CODE_RADIUS - 1, 0 ].max
+ end_on_line = [ line_number + SOURCE_CODE_RADIUS - 1, source_code.length].min
+
+ line_counter = start_on_line
+ extract = source_code[start_on_line..end_on_line].collect do |line|
+ line_counter += 1
+ "#{line_counter}: " + line
+ end
+
+ extract.join
+ end
+
+ def sub_template_of(file_name)
+ @sub_templates ||= []
+ @sub_templates << file_name
+ end
+
+ def line_number
+ begin
+ @original_exception.backtrace.join.scan(/\((?:erb)\):([0-9]*)/).first.first.to_i
+ rescue
+ begin
+ original_exception.message.scan(/\((?:eval)\):([0-9]*)/).first.first.to_i
+ rescue
+ 1
+ end
+ end
+ end
+
+ def file_name
+ strip_base_path(@file_name)
+ end
+
+ def to_s
+ "\n\n#{self.class} (#{message}) on line ##{line_number} of #{file_name}:\n" +
+ source_extract + "\n " +
+ clean_backtrace(original_exception).join("\n ") +
+ "\n\n"
+ end
+
+ private
+ def strip_base_path(file_name)
+ file_name.gsub(@base_path, "")
+ end
+
+ def clean_backtrace(exception)
+ base_dir = File.expand_path(File.dirname(__FILE__) + "/../../../../")
+ exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/public/../config/environments/../../", "").gsub("/public/../", "") }
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/vendor/builder.rb b/actionpack/lib/action_view/vendor/builder.rb
new file mode 100644
index 0000000000..9719277669
--- /dev/null
+++ b/actionpack/lib/action_view/vendor/builder.rb
@@ -0,0 +1,13 @@
+#!/usr/bin/env ruby
+
+#--
+# Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
+# All rights reserved.
+
+# Permission is granted for use, copying, modification, distribution,
+# and distribution of modified versions of this work as long as the
+# above copyright notice is included.
+#++
+
+require 'builder/xmlmarkup'
+require 'builder/xmlevents'
diff --git a/actionpack/lib/action_view/vendor/builder/blankslate.rb b/actionpack/lib/action_view/vendor/builder/blankslate.rb
new file mode 100644
index 0000000000..25307b0e56
--- /dev/null
+++ b/actionpack/lib/action_view/vendor/builder/blankslate.rb
@@ -0,0 +1,51 @@
+#!/usr/bin/env ruby
+#--
+# Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
+# All rights reserved.
+
+# Permission is granted for use, copying, modification, distribution,
+# and distribution of modified versions of this work as long as the
+# above copyright notice is included.
+#++
+
+module Builder #:nodoc:
+
+ # BlankSlate provides an abstract base class with no predefined
+ # methods (except for <tt>\_\_send__</tt> and <tt>\_\_id__</tt>).
+ # BlankSlate is useful as a base class when writing classes that
+ # depend upon <tt>method_missing</tt> (e.g. dynamic proxies).
+ class BlankSlate #:nodoc:
+ class << self
+ def hide(name)
+ undef_method name unless name =~ /^(__|instance_eval)/
+ end
+ end
+
+ instance_methods.each { |m| hide(m) }
+ end
+end
+
+# Since Ruby is very dynamic, methods added to the ancestors of
+# BlankSlate <em>after BlankSlate is defined</em> will show up in the
+# list of available BlankSlate methods. We handle this by defining a hook in the Object and Kernel classes that will hide any defined
+module Kernel #:nodoc:
+ class << self
+ alias_method :blank_slate_method_added, :method_added
+ def method_added(name)
+ blank_slate_method_added(name)
+ return if self != Kernel
+ Builder::BlankSlate.hide(name)
+ end
+ end
+end
+
+class Object #:nodoc:
+ class << self
+ alias_method :blank_slate_method_added, :method_added
+ def method_added(name)
+ blank_slate_method_added(name)
+ return if self != Object
+ Builder::BlankSlate.hide(name)
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/vendor/builder/xmlbase.rb b/actionpack/lib/action_view/vendor/builder/xmlbase.rb
new file mode 100644
index 0000000000..d065d6fae1
--- /dev/null
+++ b/actionpack/lib/action_view/vendor/builder/xmlbase.rb
@@ -0,0 +1,143 @@
+#!/usr/bin/env ruby
+
+require 'builder/blankslate'
+
+module Builder #:nodoc:
+
+ # Generic error for builder
+ class IllegalBlockError < RuntimeError #:nodoc:
+ end
+
+ # XmlBase is a base class for building XML builders. See
+ # Builder::XmlMarkup and Builder::XmlEvents for examples.
+ class XmlBase < BlankSlate #:nodoc:
+
+ # Create an XML markup builder.
+ #
+ # out:: Object receiving the markup.1 +out+ must respond to
+ # <tt><<</tt>.
+ # indent:: Number of spaces used for indentation (0 implies no
+ # indentation and no line breaks).
+ # initial:: Level of initial indentation.
+ #
+ def initialize(indent=0, initial=0)
+ @indent = indent
+ @level = initial
+ end
+
+ # Create a tag named +sym+. Other than the first argument which
+ # is the tag name, the arguements are the same as the tags
+ # implemented via <tt>method_missing</tt>.
+ def tag!(sym, *args, &block)
+ self.__send__(sym, *args, &block)
+ end
+
+ # Create XML markup based on the name of the method. This method
+ # is never invoked directly, but is called for each markup method
+ # in the markup block.
+ def method_missing(sym, *args, &block)
+ text = nil
+ attrs = nil
+ sym = "#{sym}:#{args.shift}" if args.first.kind_of?(Symbol)
+ args.each do |arg|
+ case arg
+ when Hash
+ attrs ||= {}
+ attrs.merge!(arg)
+ else
+ text ||= ''
+ text << arg.to_s
+ end
+ end
+ if block
+ unless text.nil?
+ raise ArgumentError, "XmlMarkup cannot mix a text argument with a block"
+ end
+ _capture_outer_self(block) if @self.nil?
+ _indent
+ _start_tag(sym, attrs)
+ _newline
+ _nested_structures(block)
+ _indent
+ _end_tag(sym)
+ _newline
+ elsif text.nil?
+ _indent
+ _start_tag(sym, attrs, true)
+ _newline
+ else
+ _indent
+ _start_tag(sym, attrs)
+ text! text
+ _end_tag(sym)
+ _newline
+ end
+ @target
+ end
+
+ # Append text to the output target. Escape any markup. May be
+ # used within the markup brakets as:
+ #
+ # builder.p { br; text! "HI" } #=> <p><br/>HI</p>
+ def text!(text)
+ _text(_escape(text))
+ end
+
+ # Append text to the output target without escaping any markup.
+ # May be used within the markup brakets as:
+ #
+ # builder.p { |x| x << "<br/>HI" } #=> <p><br/>HI</p>
+ #
+ # This is useful when using non-builder enabled software that
+ # generates strings. Just insert the string directly into the
+ # builder without changing the inserted markup.
+ #
+ # It is also useful for stacking builder objects. Builders only
+ # use <tt><<</tt> to append to the target, so by supporting this
+ # method/operation builders can use oother builders as their
+ # targets.
+ def <<(text)
+ _text(text)
+ end
+
+ # For some reason, nil? is sent to the XmlMarkup object. If nil?
+ # is not defined and method_missing is invoked, some strange kind
+ # of recursion happens. Since nil? won't ever be an XML tag, it
+ # is pretty safe to define it here. (Note: this is an example of
+ # cargo cult programming,
+ # cf. http://fishbowl.pastiche.org/2004/10/13/cargo_cult_programming).
+ def nil?
+ false
+ end
+
+ private
+
+ def _escape(text)
+ text.
+ gsub(%r{&}, '&amp;').
+ gsub(%r{<}, '&lt;').
+ gsub(%r{>}, '&gt;')
+ end
+
+ def _capture_outer_self(block)
+ @self = eval("self", block)
+ end
+
+ def _newline
+ return if @indent == 0
+ text! "\n"
+ end
+
+ def _indent
+ return if @indent == 0 || @level == 0
+ text!(" " * (@level * @indent))
+ end
+
+ def _nested_structures(block)
+ @level += 1
+ block.call(self)
+ ensure
+ @level -= 1
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/vendor/builder/xmlevents.rb b/actionpack/lib/action_view/vendor/builder/xmlevents.rb
new file mode 100644
index 0000000000..15dc7b6421
--- /dev/null
+++ b/actionpack/lib/action_view/vendor/builder/xmlevents.rb
@@ -0,0 +1,63 @@
+#!/usr/bin/env ruby
+
+#--
+# Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
+# All rights reserved.
+
+# Permission is granted for use, copying, modification, distribution,
+# and distribution of modified versions of this work as long as the
+# above copyright notice is included.
+#++
+
+require 'builder/xmlmarkup'
+
+module Builder
+
+ # Create a series of SAX-like XML events (e.g. start_tag, end_tag)
+ # from the markup code. XmlEvent objects are used in a way similar
+ # to XmlMarkup objects, except that a series of events are generated
+ # and passed to a handler rather than generating character-based
+ # markup.
+ #
+ # Usage:
+ # xe = Builder::XmlEvents.new(hander)
+ # xe.title("HI") # Sends start_tag/end_tag/text messages to the handler.
+ #
+ # Indentation may also be selected by providing value for the
+ # indentation size and initial indentation level.
+ #
+ # xe = Builder::XmlEvents.new(handler, indent_size, initial_indent_level)
+ #
+ # == XML Event Handler
+ #
+ # The handler object must expect the following events.
+ #
+ # [<tt>start_tag(tag, attrs)</tt>]
+ # Announces that a new tag has been found. +tag+ is the name of
+ # the tag and +attrs+ is a hash of attributes for the tag.
+ #
+ # [<tt>end_tag(tag)</tt>]
+ # Announces that an end tag for +tag+ has been found.
+ #
+ # [<tt>text(text)</tt>]
+ # Announces that a string of characters (+text+) has been found.
+ # A series of characters may be broken up into more than one
+ # +text+ call, so the client cannot assume that a single
+ # callback contains all the text data.
+ #
+ class XmlEvents < XmlMarkup #:nodoc:
+ def text!(text)
+ @target.text(text)
+ end
+
+ def _start_tag(sym, attrs, end_too=false)
+ @target.start_tag(sym, attrs)
+ _end_tag(sym) if end_too
+ end
+
+ def _end_tag(sym)
+ @target.end_tag(sym)
+ end
+ end
+
+end
diff --git a/actionpack/lib/action_view/vendor/builder/xmlmarkup.rb b/actionpack/lib/action_view/vendor/builder/xmlmarkup.rb
new file mode 100644
index 0000000000..716ff52535
--- /dev/null
+++ b/actionpack/lib/action_view/vendor/builder/xmlmarkup.rb
@@ -0,0 +1,288 @@
+#!/usr/bin/env ruby
+#--
+# Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
+# All rights reserved.
+
+# Permission is granted for use, copying, modification, distribution,
+# and distribution of modified versions of this work as long as the
+# above copyright notice is included.
+#++
+
+# Provide a flexible and easy to use Builder for creating XML markup.
+# See XmlBuilder for usage details.
+
+require 'builder/xmlbase'
+
+module Builder
+
+ # Create XML markup easily. All (well, almost all) methods sent to
+ # an XmlMarkup object will be translated to the equivalent XML
+ # markup. Any method with a block will be treated as an XML markup
+ # tag with nested markup in the block.
+ #
+ # Examples will demonstrate this easier than words. In the
+ # following, +xm+ is an +XmlMarkup+ object.
+ #
+ # xm.em("emphasized") # => <em>emphasized</em>
+ # xm.em { xmm.b("emp & bold") } # => <em><b>emph &amp; bold</b></em>
+ # xm.a("A Link", "href"=>"http://onestepback.org")
+ # # => <a href="http://onestepback.org">A Link</a>
+ # xm.div { br } # => <div><br/></div>
+ # xm.target("name"=>"compile", "option"=>"fast")
+ # # => <target option="fast" name="compile"\>
+ # # NOTE: order of attributes is not specified.
+ #
+ # xm.instruct! # <?xml version="1.0" encoding="UTF-8"?>
+ # xm.html { # <html>
+ # xm.head { # <head>
+ # xm.title("History") # <title>History</title>
+ # } # </head>
+ # xm.body { # <body>
+ # xm.comment! "HI" # <!-- HI -->
+ # xm.h1("Header") # <h1>Header</h1>
+ # xm.p("paragraph") # <p>paragraph</p>
+ # } # </body>
+ # } # </html>
+ #
+ # == Notes:
+ #
+ # * The order that attributes are inserted in markup tags is
+ # undefined.
+ #
+ # * Sometimes you wish to insert text without enclosing tags. Use
+ # the <tt>text!</tt> method to accomplish this.
+ #
+ # Example:
+ #
+ # xm.div { # <div>
+ # xm.text! "line"; xm.br # line<br/>
+ # xm.text! "another line"; xmbr # another line<br/>
+ # } # </div>
+ #
+ # * The special XML characters <, >, and & are converted to &lt;,
+ # &gt; and &amp; automatically. Use the <tt><<</tt> operation to
+ # insert text without modification.
+ #
+ # * Sometimes tags use special characters not allowed in ruby
+ # identifiers. Use the <tt>tag!</tt> method to handle these
+ # cases.
+ #
+ # Example:
+ #
+ # xml.tag!("SOAP:Envelope") { ... }
+ #
+ # will produce ...
+ #
+ # <SOAP:Envelope> ... </SOAP:Envelope>"
+ #
+ # <tt>tag!</tt> will also take text and attribute arguments (after
+ # the tag name) like normal markup methods. (But see the next
+ # bullet item for a better way to handle XML namespaces).
+ #
+ # * Direct support for XML namespaces is now available. If the
+ # first argument to a tag call is a symbol, it will be joined to
+ # the tag to produce a namespace:tag combination. It is easier to
+ # show this than describe it.
+ #
+ # xml.SOAP :Envelope do ... end
+ #
+ # Just put a space before the colon in a namespace to produce the
+ # right form for builder (e.g. "<tt>SOAP:Envelope</tt>" =>
+ # "<tt>xml.SOAP :Envelope</tt>")
+ #
+ # * XmlMarkup builds the markup in any object (called a _target_)
+ # that accepts the <tt><<</tt> method. If no target is given,
+ # then XmlMarkup defaults to a string target.
+ #
+ # Examples:
+ #
+ # xm = Builder::XmlMarkup.new
+ # result = xm.title("yada")
+ # # result is a string containing the markup.
+ #
+ # buffer = ""
+ # xm = Builder::XmlMarkup.new(buffer)
+ # # The markup is appended to buffer (using <<)
+ #
+ # xm = Builder::XmlMarkup.new(STDOUT)
+ # # The markup is written to STDOUT (using <<)
+ #
+ # xm = Builder::XmlMarkup.new
+ # x2 = Builder::XmlMarkup.new(:target=>xm)
+ # # Markup written to +x2+ will be send to +xm+.
+ #
+ # * Indentation is enabled by providing the number of spaces to
+ # indent for each level as a second argument to XmlBuilder.new.
+ # Initial indentation may be specified using a third parameter.
+ #
+ # Example:
+ #
+ # xm = Builder.new(:ident=>2)
+ # # xm will produce nicely formatted and indented XML.
+ #
+ # xm = Builder.new(:indent=>2, :margin=>4)
+ # # xm will produce nicely formatted and indented XML with 2
+ # # spaces per indent and an over all indentation level of 4.
+ #
+ # builder = Builder::XmlMarkup.new(:target=>$stdout, :indent=>2)
+ # builder.name { |b| b.first("Jim"); b.last("Weirich) }
+ # # prints:
+ # # <name>
+ # # <first>Jim</first>
+ # # <last>Weirich</last>
+ # # </name>
+ #
+ # * The instance_eval implementation which forces self to refer to
+ # the message receiver as self is now obsolete. We now use normal
+ # block calls to execute the markup block. This means that all
+ # markup methods must now be explicitly send to the xml builder.
+ # For instance, instead of
+ #
+ # xml.div { strong("text") }
+ #
+ # you need to write:
+ #
+ # xml.div { xml.strong("text") }
+ #
+ # Although more verbose, the subtle change in semantics within the
+ # block was found to be prone to error. To make this change a
+ # little less cumbersome, the markup block now gets the markup
+ # object sent as an argument, allowing you to use a shorter alias
+ # within the block.
+ #
+ # For example:
+ #
+ # xml_builder = Builder::XmlMarkup.new
+ # xml_builder.div { |xml|
+ # xml.stong("text")
+ # }
+ #
+ class XmlMarkup < XmlBase
+
+ # Create an XML markup builder. Parameters are specified by an
+ # option hash.
+ #
+ # :target=><em>target_object</em>::
+ # Object receiving the markup. +out+ must respond to the
+ # <tt><<</tt> operator. The default is a plain string target.
+ # :indent=><em>indentation</em>::
+ # Number of spaces used for indentation. The default is no
+ # indentation and no line breaks.
+ # :margin=><em>initial_indentation_level</em>::
+ # Amount of initial indentation (specified in levels, not
+ # spaces).
+ #
+ def initialize(options={})
+ indent = options[:indent] || 0
+ margin = options[:margin] || 0
+ super(indent, margin)
+ @target = options[:target] || ""
+ end
+
+ # Return the target of the builder.
+ def target!
+ @target
+ end
+
+ def comment!(comment_text)
+ _ensure_no_block block_given?
+ _special("<!-- ", " -->", comment_text, nil)
+ end
+
+ # Insert an XML declaration into the XML markup.
+ #
+ # For example:
+ #
+ # xml.declare! :ELEMENT, :blah, "yada"
+ # # => <!ELEMENT blah "yada">
+ def declare!(inst, *args, &block)
+ _indent
+ @target << "<!#{inst}"
+ args.each do |arg|
+ case arg
+ when String
+ @target << %{ "#{arg}"}
+ when Symbol
+ @target << " #{arg}"
+ end
+ end
+ if block_given?
+ @target << " ["
+ _newline
+ _nested_structures(block)
+ @target << "]"
+ end
+ @target << ">"
+ _newline
+ end
+
+ # Insert a processing instruction into the XML markup. E.g.
+ #
+ # For example:
+ #
+ # xml.instruct!
+ # #=> <?xml encoding="UTF-8" version="1.0"?>
+ # xml.instruct! :aaa, :bbb=>"ccc"
+ # #=> <?aaa bbb="ccc"?>
+ #
+ def instruct!(directive_tag=:xml, attrs={})
+ _ensure_no_block block_given?
+ if directive_tag == :xml
+ a = { :version=>"1.0", :encoding=>"UTF-8" }
+ attrs = a.merge attrs
+ end
+ _special("<?#{directive_tag}", "?>", nil, attrs)
+ end
+
+ private
+
+ # NOTE: All private methods of a builder object are prefixed when
+ # a "_" character to avoid possible conflict with XML tag names.
+
+ # Insert text directly in to the builder's target.
+ def _text(text)
+ @target << text
+ end
+
+ # Insert special instruction.
+ def _special(open, close, data=nil, attrs=nil)
+ _indent
+ @target << open
+ @target << data if data
+ _insert_attributes(attrs) if attrs
+ @target << close
+ _newline
+ end
+
+ # Start an XML tag. If <tt>end_too</tt> is true, then the start
+ # tag is also the end tag (e.g. <br/>
+ def _start_tag(sym, attrs, end_too=false)
+ @target << "<#{sym}"
+ _insert_attributes(attrs)
+ @target << "/" if end_too
+ @target << ">"
+ end
+
+ # Insert an ending tag.
+ def _end_tag(sym)
+ @target << "</#{sym}>"
+ end
+
+ # Insert the attributes (given in the hash).
+ def _insert_attributes(attrs)
+ return if attrs.nil?
+ attrs.each do |k, v|
+ @target << %{ #{k}="#{v}"}
+ end
+ end
+
+ def _ensure_no_block(got_block)
+ if got_block
+ fail IllegalBlockError,
+ "Blocks are not allowed on XML instructions"
+ end
+ end
+
+ end
+
+end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
new file mode 100644
index 0000000000..0fcb5e852d
--- /dev/null
+++ b/actionpack/test/abstract_unit.rb
@@ -0,0 +1,9 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+
+require 'test/unit'
+require 'action_controller'
+
+require 'action_controller/test_process'
+
+ActionController::Base.logger = nil
+ActionController::Base.ignore_missing_templates = true \ No newline at end of file
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
new file mode 100644
index 0000000000..6d727be5a2
--- /dev/null
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -0,0 +1,323 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+# a controller class to facilitate the tests
+class ActionPackAssertionsController < ActionController::Base
+
+ # this does absolutely nothing
+ def nothing() render_text ""; end
+
+ # a standard template
+ def hello_world() render "test/hello_world"; end
+
+ # a standard template
+ def hello_xml_world() render "test/hello_xml_world"; end
+
+ # a redirect to an internal location
+ def redirect_internal() redirect_to "nothing"; end
+
+ # a redirect to an external location
+ def redirect_external() redirect_to_url "http://www.rubyonrails.org"; end
+
+ # a 404
+ def response404() render_text "", "404 AWOL"; end
+
+ # a 500
+ def response500() render_text "", "500 Sorry"; end
+
+ # a fictional 599
+ def response599() render_text "", "599 Whoah!"; end
+
+ # putting stuff in the flash
+ def flash_me
+ flash['hello'] = 'my name is inigo montoya...'
+ render_text "Inconceivable!"
+ end
+
+ # we have a flash, but nothing is in it
+ def flash_me_naked
+ flash.clear
+ render_text "wow!"
+ end
+
+ # assign some template instance variables
+ def assign_this
+ @howdy = "ho"
+ render_text "Mr. Henke"
+ end
+
+ def render_based_on_parameters
+ render_text "Mr. #{@params["name"]}"
+ end
+
+ # puts something in the session
+ def session_stuffing
+ session['xmas'] = 'turkey'
+ render_text "ho ho ho"
+ end
+
+ # 911
+ def rescue_action(e) raise; end
+
+end
+
+# ---------------------------------------------------------------------------
+
+
+# tell the controller where to find its templates but start from parent
+# directory of test_request_response to simulate the behaviour of a
+# production environment
+ActionPackAssertionsController.template_root = File.dirname(__FILE__) + "/../fixtures/"
+
+
+# a test case to exercise the new capabilities TestRequest & TestResponse
+class ActionPackAssertionsControllerTest < Test::Unit::TestCase
+ # let's get this party started
+ def setup
+ @controller = ActionPackAssertionsController.new
+ @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
+ end
+
+ # -- assertion-based testing ------------------------------------------------
+
+ # test the session assertion to make sure something is there.
+ def test_assert_session_has
+ process :session_stuffing
+ assert_session_has 'xmas'
+ assert_session_has_no 'halloween'
+ end
+
+ # test the assertion of goodies in the template
+ def test_assert_template_has
+ process :assign_this
+ assert_template_has 'howdy'
+ end
+
+ # test the assertion for goodies that shouldn't exist in the template
+ def test_assert_template_has_no
+ process :nothing
+ assert_template_has_no 'maple syrup'
+ assert_template_has_no 'howdy'
+ end
+
+ # test the redirection assertions
+ def test_assert_redirect
+ process :redirect_internal
+ assert_redirect
+ end
+
+ # test the redirect url string
+ def test_assert_redirect_url
+ process :redirect_external
+ assert_redirect_url 'http://www.rubyonrails.org'
+ end
+
+ # test the redirection pattern matching on a string
+ def test_assert_redirect_url_match_string
+ process :redirect_external
+ assert_redirect_url_match 'rails.org'
+ end
+
+ # test the redirection pattern matching on a pattern
+ def test_assert_redirect_url_match_pattern
+ process :redirect_external
+ assert_redirect_url_match /ruby/
+ end
+
+ # test the flash-based assertions with something is in the flash
+ def test_flash_assertions_full
+ process :flash_me
+ assert @response.has_flash_with_contents?
+ assert_flash_exists
+ assert ActionController::TestResponse.assertion_target.has_flash_with_contents?
+ assert_flash_not_empty
+ assert_flash_has 'hello'
+ assert_flash_has_no 'stds'
+ end
+
+ # test the flash-based assertions with no flash at all
+ def test_flash_assertions_negative
+ process :nothing
+ assert_flash_not_exists
+ assert_flash_empty
+ assert_flash_has_no 'hello'
+ assert_flash_has_no 'qwerty'
+ end
+
+ # test the assert_rendered_file
+ def test_assert_rendered_file
+ process :hello_world
+ assert_rendered_file 'test/hello_world'
+ assert_rendered_file 'hello_world'
+ assert_rendered_file
+ end
+
+ # test the assert_success assertion
+ def test_assert_success
+ process :nothing
+ assert_success
+ end
+
+ # -- standard request/reponse object testing --------------------------------
+
+ # ensure our session is working properly
+ def test_session_objects
+ process :session_stuffing
+ assert @response.has_session_object?('xmas')
+ assert_session_equal 'turkey', 'xmas'
+ assert !@response.has_session_object?('easter')
+ end
+
+ # make sure that the template objects exist
+ def test_template_objects_alive
+ process :assign_this
+ assert !@response.has_template_object?('hi')
+ assert @response.has_template_object?('howdy')
+ end
+
+ # make sure we don't have template objects when we shouldn't
+ def test_template_object_missing
+ process :nothing
+ assert_nil @response.template_objects['howdy']
+ end
+
+ def test_assigned_equal
+ process :assign_this
+ assert_assigned_equal "ho", :howdy
+ end
+
+ # check the empty flashing
+ def test_flash_me_naked
+ process :flash_me_naked
+ assert @response.has_flash?
+ assert !@response.has_flash_with_contents?
+ end
+
+ # check if we have flash objects
+ def test_flash_haves
+ process :flash_me
+ assert @response.has_flash?
+ assert @response.has_flash_with_contents?
+ assert @response.has_flash_object?('hello')
+ end
+
+ # ensure we don't have flash objects
+ def test_flash_have_nots
+ process :nothing
+ assert !@response.has_flash?
+ assert !@response.has_flash_with_contents?
+ assert_nil @response.flash['hello']
+ end
+
+ # examine that the flash objects are what we expect
+ def test_flash_equals
+ process :flash_me
+ assert_flash_equal 'my name is inigo montoya...', 'hello'
+ end
+
+
+ # check if we were rendered by a file-based template?
+ def test_rendered_action
+ process :nothing
+ assert !@response.rendered_with_file?
+
+ process :hello_world
+ assert @response.rendered_with_file?
+ assert 'hello_world', @response.rendered_file
+ end
+
+ # check the redirection location
+ def test_redirection_location
+ process :redirect_internal
+ assert_equal 'nothing', @response.redirect_url
+
+ process :redirect_external
+ assert_equal 'http://www.rubyonrails.org', @response.redirect_url
+
+ process :nothing
+ assert_nil @response.redirect_url
+ end
+
+
+ # check server errors
+ def test_server_error_response_code
+ process :response500
+ assert @response.server_error?
+
+ process :response599
+ assert @response.server_error?
+
+ process :response404
+ assert !@response.server_error?
+ end
+
+ # check a 404 response code
+ def test_missing_response_code
+ process :response404
+ assert @response.missing?
+ end
+
+ # check to see if our redirection matches a pattern
+ def test_redirect_url_match
+ process :redirect_external
+ assert @response.redirect?
+ assert @response.redirect_url_match?("rubyonrails")
+ assert @response.redirect_url_match?(/rubyonrails/)
+ assert !@response.redirect_url_match?("phpoffrails")
+ assert !@response.redirect_url_match?(/perloffrails/)
+ end
+
+ # check for a redirection
+ def test_redirection
+ process :redirect_internal
+ assert @response.redirect?
+
+ process :redirect_external
+ assert @response.redirect?
+
+ process :nothing
+ assert !@response.redirect?
+ end
+
+ # check a successful response code
+ def test_successful_response_code
+ process :nothing
+ assert @response.success?
+ end
+
+ # a basic check to make sure we have a TestResponse object
+ def test_has_response
+ process :nothing
+ assert_kind_of ActionController::TestResponse, @response
+ end
+
+ def test_render_based_on_parameters
+ process :render_based_on_parameters, "name" => "David"
+ assert_equal "Mr. David", @response.body
+ end
+
+ def test_simple_one_element_xpath_match
+ process :hello_xml_world
+ assert_template_xpath_match('//title', "Hello World")
+ end
+
+ def test_array_of_elements_in_xpath_match
+ process :hello_xml_world
+ assert_template_xpath_match('//p', %w( abes monks wiseguys ))
+ end
+end
+
+class ActionPackHeaderTest < Test::Unit::TestCase
+ def setup
+ @controller = ActionPackAssertionsController.new
+ @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
+ end
+ def test_rendering_xml_sets_content_type
+ process :hello_xml_world
+ assert_equal('text/xml', @controller.headers['Content-Type'])
+ end
+ def test_rendering_xml_respects_content_type
+ @response.headers['Content-Type'] = 'application/pdf'
+ process :hello_xml_world
+ assert_equal('application/pdf', @controller.headers['Content-Type'])
+ end
+end
diff --git a/actionpack/test/controller/active_record_assertions_test.rb b/actionpack/test/controller/active_record_assertions_test.rb
new file mode 100644
index 0000000000..53106aaee7
--- /dev/null
+++ b/actionpack/test/controller/active_record_assertions_test.rb
@@ -0,0 +1,119 @@
+path_to_ar = File.dirname(__FILE__) + '/../../../activerecord'
+
+if Object.const_defined?("ActiveRecord") || File.exist?(path_to_ar)
+# This test is very different than the others. It requires ActiveRecord to
+# run. There's a bunch of stuff we are assuming here:
+#
+# 1. activerecord exists as a sibling directory to actionpack
+# (i.e., actionpack/../activerecord)
+# 2. you've created the appropriate database to run the active_record unit tests
+# 3. you set the appropriate database connection below
+
+driver_to_use = 'native_sqlite'
+
+$: << path_to_ar + '/lib/'
+$: << path_to_ar + '/test/'
+require 'active_record' unless Object.const_defined?("ActiveRecord")
+require "connections/#{driver_to_use}/connection"
+require 'fixtures/company'
+
+# -----------------------------------------------------------------------------
+
+# add some validation rules to trip up the assertions
+class Company
+ def validate
+ errors.add_on_empty('name')
+ errors.add('rating', 'rating should not be 2') if rating == 2
+ errors.add_to_base('oh oh') if rating == 3
+ end
+end
+
+# -----------------------------------------------------------------------------
+
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+# a controller class to handle the AR assertions
+class ActiveRecordAssertionsController < ActionController::Base
+ # fail with 1 bad column
+ def nasty_columns_1
+ @company = Company.new
+ @company.name = "B"
+ @company.rating = 2
+ render_text "snicker...."
+ end
+
+ # fail with 2 bad column
+ def nasty_columns_2
+ @company = Company.new
+ @company.name = ""
+ @company.rating = 2
+ render_text "double snicker...."
+ end
+
+ # this will pass validation
+ def good_company
+ @company = Company.new
+ @company.name = "A"
+ @company.rating = 69
+ render_text "Goodness Gracious!"
+ end
+
+ # this will fail validation
+ def bad_company
+ @company = Company.new
+ render_text "Who's Bad?"
+ end
+
+ # the safety dance......
+ def rescue_action(e) raise; end
+end
+
+# -----------------------------------------------------------------------------
+
+ActiveRecordAssertionsController.template_root = File.dirname(__FILE__) + "/../fixtures/"
+
+# The test case to try the AR assertions
+class ActiveRecordAssertionsControllerTest < Test::Unit::TestCase
+ # set it up
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ @controller = ActiveRecordAssertionsController.new
+ end
+
+ # test for 1 bad apple column
+ def test_some_invalid_columns
+ process :nasty_columns_1
+ assert_success
+ assert_invalid_record 'company'
+ assert_invalid_column_on_record 'company', 'rating'
+ assert_valid_column_on_record 'company', 'name'
+ assert_valid_column_on_record 'company', ['name','id']
+ end
+
+ # test for 2 bad apples columns
+ def test_all_invalid_columns
+ process :nasty_columns_2
+ assert_success
+ assert_invalid_record 'company'
+ assert_invalid_column_on_record 'company', 'rating'
+ assert_invalid_column_on_record 'company', 'name'
+ assert_invalid_column_on_record 'company', ['name','rating']
+ end
+
+ # ensure we have no problems with an ActiveRecord
+ def test_valid_record
+ process :good_company
+ assert_success
+ assert_valid_record 'company'
+ end
+
+ # ensure we have problems with an ActiveRecord
+ def test_invalid_record
+ process :bad_company
+ assert_success
+ assert_invalid_record 'company'
+ end
+end
+
+end \ No newline at end of file
diff --git a/actionpack/test/controller/cgi_test.rb b/actionpack/test/controller/cgi_test.rb
new file mode 100755
index 0000000000..46e24ab403
--- /dev/null
+++ b/actionpack/test/controller/cgi_test.rb
@@ -0,0 +1,142 @@
+$:.unshift(File.dirname(__FILE__) + '/../../lib')
+
+require 'test/unit'
+require 'action_controller/cgi_ext/cgi_methods'
+require 'stringio'
+
+class MockUploadedFile < StringIO
+ def content_type
+ "img/jpeg"
+ end
+
+ def original_filename
+ "my_file.doc"
+ end
+end
+
+class CGITest < Test::Unit::TestCase
+ def setup
+ @query_string = "action=create_customer&full_name=David%20Heinemeier%20Hansson&customerId=1"
+ @query_string_with_nil = "action=create_customer&full_name="
+ @query_string_with_array = "action=create_customer&selected[]=1&selected[]=2&selected[]=3"
+ @query_string_with_amps = "action=create_customer&name=Don%27t+%26+Does"
+ @query_string_with_multiple_of_same_name =
+ "action=update_order&full_name=Lau%20Taarnskov&products=4&products=2&products=3"
+ end
+
+ def test_query_string
+ assert_equal(
+ { "action" => "create_customer", "full_name" => "David Heinemeier Hansson", "customerId" => "1"},
+ CGIMethods.parse_query_parameters(@query_string)
+ )
+ end
+
+ def test_query_string_with_nil
+ assert_equal(
+ { "action" => "create_customer", "full_name" => nil},
+ CGIMethods.parse_query_parameters(@query_string_with_nil)
+ )
+ end
+
+ def test_query_string_with_array
+ assert_equal(
+ { "action" => "create_customer", "selected" => ["1", "2", "3"]},
+ CGIMethods.parse_query_parameters(@query_string_with_array)
+ )
+ end
+
+ def test_query_string_with_amps
+ assert_equal(
+ { "action" => "create_customer", "name" => "Don't & Does"},
+ CGIMethods.parse_query_parameters(@query_string_with_amps)
+ )
+ end
+
+ def test_parse_params
+ input = {
+ "customers[boston][first][name]" => [ "David" ],
+ "customers[boston][first][url]" => [ "http://David" ],
+ "customers[boston][second][name]" => [ "Allan" ],
+ "customers[boston][second][url]" => [ "http://Allan" ],
+ "something_else" => [ "blah" ],
+ "something_nil" => [ nil ],
+ "something_empty" => [ "" ],
+ "products[first]" => [ "Apple Computer" ],
+ "products[second]" => [ "Pc" ]
+ }
+
+ expected_output = {
+ "customers" => {
+ "boston" => {
+ "first" => {
+ "name" => "David",
+ "url" => "http://David"
+ },
+ "second" => {
+ "name" => "Allan",
+ "url" => "http://Allan"
+ }
+ }
+ },
+ "something_else" => "blah",
+ "something_empty" => "",
+ "something_nil" => "",
+ "products" => {
+ "first" => "Apple Computer",
+ "second" => "Pc"
+ }
+ }
+
+ assert_equal expected_output, CGIMethods.parse_request_parameters(input)
+ end
+
+ def test_parse_params_from_multipart_upload
+ mock_file = MockUploadedFile.new
+
+ input = {
+ "something" => [ StringIO.new("") ],
+ "products[string]" => [ StringIO.new("Apple Computer") ],
+ "products[file]" => [ mock_file ]
+ }
+
+ expected_output = {
+ "something" => "",
+ "products" => {
+ "string" => "Apple Computer",
+ "file" => mock_file
+ }
+ }
+
+ assert_equal expected_output, CGIMethods.parse_request_parameters(input)
+ end
+
+ def test_parse_params_with_file
+ input = {
+ "customers[boston][first][name]" => [ "David" ],
+ "something_else" => [ "blah" ],
+ "logo" => [ File.new(File.dirname(__FILE__) + "/cgi_test.rb").path ]
+ }
+
+ expected_output = {
+ "customers" => {
+ "boston" => {
+ "first" => {
+ "name" => "David"
+ }
+ }
+ },
+ "something_else" => "blah",
+ "logo" => File.new(File.dirname(__FILE__) + "/cgi_test.rb").path,
+ }
+
+ assert_equal expected_output, CGIMethods.parse_request_parameters(input)
+ end
+
+ def test_parse_params_with_array
+ input = { "selected[]" => [ "1", "2", "3" ] }
+
+ expected_output = { "selected" => [ "1", "2", "3" ] }
+
+ assert_equal expected_output, CGIMethods.parse_request_parameters(input)
+ end
+end
diff --git a/actionpack/test/controller/cookie_test.rb b/actionpack/test/controller/cookie_test.rb
new file mode 100644
index 0000000000..d3099bcd99
--- /dev/null
+++ b/actionpack/test/controller/cookie_test.rb
@@ -0,0 +1,38 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+class CookieTest < Test::Unit::TestCase
+ class TestController < ActionController::Base
+ def authenticate
+ cookie "name" => "user_name", "value" => "david"
+ render_text "hello world"
+ end
+
+ def access_frozen_cookies
+ @cookies["wont"] = "work"
+ end
+
+ def rescue_action(e) raise end
+ end
+
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_setting_cookie
+ @request.action = "authenticate"
+ assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david") ], process_request.headers["cookie"]
+ end
+
+ def test_setting_cookie
+ @request.action = "access_frozen_cookies"
+ assert_raises(TypeError) { process_request }
+ end
+
+ private
+ def process_request
+ TestController.process(@request, @response)
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
new file mode 100644
index 0000000000..f4d7a689b5
--- /dev/null
+++ b/actionpack/test/controller/filters_test.rb
@@ -0,0 +1,159 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+class FilterTest < Test::Unit::TestCase
+ class TestController < ActionController::Base
+ before_filter :ensure_login
+
+ def show
+ render_text "ran action"
+ end
+
+ private
+ def ensure_login
+ @ran_filter ||= []
+ @ran_filter << "ensure_login"
+ end
+ end
+
+ class PrependingController < TestController
+ prepend_before_filter :wonderful_life
+
+ private
+ def wonderful_life
+ @ran_filter ||= []
+ @ran_filter << "wonderful_life"
+ end
+ end
+
+ class ProcController < PrependingController
+ before_filter(proc { |c| c.assigns["ran_proc_filter"] = true })
+ end
+
+ class ImplicitProcController < PrependingController
+ before_filter { |c| c.assigns["ran_proc_filter"] = true }
+ end
+
+ class AuditFilter
+ def self.filter(controller)
+ controller.assigns["was_audited"] = true
+ end
+ end
+
+ class AroundFilter
+ def before(controller)
+ @execution_log = "before"
+ controller.class.execution_log << " before aroundfilter " if controller.respond_to? :execution_log
+ controller.assigns["before_ran"] = true
+ end
+
+ def after(controller)
+ controller.assigns["execution_log"] = @execution_log + " and after"
+ controller.assigns["after_ran"] = true
+ controller.class.execution_log << " after aroundfilter " if controller.respond_to? :execution_log
+ end
+ end
+
+ class AppendedAroundFilter
+ def before(controller)
+ controller.class.execution_log << " before appended aroundfilter "
+ end
+
+ def after(controller)
+ controller.class.execution_log << " after appended aroundfilter "
+ end
+ end
+
+ class AuditController < ActionController::Base
+ before_filter(AuditFilter)
+
+ def show
+ render_text "hello"
+ end
+ end
+
+ class BadFilterController < ActionController::Base
+ before_filter 2
+
+ def show() "show" end
+
+ protected
+ def rescue_action(e) raise(e) end
+ end
+
+ class AroundFilterController < PrependingController
+ around_filter AroundFilter.new
+ end
+
+ class MixedFilterController < PrependingController
+ cattr_accessor :execution_log
+ def initialize
+ @@execution_log = ""
+ end
+
+ before_filter { |c| c.class.execution_log << " before procfilter " }
+ prepend_around_filter AroundFilter.new
+
+ after_filter { |c| c.class.execution_log << " after procfilter " }
+ append_around_filter AppendedAroundFilter.new
+ end
+
+
+ def test_added_filter_to_inheritance_graph
+ assert_equal [ :fire_flash, :ensure_login ], TestController.before_filters
+ end
+
+ def test_base_class_in_isolation
+ assert_equal [ :fire_flash ], ActionController::Base.before_filters
+ end
+
+ def test_prepending_filter
+ assert_equal [ :wonderful_life, :fire_flash, :ensure_login ], PrependingController.before_filters
+ end
+
+ def test_running_filters
+ assert_equal %w( wonderful_life ensure_login ), test_process(PrependingController).template.assigns["ran_filter"]
+ end
+
+ def test_running_filters_with_proc
+ assert test_process(ProcController).template.assigns["ran_proc_filter"]
+ end
+
+ def test_running_filters_with_implicit_proc
+ assert test_process(ImplicitProcController).template.assigns["ran_proc_filter"]
+ end
+
+ def test_running_filters_with_class
+ assert test_process(AuditController).template.assigns["was_audited"]
+ end
+
+ def test_bad_filter
+ assert_raises(ActionController::ActionControllerError) {
+ test_process(BadFilterController)
+ }
+ end
+
+ def test_around_filter
+ controller = test_process(AroundFilterController)
+ assert controller.template.assigns["before_ran"]
+ assert controller.template.assigns["after_ran"]
+ end
+
+ def test_having_properties_in_around_filter
+ controller = test_process(AroundFilterController)
+ assert_equal "before and after", controller.template.assigns["execution_log"]
+ end
+
+ def test_prepending_and_appending_around_filter
+ controller = test_process(MixedFilterController)
+ assert_equal " before aroundfilter before procfilter before appended aroundfilter " +
+ " after appended aroundfilter after aroundfilter after procfilter ",
+ MixedFilterController.execution_log
+ end
+
+ private
+ def test_process(controller)
+ request = ActionController::TestRequest.new
+ request.action = "show"
+ controller.process(request, ActionController::TestResponse.new)
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
new file mode 100644
index 0000000000..033477fe39
--- /dev/null
+++ b/actionpack/test/controller/flash_test.rb
@@ -0,0 +1,69 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+class FlashTest < Test::Unit::TestCase
+ class TestController < ActionController::Base
+ def set_flash
+ flash["that"] = "hello"
+ render_text "hello"
+ end
+
+ def use_flash
+ @flashy = flash["that"]
+ render_text "hello"
+ end
+
+ def use_flash_and_keep_it
+ @flashy = flash["that"]
+ keep_flash
+ render_text "hello"
+ end
+
+ def rescue_action(e)
+ raise unless ActionController::MissingTemplate === e
+ end
+ end
+
+ def setup
+ initialize_request_and_response
+ end
+
+ def test_flash
+ @request.action = "set_flash"
+ response = process_request
+
+ @request.action = "use_flash"
+ first_response = process_request
+ assert_equal "hello", first_response.template.assigns["flash"]["that"]
+ assert_equal "hello", first_response.template.assigns["flashy"]
+
+ second_response = process_request
+ assert_nil second_response.template.assigns["flash"]["that"], "On second flash"
+ end
+
+ def test_keep_flash
+ @request.action = "set_flash"
+ response = process_request
+
+ @request.action = "use_flash_and_keep_it"
+ first_response = process_request
+ assert_equal "hello", first_response.template.assigns["flash"]["that"]
+ assert_equal "hello", first_response.template.assigns["flashy"]
+
+ @request.action = "use_flash"
+ second_response = process_request
+ assert_equal "hello", second_response.template.assigns["flash"]["that"], "On second flash"
+
+ third_response = process_request
+ assert_nil third_response.template.assigns["flash"]["that"], "On third flash"
+ end
+
+ private
+ def initialize_request_and_response
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def process_request
+ TestController.process(@request, @response)
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb
new file mode 100644
index 0000000000..9d1da53241
--- /dev/null
+++ b/actionpack/test/controller/helper_test.rb
@@ -0,0 +1,110 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+class HelperTest < Test::Unit::TestCase
+ HELPER_PATHS = %w(/../fixtures/helpers)
+
+ class TestController < ActionController::Base
+ attr_accessor :delegate_attr
+ def delegate_method() end
+ def rescue_action(e) raise end
+ end
+
+ module LocalAbcHelper
+ def a() end
+ def b() end
+ def c() end
+ end
+
+
+ def setup
+ # Increment symbol counter.
+ @symbol = (@@counter ||= 'A0').succ!.dup
+
+ # Generate new controller class.
+ controller_class_name = "Helper#{@symbol}Controller"
+ eval("class #{controller_class_name} < TestController; end")
+ @controller_class = self.class.const_get(controller_class_name)
+
+ # Generate new template class and assign to controller.
+ template_class_name = "Test#{@symbol}View"
+ eval("class #{template_class_name} < ActionView::Base; end")
+ @template_class = self.class.const_get(template_class_name)
+ @controller_class.template_class = @template_class
+
+ # Add helper paths to LOAD_PATH.
+ HELPER_PATHS.each { |path|
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + path)
+ }
+
+ # Set default test helper.
+ self.test_helper = LocalAbcHelper
+ end
+
+ def teardown
+ # Reset template class.
+ #ActionController::Base.template_class = ActionView::Base
+
+ # Remove helper paths from LOAD_PATH.
+ HELPER_PATHS.each { |path|
+ $LOAD_PATH.delete(File.dirname(__FILE__) + path)
+ }
+ end
+
+
+ def test_deprecated_helper
+ assert_equal helper_methods, missing_methods
+ assert_nothing_raised { @controller_class.helper TestHelper }
+ assert_equal [], missing_methods
+ end
+
+ def test_declare_helper
+ require 'abc_helper'
+ self.test_helper = AbcHelper
+ assert_equal helper_methods, missing_methods
+ assert_nothing_raised { @controller_class.helper :abc }
+ assert_equal [], missing_methods
+ end
+
+ def test_declare_missing_helper
+ assert_equal helper_methods, missing_methods
+ assert_raise(LoadError) { @controller_class.helper :missing }
+ end
+
+ def test_helper_block
+ assert_nothing_raised {
+ @controller_class.helper { def block_helper_method; end }
+ }
+ assert template_methods.include?('block_helper_method')
+ end
+
+ def test_helper_block_include
+ assert_equal helper_methods, missing_methods
+ assert_nothing_raised {
+ @controller_class.helper { include TestHelper }
+ }
+ assert [], missing_methods
+ end
+
+ def test_helper_method
+ assert_nothing_raised { @controller_class.helper_method :delegate_method }
+ assert template_methods.include?('delegate_method')
+ end
+
+ def test_helper_attr
+ assert_nothing_raised { @controller_class.helper_attr :delegate_attr }
+ assert template_methods.include?('delegate_attr')
+ assert template_methods.include?('delegate_attr=')
+ end
+
+
+ private
+ def helper_methods; TestHelper.instance_methods end
+ def template_methods; @template_class.instance_methods end
+ def missing_methods; helper_methods - template_methods end
+
+ def test_helper=(helper_module)
+ old_verbose, $VERBOSE = $VERBOSE, nil
+ self.class.const_set('TestHelper', helper_module)
+ $VERBOSE = old_verbose
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb
new file mode 100644
index 0000000000..f652453ebd
--- /dev/null
+++ b/actionpack/test/controller/layout_test.rb
@@ -0,0 +1,49 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+class TestLayoutController < ActionController::Base
+ layout "layouts/standard"
+
+ def hello_world
+ end
+
+ def hello_world_outside_layout
+ end
+
+ def rescue_action(e) raise end
+end
+
+class ChildWithoutTestLayoutController < TestLayoutController
+ layout nil
+
+ def hello_world
+ end
+end
+
+class ChildWithOtherTestLayoutController < TestLayoutController
+ layout nil
+
+ def hello_world
+ end
+end
+
+class RenderTest < Test::Unit::TestCase
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_layout_rendering
+ @request.action = "hello_world"
+ response = process_request
+ assert_equal "200 OK", response.headers["Status"]
+ assert_equal "layouts/standard", response.template.template_name
+ end
+
+
+ private
+ def process_request
+ TestLayoutController.process(@request, @response)
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
new file mode 100755
index 0000000000..6302016a53
--- /dev/null
+++ b/actionpack/test/controller/redirect_test.rb
@@ -0,0 +1,44 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+class RedirectTest < Test::Unit::TestCase
+ class RedirectController < ActionController::Base
+ def simple_redirect
+ redirect_to :action => "hello_world"
+ end
+
+ def method_redirect
+ redirect_to :dashbord_url, 1, "hello"
+ end
+
+ def rescue_errors(e) raise e end
+
+ protected
+ def dashbord_url(id, message)
+ url_for :action => "dashboard", :params => { "id" => id, "message" => message }
+ end
+ end
+
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_simple_redirect
+ @request.path = "/redirect/simple_redirect"
+ @request.action = "simple_redirect"
+ response = process_request
+ assert_equal "http://test.host/redirect/hello_world", response.headers["location"]
+ end
+
+ def test_redirect_with_method_reference_and_parameters
+ @request.path = "/redirect/method_redirect"
+ @request.action = "method_redirect"
+ response = process_request
+ assert_equal "http://test.host/redirect/dashboard?message=hello&id=1", response.headers["location"]
+ end
+
+ private
+ def process_request
+ RedirectController.process(@request, @response)
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
new file mode 100644
index 0000000000..ce778e1d7d
--- /dev/null
+++ b/actionpack/test/controller/render_test.rb
@@ -0,0 +1,178 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+Customer = Struct.new("Customer", :name)
+
+class RenderTest < Test::Unit::TestCase
+ class TestController < ActionController::Base
+ layout :determine_layout
+
+ def hello_world
+ end
+
+ def render_hello_world
+ render "test/hello_world"
+ end
+
+ def render_hello_world_from_variable
+ @person = "david"
+ render_text "hello #{@person}"
+ end
+
+ def render_action_hello_world
+ render_action "hello_world"
+ end
+
+ def render_text_hello_world
+ render_text "hello world"
+ end
+
+ def render_custom_code
+ render_text "hello world", "404 Moved"
+ end
+
+ def render_xml_hello
+ @name = "David"
+ render "test/hello"
+ end
+
+ def greeting
+ # let's just rely on the template
+ end
+
+ def layout_test
+ render_action "hello_world"
+ end
+
+ def builder_layout_test
+ render_action "hello"
+ end
+
+ def partials_list
+ @customers = [ Customer.new("david"), Customer.new("mary") ]
+ render_action "list"
+ end
+
+ def modgreet
+ end
+
+ def rescue_action(e) raise end
+
+ private
+ def determine_layout
+ case action_name
+ when "layout_test": "layouts/standard"
+ when "builder_layout_test": "layouts/builder"
+ end
+ end
+ end
+
+ TestController.template_root = File.dirname(__FILE__) + "/../fixtures/"
+
+ class TestLayoutController < ActionController::Base
+ layout "layouts/standard"
+
+ def hello_world
+ end
+
+ def hello_world_outside_layout
+ end
+
+ def rescue_action(e)
+ raise unless ActionController::MissingTemplate === e
+ end
+ end
+
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_simple_show
+ @request.action = "hello_world"
+ response = process_request
+ assert_equal "200 OK", response.headers["Status"]
+ assert_equal "test/hello_world", response.template.first_render
+ end
+
+ def test_do_with_render
+ @request.action = "render_hello_world"
+ assert_equal "test/hello_world", process_request.template.first_render
+ end
+
+ def test_do_with_render_from_variable
+ @request.action = "render_hello_world_from_variable"
+ assert_equal "hello david", process_request.body
+ end
+
+ def test_do_with_render_action
+ @request.action = "render_action_hello_world"
+ assert_equal "test/hello_world", process_request.template.first_render
+ end
+
+ def test_do_with_render_text
+ @request.action = "render_text_hello_world"
+ assert_equal "hello world", process_request.body
+ end
+
+ def test_do_with_render_custom_code
+ @request.action = "render_custom_code"
+ assert_equal "404 Moved", process_request.headers["Status"]
+ end
+
+ def test_attempt_to_access_object_method
+ @request.action = "clone"
+ assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { process_request }
+ end
+
+ def test_access_to_request_in_view
+ ActionController::Base.view_controller_internals = false
+
+ @request.action = "hello_world"
+ response = process_request
+ assert_nil response.template.assigns["request"]
+
+ ActionController::Base.view_controller_internals = true
+
+ @request.action = "hello_world"
+ response = process_request
+ assert_kind_of ActionController::AbstractRequest, response.template.assigns["request"]
+ end
+
+ def test_render_xml
+ @request.action = "render_xml_hello"
+ assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", process_request.body
+ end
+
+ def test_render_xml_with_default
+ @request.action = "greeting"
+ assert_equal "<p>This is grand!</p>\n", process_request.body
+ end
+
+ def test_layout_rendering
+ @request.action = "layout_test"
+ assert_equal "<html>Hello world!</html>", process_request.body
+ end
+
+ def test_render_xml_with_layouts
+ @request.action = "builder_layout_test"
+ assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", process_request.body
+ end
+
+ def test_partials_list
+ @request.action = "partials_list"
+ assert_equal "Hello: davidHello: mary", process_request.body
+ end
+
+ def test_module_rendering
+ @request.action = "modgreet"
+ @request.parameters["module"] = "scope"
+ assert_equal "<p>Beautiful modules!</p>", process_request.body
+ end
+
+ private
+ def process_request
+ TestController.process(@request, @response)
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
new file mode 100644
index 0000000000..57205a01c5
--- /dev/null
+++ b/actionpack/test/controller/send_file_test.rb
@@ -0,0 +1,68 @@
+require File.join(File.dirname(__FILE__), '..', 'abstract_unit')
+
+
+module TestFileUtils
+ def file_name() File.basename(__FILE__) end
+ def file_path() File.expand_path(__FILE__) end
+ def file_data() File.open(file_path, 'rb') { |f| f.read } end
+end
+
+
+class SendFileController < ActionController::Base
+ include TestFileUtils
+
+ attr_writer :options
+ def options() @options ||= {} end
+
+ def file() send_file(file_path, options) end
+ def data() send_data(file_data, options) end
+
+ def rescue_action(e) raise end
+end
+
+
+class SendFileTest < Test::Unit::TestCase
+ include TestFileUtils
+
+ def setup
+ @controller = SendFileController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_file_nostream
+ @controller.options = { :stream => false }
+ response = nil
+ assert_nothing_raised { response = process('file') }
+ assert_not_nil response
+ assert_kind_of String, response.body
+ assert_equal file_data, response.body
+ end
+
+ def test_file_stream
+ response = nil
+ assert_nothing_raised { response = process('file') }
+ assert_not_nil response
+ assert_kind_of Proc, response.body
+
+ old_stdout = $stdout
+ begin
+ require 'stringio'
+ $stdout = StringIO.new
+ $stdout.binmode
+ assert_nothing_raised { response.body.call }
+ assert_equal file_data, $stdout.string
+ ensure
+ $stdout = old_stdout
+ end
+ end
+
+ def test_data
+ response = nil
+ assert_nothing_raised { response = process('data') }
+ assert_not_nil response
+
+ assert_kind_of String, response.body
+ assert_equal file_data, response.body
+ end
+end
diff --git a/actionpack/test/controller/url_test.rb b/actionpack/test/controller/url_test.rb
new file mode 100644
index 0000000000..bf6a7aab75
--- /dev/null
+++ b/actionpack/test/controller/url_test.rb
@@ -0,0 +1,368 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+require 'action_controller/url_rewriter'
+
+MockRequest = Struct.new("MockRequest", :protocol, :host, :port, :path, :parameters)
+class MockRequest
+ def host_with_port
+ if (protocol == "http://" && port == 80) || (protocol == "https://" && port == 443)
+ host
+ else
+ host + ":#{port}"
+ end
+ end
+end
+
+class UrlTest < Test::Unit::TestCase
+ def setup
+ @library_url = ActionController::UrlRewriter.new(MockRequest.new(
+ "http://",
+ "www.singlefile.com",
+ 80,
+ "/library/books/ISBN/0743536703/show",
+ { "type" => "ISBN", "code" => "0743536703" }
+ ), "books", "show")
+
+ @library_url_on_index = ActionController::UrlRewriter.new(MockRequest.new(
+ "http://",
+ "www.singlefile.com",
+ 80,
+ "/library/books/ISBN/0743536703/",
+ { "type" => "ISBN", "code" => "0743536703" }
+ ), "books", "index")
+
+ @clean_urls = [
+ ActionController::UrlRewriter.new(MockRequest.new(
+ "http://", "www.singlefile.com", 80, "/identity/", {}
+ ), "identity", "index"),
+ ActionController::UrlRewriter.new(MockRequest.new(
+ "http://", "www.singlefile.com", 80, "/identity", {}
+ ), "identity", "index")
+ ]
+
+ @clean_url_with_id = ActionController::UrlRewriter.new(MockRequest.new(
+ "http://", "www.singlefile.com", 80, "/identity/show/5", { "id" => "5" }
+ ), "identity", "show")
+
+ @clean_url_with_id_as_char = ActionController::UrlRewriter.new(MockRequest.new(
+ "http://", "www.singlefile.com", 80, "/teachers/show/t", { "id" => "t" }
+ ), "teachers", "show")
+ end
+
+ def test_clean_action
+ assert_equal "http://www.singlefile.com/library/books/ISBN/0743536703/edit", @library_url.rewrite(:action => "edit")
+ end
+
+ def test_clean_action_with_only_path
+ assert_equal "/library/books/ISBN/0743536703/edit", @library_url.rewrite(:action => "edit", :only_path => true)
+ end
+
+ def test_action_from_index
+ assert_equal "http://www.singlefile.com/library/books/ISBN/0743536703/edit", @library_url_on_index.rewrite(:action => "edit")
+ end
+
+ def test_action_from_index_on_clean
+ @clean_urls.each do |url|
+ assert_equal "http://www.singlefile.com/identity/edit", url.rewrite(:action => "edit")
+ end
+ end
+
+ def test_action_without_prefix
+ assert_equal "http://www.singlefile.com/library/books/", @library_url.rewrite(:action => "index", :action_prefix => "")
+ end
+
+ def test_action_with_prefix
+ assert_equal(
+ "http://www.singlefile.com/library/books/XTC/123/show",
+ @library_url.rewrite(:action => "show", :action_prefix => "XTC/123")
+ )
+ end
+
+ def test_action_prefix_alone
+ assert_equal(
+ "http://www.singlefile.com/library/books/XTC/123/",
+ @library_url.rewrite(:action_prefix => "XTC/123")
+ )
+ end
+
+ def test_action_with_suffix
+ assert_equal(
+ "http://www.singlefile.com/library/books/show/XTC/123",
+ @library_url.rewrite(:action => "show", :action_prefix => "", :action_suffix => "XTC/123")
+ )
+ end
+
+ def test_clean_controller
+ assert_equal "http://www.singlefile.com/library/settings/", @library_url.rewrite(:controller => "settings")
+ end
+
+ def test_clean_controller_prefix
+ assert_equal "http://www.singlefile.com/shop/", @library_url.rewrite(:controller_prefix => "shop")
+ end
+
+ def test_clean_controller_with_module
+ assert_equal "http://www.singlefile.com/shop/purchases/", @library_url.rewrite(:module => "shop", :controller => "purchases")
+ end
+
+ def test_controller_and_action
+ assert_equal "http://www.singlefile.com/library/settings/show", @library_url.rewrite(:controller => "settings", :action => "show")
+ end
+
+ def test_controller_and_action_and_anchor
+ assert_equal(
+ "http://www.singlefile.com/library/settings/show#5",
+ @library_url.rewrite(:controller => "settings", :action => "show", :anchor => "5")
+ )
+ end
+
+ def test_controller_and_action_and_empty_overwrite_params_and_anchor
+ assert_equal(
+ "http://www.singlefile.com/library/settings/show?code=0743536703&type=ISBN#5",
+ @library_url.rewrite(:controller => "settings", :action => "show", :overwrite_params => {}, :anchor => "5")
+ )
+ end
+
+ def test_controller_and_action_and_overwrite_params_and_anchor
+ assert_equal(
+ "http://www.singlefile.com/library/settings/show?code=0000001&type=ISBN#5",
+ @library_url.rewrite(:controller => "settings", :action => "show", :overwrite_params => {"code"=>"0000001"}, :anchor => "5")
+ )
+ end
+
+ def test_controller_and_action_and_overwrite_params_with_nil_value_and_anchor
+ assert_equal(
+ "http://www.singlefile.com/library/settings/show?type=ISBN#5",
+ @library_url.rewrite(:controller => "settings", :action => "show", :overwrite_params => {"code" => nil}, :anchor => "5")
+ )
+ end
+
+ def test_controller_and_action_params_and_overwrite_params_and_anchor
+ assert_equal(
+ "http://www.singlefile.com/library/settings/show?code=0000001&version=5.0#5",
+ @library_url.rewrite(:controller => "settings", :action => "show", :params=>{"version" => "5.0"}, :overwrite_params => {"code"=>"0000001"}, :anchor => "5")
+ )
+ end
+
+ def test_controller_and_action_and_params_anchor
+ assert_equal(
+ "http://www.singlefile.com/library/settings/show?update=1#5",
+ @library_url.rewrite(:controller => "settings", :action => "show", :params => { "update" => "1"}, :anchor => "5")
+ )
+ end
+
+ def test_controller_and_index_action
+ assert_equal "http://www.singlefile.com/library/settings/", @library_url.rewrite(:controller => "settings", :action => "index")
+ end
+
+ def test_controller_and_action_with_same_name_as_controller
+ @clean_urls.each do |url|
+ assert_equal "http://www.singlefile.com/anything/identity", url.rewrite(:controller => "anything", :action => "identity")
+ end
+ end
+
+ def test_controller_and_index_action_without_controller_prefix
+ assert_equal(
+ "http://www.singlefile.com/settings/",
+ @library_url.rewrite(:controller => "settings", :action => "index", :controller_prefix => "")
+ )
+ end
+
+ def test_controller_and_index_action_with_controller_prefix
+ assert_equal(
+ "http://www.singlefile.com/fantastic/settings/show",
+ @library_url.rewrite(:controller => "settings", :action => "show", :controller_prefix => "fantastic")
+ )
+ end
+
+ def test_path_parameters
+ assert_equal "http://www.singlefile.com/library/books/EXBC/0743536703/show", @library_url.rewrite(:path_params => {"type" => "EXBC"})
+ end
+
+ def test_parameters
+ assert_equal(
+ "http://www.singlefile.com/library/books/ISBN/0743536703/show?delete=1&name=David",
+ @library_url.rewrite(:params => {"delete" => "1", "name" => "David"})
+ )
+ end
+
+ def test_parameters_with_id
+ @clean_urls.each do |url|
+ assert_equal(
+ "http://www.singlefile.com/identity/show?name=David&id=5",
+ url.rewrite(
+ :action => "show",
+ :params => { "id" => "5", "name" => "David" }
+ )
+ )
+ end
+ end
+
+ def test_action_with_id
+ assert_equal(
+ "http://www.singlefile.com/identity/show/7",
+ @clean_url_with_id.rewrite(
+ :action => "show",
+ :id => 7
+ )
+ )
+ @clean_urls.each do |url|
+ assert_equal(
+ "http://www.singlefile.com/identity/index/7",
+ url.rewrite(:id => 7)
+ )
+ end
+ end
+
+ def test_parameters_with_id_and_away
+ assert_equal(
+ "http://www.singlefile.com/identity/show/25?name=David",
+ @clean_url_with_id.rewrite(
+ :path_params => { "id" => "25" },
+ :params => { "name" => "David" }
+ )
+ )
+ end
+
+ def test_parameters_with_index_and_id
+ @clean_urls.each do |url|
+ assert_equal(
+ "http://www.singlefile.com/identity/index/25?name=David",
+ url.rewrite(
+ :path_params => { "id" => "25" },
+ :params => { "name" => "David" }
+ )
+ )
+ end
+ end
+
+ def test_action_going_away_from_id
+ assert_equal(
+ "http://www.singlefile.com/identity/list",
+ @clean_url_with_id.rewrite(
+ :action => "list"
+ )
+ )
+ end
+
+ def test_parameters_with_direct_id_and_away
+ assert_equal(
+ "http://www.singlefile.com/identity/show/25?name=David",
+ @clean_url_with_id.rewrite(
+ :id => "25",
+ :params => { "name" => "David" }
+ )
+ )
+ end
+
+ def test_parameters_with_direct_id_and_away
+ assert_equal(
+ "http://www.singlefile.com/store/open/25?name=David",
+ @clean_url_with_id.rewrite(
+ :controller => "store",
+ :action => "open",
+ :id => "25",
+ :params => { "name" => "David" }
+ )
+ )
+ end
+
+ def test_parameters_to_id
+ @clean_urls.each do |url|
+ %w(show index).each do |action|
+ assert_equal(
+ "http://www.singlefile.com/identity/#{action}/25?name=David",
+ url.rewrite(
+ :action => action,
+ :path_params => { "id" => "25" },
+ :params => { "name" => "David" }
+ )
+ )
+ end
+ end
+ end
+
+ def test_parameters_from_id
+ assert_equal(
+ "http://www.singlefile.com/identity/",
+ @clean_url_with_id.rewrite(
+ :action => "index"
+ )
+ )
+ end
+
+ def test_id_as_char_and_part_of_controller
+ assert_equal(
+ "http://www.singlefile.com/teachers/skill/5",
+ @clean_url_with_id_as_char.rewrite(
+ :action => "skill",
+ :id => 5
+ )
+ )
+ end
+
+ def test_from_clean_to_library
+ @clean_urls.each do |url|
+ assert_equal(
+ "http://www.singlefile.com/library/books/ISBN/0743536703/show?delete=1&name=David",
+ url.rewrite(
+ :controller_prefix => "library",
+ :controller => "books",
+ :action_prefix => "ISBN/0743536703",
+ :action => "show",
+ :params => { "delete" => "1", "name" => "David" }
+ )
+ )
+ end
+ end
+
+ def test_from_library_to_clean
+ assert_equal(
+ "http://www.singlefile.com/identity/",
+ @library_url.rewrite(
+ :controller => "identity", :controller_prefix => ""
+ )
+ )
+ end
+
+ def test_from_another_port
+ @library_url = ActionController::UrlRewriter.new(MockRequest.new(
+ "http://",
+ "www.singlefile.com",
+ 8080,
+ "/library/books/ISBN/0743536703/show",
+ { "type" => "ISBN", "code" => "0743536703" }
+ ), "books", "show")
+
+ assert_equal(
+ "http://www.singlefile.com:8080/identity/",
+ @library_url.rewrite(
+ :controller => "identity", :controller_prefix => ""
+ )
+ )
+ end
+
+ def test_basecamp
+ basecamp_url = ActionController::UrlRewriter.new(MockRequest.new(
+ "http://",
+ "projects.basecamp",
+ 80,
+ "/clients/disarray/1/msg/transcripts/",
+ {"category_name"=>"transcripts", "client_name"=>"disarray", "action"=>"index", "controller"=>"msg", "project_name"=>"1"}
+ ), "msg", "index")
+
+ assert_equal(
+ "http://projects.basecamp/clients/disarray/1/msg/transcripts/1/comments",
+ basecamp_url.rewrite(:action_prefix => "transcripts/1", :action => "comments")
+ )
+ end
+
+ def test_on_explicit_index_page # My index page is very modest, thank you...
+ url = ActionController::UrlRewriter.new(
+ MockRequest.new(
+ "http://", "example.com", 80, "/controller/index",
+ {"controller"=>"controller", "action"=>"index"}
+ ), "controller", "index"
+ )
+ assert_equal("http://example.com/controller/foo", url.rewrite(:action => 'foo'))
+ end
+
+end
diff --git a/actionpack/test/fixtures/helpers/abc_helper.rb b/actionpack/test/fixtures/helpers/abc_helper.rb
new file mode 100644
index 0000000000..7104ff3730
--- /dev/null
+++ b/actionpack/test/fixtures/helpers/abc_helper.rb
@@ -0,0 +1,5 @@
+module AbcHelper
+ def bare_a() end
+ def bare_b() end
+ def bare_c() end
+end
diff --git a/actionpack/test/fixtures/layouts/builder.rxml b/actionpack/test/fixtures/layouts/builder.rxml
new file mode 100644
index 0000000000..729af4b8bc
--- /dev/null
+++ b/actionpack/test/fixtures/layouts/builder.rxml
@@ -0,0 +1,3 @@
+xml.wrapper do
+ xml << @content_for_layout
+end \ No newline at end of file
diff --git a/actionpack/test/fixtures/layouts/standard.rhtml b/actionpack/test/fixtures/layouts/standard.rhtml
new file mode 100644
index 0000000000..fcb28ec755
--- /dev/null
+++ b/actionpack/test/fixtures/layouts/standard.rhtml
@@ -0,0 +1 @@
+<html><%= @content_for_layout %></html> \ No newline at end of file
diff --git a/actionpack/test/fixtures/scope/test/modgreet.rhtml b/actionpack/test/fixtures/scope/test/modgreet.rhtml
new file mode 100644
index 0000000000..8947726e89
--- /dev/null
+++ b/actionpack/test/fixtures/scope/test/modgreet.rhtml
@@ -0,0 +1 @@
+<p>Beautiful modules!</p> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_customer.rhtml b/actionpack/test/fixtures/test/_customer.rhtml
new file mode 100644
index 0000000000..872d8c44e6
--- /dev/null
+++ b/actionpack/test/fixtures/test/_customer.rhtml
@@ -0,0 +1 @@
+Hello: <%= customer.name %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/greeting.rhtml b/actionpack/test/fixtures/test/greeting.rhtml
new file mode 100644
index 0000000000..62fb0293f0
--- /dev/null
+++ b/actionpack/test/fixtures/test/greeting.rhtml
@@ -0,0 +1 @@
+<p>This is grand!</p>
diff --git a/actionpack/test/fixtures/test/hello.rxml b/actionpack/test/fixtures/test/hello.rxml
new file mode 100644
index 0000000000..82a4a310d3
--- /dev/null
+++ b/actionpack/test/fixtures/test/hello.rxml
@@ -0,0 +1,4 @@
+xml.html do
+ xml.p "Hello #{@name}"
+ xml << render_file("test/greeting")
+end \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/hello_world.rhtml b/actionpack/test/fixtures/test/hello_world.rhtml
new file mode 100644
index 0000000000..6769dd60bd
--- /dev/null
+++ b/actionpack/test/fixtures/test/hello_world.rhtml
@@ -0,0 +1 @@
+Hello world! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/hello_xml_world.rxml b/actionpack/test/fixtures/test/hello_xml_world.rxml
new file mode 100644
index 0000000000..02b14fe87c
--- /dev/null
+++ b/actionpack/test/fixtures/test/hello_xml_world.rxml
@@ -0,0 +1,11 @@
+xml.html do
+ xml.head do
+ xml.title "Hello World"
+ end
+
+ xml.body do
+ xml.p "abes"
+ xml.p "monks"
+ xml.p "wiseguys"
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/list.rhtml b/actionpack/test/fixtures/test/list.rhtml
new file mode 100644
index 0000000000..39e2cad966
--- /dev/null
+++ b/actionpack/test/fixtures/test/list.rhtml
@@ -0,0 +1 @@
+<%= render_collection_of_partials "customer", @customers %> \ No newline at end of file
diff --git a/actionpack/test/template/active_record_helper_test.rb b/actionpack/test/template/active_record_helper_test.rb
new file mode 100644
index 0000000000..4b32f4dd48
--- /dev/null
+++ b/actionpack/test/template/active_record_helper_test.rb
@@ -0,0 +1,76 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/date_helper'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_helper'
+# require File.dirname(__FILE__) + '/../../lib/action_view/helpers/active_record_helper'
+
+class ActiveRecordHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::FormHelper
+ include ActionView::Helpers::ActiveRecordHelper
+
+ Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on)
+ Column = Struct.new("Column", :type, :name, :human_name)
+
+ def setup
+ @post = Post.new
+ def @post.errors() Class.new{ def on(field) field == "author_name" || field == "body" end }.new end
+ def @post.new_record?() true end
+
+ def @post.column_for_attribute(attr_name)
+ Post.content_columns.select { |column| column.name == attr_name }.first
+ end
+
+ def Post.content_columns() [ Column.new(:string, "title", "Title"), Column.new(:text, "body", "Body") ] end
+
+ @post.title = "Hello World"
+ @post.author_name = ""
+ @post.body = "Back to the hill and over it again!"
+ @post.secret = 1
+ @post.written_on = Date.new(2004, 6, 15)
+ end
+
+ def test_generic_input_tag
+ assert_equal(
+ '<input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />', input("post", "title")
+ )
+ end
+
+ def test_text_area_with_errors
+ assert_equal(
+ "<div class=\"fieldWithErrors\"><textarea cols=\"40\" id=\"post_body\" name=\"post[body]\" rows=\"20\" wrap=\"virtual\">Back to the hill and over it again!</textarea></div>",
+ text_area("post", "body")
+ )
+ end
+
+ def test_text_field_with_errors
+ assert_equal(
+ '<div class="fieldWithErrors"><input id="post_author_name" name="post[author_name]" size="30" type="text" value="" /></div>',
+ text_field("post", "author_name")
+ )
+ end
+
+ def test_form_with_string
+ assert_equal(
+ "<form action='create' method='POST'><p><label for=\"post_title\">Title</label><br /><input id=\"post_title\" name=\"post[title]\" size=\"30\" type=\"text\" value=\"Hello World\" /></p>\n<p><label for=\"post_body\">Body</label><br /><div class=\"fieldWithErrors\"><textarea cols=\"40\" id=\"post_body\" name=\"post[body]\" rows=\"20\" wrap=\"virtual\">Back to the hill and over it again!</textarea></div></p><input type='submit' value='Create' /></form>",
+ form("post")
+ )
+ end
+
+ def test_form_with_date
+ def Post.content_columns() [ Column.new(:date, "written_on", "Written on") ] end
+
+ assert_equal(
+ "<form action='create' method='POST'><p><label for=\"post_written_on\">Written on</label><br /><select name='post[written_on(1i)]'>\n<option>1999</option>\n<option>2000</option>\n<option>2001</option>\n<option>2002</option>\n<option>2003</option>\n<option selected=\"selected\">2004</option>\n<option>2005</option>\n<option>2006</option>\n<option>2007</option>\n<option>2008</option>\n<option>2009</option>\n</select>\n<select name='post[written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option value='6' selected=\"selected\">June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n<select name='post[written_on(3i)]'>\n<option>1</option>\n<option>2</option>\n<option>3</option>\n<option>4</option>\n<option>5</option>\n<option>6</option>\n<option>7</option>\n<option>8</option>\n<option>9</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option selected=\"selected\">15</option>\n<option>16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option>30</option>\n<option>31</option>\n</select>\n</p><input type='submit' value='Create' /></form>",
+ form("post")
+ )
+ end
+
+ def test_form_with_datetime
+ def Post.content_columns() [ Column.new(:datetime, "written_on", "Written on") ] end
+ @post.written_on = Time.gm(2004, 6, 15, 16, 30)
+
+ assert_equal(
+ "<form action='create' method='POST'><p><label for=\"post_written_on\">Written on</label><br /><select name='post[written_on(1i)]'>\n<option>1999</option>\n<option>2000</option>\n<option>2001</option>\n<option>2002</option>\n<option>2003</option>\n<option selected=\"selected\">2004</option>\n<option>2005</option>\n<option>2006</option>\n<option>2007</option>\n<option>2008</option>\n<option>2009</option>\n</select>\n<select name='post[written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option value='6' selected=\"selected\">June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n<select name='post[written_on(3i)]'>\n<option>1</option>\n<option>2</option>\n<option>3</option>\n<option>4</option>\n<option>5</option>\n<option>6</option>\n<option>7</option>\n<option>8</option>\n<option>9</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option selected=\"selected\">15</option>\n<option>16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option>30</option>\n<option>31</option>\n</select>\n &mdash; <select name='post[written_on(4i)]'>\n<option>00</option>\n<option>01</option>\n<option>02</option>\n<option>03</option>\n<option>04</option>\n<option>05</option>\n<option>06</option>\n<option>07</option>\n<option>08</option>\n<option>09</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option>15</option>\n<option selected=\"selected\">16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n</select>\n : <select name='post[written_on(5i)]'>\n<option>00</option>\n<option>01</option>\n<option>02</option>\n<option>03</option>\n<option>04</option>\n<option>05</option>\n<option>06</option>\n<option>07</option>\n<option>08</option>\n<option>09</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option>15</option>\n<option>16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option selected=\"selected\">30</option>\n<option>31</option>\n<option>32</option>\n<option>33</option>\n<option>34</option>\n<option>35</option>\n<option>36</option>\n<option>37</option>\n<option>38</option>\n<option>39</option>\n<option>40</option>\n<option>41</option>\n<option>42</option>\n<option>43</option>\n<option>44</option>\n<option>45</option>\n<option>46</option>\n<option>47</option>\n<option>48</option>\n<option>49</option>\n<option>50</option>\n<option>51</option>\n<option>52</option>\n<option>53</option>\n<option>54</option>\n<option>55</option>\n<option>56</option>\n<option>57</option>\n<option>58</option>\n<option>59</option>\n</select>\n</p><input type='submit' value='Create' /></form>",
+ form("post")
+ )
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb
new file mode 100755
index 0000000000..a8ad37918d
--- /dev/null
+++ b/actionpack/test/template/date_helper_test.rb
@@ -0,0 +1,104 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/date_helper'
+
+class DateHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::DateHelper
+
+ def test_distance_in_words
+ from = Time.mktime(2004, 3, 6, 21, 41, 18)
+
+ assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 25))
+ assert_equal "5 minutes", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 46, 25))
+ assert_equal "about 1 hour", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 22, 47, 25))
+ assert_equal "about 3 hours", distance_of_time_in_words(from, Time.mktime(2004, 3, 7, 0, 41))
+ assert_equal "about 4 hours", distance_of_time_in_words(from, Time.mktime(2004, 3, 7, 1, 20))
+ assert_equal "2 days", distance_of_time_in_words(from, Time.mktime(2004, 3, 9, 15, 40))
+ end
+
+ def test_select_day
+ expected = "<select name='date[day]'>\n"
+ expected <<
+"<option>1</option>\n<option>2</option>\n<option>3</option>\n<option>4</option>\n<option>5</option>\n<option>6</option>\n<option>7</option>\n<option>8</option>\n<option>9</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option>15</option>\n<option selected=\"selected\">16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option>30</option>\n<option>31</option>\n"
+ expected << "</select>\n"
+
+ assert_equal expected, select_day(Time.mktime(2003, 8, 16))
+ assert_equal expected, select_day(16)
+ end
+
+ def test_select_day_with_blank
+ expected = "<select name='date[day]'>\n"
+ expected <<
+"<option></option>\n<option>1</option>\n<option>2</option>\n<option>3</option>\n<option>4</option>\n<option>5</option>\n<option>6</option>\n<option>7</option>\n<option>8</option>\n<option>9</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option>15</option>\n<option selected=\"selected\">16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option>30</option>\n<option>31</option>\n"
+ expected << "</select>\n"
+
+ assert_equal expected, select_day(Time.mktime(2003, 8, 16), :include_blank => true)
+ assert_equal expected, select_day(16, :include_blank => true)
+ end
+
+ def test_select_month
+ expected = "<select name='date[month]'>\n"
+ expected << "<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option value='6'>June</option>\n<option value='7'>July</option>\n<option value='8' selected=\"selected\">August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n"
+ expected << "</select>\n"
+
+ assert_equal expected, select_month(Time.mktime(2003, 8, 16))
+ assert_equal expected, select_month(8)
+ end
+
+ def test_select_month_with_numbers
+ expected = "<select name='date[month]'>\n"
+ expected << "<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8' selected=\"selected\">8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n"
+ expected << "</select>\n"
+
+ assert_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_numbers => true)
+ assert_equal expected, select_month(8, :use_month_numbers => true)
+ end
+
+ def test_select_month_with_numbers_and_names
+ expected = "<select name='date[month]'>\n"
+ expected << "<option value='1'>1 - January</option>\n<option value='2'>2 - February</option>\n<option value='3'>3 - March</option>\n<option value='4'>4 - April</option>\n<option value='5'>5 - May</option>\n<option value='6'>6 - June</option>\n<option value='7'>7 - July</option>\n<option value='8' selected=\"selected\">8 - August</option>\n<option value='9'>9 - September</option>\n<option value='10'>10 - October</option>\n<option value='11'>11 - November</option>\n<option value='12'>12 - December</option>\n"
+ expected << "</select>\n"
+
+ assert_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true)
+ assert_equal expected, select_month(8, :add_month_numbers => true)
+ end
+
+ def test_select_year
+ expected = "<select name='date[year]'>\n"
+ expected << "<option selected=\"selected\">2003</option>\n<option>2004</option>\n<option>2005</option>\n"
+ expected << "</select>\n"
+
+ assert_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005)
+ assert_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005)
+ end
+
+ def test_select_year_with_type_discarding
+ expected = "<select name='date_year'>\n"
+ expected << "<option selected=\"selected\">2003</option>\n<option>2004</option>\n<option>2005</option>\n"
+ expected << "</select>\n"
+
+ assert_equal expected, select_year(
+ Time.mktime(2003, 8, 16), :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
+ assert_equal expected, select_year(
+ 2003, :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
+ end
+
+
+ def test_select_date
+ expected = "<select name='date[first][year]'>\n"
+ expected << "<option selected=\"selected\">2003</option>\n<option>2004</option>\n<option>2005</option>\n"
+ expected << "</select>\n"
+
+ expected << "<select name='date[first][month]'>\n"
+ expected << "<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option value='6'>June</option>\n<option value='7'>July</option>\n<option value='8' selected=\"selected\">August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n"
+ expected << "</select>\n"
+
+ expected << "<select name='date[first][day]'>\n"
+ expected <<
+"<option>1</option>\n<option>2</option>\n<option>3</option>\n<option>4</option>\n<option>5</option>\n<option>6</option>\n<option>7</option>\n<option>8</option>\n<option>9</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option>15</option>\n<option selected=\"selected\">16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option>30</option>\n<option>31</option>\n"
+ expected << "</select>\n"
+
+ assert_equal expected, select_date(
+ Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]"
+ )
+ end
+end
diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb
new file mode 100644
index 0000000000..8f3d5ebb94
--- /dev/null
+++ b/actionpack/test/template/form_helper_test.rb
@@ -0,0 +1,124 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_helper'
+
+class FormHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::FormHelper
+
+ old_verbose, $VERBOSE = $VERBOSE, nil
+ Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on)
+ $VERBOSE = old_verbose
+
+ def setup
+ @post = Post.new
+ def @post.errors() Class.new{ def on(field) field == "author_name" end }.new end
+
+ @post.title = "Hello World"
+ @post.author_name = ""
+ @post.body = "Back to the hill and over it again!"
+ @post.secret = 1
+ @post.written_on = Date.new(2004, 6, 15)
+ end
+
+ def test_text_field
+ assert_equal(
+ '<input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />', text_field("post", "title")
+ )
+ assert_equal(
+ '<input id="post_title" name="post[title]" size="30" type="password" value="Hello World" />', password_field("post", "title")
+ )
+ assert_equal(
+ '<input id="person_name" name="person[name]" size="30" type="password" value="" />', password_field("person", "name")
+ )
+ end
+
+ def test_text_field_with_escapes
+ @post.title = "<b>Hello World</b>"
+ assert_equal(
+ '<input id="post_title" name="post[title]" size="30" type="text" value="&lt;b&gt;Hello World&lt;/b&gt;" />', text_field("post", "title")
+ )
+ end
+
+ def test_text_field_with_options
+ assert_equal(
+ '<input id="post_title" name="post[title]" size="35" type="text" value="Hello World" />',
+ text_field("post", "title", "size" => "35")
+ )
+ end
+
+ def test_text_field_assuming_size
+ assert_equal(
+ '<input id="post_title" maxlength="35" name="post[title]" size="35" type="text" value="Hello World" />',
+ text_field("post", "title", "maxlength" => 35)
+ )
+ end
+
+ def test_check_box
+ assert_equal(
+ '<input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" /><input name="post[secret]" type="hidden" value="0" />',
+ check_box("post", "secret")
+ )
+
+ @post.secret = 0
+ assert_equal(
+ '<input id="post_secret" name="post[secret]" type="checkbox" value="1" /><input name="post[secret]" type="hidden" value="0" />',
+ check_box("post", "secret")
+ )
+
+ @post.secret = true
+ assert_equal(
+ '<input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" /><input name="post[secret]" type="hidden" value="0" />',
+ check_box("post", "secret")
+ )
+ end
+
+ def test_text_area
+ assert_equal(
+ '<textarea cols="40" id="post_body" name="post[body]" rows="20" wrap="virtual">Back to the hill and over it again!</textarea>',
+ text_area("post", "body")
+ )
+ end
+
+ def test_text_area_with_escapes
+ @post.body = "Back to <i>the</i> hill and over it again!"
+ assert_equal(
+ '<textarea cols="40" id="post_body" name="post[body]" rows="20" wrap="virtual">Back to &lt;i&gt;the&lt;/i&gt; hill and over it again!</textarea>',
+ text_area("post", "body")
+ )
+ end
+
+ def test_date_selects
+ assert_equal(
+ '<textarea cols="40" id="post_body" name="post[body]" rows="20" wrap="virtual">Back to the hill and over it again!</textarea>',
+ text_area("post", "body")
+ )
+ end
+
+
+ def test_explicit_name
+ assert_equal(
+ '<input id="post_title" name="dont guess" size="30" type="text" value="Hello World" />', text_field("post", "title", "name" => "dont guess")
+ )
+ assert_equal(
+ '<textarea cols="40" id="post_body" name="really!" rows="20" wrap="virtual">Back to the hill and over it again!</textarea>',
+ text_area("post", "body", "name" => "really!")
+ )
+ assert_equal(
+ '<input checked="checked" id="post_secret" name="i mean it" type="checkbox" value="1" /><input name="i mean it" type="hidden" value="0" />',
+ check_box("post", "secret", "name" => "i mean it")
+ )
+ end
+
+ def test_explicit_id
+ assert_equal(
+ '<input id="dont guess" name="post[title]" size="30" type="text" value="Hello World" />', text_field("post", "title", "id" => "dont guess")
+ )
+ assert_equal(
+ '<textarea cols="40" id="really!" name="post[body]" rows="20" wrap="virtual">Back to the hill and over it again!</textarea>',
+ text_area("post", "body", "id" => "really!")
+ )
+ assert_equal(
+ '<input checked="checked" id="i mean it" name="post[secret]" type="checkbox" value="1" /><input name="post[secret]" type="hidden" value="0" />',
+ check_box("post", "secret", "id" => "i mean it")
+ )
+ end
+end
diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb
new file mode 100644
index 0000000000..fa0a37aa36
--- /dev/null
+++ b/actionpack/test/template/form_options_helper_test.rb
@@ -0,0 +1,165 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_options_helper'
+
+class FormOptionsHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::FormOptionsHelper
+
+ old_verbose, $VERBOSE = $VERBOSE, nil
+ Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin)
+ Continent = Struct.new('Continent', :continent_name, :countries)
+ Country = Struct.new('Country', :country_id, :country_name)
+ $VERBOSE = old_verbose
+
+ def test_collection_options
+ @posts = [
+ Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!")
+ ]
+
+ assert_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
+ options_from_collection_for_select(@posts, "author_name", "title")
+ )
+ end
+
+
+ def test_collection_options_with_preselected_value
+ @posts = [
+ Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!")
+ ]
+
+ assert_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
+ options_from_collection_for_select(@posts, "author_name", "title", "Babe")
+ )
+ end
+
+ def test_collection_options_with_preselected_value_array
+ @posts = [
+ Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!")
+ ]
+
+ assert_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>",
+ options_from_collection_for_select(@posts, "author_name", "title", [ "Babe", "Cabe" ])
+ )
+ end
+
+ def test_array_options_for_select
+ assert_equal(
+ "<option>&lt;Denmark&gt;</option>\n<option>USA</option>\n<option>Sweden</option>",
+ options_for_select([ "<Denmark>", "USA", "Sweden" ])
+ )
+ end
+
+ def test_array_options_for_select_with_selection
+ assert_equal(
+ "<option>Denmark</option>\n<option selected=\"selected\">&lt;USA&gt;</option>\n<option>Sweden</option>",
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], "<USA>")
+ )
+ end
+
+ def test_array_options_for_select_with_selection_array
+ assert_equal(
+ "<option>Denmark</option>\n<option selected=\"selected\">&lt;USA&gt;</option>\n<option selected=\"selected\">Sweden</option>",
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], [ "<USA>", "Sweden" ])
+ )
+ end
+
+ def test_hash_options_for_select
+ assert_equal(
+ "<option value=\"&lt;Kroner&gt;\">&lt;DKR&gt;</option>\n<option value=\"Dollar\">$</option>",
+ options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" })
+ )
+ end
+
+ def test_hash_options_for_select_with_selection
+ assert_equal(
+ "<option value=\"&lt;Kroner&gt;\">&lt;DKR&gt;</option>\n<option value=\"Dollar\" selected=\"selected\">$</option>",
+ options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }, "Dollar")
+ )
+ end
+
+ def test_hash_options_for_select_with_selection
+ assert_equal(
+ "<option value=\"&lt;Kroner&gt;\" selected=\"selected\">&lt;DKR&gt;</option>\n<option value=\"Dollar\" selected=\"selected\">$</option>",
+ options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }, [ "Dollar", "<Kroner>" ])
+ )
+ end
+
+ def test_html_option_groups_from_collection
+ @continents = [
+ Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")] ),
+ Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] )
+ ]
+
+ assert_equal(
+ "<optgroup label=\"&lt;Africa&gt;\"><option value=\"&lt;sa&gt;\">&lt;South Africa&gt;</option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>",
+ option_groups_from_collection_for_select(@continents, "countries", "continent_name", "country_id", "country_name", "dk")
+ )
+ end
+
+ def test_select
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option>abe</option>\n<option selected=\"selected\">&lt;mus&gt;</option>\n<option>hest</option></select>",
+ select("post", "category", %w( abe <mus> hest))
+ )
+ end
+
+ def test_select_with_blank
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option></option>\n<option>abe</option>\n<option selected=\"selected\">&lt;mus&gt;</option>\n<option>hest</option></select>",
+ select("post", "category", %w( abe <mus> hest), :include_blank => true)
+ )
+ end
+
+ def test_collection_select
+ @posts = [
+ Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!")
+ ]
+
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ assert_equal(
+ "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
+ collection_select("post", "author_name", @posts, "author_name", "author_name")
+ )
+ end
+
+ def test_collection_select_with_blank_and_style
+ @posts = [
+ Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!")
+ ]
+
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ assert_equal(
+ "<select id=\"post_author_name\" name=\"post[author_name]\" style=\"width: 200px\"><option></option>\n<option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
+ collection_select("post", "author_name", @posts, "author_name", "author_name", { :include_blank => true }, "style" => "width: 200px")
+ )
+ end
+
+ def test_country_select
+ @post = Post.new
+ @post.origin = "Denmark"
+ assert_equal(
+ "<select id=\"post_origin\" name=\"post[origin]\"><option>Albania</option>\n<option>Algeria</option>\n<option>American Samoa</option>\n<option>Andorra</option>\n<option>Angola</option>\n<option>Anguilla</option>\n<option>Antarctica</option>\n<option>Antigua And Barbuda</option>\n<option>Argentina</option>\n<option>Armenia</option>\n<option>Aruba</option>\n<option>Australia</option>\n<option>Austria</option>\n<option>Azerbaijan</option>\n<option>Bahamas</option>\n<option>Bahrain</option>\n<option>Bangladesh</option>\n<option>Barbados</option>\n<option>Belarus</option>\n<option>Belgium</option>\n<option>Belize</option>\n<option>Benin</option>\n<option>Bermuda</option>\n<option>Bhutan</option>\n<option>Bolivia</option>\n<option>Bosnia and Herzegowina</option>\n<option>Botswana</option>\n<option>Bouvet Island</option>\n<option>Brazil</option>\n<option>British Indian Ocean Territory</option>\n<option>Brunei Darussalam</option>\n<option>Bulgaria</option>\n<option>Burkina Faso</option>\n<option>Burma</option>\n<option>Burundi</option>\n<option>Cambodia</option>\n<option>Cameroon</option>\n<option>Canada</option>\n<option>Cape Verde</option>\n<option>Cayman Islands</option>\n<option>Central African Republic</option>\n<option>Chad</option>\n<option>Chile</option>\n<option>China</option>\n<option>Christmas Island</option>\n<option>Cocos (Keeling) Islands</option>\n<option>Colombia</option>\n<option>Comoros</option>\n<option>Congo</option>\n<option>Congo, the Democratic Republic of the</option>\n<option>Cook Islands</option>\n<option>Costa Rica</option>\n<option>Cote d'Ivoire</option>\n<option>Croatia</option>\n<option>Cyprus</option>\n<option>Czech Republic</option>\n<option selected=\"selected\">Denmark</option>\n<option>Djibouti</option>\n<option>Dominica</option>\n<option>Dominican Republic</option>\n<option>East Timor</option>\n<option>Ecuador</option>\n<option>Egypt</option>\n<option>El Salvador</option>\n<option>England</option>\n<option>Equatorial Guinea</option>\n<option>Eritrea</option>\n<option>Espana</option>\n<option>Estonia</option>\n<option>Ethiopia</option>\n<option>Falkland Islands</option>\n<option>Faroe Islands</option>\n<option>Fiji</option>\n<option>Finland</option>\n<option>France</option>\n<option>French Guiana</option>\n<option>French Polynesia</option>\n<option>French Southern Territories</option>\n<option>Gabon</option>\n<option>Gambia</option>\n<option>Georgia</option>\n<option>Germany</option>\n<option>Ghana</option>\n<option>Gibraltar</option>\n<option>Great Britain</option>\n<option>Greece</option>\n<option>Greenland</option>\n<option>Grenada</option>\n<option>Guadeloupe</option>\n<option>Guam</option>\n<option>Guatemala</option>\n<option>Guinea</option>\n<option>Guinea-Bissau</option>\n<option>Guyana</option>\n<option>Haiti</option>\n<option>Heard and Mc Donald Islands</option>\n<option>Honduras</option>\n<option>Hong Kong</option>\n<option>Hungary</option>\n<option>Iceland</option>\n<option>India</option>\n<option>Indonesia</option>\n<option>Ireland</option>\n<option>Israel</option>\n<option>Italy</option>\n<option>Jamaica</option>\n<option>Japan</option>\n<option>Jordan</option>\n<option>Kazakhstan</option>\n<option>Kenya</option>\n<option>Kiribati</option>\n<option>Korea, Republic of</option>\n<option>Korea (South)</option>\n<option>Kuwait</option>\n<option>Kyrgyzstan</option>\n<option>Lao People's Democratic Republic</option>\n<option>Latvia</option>\n<option>Lebanon</option>\n<option>Lesotho</option>\n<option>Liberia</option>\n<option>Liechtenstein</option>\n<option>Lithuania</option>\n<option>Luxembourg</option>\n<option>Macau</option>\n<option>Macedonia</option>\n<option>Madagascar</option>\n<option>Malawi</option>\n<option>Malaysia</option>\n<option>Maldives</option>\n<option>Mali</option>\n<option>Malta</option>\n<option>Marshall Islands</option>\n<option>Martinique</option>\n<option>Mauritania</option>\n<option>Mauritius</option>\n<option>Mayotte</option>\n<option>Mexico</option>\n<option>Micronesia, Federated States of</option>\n<option>Moldova, Republic of</option>\n<option>Monaco</option>\n<option>Mongolia</option>\n<option>Montserrat</option>\n<option>Morocco</option>\n<option>Mozambique</option>\n<option>Myanmar</option>\n<option>Namibia</option>\n<option>Nauru</option>\n<option>Nepal</option>\n<option>Netherlands</option>\n<option>Netherlands Antilles</option>\n<option>New Caledonia</option>\n<option>New Zealand</option>\n<option>Nicaragua</option>\n<option>Niger</option>\n<option>Nigeria</option>\n<option>Niue</option>\n<option>Norfolk Island</option>\n<option>Northern Ireland</option>\n<option>Northern Mariana Islands</option>\n<option>Norway</option>\n<option>Oman</option>\n<option>Pakistan</option>\n<option>Palau</option>\n<option>Panama</option>\n<option>Papua New Guinea</option>\n<option>Paraguay</option>\n<option>Peru</option>\n<option>Philippines</option>\n<option>Pitcairn</option>\n<option>Poland</option>\n<option>Portugal</option>\n<option>Puerto Rico</option>\n<option>Qatar</option>\n<option>Reunion</option>\n<option>Romania</option>\n<option>Russia</option>\n<option>Rwanda</option>\n<option>Saint Kitts and Nevis</option>\n<option>Saint Lucia</option>\n<option>Saint Vincent and the Grenadines</option>\n<option>Samoa (Independent)</option>\n<option>San Marino</option>\n<option>Sao Tome and Principe</option>\n<option>Saudi Arabia</option>\n<option>Scotland</option>\n<option>Senegal</option>\n<option>Seychelles</option>\n<option>Sierra Leone</option>\n<option>Singapore</option>\n<option>Slovakia</option>\n<option>Slovenia</option>\n<option>Solomon Islands</option>\n<option>Somalia</option>\n<option>South Africa</option>\n<option>South Georgia and the South Sandwich Islands</option>\n<option>South Korea</option>\n<option>Spain</option>\n<option>Sri Lanka</option>\n<option>St. Helena</option>\n<option>St. Pierre and Miquelon</option>\n<option>Suriname</option>\n<option>Svalbard and Jan Mayen Islands</option>\n<option>Swaziland</option>\n<option>Sweden</option>\n<option>Switzerland</option>\n<option>Taiwan</option>\n<option>Tajikistan</option>\n<option>Tanzania</option>\n<option>Thailand</option>\n<option>Togo</option>\n<option>Tokelau</option>\n<option>Tonga</option>\n<option>Trinidad</option>\n<option>Trinidad and Tobago</option>\n<option>Tunisia</option>\n<option>Turkey</option>\n<option>Turkmenistan</option>\n<option>Turks and Caicos Islands</option>\n<option>Tuvalu</option>\n<option>Uganda</option>\n<option>Ukraine</option>\n<option>United Arab Emirates</option>\n<option>United Kingdom</option>\n<option>United States</option>\n<option>United States Minor Outlying Islands</option>\n<option>Uruguay</option>\n<option>Uzbekistan</option>\n<option>Vanuatu</option>\n<option>Vatican City State (Holy See)</option>\n<option>Venezuela</option>\n<option>Viet Nam</option>\n<option>Virgin Islands (British)</option>\n<option>Virgin Islands (U.S.)</option>\n<option>Wales</option>\n<option>Wallis and Futuna Islands</option>\n<option>Western Sahara</option>\n<option>Yemen</option>\n<option>Zambia</option>\n<option>Zimbabwe</option></select>",
+ country_select("post", "origin")
+ )
+ end
+end
diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb
new file mode 100644
index 0000000000..c3289af50c
--- /dev/null
+++ b/actionpack/test/template/tag_helper_test.rb
@@ -0,0 +1,18 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/tag_helper'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/url_helper'
+
+class TagHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::TagHelper
+ include ActionView::Helpers::UrlHelper
+
+ def test_tag
+ assert_equal "<p class=\"show\" />", tag("p", "class" => "show")
+ end
+
+ def test_content_tag
+ assert_equal "<a href=\"create\">Create</a>", content_tag("a", "Create", "href" => "create")
+ end
+
+ # FIXME: Test form tag
+end \ No newline at end of file
diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb
new file mode 100644
index 0000000000..347420a72b
--- /dev/null
+++ b/actionpack/test/template/text_helper_test.rb
@@ -0,0 +1,62 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/text_helper'
+
+class TextHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::TextHelper
+
+ def test_truncate
+ assert_equal "Hello World!", truncate("Hello World!", 12)
+ assert_equal "Hello Worl...", truncate("Hello World!!", 12)
+ end
+
+ def test_strip_links
+ assert_equal "on my mind", strip_links("<a href='almost'>on my mind</a>")
+ end
+
+ def test_highlighter
+ assert_equal(
+ "This is a <strong class=\"highlight\">beautiful</strong> morning",
+ highlight("This is a beautiful morning", "beautiful")
+ )
+
+ assert_equal(
+ "This is a <strong class=\"highlight\">beautiful</strong> morning, but also a <strong class=\"highlight\">beautiful</strong> day",
+ highlight("This is a beautiful morning, but also a beautiful day", "beautiful")
+ )
+
+ assert_equal(
+ "This is a <b>beautiful</b> morning, but also a <b>beautiful</b> day",
+ highlight("This is a beautiful morning, but also a beautiful day", "beautiful", '<b>\1</b>')
+ )
+ end
+
+ def test_highlighter_with_regexp
+ assert_equal(
+ "This is a <strong class=\"highlight\">beautiful!</strong> morning",
+ highlight("This is a beautiful! morning", "beautiful!")
+ )
+
+ assert_equal(
+ "This is a <strong class=\"highlight\">beautiful! morning</strong>",
+ highlight("This is a beautiful! morning", "beautiful! morning")
+ )
+
+ assert_equal(
+ "This is a <strong class=\"highlight\">beautiful? morning</strong>",
+ highlight("This is a beautiful? morning", "beautiful? morning")
+ )
+ end
+
+ def test_excerpt
+ assert_equal("...is a beautiful morni...", excerpt("This is a beautiful morning", "beautiful", 5))
+ assert_equal("This is a...", excerpt("This is a beautiful morning", "this", 5))
+ assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", 5))
+ assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", 5))
+ assert_nil excerpt("This is a beautiful morning", "day")
+ end
+
+ def test_pluralization
+ assert_equal("1 count", pluralize(1, "count"))
+ assert_equal("2 counts", pluralize(2, "count"))
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb
new file mode 100644
index 0000000000..198b26b113
--- /dev/null
+++ b/actionpack/test/template/url_helper_test.rb
@@ -0,0 +1,49 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/url_helper'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/tag_helper'
+
+class UrlHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::UrlHelper
+ include ActionView::Helpers::TagHelper
+
+ def setup
+ @controller = Class.new do
+ def url_for(options, *parameters_for_method_reference)
+ "http://www.world.com"
+ end
+ end
+ @controller = @controller.new
+ end
+
+ # todo: missing test cases
+ def test_link_tag_with_straight_url
+ assert_equal "<a href=\"http://www.world.com\">Hello</a>", link_to("Hello", "http://www.world.com")
+ end
+
+ def test_link_tag_with_javascript_confirm
+ assert_equal(
+ "<a href=\"http://www.world.com\" onclick=\"return confirm('Are you sure?');\">Hello</a>",
+ link_to("Hello", "http://www.world.com", :confirm => "Are you sure?")
+ )
+ end
+
+ def test_link_unless_current
+ @params = { "controller" => "weblog", "action" => "show"}
+ assert_equal "Showing", link_to_unless_current("Showing", :action => "show", :controller => "weblog")
+ assert "<a href=\"http://www.world.com\">Listing</a>", link_to_unless_current("Listing", :action => "list", :controller => "weblog")
+ end
+
+ def test_mail_to
+ assert_equal "<a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>", mail_to("david@loudthinking.com")
+ assert_equal "<a href=\"mailto:david@loudthinking.com\">David Heinemeier Hansson</a>", mail_to("david@loudthinking.com", "David Heinemeier Hansson")
+ assert_equal(
+ "<a class=\"admin\" href=\"mailto:david@loudthinking.com\">David Heinemeier Hansson</a>",
+ mail_to("david@loudthinking.com", "David Heinemeier Hansson", "class" => "admin")
+ )
+ end
+
+ def test_link_with_nil_html_options
+ assert "<a href=\"http://www.world.com\">Hello</a>",
+ link_to("Hello", {:action => 'myaction'}, nil)
+ end
+end \ No newline at end of file
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
new file mode 100644
index 0000000000..538acf172e
--- /dev/null
+++ b/activerecord/CHANGELOG
@@ -0,0 +1,757 @@
+*CVS*
+
+* Added ADO-based SQLServerAdapter (only works on Windows) [Joey Gibson]
+
+* Fixed problems with primary keys and postgresql sequences (#230) [Tim Bates]
+
+* Fixed problems with nested transactions (#231) [Tim Bates]
+
+* Added reloading for associations under cached environments like FastCGI and mod_ruby. This makes it possible to use those environments for development.
+ This is turned on by default, but can be turned off with ActiveRecord::Base.reload_dependencies = false in production environments.
+
+ NOTE: This will only have an effect if you let the associations manage the requiring of model classes. All libraries loaded through
+ require will be "forever" cached. You can, however, use ActiveRecord::Base.load_or_require("library") to get this behavior outside of the
+ auto-loading associations.
+
+* Added ERB capabilities to the fixture files for dynamic fixture generation. You don't need to do anything, just include ERB blocks like:
+
+ david:
+ id: 1
+ name: David
+
+ jamis:
+ id: 2
+ name: Jamis
+
+ <% for digit in 3..10 %>
+ dev_<%= digit %>:
+ id: <%= digit %>
+ name: fixture_<%= digit %>
+ <% end %>
+
+* Changed the yaml fixture searcher to look in the root of the fixtures directory, so when you before could have something like:
+
+ fixtures/developers/fixtures.yaml
+ fixtures/accounts/fixtures.yaml
+
+ ...you now need to do:
+
+ fixtures/developers.yaml
+ fixtures/accounts.yaml
+
+* Changed the fixture format from:
+
+ name: david
+ data:
+ id: 1
+ name: David Heinemeier Hansson
+ birthday: 1979-10-15
+ profession: Systems development
+ ---
+ name: steve
+ data:
+ id: 2
+ name: Steve Ross Kellock
+ birthday: 1974-09-27
+ profession: guy with keyboard
+
+ ...to:
+
+ david:
+ id: 1
+ name: David Heinemeier Hansson
+ birthday: 1979-10-15
+ profession: Systems development
+
+ steve:
+ id: 2
+ name: Steve Ross Kellock
+ birthday: 1974-09-27
+ profession: guy with keyboard
+
+ The change is NOT backwards compatible. Fixtures written in the old YAML style needs to be rewritten!
+
+* All associations will now attempt to require the classes that they associate to. Relieving the need for most explicit 'require' statements.
+
+*1.1.0* (34)
+
+* Added automatic fixture setup and instance variable availability. Fixtures can also be automatically
+ instantiated in instance variables relating to their names using the following style:
+
+ class FixturesTest < Test::Unit::TestCase
+ fixtures :developers # you can add more with comma separation
+
+ def test_developers
+ assert_equal 3, @developers.size # the container for all the fixtures is automatically set
+ assert_kind_of Developer, @david # works like @developers["david"].find
+ assert_equal "David Heinemeier Hansson", @david.name
+ end
+ end
+
+* Added HasAndBelongsToManyAssociation#push_with_attributes(object, join_attributes) that can create associations in the join table with additional
+ attributes. This is really useful when you have information that's only relevant to the join itself, such as a "added_on" column for an association
+ between post and category. The added attributes will automatically be injected into objects retrieved through the association similar to the piggy-back
+ approach:
+
+ post.categories.push_with_attributes(category, :added_on => Date.today)
+ post.categories.first.added_on # => Date.today
+
+ NOTE: The categories table doesn't have a added_on column, it's the categories_post join table that does!
+
+* Fixed that :exclusively_dependent and :dependent can't be activated at the same time on has_many associations [bitsweat]
+
+* Fixed that database passwords couldn't be all numeric [bitsweat]
+
+* Fixed that calling id would create the instance variable for new_records preventing them from being saved correctly [bitsweat]
+
+* Added sanitization feature to HasManyAssociation#find_all so it works just like Base.find_all [Sam Stephenson/bitsweat]
+
+* Added that you can pass overlapping ids to find without getting duplicated records back [bitsweat]
+
+* Added that Base.benchmark returns the result of the block [bitsweat]
+
+* Fixed problem with unit tests on Windows with SQLite [paterno]
+
+* Fixed that quotes would break regular non-yaml fixtures [Dmitry Sabanin/daft]
+
+* Fixed fixtures on windows with line endings cause problems under unix / mac [Tobias Luetke]
+
+* Added HasAndBelongsToManyAssociation#find(id) that'll search inside the collection and find the object or record with that id
+
+* Added :conditions option to has_and_belongs_to_many that works just like the one on all the other associations
+
+* Added AssociationCollection#clear to remove all associations from has_many and has_and_belongs_to_many associations without destroying the records [geech]
+
+* Added type-checking and remove in 1-instead-of-N sql statements to AssociationCollection#delete [geech]
+
+* Added a return of self to AssociationCollection#<< so appending can be chained, like project << Milestone.create << Milestone.create [geech]
+
+* Added Base#hash and Base#eql? which means that all of the equality using features of array and other containers now works:
+
+ [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
+
+* Added :uniq as an option to has_and_belongs_to_many which will automatically ensure that AssociateCollection#uniq is called
+ before pulling records out of the association. This is especially useful for three-way (and above) has_and_belongs_to_many associations.
+
+* Added AssociateCollection#uniq which is especially useful for has_and_belongs_to_many associations that can include duplicates,
+ which is common on associations that also use metadata. Usage: post.categories.uniq
+
+* Fixed respond_to? to use a subclass specific hash instead of an Active Record-wide one
+
+* Fixed has_and_belongs_to_many to treat associations between classes in modules properly [Florian Weber]
+
+* Added a NoMethod exception to be raised when query and writer methods are called for attributes that doesn't exist [geech]
+
+* Added a more robust version of Fixtures that throws meaningful errors when on formatting issues [geech]
+
+* Added Base#transaction as a compliment to Base.transaction for prettier use in instance methods [geech]
+
+* Improved the speed of respond_to? by placing the dynamic methods lookup table in a hash [geech]
+
+* Added that any additional fields added to the join table in a has_and_belongs_to_many association
+ will be placed as attributes when pulling records out through has_and_belongs_to_many associations.
+ This is helpful when have information about the association itself that you want available on retrival.
+
+* Added better loading exception catching and RubyGems retries to the database adapters [alexeyv]
+
+* Fixed bug with per-model transactions [daniel]
+
+* Fixed Base#transaction so that it returns the result of the last expression in the transaction block [alexeyv]
+
+* Added Fixture#find to find the record corresponding to the fixture id. The record
+ class name is guessed by using Inflector#classify (also new) on the fixture directory name.
+
+ Before: Document.find(@documents["first"]["id"])
+ After : @documents["first"].find
+
+* Fixed that the table name part of column names ("TABLE.COLUMN") wasn't removed properly [Andreas Schwarz]
+
+* Fixed a bug with Base#size when a finder_sql was used that didn't capitalize SELECT and FROM [geech]
+
+* Fixed quoting problems on SQLite by adding quote_string to the AbstractAdapter that can be overwritten by the concrete
+ adapters for a call to the dbm. [Andreas Schwarz]
+
+* Removed RubyGems backup strategy for requiring SQLite-adapter -- if people want to use gems, they're already doing it with AR.
+
+
+*1.0.0 (35)*
+
+* Added OO-style associations methods [Florian Weber]. Examples:
+
+ Project#milestones_count => Project#milestones.size
+ Project#build_to_milestones => Project#milestones.build
+ Project#create_for_milestones => Project#milestones.create
+ Project#find_in_milestones => Project#milestones.find
+ Project#find_all_in_milestones => Project#milestones.find_all
+
+* Added serialize as a new class method to control when text attributes should be YAMLized or not. This means that automated
+ serialization of hashes, arrays, and so on WILL NO LONGER HAPPEN (#10). You need to do something like this:
+
+ class User < ActiveRecord::Base
+ serialize :settings
+ end
+
+ This will assume that settings is a text column and will now YAMLize any object put in that attribute. You can also specify
+ an optional :class_name option that'll raise an exception if a serialized object is retrieved as a descendent of a class not in
+ the hierarchy. Example:
+
+ class User < ActiveRecord::Base
+ serialize :settings, :class_name => "Hash"
+ end
+
+ user = User.create("settings" => %w( one two three ))
+ User.find(user.id).settings # => raises SerializationTypeMismatch
+
+* Added the option to connect to a different database for one model at a time. Just call establish_connection on the class
+ you want to have connected to another database than Base. This will automatically also connect decendents of that class
+ to the different database [Renald Buter].
+
+* Added transactional protection for Base#save. Validations can now check for values knowing that it happens in a transaction and callbacks
+ can raise exceptions knowing that the save will be rolled back. [Suggested by Alexey Verkhovsky]
+
+* Added column name quoting so reserved words, such as "references", can be used as column names [Ryan Platte]
+
+* Added the possibility to chain the return of what happened inside a logged block [geech]:
+
+ This now works:
+ log { ... }.map { ... }
+
+ Instead of doing:
+ result = []
+ log { result = ... }
+ result.map { ... }
+
+* Added "socket" option for the MySQL adapter, so you can change it to something else than "/tmp/mysql.sock" [Anna Lissa Cruz]
+
+* Added respond_to? answers for all the attribute methods. So if Person has a name attribute retrieved from the table schema,
+ person.respond_to? "name" will return true.
+
+* Added Base.benchmark which can be used to aggregate logging and benchmark, so you can measure and represent multiple statements in a single block.
+ Usage (hides all the SQL calls for the individual actions and calculates total runtime for them all):
+
+ Project.benchmark("Creating project") do
+ project = Project.create("name" => "stuff")
+ project.create_manager("name" => "David")
+ project.milestones << Milestone.find_all
+ end
+
+* Added logging of invalid SQL statements [Suggested by Daniel Von Fange]
+
+* Added alias Errors#[] for Errors#on, so you can now say person.errors["name"] to retrieve the errors for name [Andreas Schwarz]
+
+* Added RubyGems require attempt if sqlite-ruby is not available through regular methods.
+
+* Added compatibility with 2.x series of sqlite-ruby drivers. [Jamis Buck]
+
+* Added type safety for association assignments, so a ActiveRecord::AssociationTypeMismatch will be raised if you attempt to
+ assign an object that's not of the associated class. This cures the problem with nil giving id = 4 and fixnums giving id = 1 on
+ mistaken association assignments. [Reported by Andreas Schwarz]
+
+* Added the option to keep many fixtures in one single YAML document [what-a-day]
+
+* Added the class method "inheritance_column" that can be overwritten to return the name of an alternative column than "type" for storing
+ the type for inheritance hierarchies. [Dave Steinberg]
+
+* Added [] and []= as an alternative way to access attributes when the regular methods have been overwritten [Dave Steinberg]
+
+* Added the option to observer more than one class at the time by specifying observed_class as an array
+
+* Added auto-id propagation support for tables with arbitrary primary keys that have autogenerated sequences associated with them
+ on PostgreSQL. [Dave Steinberg]
+
+* Changed that integer and floats set to "" through attributes= remain as NULL. This was especially a problem for scaffolding and postgresql. (#49)
+
+* Changed the MySQL Adapter to rely on MySQL for its defaults for socket, host, and port [Andreas Schwarz]
+
+* Changed ActionControllerError to decent from StandardError instead of Exception. It can now be caught by a generic rescue.
+
+* Changed class inheritable attributes to not use eval [Caio Chassot]
+
+* Changed Errors#add to now use "invalid" as the default message instead of true, which means full_messages work with those [Marcel Molina Jr]
+
+* Fixed spelling on Base#add_on_boundry_breaking to Base#add_on_boundary_breaking (old naming still works) [Marcel Molina Jr.]
+
+* Fixed that entries in the has_and_belongs_to_many join table didn't get removed when an associated object was destroyed.
+
+* Fixed unnecessary calls to SET AUTOCOMMIT=0/1 for MySQL adapter [Andreas Schwarz]
+
+* Fixed PostgreSQL defaults are now handled gracefully [Dave Steinberg]
+
+* Fixed increment/decrement_counter are now atomic updates [Andreas Schwarz]
+
+* Fixed the problems the Inflector had turning Attachment into attuchments and Cases into Casis [radsaq/Florian Gross]
+
+* Fixed that cloned records would point attribute references on the parent object [Andreas Schwarz]
+
+* Fixed SQL for type call on inheritance hierarchies [Caio Chassot]
+
+* Fixed bug with typed inheritance [Florian Weber]
+
+* Fixed a bug where has_many collection_count wouldn't use the conditions specified for that association
+
+
+*0.9.5*
+
+* Expanded the table_name guessing rules immensely [Florian Green]. Documentation:
+
+ Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
+ directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used
+ to guess the table name from even when called on Reply. The guessing rules are as follows:
+ * Class name ends in "x", "ch" or "ss": "es" is appended, so a Search class becomes a searches table.
+ * Class name ends in "y" preceded by a consonant or "qu": The "y" is replaced with "ies",
+ so a Category class becomes a categories table.
+ * Class name ends in "fe": The "fe" is replaced with "ves", so a Wife class becomes a wives table.
+ * Class name ends in "lf" or "rf": The "f" is replaced with "ves", so a Half class becomes a halves table.
+ * Class name ends in "person": The "person" is replaced with "people", so a Salesperson class becomes a salespeople table.
+ * Class name ends in "man": The "man" is replaced with "men", so a Spokesman class becomes a spokesmen table.
+ * Class name ends in "sis": The "i" is replaced with an "e", so a Basis class becomes a bases table.
+ * Class name ends in "tum" or "ium": The "um" is replaced with an "a", so a Datum class becomes a data table.
+ * Class name ends in "child": The "child" is replaced with "children", so a NodeChild class becomes a node_children table.
+ * Class name ends in an "s": No additional characters are added or removed.
+ * Class name doesn't end in "s": An "s" is appended, so a Comment class becomes a comments table.
+ * Class name with word compositions: Compositions are underscored, so CreditCard class becomes a credit_cards table.
+ Additionally, the class-level table_name_prefix is prepended to the table_name and the table_name_suffix is appended.
+ So if you have "myapp_" as a prefix, the table name guess for an Account class becomes "myapp_accounts".
+
+ You can also overwrite this class method to allow for unguessable links, such as a Mouse class with a link to a
+ "mice" table. Example:
+
+ class Mouse < ActiveRecord::Base
+ def self.table_name() "mice" end
+ end
+
+ This conversion is now done through an external class called Inflector residing in lib/active_record/support/inflector.rb.
+
+* Added find_all_in_collection to has_many defined collections. Works like this:
+
+ class Firm < ActiveRecord::Base
+ has_many :clients
+ end
+
+ firm.id # => 1
+ firm.find_all_in_clients "revenue > 1000" # SELECT * FROM clients WHERE firm_id = 1 AND revenue > 1000
+
+ [Requested by Dave Thomas]
+
+* Fixed finders for inheritance hierarchies deeper than one level [Florian Weber]
+
+* Added add_on_boundry_breaking to errors to accompany add_on_empty as a default validation method. It's used like this:
+
+ class Person < ActiveRecord::Base
+ protected
+ def validation
+ errors.add_on_boundry_breaking "password", 3..20
+ end
+ end
+
+ This will add an error to the tune of "is too short (min is 3 characters)" or "is too long (min is 20 characters)" if
+ the password is outside the boundry. The messages can be changed by passing a third and forth parameter as message strings.
+
+* Implemented a clone method that works properly with AR. It returns a clone of the record that
+ hasn't been assigned an id yet and is treated as a new record.
+
+* Allow for domain sockets in PostgreSQL by not assuming localhost when no host is specified [Scott Barron]
+
+* Fixed that bignums are saved properly instead of attempted to be YAMLized [Andreas Schwartz]
+
+* Fixed a bug in the GEM where the rdoc options weren't being passed according to spec [Chad Fowler]
+
+* Fixed a bug with the exclusively_dependent option for has_many
+
+
+*0.9.4*
+
+* Correctly guesses the primary key when the class is inside a module [Dave Steinberg].
+
+* Added [] and []= as alternatives to read_attribute and write_attribute [Dave Steinberg]
+
+* has_and_belongs_to_many now accepts an :order key to determine in which order the collection is returned [radsaq].
+
+* The ids passed to find and find_on_conditions are now automatically sanitized.
+
+* Added escaping of plings in YAML content.
+
+* Multi-parameter assigns where all the parameters are empty will now be set to nil instead of a new instance of their class.
+
+* Proper type within an inheritance hierarchy is now ensured already at object initialization (instead of first at create)
+
+
+*0.9.3*
+
+* Fixed bug with using a different primary key name together with has_and_belongs_to_many [Investigation by Scott]
+
+* Added :exclusively_dependent option to the has_many association macro. The doc reads:
+
+ If set to true all the associated object are deleted in one SQL statement without having their
+ before_destroy callback run. This should only be used on associations that depend solely on
+ this class and don't need to do any clean-up in before_destroy. The upside is that it's much
+ faster, especially if there's a counter_cache involved.
+
+* Added :port key to connection options, so the PostgreSQL and MySQL adapters can connect to a database server
+ running on another port than the default.
+
+* Converted the new natural singleton methods that prevented AR objects from being saved by PStore
+ (and hence be placed in a Rails session) to a module. [Florian Weber]
+
+* Fixed the use of floats (was broken since 0.9.0+)
+
+* Fixed PostgreSQL adapter so default values are displayed properly when used in conjunction with
+ Action Pack scaffolding.
+
+* Fixed booleans support for PostgreSQL (use real true/false on boolean fields instead of 0/1 on tinyints) [radsaq]
+
+
+*0.9.2*
+
+* Added static method for instantly updating a record
+
+* Treat decimal and numeric as Ruby floats [Andreas Schwartz]
+
+* Treat chars as Ruby strings (fixes problem for Action Pack form helpers too)
+
+* Removed debugging output accidently left in (which would screw web applications)
+
+
+*0.9.1*
+
+* Added MIT license
+
+* Added natural object-style assignment for has_and_belongs_to_many associations. Consider the following model:
+
+ class Event < ActiveRecord::Base
+ has_one_and_belongs_to_many :sponsors
+ end
+
+ class Sponsor < ActiveRecord::Base
+ has_one_and_belongs_to_many :sponsors
+ end
+
+ Earlier, you'd have to use synthetic methods for creating associations between two objects of the above class:
+
+ roskilde_festival.add_to_sponsors(carlsberg)
+ roskilde_festival.remove_from_sponsors(carlsberg)
+
+ nike.add_to_events(world_cup)
+ nike.remove_from_events(world_cup)
+
+ Now you can use regular array-styled methods:
+
+ roskilde_festival.sponsors << carlsberg
+ roskilde_festival.sponsors.delete(carlsberg)
+
+ nike.events << world_cup
+ nike.events.delete(world_cup)
+
+* Added delete method for has_many associations. Using this will nullify an association between the has_many and the belonging
+ object by setting the foreign key to null. Consider this model:
+
+ class Post < ActiveRecord::Base
+ has_many :comments
+ end
+
+ class Comment < ActiveRecord::Base
+ belongs_to :post
+ end
+
+ You could do something like:
+
+ funny_comment.has_post? # => true
+ announcement.comments.delete(funny_comment)
+ funny_comment.has_post? # => false
+
+
+*0.9.0*
+
+* Active Record is now thread safe! (So you can use it with Cerise and WEBrick applications)
+ [Implementation idea by Michael Neumann, debugging assistance by Jamis Buck]
+
+* Improved performance by roughly 400% on a basic test case of pulling 100 records and querying one attribute.
+ This brings the tax for using Active Record instead of "riding on the metal" (using MySQL-ruby C-driver directly) down to ~50%.
+ Done by doing lazy type conversions and caching column information on the class-level.
+
+* Added callback objects and procs as options for implementing the target for callback macros.
+
+* Added "counter_cache" option to belongs_to that automates the usage of increment_counter and decrement_counter. Consider:
+
+ class Post < ActiveRecord::Base
+ has_many :comments
+ end
+
+ class Comment < ActiveRecord::Base
+ belongs_to :post
+ end
+
+ Iterating over 100 posts like this:
+
+ <% for post in @posts %>
+ <%= post.title %> has <%= post.comments_count %> comments
+ <% end %>
+
+ Will generate 100 SQL count queries -- one for each call to post.comments_count. If you instead add a "comments_count" int column
+ to the posts table and rewrite the comments association macro with:
+
+ class Comment < ActiveRecord::Base
+ belongs_to :post, :counter_cache => true
+ end
+
+ Those 100 SQL count queries will be reduced to zero. Beware that counter caching is only appropriate for objects that begin life
+ with the object it's specified to belong with and is destroyed like that as well. Typically objects where you would also specify
+ :dependent => true. If your objects switch from one belonging to another (like a post that can be move from one category to another),
+ you'll have to manage the counter yourself.
+
+* Added natural object-style assignment for has_one and belongs_to associations. Consider the following model:
+
+ class Project < ActiveRecord::Base
+ has_one :manager
+ end
+
+ class Manager < ActiveRecord::Base
+ belongs_to :project
+ end
+
+ Earlier, assignments would work like following regardless of which way the assignment told the best story:
+
+ active_record.manager_id = david.id
+
+ Now you can do it either from the belonging side:
+
+ david.project = active_record
+
+ ...or from the having side:
+
+ active_record.manager = david
+
+ If the assignment happens from the having side, the assigned object is automatically saved. So in the example above, the
+ project_id attribute on david would be set to the id of active_record, then david would be saved.
+
+* Added natural object-style assignment for has_many associations [Florian Weber]. Consider the following model:
+
+ class Project < ActiveRecord::Base
+ has_many :milestones
+ end
+
+ class Milestone < ActiveRecord::Base
+ belongs_to :project
+ end
+
+ Earlier, assignments would work like following regardless of which way the assignment told the best story:
+
+ deadline.project_id = active_record.id
+
+ Now you can do it either from the belonging side:
+
+ deadline.project = active_record
+
+ ...or from the having side:
+
+ active_record.milestones << deadline
+
+ The milestone is automatically saved with the new foreign key.
+
+* API CHANGE: Attributes for text (or blob or similar) columns will now have unknown classes stored using YAML instead of using
+ to_s. (Known classes that won't be yamelized are: String, NilClass, TrueClass, FalseClass, Fixnum, Date, and Time).
+ Likewise, data pulled out of text-based attributes will be attempted converged using Yaml if they have the "--- " header.
+ This was primarily done to be enable the storage of hashes and arrays without wrapping them in aggregations, so now you can do:
+
+ user = User.find(1)
+ user.preferences = { "background" => "black", "display" => large }
+ user.save
+
+ User.find(1).preferences # => { "background" => "black", "display" => large }
+
+ Please note that this method should only be used when you don't care about representing the object in proper columns in
+ the database. A money object consisting of an amount and a currency is still a much better fit for a value object done through
+ aggregations than this new option.
+
+* POSSIBLE CODE BREAKAGE: As a consequence of the lazy type conversions, it's a bad idea to reference the @attributes hash
+ directly (it always was, but now it's paramount that you don't). If you do, you won't get the type conversion. So to implement
+ new accessors for existing attributes, use read_attribute(attr_name) and write_attribute(attr_name, value) instead. Like this:
+
+ class Song < ActiveRecord::Base
+ # Uses an integer of seconds to hold the length of the song
+
+ def length=(minutes)
+ write_attribute("length", minutes * 60)
+ end
+
+ def length
+ read_attribute("length") / 60
+ end
+ end
+
+ The clever kid will notice that this opens a door to sidestep the automated type conversion by using @attributes directly.
+ This is not recommended as read/write_attribute may be granted additional responsibilities in the future, but if you think
+ you know what you're doing and aren't afraid of future consequences, this is an option.
+
+* Applied a few minor bug fixes reported by Daniel Von Fange.
+
+
+*0.8.4*
+
+_Reflection_
+
+* Added ActiveRecord::Reflection with a bunch of methods and classes for reflecting in aggregations and associations.
+
+* Added Base.columns and Base.content_columns which returns arrays of column description (type, default, etc) objects.
+
+* Added Base#attribute_names which returns an array of names for the attributes available on the object.
+
+* Added Base#column_for_attribute(name) which returns the column description object for the named attribute.
+
+
+_Misc_
+
+* Added multi-parameter assignment:
+
+ # Instantiate objects for all attribute classes that needs more than one constructor parameter. This is done
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
+ # parenteses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float,
+ # s for String, and a for Array.
+
+ This is incredibly useful for assigning dates from HTML drop-downs of month, year, and day.
+
+* Fixed bug with custom primary key column name and Base.find on multiple parameters.
+
+* Fixed bug with dependent option on has_one associations if there was no associated object.
+
+
+*0.8.3*
+
+_Transactions_
+
+* Added transactional protection for destroy (important for the new :dependent option) [Suggested by Carl Youngblood]
+
+* Fixed so transactions are ignored on MyISAM tables for MySQL (use InnoDB to get transactions)
+
+* Changed transactions so only exceptions will cause a rollback, not returned false.
+
+
+_Mapping_
+
+* Added support for non-integer primary keys [Aredridel/earlier work by Michael Neumann]
+
+ User.find "jdoe"
+ Product.find "PDKEY-INT-12"
+
+* Added option to specify naming method for primary key column. ActiveRecord::Base.primary_key_prefix_type can either
+ be set to nil, :table_name, or :table_name_with_underscore. :table_name will assume that Product class has a primary key
+ of "productid" and :table_name_with_underscore will assume "product_id". The default nil will just give "id".
+
+* Added an overwriteable primary_key method that'll instruct AR to the name of the
+ id column [Aredridele/earlier work by Guan Yang]
+
+ class Project < ActiveRecord::Base
+ def self.primary_key() "project_id" end
+ end
+
+* Fixed that Active Records can safely associate inside and out of modules.
+
+ class MyApplication::Account < ActiveRecord::Base
+ has_many :clients # will look for MyApplication::Client
+ has_many :interests, :class_name => "Business::Interest" # will look for Business::Interest
+ end
+
+* Fixed that Active Records can safely live inside modules [Aredridel]
+
+ class MyApplication::Account < ActiveRecord::Base
+ end
+
+
+_Misc_
+
+* Added freeze call to value object assignments to ensure they remain immutable [Spotted by Gavin Sinclair]
+
+* Changed interface for specifying observed class in observers. Was OBSERVED_CLASS constant, now is
+ observed_class() class method. This is more consistant with things like self.table_name(). Works like this:
+
+ class AuditObserver < ActiveRecord::Observer
+ def self.observed_class() Account end
+ def after_update(account)
+ AuditTrail.new(account, "UPDATED")
+ end
+ end
+
+ [Suggested by Gavin Sinclair]
+
+* Create new Active Record objects by setting the attributes through a block. Like this:
+
+ person = Person.new do |p|
+ p.name = 'Freddy'
+ p.age = 19
+ end
+
+ [Suggested by Gavin Sinclair]
+
+
+*0.8.2*
+
+* Added inheritable callback queues that can ensure that certain callback methods or inline fragments are
+ run throughout the entire inheritance hierarchy. Regardless of whether a descendent overwrites the callback
+ method:
+
+ class Topic < ActiveRecord::Base
+ before_destroy :destroy_author, 'puts "I'm an inline fragment"'
+ end
+
+ Learn more in link:classes/ActiveRecord/Callbacks.html
+
+* Added :dependent option to has_many and has_one, which will automatically destroy associated objects when
+ the holder is destroyed:
+
+ class Album < ActiveRecord::Base
+ has_many :tracks, :dependent => true
+ end
+
+ All the associated tracks are destroyed when the album is.
+
+* Added Base.create as a factory that'll create, save, and return a new object in one step.
+
+* Automatically convert strings in config hashes to symbols for the _connection methods. This allows you
+ to pass the argument hashes directly from yaml. (Luke)
+
+* Fixed the install.rb to include simple.rb [Spotted by Kevin Bullock]
+
+* Modified block syntax to better follow our code standards outlined in
+ http://www.rubyonrails.org/CodingStandards
+
+
+*0.8.1*
+
+* Added object-level transactions [Thanks to Austin Ziegler for Transaction::Simple]
+
+* Changed adapter-specific connection methods to use centralized ActiveRecord::Base.establish_connection,
+ which is parametized through a config hash with symbol keys instead of a regular parameter list.
+ This will allow for database connections to be opened in a more generic fashion. (Luke)
+
+ NOTE: This requires all *_connections to be updated! Read more in:
+ http://ar.rubyonrails.org/classes/ActiveRecord/Base.html#M000081
+
+* Fixed SQLite adapter so objects fetched from has_and_belongs_to_many have proper attributes
+ (t.name is now name). [Spotted by Garrett Rooney]
+
+* Fixed SQLite adapter so dates are returned as Date objects, not Time objects [Spotted by Gavin Sinclair]
+
+* Fixed requirement of date class, so date conversions are succesful regardless of whether you
+ manually require date or not.
+
+
+*0.8.0*
+
+* Added transactions
+
+* Changed Base.find to also accept either a list (1, 5, 6) or an array of ids ([5, 7])
+ as parameter and then return an array of objects instead of just an object
+
+* Fixed method has_collection? for has_and_belongs_to_many macro to behave as a
+ collection, not an association
+
+* Fixed SQLite adapter so empty or nil values in columns of datetime, date, or time type
+ aren't treated as current time [Spotted by Gavin Sinclair]
+
+
+*0.7.6*
+
+* Fixed the install.rb to create the lib/active_record/support directory [Spotted by Gavin Sinclair]
+* Fixed that has_association? would always return true [Spotted by Daniel Von Fange]
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
new file mode 100644
index 0000000000..5919c288e4
--- /dev/null
+++ b/activerecord/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2004 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/activerecord/README b/activerecord/README
new file mode 100755
index 0000000000..258b98f296
--- /dev/null
+++ b/activerecord/README
@@ -0,0 +1,361 @@
+= Active Record -- Object-relation mapping put on rails
+
+Active Record connects business objects and database tables to create a persistable
+domain model where logic and data is presented in one wrapping. It's an implementation
+of the object-relational mapping (ORM) pattern[http://www.martinfowler.com/eaaCatalog/activeRecord.html]
+by the same name as described by Martin Fowler:
+
+ "An object that wraps a row in a database table or view, encapsulates
+ the database access, and adds domain logic on that data."
+
+Active Records main contribution to the pattern is to relieve the original of two stunting problems:
+lack of associations and inheritance. By adding a simple domain language-like set of macros to describe
+the former and integrating the Single Table Inheritance pattern for the latter, Active Record narrows the
+gap of functionality between the data mapper and active record approach.
+
+A short rundown of the major features:
+
+* Automated mapping between classes and tables, attributes and columns.
+
+ class Product < ActiveRecord::Base; end
+
+ ...is automatically mapped to the table named "products", such as:
+
+ CREATE TABLE products (
+ id int(11) NOT NULL auto_increment,
+ name varchar(255),
+ PRIMARY KEY (id)
+ );
+
+ ...which again gives Product#name and Product#name=(new_name)
+
+ Learn more in link:classes/ActiveRecord/Base.html
+
+
+* Associations between objects controlled by simple meta-programming macros.
+
+ class Firm < ActiveRecord::Base
+ has_many :clients
+ has_one :account
+ belongs_to :conglomorate
+ end
+
+ Learn more in link:classes/ActiveRecord/Associations/ClassMethods.html
+
+
+* Aggregations of value objects controlled by simple meta-programming macros.
+
+ class Account < ActiveRecord::Base
+ composed_of :balance, :class_name => "Money",
+ :mapping => %w(balance amount)
+ composed_of :address,
+ :mapping => [%w(address_street street), %w(address_city city)]
+ end
+
+ Learn more in link:classes/ActiveRecord/Aggregations/ClassMethods.html
+
+
+* Validation rules that can differ for new or existing objects.
+
+ class Post < ActiveRecord::Base
+ def validate # validates on both creates and updates
+ errors.add_on_empty "title"
+ end
+
+ def validate_on_update
+ errors.add_on_empty "password"
+ end
+ end
+
+ Learn more in link:classes/ActiveRecord/Validations.html
+
+
+* Callbacks as methods or queues on the entire lifecycle (instantiation, saving, destroying, validating, etc).
+
+ class Person < ActiveRecord::Base
+ def before_destroy # is called just before Person#destroy
+ CreditCard.find(credit_card_id).destroy
+ end
+ end
+
+ class Account < ActiveRecord::Base
+ after_find :eager_load, 'self.class.announce(#{id})'
+ end
+
+ Learn more in link:classes/ActiveRecord/Callbacks.html
+
+
+* Observers for the entire lifecycle
+
+ class CommentObserver < ActiveRecord::Observer
+ def after_create(comment) # is called just after Comment#save
+ NotificationService.send_email("david@loudthinking.com", comment)
+ end
+ end
+
+ Learn more in link:classes/ActiveRecord/Observer.html
+
+
+* Inheritance hierarchies
+
+ class Company < ActiveRecord::Base; end
+ class Firm < Company; end
+ class Client < Company; end
+ class PriorityClient < Client; end
+
+ Learn more in link:classes/ActiveRecord/Base.html
+
+
+* Transaction support on both a database and object level. The latter is implemented
+ by using Transaction::Simple[http://www.halostatue.ca/ruby/Transaction__Simple.html]
+
+ # Just database transaction
+ Account.transaction do
+ david.withdrawal(100)
+ mary.deposit(100)
+ end
+
+ # Database and object transaction
+ Account.transaction(david, mary) do
+ david.withdrawal(100)
+ mary.deposit(100)
+ end
+
+ Learn more in link:classes/ActiveRecord/Transactions/ClassMethods.html
+
+
+* Reflections on columns, associations, and aggregations
+
+ reflection = Firm.reflect_on_association(:clients)
+ reflection.klass # => Client (class)
+ Firm.columns # Returns an array of column descriptors for the firms table
+
+ Learn more in link:classes/ActiveRecord/Reflection/ClassMethods.html
+
+
+* Direct manipulation (instead of service invocation)
+
+ So instead of (Hibernate[http://www.hibernate.org/] example):
+
+ long pkId = 1234;
+ DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );
+ // something interesting involving a cat...
+ sess.save(cat);
+ sess.flush(); // force the SQL INSERT
+
+ Active Record lets you:
+
+ pkId = 1234
+ cat = Cat.find(pkId)
+ # something even more interesting involving a the same cat...
+ cat.save
+
+ Learn more in link:classes/ActiveRecord/Base.html
+
+
+* Database abstraction through simple adapters (~100 lines) with a shared connector
+
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite", :dbfile => "dbfile")
+
+ ActiveRecord::Base.establish_connection(
+ :adapter => "mysql",
+ :host => "localhost",
+ :username => "me",
+ :password => "secret",
+ :database => "activerecord"
+ )
+
+ Learn more in link:classes/ActiveRecord/Base.html#M000081
+
+
+* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]
+
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
+ ActiveRecord::Base.logger = Log4r::Logger.new("Application Log")
+
+
+== Simple example (1/2): Defining tables and classes (using MySQL)
+
+Data definitions are specified only in the database. Active Record queries the database for
+the column names (that then serves to determine which attributes are valid) on regular
+objects instantiation through the new constructor and relies on the column names in the rows
+with the finders.
+
+ # CREATE TABLE companies (
+ # id int(11) unsigned NOT NULL auto_increment,
+ # client_of int(11),
+ # name varchar(255),
+ # type varchar(100),
+ # PRIMARY KEY (id)
+ # )
+
+Active Record automatically links the "Company" object to the "companies" table
+
+ class Company < ActiveRecord::Base
+ has_many :people, :class_name => "Person"
+ end
+
+ class Firm < Company
+ has_many :clients
+
+ def people_with_all_clients
+ clients.inject([]) { |people, client| people + client.people }
+ end
+ end
+
+The foreign_key is only necessary because we didn't use "firm_id" in the data definition
+
+ class Client < Company
+ belongs_to :firm, :foreign_key => "client_of"
+ end
+
+ # CREATE TABLE people (
+ # id int(11) unsigned NOT NULL auto_increment,
+ # name text,
+ # company_id text,
+ # PRIMARY KEY (id)
+ # )
+
+Active Record will also automatically link the "Person" object to the "people" table
+
+ class Person < ActiveRecord::Base
+ belongs_to :company
+ end
+
+== Simple example (2/2): Using the domain
+
+Picking a database connection for all the active records
+
+ ActiveRecord::Base.establish_connection(
+ :adapter => "mysql",
+ :host => "localhost",
+ :username => "me",
+ :password => "secret",
+ :database => "activerecord"
+ )
+
+Create some fixtures
+
+ firm = Firm.new("name" => "Next Angle")
+ # SQL: INSERT INTO companies (name, type) VALUES("Next Angle", "Firm")
+ firm.save
+
+ client = Client.new("name" => "37signals", "client_of" => firm.id)
+ # SQL: INSERT INTO companies (name, client_of, type) VALUES("37signals", 1, "Firm")
+ client.save
+
+Lots of different finders
+
+ # SQL: SELECT * FROM companies WHERE id = 1
+ next_angle = Company.find(1)
+
+ # SQL: SELECT * FROM companies WHERE id = 1 AND type = 'Firm'
+ next_angle = Firm.find(1)
+
+ # SQL: SELECT * FROM companies WHERE id = 1 AND name = 'Next Angle'
+ next_angle = Company.find_first "name = 'Next Angle'"
+
+ next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first
+
+The supertype, Company, will return subtype instances
+
+ Firm === next_angle
+
+All the dynamic methods added by the has_many macro
+
+ next_angle.clients.empty? # true
+ next_angle.clients.size # total number of clients
+ all_clients = next_angle.clients
+
+Constrained finds makes access security easier when ID comes from a web-app
+
+ # SQL: SELECT * FROM companies WHERE client_of = 1 AND type = 'Client' AND id = 2
+ thirty_seven_signals = next_angle.clients.find(2)
+
+Bi-directional associations thanks to the "belongs_to" macro
+
+ thirty_seven_signals.firm.nil? # true
+
+
+== Examples
+
+Active Record ships with a couple of examples that should give you a good feel for
+operating usage. Be sure to edit the <tt>examples/shared_setup.rb</tt> file for your
+own database before running the examples. Possibly also the table definition SQL in
+the examples themselves.
+
+It's also highly recommended to have a look at the unit tests. Read more in link:files/RUNNING_UNIT_TESTS.html
+
+
+== Database support
+
+Active Record ships with adapters for MySQL/Ruby[http://www.tmtm.org/en/mysql/ruby/]
+(compatible with Ruby/MySQL[http://www.tmtm.org/ruby/mysql/README_en.html]),
+PostgreSQL[http://www.postgresql.jp/interfaces/ruby/], and
+SQLite[http://rubyforge.org/projects/sqlite-ruby/] (needs SQLite 2.8.13+ and SQLite-Ruby 1.1.2+).
+The adapters are around 100 lines of code fulfilling the interface specified by
+ActiveRecord::ConnectionAdapters::AbstractAdapter. Writing a new adapter should be a small task --
+especially considering the extensive test suite that'll make sure you're fulfilling the contract.
+
+
+== Philosophy
+
+Active Record attempts to provide a coherent wrapping for the inconvenience that is
+object-relational mapping. The prime directive for this mapping has been to minimize
+the amount of code needed to built a real-world domain model. This is made possible
+by relying on a number of conventions that make it easy for Active Record to infer
+complex relations and structures from a minimal amount of explicit direction.
+
+Convention over Configuration:
+* No XML-files!
+* Lots of reflection and run-time extension
+* Magic is not inherently a bad word
+
+Admit the Database:
+* Lets you drop down to SQL for odd cases and performance
+* Doesn't attempt to duplicate or replace data definitions
+
+
+== Download
+
+The latest version of Active Record can be found at
+
+* http://rubyforge.org/project/showfiles.php?group_id=182
+
+Documentation can be found at
+
+* http://ar.rubyonrails.org
+
+
+== Installation
+
+The prefered method of installing Active Record is through its GEM file. You'll need to have
+RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl] installed for that, though. If you have,
+then use:
+
+ % [sudo] gem install activerecord-0.9.0.gem
+
+You can also install Active Record the old-fashion way with the following command:
+
+ % [sudo] ruby install.rb
+
+from its distribution directory.
+
+
+== License
+
+Active Record is released under the same license as Ruby.
+
+
+== Support
+
+The Active Record homepage is http://activerecord.rubyonrails.org. You can find the Active Record
+RubyForge page at http://rubyforge.org/projects/activerecord. And as Jim from Rake says:
+
+ Feel free to submit commits or feature requests. If you send a patch,
+ remember to update the corresponding unit tests. If fact, I prefer
+ new feature to be submitted in the form of new unit tests.
+
+For other information, feel free to ask on the ruby-talk mailing list
+(which is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.
+
diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS
new file mode 100644
index 0000000000..393db82afb
--- /dev/null
+++ b/activerecord/RUNNING_UNIT_TESTS
@@ -0,0 +1,36 @@
+== Creating the test database
+
+The default names for the test databases are "activerecord_unittest" and
+"activerecord_unittest2". If you want to use another database name then be sure
+to update the connection adapter setups you want to test with in
+test/connections/<your database>/connection.rb.
+When you have the database online, you can import the fixture tables with
+the test/fixtures/db_definitions/*.sql files.
+
+Make sure that you create database objects with the same user that you specified in i
+connection.rb otherwise (on Postgres, at least) tests for default values will fail
+(see http://dev.rubyonrails.org/trac.cgi/ticket/118)
+
+== Running with Rake
+
+The easiest way to run the unit tests is through Rake. The default task runs
+the entire test suite for all the adapters. You can also run the suite on just
+one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
+or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
+
+Rake can be found at http://rake.rubyforge.org
+
+== Running by hand
+
+Unit tests are located in test directory. If you only want to run a single test suite,
+or don't want to bother with Rake, you can do so with something like:
+
+ cd test; ruby -I "connections/native_mysql" base_test.rb
+
+That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
+and test suite name as needed.
+
+You can also run all the suites on a specific adapter with:
+
+ cd test; all.sh "connections/native_mysql"
+
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
new file mode 100755
index 0000000000..6b17f6a61e
--- /dev/null
+++ b/activerecord/Rakefile
@@ -0,0 +1,126 @@
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+
+PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
+PKG_NAME = 'activerecord'
+PKG_VERSION = '1.1.0' + PKG_BUILD
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+
+PKG_FILES = FileList[
+ "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "[A-Z]*", "install.rb", "rakefile"
+].exclude(/\bCVS\b|~$/)
+
+
+desc "Default Task"
+task :default => [ :test_ruby_mysql, :test_mysql_ruby, :test_sqlite, :test_postgresql ]
+
+# Run the unit tests
+
+Rake::TestTask.new("test_ruby_mysql") { |t|
+ t.libs << "test" << "test/connections/native_mysql"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
+Rake::TestTask.new("test_mysql_ruby") { |t|
+ t.libs << "test" << "test/connections/native_mysql"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
+Rake::TestTask.new("test_postgresql") { |t|
+ t.libs << "test" << "test/connections/native_postgresql"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
+Rake::TestTask.new("test_sqlite") { |t|
+ t.libs << "test" << "test/connections/native_sqlite"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
+# Generate the RDoc documentation
+
+Rake::RDocTask.new { |rdoc|
+ rdoc.rdoc_dir = 'doc'
+ rdoc.title = "Active Record -- Object-relation mapping put on rails"
+ rdoc.options << '--line-numbers --inline-source --accessor cattr_accessor=object'
+ rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+ rdoc.rdoc_files.exclude('lib/active_record/vendor/*')
+ rdoc.rdoc_files.include('dev-utils/*.rb')
+}
+
+
+# Publish beta gem
+desc "Publish the beta gem"
+task :pgem => [:package] do
+ Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
+ `ssh davidhh@one.textdrive.com './gemupdate.sh'`
+end
+
+# Publish documentation
+desc "Publish the API documentation"
+task :pdoc => [:rdoc] do
+ Rake::SshDirPublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/ar", "doc").upload
+end
+
+
+# Create compressed packages
+
+dist_dirs = [ "lib", "test", "examples", "dev-utils" ]
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.summary = "Implements the ActiveRecord pattern for ORM."
+ s.description = %q{Implements the ActiveRecord pattern (Fowler, PoEAA) for ORM. It ties database tables and classes together for business objects, like Customer or Subscription, that can find, save, and destroy themselves without resorting to manual SQL.}
+
+ s.files = [ "rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG" ]
+ dist_dirs.each do |dir|
+ s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "CVS" ) }
+ end
+ s.files.delete "test/fixtures/fixture_database.sqlite"
+ s.require_path = 'lib'
+ s.autorequire = 'active_record'
+
+ s.has_rdoc = true
+ s.extra_rdoc_files = %w( README )
+ s.rdoc_options.concat ['--main', 'README']
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://activerecord.rubyonrails.org"
+ s.rubyforge_project = "activerecord"
+end
+
+Rake::GemPackageTask.new(spec) do |p|
+ p.gem_spec = spec
+ p.need_tar = true
+ p.need_zip = true
+end
+
+
+task :lines do
+ lines = 0
+ codelines = 0
+ Dir.foreach("lib/active_record") { |file_name|
+ next unless file_name =~ /.*rb/
+
+ f = File.open("lib/active_record/" + file_name)
+
+ while line = f.gets
+ lines += 1
+ next if line =~ /^\s*$/
+ next if line =~ /^\s*#/
+ codelines += 1
+ end
+ }
+ puts "Lines #{lines}, LOC #{codelines}"
+end
diff --git a/activerecord/benchmarks/benchmark.rb b/activerecord/benchmarks/benchmark.rb
new file mode 100644
index 0000000000..241d915208
--- /dev/null
+++ b/activerecord/benchmarks/benchmark.rb
@@ -0,0 +1,26 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+if ARGV[2]
+ require 'rubygems'
+ require_gem 'activerecord', ARGV[2]
+else
+ require 'active_record'
+end
+
+ActiveRecord::Base.establish_connection(:adapter => "mysql", :database => "basecamp")
+
+class Post < ActiveRecord::Base; end
+
+require 'benchmark'
+
+RUNS = ARGV[0].to_i
+if ARGV[1] == "profile" then require 'profile' end
+
+runtime = Benchmark::measure {
+ RUNS.times {
+ Post.find_all(nil,nil,100).each { |p| p.title }
+ }
+}
+
+puts "Runs: #{RUNS}"
+puts "Avg. runtime: #{runtime.real / RUNS}"
+puts "Requests/second: #{RUNS / runtime.real}"
diff --git a/activerecord/benchmarks/mysql_benchmark.rb b/activerecord/benchmarks/mysql_benchmark.rb
new file mode 100644
index 0000000000..2f9e0e6999
--- /dev/null
+++ b/activerecord/benchmarks/mysql_benchmark.rb
@@ -0,0 +1,19 @@
+require 'mysql'
+
+conn = Mysql::real_connect("localhost", "root", "", "basecamp")
+
+require 'benchmark'
+
+require 'profile' if ARGV[1] == "profile"
+RUNS = ARGV[0].to_i
+
+runtime = Benchmark::measure {
+ RUNS.times {
+ result = conn.query("SELECT * FROM posts LIMIT 100")
+ result.each_hash { |p| p["title"] }
+ }
+}
+
+puts "Runs: #{RUNS}"
+puts "Avg. runtime: #{runtime.real / RUNS}"
+puts "Requests/second: #{RUNS / runtime.real}" \ No newline at end of file
diff --git a/activerecord/dev-utils/eval_debugger.rb b/activerecord/dev-utils/eval_debugger.rb
new file mode 100644
index 0000000000..833bc6e052
--- /dev/null
+++ b/activerecord/dev-utils/eval_debugger.rb
@@ -0,0 +1,14 @@
+# Require this file to see the methods Active Record generates as they are added.
+class Module
+ alias :old_module_eval :module_eval
+ def module_eval(*args, &block)
+ if args[0]
+ puts "----"
+ print "module_eval in #{self.name}"
+ print ": file #{args[1]}" if args[1]
+ print " on line #{args[2]}" if args[2]
+ puts "\n#{args[0]}"
+ end
+ old_module_eval(*args, &block)
+ end
+end
diff --git a/activerecord/examples/associations.png b/activerecord/examples/associations.png
new file mode 100644
index 0000000000..661c7a8bbc
--- /dev/null
+++ b/activerecord/examples/associations.png
Binary files differ
diff --git a/activerecord/examples/associations.rb b/activerecord/examples/associations.rb
new file mode 100644
index 0000000000..b0df367321
--- /dev/null
+++ b/activerecord/examples/associations.rb
@@ -0,0 +1,87 @@
+require File.dirname(__FILE__) + '/shared_setup'
+
+logger = Logger.new(STDOUT)
+
+# Database setup ---------------
+
+logger.info "\nCreate tables"
+
+[ "DROP TABLE companies", "DROP TABLE people", "DROP TABLE people_companies",
+ "CREATE TABLE companies (id int(11) auto_increment, client_of int(11), name varchar(255), type varchar(100), PRIMARY KEY (id))",
+ "CREATE TABLE people (id int(11) auto_increment, name varchar(100), PRIMARY KEY (id))",
+ "CREATE TABLE people_companies (person_id int(11), company_id int(11), PRIMARY KEY (person_id, company_id))",
+].each { |statement|
+ # Tables doesn't necessarily already exist
+ begin; ActiveRecord::Base.connection.execute(statement); rescue ActiveRecord::StatementInvalid; end
+}
+
+
+# Class setup ---------------
+
+class Company < ActiveRecord::Base
+ has_and_belongs_to_many :people, :class_name => "Person", :join_table => "people_companies", :table_name => "people"
+end
+
+class Firm < Company
+ has_many :clients, :foreign_key => "client_of"
+
+ def people_with_all_clients
+ clients.inject([]) { |people, client| people + client.people }
+ end
+end
+
+class Client < Company
+ belongs_to :firm, :foreign_key => "client_of"
+end
+
+class Person < ActiveRecord::Base
+ has_and_belongs_to_many :companies, :join_table => "people_companies"
+ def self.table_name() "people" end
+end
+
+
+# Usage ---------------
+
+logger.info "\nCreate fixtures"
+
+Firm.new("name" => "Next Angle").save
+Client.new("name" => "37signals", "client_of" => 1).save
+Person.new("name" => "David").save
+
+
+logger.info "\nUsing Finders"
+
+next_angle = Company.find(1)
+next_angle = Firm.find(1)
+next_angle = Company.find_first "name = 'Next Angle'"
+next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first
+
+Firm === next_angle
+
+
+logger.info "\nUsing has_many association"
+
+next_angle.has_clients?
+next_angle.clients_count
+all_clients = next_angle.clients
+
+thirty_seven_signals = next_angle.find_in_clients(2)
+
+
+logger.info "\nUsing belongs_to association"
+
+thirty_seven_signals.has_firm?
+thirty_seven_signals.firm?(next_angle)
+
+
+logger.info "\nUsing has_and_belongs_to_many association"
+
+david = Person.find(1)
+david.add_companies(thirty_seven_signals, next_angle)
+david.companies.include?(next_angle)
+david.companies_count == 2
+
+david.remove_companies(next_angle)
+david.companies_count == 1
+
+thirty_seven_signals.people.include?(david) \ No newline at end of file
diff --git a/activerecord/examples/shared_setup.rb b/activerecord/examples/shared_setup.rb
new file mode 100644
index 0000000000..6ede4b1d35
--- /dev/null
+++ b/activerecord/examples/shared_setup.rb
@@ -0,0 +1,15 @@
+# Be sure to change the mysql_connection details and create a database for the example
+
+$: << File.dirname(__FILE__) + '/../lib'
+
+require 'active_record'
+require 'logger'; class Logger; def format_message(severity, timestamp, msg, progname) "#{msg}\n" end; end
+
+ActiveRecord::Base.logger = Logger.new(STDOUT)
+ActiveRecord::Base.establish_connection(
+ :adapter => "mysql",
+ :host => "localhost",
+ :username => "root",
+ :password => "",
+ :database => "activerecord_examples"
+)
diff --git a/activerecord/examples/validation.rb b/activerecord/examples/validation.rb
new file mode 100644
index 0000000000..334a1685f7
--- /dev/null
+++ b/activerecord/examples/validation.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/shared_setup'
+
+logger = Logger.new(STDOUT)
+
+# Database setup ---------------
+
+logger.info "\nCreate tables"
+
+[ "DROP TABLE people",
+ "CREATE TABLE people (id int(11) auto_increment, name varchar(100), pass varchar(100), email varchar(100), PRIMARY KEY (id))"
+].each { |statement|
+ begin; ActiveRecord::Base.connection.execute(statement); rescue ActiveRecord::StatementInvalid; end # Tables doesn't necessarily already exist
+}
+
+
+# Class setup ---------------
+
+class Person < ActiveRecord::Base
+ # Active Record can only guess simple table names like Card/cards, Company/companies
+ def self.table_name() "people" end
+
+ # Using
+ def self.authenticate(name, pass)
+ # find_first "name = '#{name}' AND pass = '#{pass}'" would be open to sql-injection (in a web-app scenario)
+ find_first [ "name = '%s' AND pass = '%s'", name, pass ]
+ end
+
+ def self.name_exists?(name, id = nil)
+ if id.nil?
+ condition = [ "name = '%s'", name ]
+ else
+ # Check if anyone else than the person identified by person_id has that user_name
+ condition = [ "name = '%s' AND id <> %d", name, id ]
+ end
+
+ !find_first(condition).nil?
+ end
+
+ def email_address_with_name
+ "\"#{name}\" <#{email}>"
+ end
+
+ protected
+ def validate
+ errors.add_on_empty(%w(name pass email))
+ errors.add("email", "must be valid") unless email_address_valid?
+ end
+
+ def validate_on_create
+ if attribute_present?("name") && Person.name_exists?(name)
+ errors.add("name", "is already taken by another person")
+ end
+ end
+
+ def validate_on_update
+ if attribute_present?("name") && Person.name_exists?(name, id)
+ errors.add("name", "is already taken by another person")
+ end
+ end
+
+ private
+ def email_address_valid?() email =~ /\w[-.\w]*\@[-\w]+[-.\w]*\.\w+/ end
+end
+
+# Usage ---------------
+
+logger.info "\nCreate fixtures"
+david = Person.new("name" => "David Heinemeier Hansson", "pass" => "", "email" => "")
+unless david.save
+ puts "There was #{david.errors.count} error(s)"
+ david.errors.each_full { |error| puts error }
+end
+
+david.pass = "something"
+david.email = "invalid_address"
+unless david.save
+ puts "There was #{david.errors.count} error(s)"
+ puts "It was email with: " + david.errors.on("email")
+end
+
+david.email = "david@loudthinking.com"
+if david.save then puts "David finally made it!" end
+
+
+another_david = Person.new("name" => "David Heinemeier Hansson", "pass" => "xc", "email" => "david@loudthinking")
+unless another_david.save
+ puts "Error on name: " + another_david.errors.on("name")
+end \ No newline at end of file
diff --git a/activerecord/install.rb b/activerecord/install.rb
new file mode 100644
index 0000000000..52e162a707
--- /dev/null
+++ b/activerecord/install.rb
@@ -0,0 +1,60 @@
+require 'rbconfig'
+require 'find'
+require 'ftools'
+
+include Config
+
+# this was adapted from rdoc's install.rb by ways of Log4r
+
+$sitedir = CONFIG["sitelibdir"]
+unless $sitedir
+ version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
+ $libdir = File.join(CONFIG["libdir"], "ruby", version)
+ $sitedir = $:.find {|x| x =~ /site_ruby/ }
+ if !$sitedir
+ $sitedir = File.join($libdir, "site_ruby")
+ elsif $sitedir !~ Regexp.quote(version)
+ $sitedir = File.join($sitedir, version)
+ end
+end
+
+makedirs = %w{ active_record/associations active_record/connection_adapters active_record/support active_record/vendor }
+makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))}
+
+# deprecated files that should be removed
+# deprecated = %w{ }
+
+# files to install in library path
+files = %w-
+ active_record.rb
+ active_record/aggregations.rb
+ active_record/associations.rb
+ active_record/associations/association_collection.rb
+ active_record/associations/has_and_belongs_to_many_association.rb
+ active_record/associations/has_many_association.rb
+ active_record/base.rb
+ active_record/callbacks.rb
+ active_record/connection_adapters/abstract_adapter.rb
+ active_record/connection_adapters/mysql_adapter.rb
+ active_record/connection_adapters/postgresql_adapter.rb
+ active_record/connection_adapters/sqlite_adapter.rb
+ active_record/deprecated_associations.rb
+ active_record/fixtures.rb
+ active_record/observer.rb
+ active_record/reflection.rb
+ active_record/support/class_attribute_accessors.rb
+ active_record/support/class_inheritable_attributes.rb
+ active_record/support/clean_logger.rb
+ active_record/support/inflector.rb
+ active_record/transactions.rb
+ active_record/validations.rb
+ active_record/vendor/mysql.rb
+ active_record/vendor/simple.rb
+-
+
+# the acual gruntwork
+Dir.chdir("lib")
+# File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
+files.each {|f|
+ File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
+}
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
new file mode 100755
index 0000000000..9ce79284cd
--- /dev/null
+++ b/activerecord/lib/active_record.rb
@@ -0,0 +1,50 @@
+#--
+# Copyright (c) 2004 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+
+$:.unshift(File.dirname(__FILE__))
+
+require 'active_record/support/clean_logger'
+
+require 'active_record/base'
+require 'active_record/observer'
+require 'active_record/validations'
+require 'active_record/callbacks'
+require 'active_record/associations'
+require 'active_record/aggregations'
+require 'active_record/transactions'
+require 'active_record/reflection'
+
+ActiveRecord::Base.class_eval do
+ include ActiveRecord::Validations
+ include ActiveRecord::Callbacks
+ include ActiveRecord::Associations
+ include ActiveRecord::Aggregations
+ include ActiveRecord::Transactions
+ include ActiveRecord::Reflection
+end
+
+require 'active_record/connection_adapters/mysql_adapter'
+require 'active_record/connection_adapters/postgresql_adapter'
+require 'active_record/connection_adapters/sqlite_adapter'
+require 'active_record/connection_adapters/sqlserver_adapter' \ No newline at end of file
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
new file mode 100644
index 0000000000..82011018a2
--- /dev/null
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -0,0 +1,165 @@
+module ActiveRecord
+ module Aggregations # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
+ # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
+ # composed of [an] address". Each call to the macro adds a description on how the value objects are created from the
+ # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing)
+ # and how it can be turned back into attributes (when the entity is saved to the database). Example:
+ #
+ # class Customer < ActiveRecord::Base
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
+ # end
+ #
+ # The customer class now has the following methods to manipulate the value objects:
+ # * <tt>Customer#balance, Customer#balance=(money)</tt>
+ # * <tt>Customer#address, Customer#address=(address)</tt>
+ #
+ # These methods will operate with value objects like the ones described below:
+ #
+ # class Money
+ # include Comparable
+ # attr_reader :amount, :currency
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
+ #
+ # def initialize(amount, currency = "USD")
+ # @amount, @currency = amount, currency
+ # end
+ #
+ # def exchange_to(other_currency)
+ # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
+ # Money.new(exchanged_amount, other_currency)
+ # end
+ #
+ # def ==(other_money)
+ # amount == other_money.amount && currency == other_money.currency
+ # end
+ #
+ # def <=>(other_money)
+ # if currency == other_money.currency
+ # amount <=> amount
+ # else
+ # amount <=> other_money.exchange_to(currency).amount
+ # end
+ # end
+ # end
+ #
+ # class Address
+ # attr_reader :street, :city
+ # def initialize(street, city)
+ # @street, @city = street, city
+ # end
+ #
+ # def close_to?(other_address)
+ # city == other_address.city
+ # end
+ #
+ # def ==(other_address)
+ # city == other_address.city && street == other_address.street
+ # end
+ # end
+ #
+ # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
+ # composition the same as the attributes name, it will be the only way to access that attribute. That's the case with our
+ # +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
+ #
+ # customer.balance = Money.new(20) # sets the Money value object and the attribute
+ # customer.balance # => Money value object
+ # customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
+ # customer.balance > Money.new(10) # => true
+ # customer.balance == Money.new(20) # => true
+ # customer.balance < Money.new(5) # => false
+ #
+ # Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will
+ # determine the order of the parameters. Example:
+ #
+ # customer.address_street = "Hyancintvej"
+ # customer.address_city = "Copenhagen"
+ # customer.address # => Address.new("Hyancintvej", "Copenhagen")
+ # customer.address = Address.new("May Street", "Chicago")
+ # customer.address_street # => "May Street"
+ # customer.address_city # => "Chicago"
+ #
+ # == Writing value objects
+ #
+ # Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
+ # $5. Two Money objects both representing $5 should be equal (through methods such == and <=> from Comparable if ranking makes
+ # sense). This is unlike a entity objects where equality is determined by identity. An entity class such as Customer can
+ # easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
+ # relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
+ #
+ # It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
+ # creation. Create a new money object with the new value instead. This is examplified by the Money#exchanged_to method that
+ # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
+ # changed through other means than the writer method.
+ #
+ # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
+ # change it afterwards will result in a TypeError.
+ #
+ # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
+ # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
+ module ClassMethods
+ # Adds the a reader and writer method for manipulating a value object, so
+ # <tt>composed_of :address</tt> would add <tt>address</tt> and <tt>address=(new_address)</tt>.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the part id. So <tt>composed_of :address</tt> will by default be linked to the +Address+ class, but
+ # if the real class name is +CompanyAddress+, you'll have to specify it with this option.
+ # * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
+ # to a constructor parameter on the value class.
+ #
+ # Option examples:
+ # composed_of :temperature, :mapping => %w(reading celsius)
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
+ def composed_of(part_id, options = {})
+ validate_options([ :class_name, :mapping ], options.keys)
+
+ name = part_id.id2name
+ class_name = options[:class_name] || name_to_class_name(name)
+ mapping = options[:mapping]
+
+ reader_method(name, class_name, mapping)
+ writer_method(name, class_name, mapping)
+ end
+
+ private
+ # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
+ def validate_options(valid_option_keys, supplied_option_keys)
+ unknown_option_keys = supplied_option_keys - valid_option_keys
+ raise(ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
+ end
+
+ def name_to_class_name(name)
+ name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
+ end
+
+ def reader_method(name, class_name, mapping)
+ module_eval <<-end_eval
+ def #{name}(force_reload = false)
+ if @#{name}.nil? || force_reload
+ @#{name} = #{class_name}.new(#{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "read_attribute(\"#{pair.first}\")"}.join(", ")})
+ end
+
+ return @#{name}
+ end
+ end_eval
+ end
+
+ def writer_method(name, class_name, mapping)
+ module_eval <<-end_eval
+ def #{name}=(part)
+ @#{name} = part.freeze
+ #{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
+ end
+ end_eval
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
new file mode 100755
index 0000000000..6285a59882
--- /dev/null
+++ b/activerecord/lib/active_record/associations.rb
@@ -0,0 +1,576 @@
+require 'active_record/associations/association_collection'
+require 'active_record/associations/has_many_association'
+require 'active_record/associations/has_and_belongs_to_many_association'
+require 'active_record/deprecated_associations'
+
+module ActiveRecord
+ module Associations # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
+ # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
+ # specialized according to the collection or association symbol and the options hash. It works much the same was as Ruby's own attr*
+ # methods. Example:
+ #
+ # class Project < ActiveRecord::Base
+ # belongs_to :portfolio
+ # has_one :project_manager
+ # has_many :milestones
+ # has_and_belongs_to_many :categories
+ # end
+ #
+ # The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
+ # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
+ # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
+ # <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
+ # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
+ # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
+ # <tt>Project#milestones.build, Project#milestones.create</tt>
+ # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
+ # <tt>Project#categories.delete(category1)</tt>
+ #
+ # == Example
+ #
+ # link:../examples/associations.png
+ #
+ # == Is it belongs_to or has_one?
+ #
+ # Both express a 1-1 relationship, the difference is mostly where to place the foreign key, which goes on the table for the class
+ # saying belongs_to. Example:
+ #
+ # class Post < ActiveRecord::Base
+ # has_one :author
+ # end
+ #
+ # class Author < ActiveRecord::Base
+ # belongs_to :post
+ # end
+ #
+ # The tables for these classes could look something like:
+ #
+ # CREATE TABLE posts (
+ # id int(11) NOT NULL auto_increment,
+ # title varchar default NULL,
+ # PRIMARY KEY (id)
+ # )
+ #
+ # CREATE TABLE authors (
+ # id int(11) NOT NULL auto_increment,
+ # post_id int(11) default NULL,
+ # name varchar default NULL,
+ # PRIMARY KEY (id)
+ # )
+ #
+ # == Caching
+ #
+ # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
+ # instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without
+ # worrying too much about performance at the first go. Example:
+ #
+ # project.milestones # fetches milestones from the database
+ # project.milestones.size # uses the milestone cache
+ # project.milestones.empty? # uses the milestone cache
+ # project.milestones(true).size # fetches milestones from the database
+ # project.milestones # uses the milestone cache
+ #
+ # == Modules
+ #
+ # By default, associations will look for objects within the current module scope. Consider:
+ #
+ # module MyApplication
+ # module Business
+ # class Firm < ActiveRecord::Base
+ # has_many :clients
+ # end
+ #
+ # class Company < ActiveRecord::Base; end
+ # end
+ # end
+ #
+ # When Firm#clients is called, it'll in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
+ # with a class in another module scope this can be done by specifying the complete class name, such as:
+ #
+ # module MyApplication
+ # module Business
+ # class Firm < ActiveRecord::Base; end
+ # end
+ #
+ # module Billing
+ # class Account < ActiveRecord::Base
+ # belongs_to :firm, :class_name => "MyApplication::Business::Firm"
+ # end
+ # end
+ # end
+ #
+ # == Type safety with ActiveRecord::AssociationTypeMismatch
+ #
+ # If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll
+ # get a ActiveRecord::AssociationTypeMismatch.
+ #
+ # == Options
+ #
+ # All of the association macros can be specialized through options which makes more complex cases than the simple and guessable ones
+ # possible.
+ module ClassMethods
+ # Adds the following methods for retrival and query of collections of associated objects.
+ # +collection+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
+ # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
+ # An empty array is returned if none are found.
+ # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
+ # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL. This does not destroy the objects.
+ # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
+ # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
+ # * <tt>collection.size</tt> - returns the number of associated objects.
+ # * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
+ # meets the condition that it has to be associated with this object.
+ # * <tt>collection.find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)</tt> - finds all associated objects responding
+ # criterias mentioned (like in the standard find_all) and that meets the condition that it has to be associated with this object.
+ # * <tt>collection.build(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
+ # * <tt>collection.create(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
+ #
+ # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
+ # * <tt>Firm#clients</tt> (similar to <tt>Clients.find_all "firm_id = #{id}"</tt>)
+ # * <tt>Firm#clients<<</tt>
+ # * <tt>Firm#clients.delete</tt>
+ # * <tt>Firm#clients.clear</tt>
+ # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
+ # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
+ # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find_on_conditions(id, "firm_id = #{id}")</tt>)
+ # * <tt>Firm#clients.find_all</tt> (similar to <tt>Client.find_all "firm_id = #{id}"</tt>)
+ # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
+ # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("client_id" => id); c.save; c</tt>)
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_many :products</tt> will by default be linked to the +Product+ class, but
+ # if the real class name is +SpecialProduct+, you'll have to specify it with this option.
+ # * <tt>:conditions</tt> - specify the conditions that the associated objects must meet in order to be included as a "WHERE"
+ # sql fragment, such as "price > 5 AND name LIKE 'B%'".
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment,
+ # such as "last_name, first_name DESC"
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
+ # as the default foreign_key.
+ # * <tt>:dependent</tt> - if set to true all the associated object are destroyed alongside this object.
+ # May not be set if :exclusively_dependent is also set.
+ # * <tt>:exclusively_dependent</tt> - if set to true all the associated object are deleted in one SQL statement without having their
+ # before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any
+ # clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved.
+ # May not be set if :dependent is also set.
+ # * <tt>:finder_sql</tt> - specify a complete SQL statement to fetch the association. This is a good way to go for complex
+ # associations that depends on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
+ #
+ # Option examples:
+ # has_many :comments, :order => "posted_on"
+ # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
+ # has_many :tracks, :order => "position", :dependent => true
+ # has_many :subscribers, :class_name => "Person", :finder_sql =>
+ # 'SELECT DISTINCT people.* ' +
+ # 'FROM people p, post_subscriptions ps ' +
+ # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
+ # 'ORDER BY p.first_name'
+ def has_many(association_id, options = {})
+ validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql ], options.keys)
+ association_name, association_class_name, association_class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key])
+
+ require_association_class(association_class_name)
+
+ if options[:dependent] and options[:exclusively_dependent]
+ raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' # ' ruby-mode
+ elsif options[:dependent]
+ module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
+ elsif options[:exclusively_dependent]
+ module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = '\#{record.id}')) }"
+ end
+
+ define_method(association_name) do |*params|
+ force_reload = params.first unless params.empty?
+ association = instance_variable_get("@#{association_name}")
+ if association.nil?
+ association = HasManyAssociation.new(self,
+ association_name, association_class_name,
+ association_class_primary_key_name, options)
+ instance_variable_set("@#{association_name}", association)
+ end
+ association.reload if force_reload
+ association
+ end
+
+ # deprecated api
+ deprecated_collection_count_method(association_name)
+ deprecated_add_association_relation(association_name)
+ deprecated_remove_association_relation(association_name)
+ deprecated_has_collection_method(association_name)
+ deprecated_find_in_collection_method(association_name)
+ deprecated_find_all_in_collection_method(association_name)
+ deprecated_create_method(association_name)
+ deprecated_build_method(association_name)
+ end
+
+ # Adds the following methods for retrival and query of a single associated object.
+ # +association+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_one :manager</tt> would add among others <tt>has_manager?</tt>.
+ # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
+ # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
+ # and saves the associate object.
+ # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
+ # same id as the associated object.
+ # * <tt>association.nil?</tt> - returns true if there is no associated object.
+ # * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
+ # * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
+ #
+ # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
+ # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
+ # * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
+ # * <tt>Account#beneficiary.nil?</tt>
+ # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
+ # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_one :manager</tt> will by default be linked to the +Manager+ class, but
+ # if the real class name is +Person+, you'll have to specify it with this option.
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "rank = 5".
+ # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
+ # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
+ # * <tt>:dependent</tt> - if set to true the associated object is destroyed alongside this object
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id"
+ # as the default foreign_key.
+ #
+ # Option examples:
+ # has_one :credit_card, :dependent => true
+ # has_one :last_comment, :class_name => "Comment", :order => "posted_on"
+ # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
+ def has_one(association_id, options = {})
+ options.merge!({ :remote => true })
+ belongs_to(association_id, options)
+
+ association_name, association_class_name, class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key], false)
+
+ require_association_class(association_class_name)
+
+ has_one_writer_method(association_name, association_class_name, class_primary_key_name)
+ build_method("build_", association_name, association_class_name, class_primary_key_name)
+ create_method("create_", association_name, association_class_name, class_primary_key_name)
+
+ module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
+ end
+
+ # Adds the following methods for retrival and query for a single associated object that this object holds an id to.
+ # +association+ is replaced with the symbol passed as the first argument, so
+ # <tt>belongs_to :author</tt> would add among others <tt>has_author?</tt>.
+ # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
+ # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
+ # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
+ # same id as the associated object.
+ # * <tt>association.nil?</tt> - returns true if there is no associated object.
+ #
+ # Example: An Post class declares <tt>has_one :author</tt>, which will add:
+ # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
+ # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
+ # * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
+ # * <tt>Post#author.nil?</tt>
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_one :author</tt> will by default be linked to the +Author+ class, but
+ # if the real class name is +Person+, you'll have to specify it with this option.
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "authorized = 1".
+ # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
+ # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of the associated class in lower-case and "_id" suffixed. So a +Person+ class that makes a belongs_to association to a
+ # +Boss+ class will use "boss_id" as the default foreign_key.
+ # * <tt>:counter_cache</tt> - caches the number of belonging objects on the associate class through use of increment_counter
+ # and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's
+ # destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class)
+ # is used on the associate class (such as a Post class).
+ #
+ # Option examples:
+ # belongs_to :firm, :foreign_key => "client_of"
+ # belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
+ # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
+ # :conditions => 'discounts > #{payments_count}'
+ def belongs_to(association_id, options = {})
+ validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
+
+ association_name, association_class_name, class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key], false)
+
+ require_association_class(association_class_name)
+
+ association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
+
+ if options[:remote]
+ association_finder = <<-"end_eval"
+ #{association_class_name}.find_first(
+ "#{class_primary_key_name} = '\#{id}'#{options[:conditions] ? " AND " + options[:conditions] : ""}",
+ #{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
+ )
+ end_eval
+ else
+ association_finder = options[:conditions] ?
+ "#{association_class_name}.find_on_conditions(#{association_class_primary_key_name}, \"#{options[:conditions]}\")" :
+ "#{association_class_name}.find(#{association_class_primary_key_name})"
+ end
+
+ has_association_method(association_name)
+ association_reader_method(association_name, association_finder)
+ belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
+ association_comparison_method(association_name, association_class_name)
+
+ if options[:counter_cache]
+ module_eval(
+ "after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
+ " if has_#{association_name}?'"
+ )
+
+ module_eval(
+ "before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
+ " if has_#{association_name}?'"
+ )
+ end
+ end
+
+ # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
+ # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
+ # will give the default join table name of "developers_projects" because "D" outranks "P".
+ #
+ # Any additional fields added to the join table will be placed as attributes when pulling records out through
+ # has_and_belongs_to_many associations. This is helpful when have information about the association itself
+ # that you want available on retrival.
+ #
+ # Adds the following methods for retrival and query.
+ # +collection+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
+ # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
+ # An empty array is returned if none is found.
+ # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table
+ # (collection.push and collection.concat are aliases to this method).
+ # * <tt>collection.push_with_attributes(object, join_attributes)</tt> - adds one to the collection by creating an association in the join table that
+ # also holds the attributes from <tt>join_attributes</tt> (should be a hash with the column names as keys). This can be used to have additional
+ # attributes on the join, which will be injected into the associated objects when they are retrieved through the collection.
+ # (collection.concat_with_attributes is an alias to this method).
+ # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.
+ # This does not destroy the objects.
+ # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
+ # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
+ # * <tt>collection.size</tt> - returns the number of associated objects.
+ #
+ # Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
+ # * <tt>Developer#projects</tt>
+ # * <tt>Developer#projects<<</tt>
+ # * <tt>Developer#projects.delete</tt>
+ # * <tt>Developer#projects.clear</tt>
+ # * <tt>Developer#projects.empty?</tt>
+ # * <tt>Developer#projects.size</tt>
+ # * <tt>Developer#projects.find(id)</tt>
+ # The declaration may include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
+ # +Project+ class, but if the real class name is +SuperProject+, you'll have to specify it with this option.
+ # * <tt>:join_table</tt> - specify the name of the join table if the default based on lexical order isn't what you want.
+ # WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any
+ # has_and_belongs_to_many declaration in order to work.
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_and_belongs_to_many association
+ # will use "person_id" as the default foreign_key.
+ # * <tt>:association_foreign_key</tt> - specify the association foreign key used for the association. By default this is
+ # guessed to be the name of the associated class in lower-case and "_id" suffixed. So the associated class is +Project+
+ # that makes a has_and_belongs_to_many association will use "project_id" as the default association foreign_key.
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "authorized = 1".
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC"
+ # * <tt>:uniq</tt> - if set to true, duplicate associated objects will be ignored by accessors and query methods
+ # * <tt>:finder_sql</tt> - overwrite the default generated SQL used to fetch the association with a manual one
+ # * <tt>:delete_sql</tt> - overwrite the default generated SQL used to remove links between the associated
+ # classes with a manual one
+ # * <tt>:insert_sql</tt> - overwrite the default generated SQL used to add links between the associated classes
+ # with a manual one
+ #
+ # Option examples:
+ # has_and_belongs_to_many :projects
+ # has_and_belongs_to_many :nations, :class_name => "Country"
+ # has_and_belongs_to_many :categories, :join_table => "prods_cats"
+ def has_and_belongs_to_many(association_id, options = {})
+ validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
+ :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq ], options.keys)
+ association_name, association_class_name, association_class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key])
+
+ require_association_class(association_class_name)
+
+ join_table = options[:join_table] ||
+ join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
+
+ define_method(association_name) do |*params|
+ force_reload = params.first unless params.empty?
+ association = instance_variable_get("@#{association_name}")
+ if association.nil?
+ association = HasAndBelongsToManyAssociation.new(self,
+ association_name, association_class_name,
+ association_class_primary_key_name, join_table, options)
+ instance_variable_set("@#{association_name}", association)
+ end
+ association.reload if force_reload
+ association
+ end
+
+ before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = '\\\#{self.id}'"
+ module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
+
+ # deprecated api
+ deprecated_collection_count_method(association_name)
+ deprecated_add_association_relation(association_name)
+ deprecated_remove_association_relation(association_name)
+ deprecated_has_collection_method(association_name)
+ end
+
+ private
+ # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
+ def validate_options(valid_option_keys, supplied_option_keys)
+ unknown_option_keys = supplied_option_keys - valid_option_keys
+ raise(ActiveRecord::ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
+ end
+
+ def join_table_name(first_table_name, second_table_name)
+ if first_table_name < second_table_name
+ join_table = "#{first_table_name}_#{second_table_name}"
+ else
+ join_table = "#{second_table_name}_#{first_table_name}"
+ end
+
+ table_name_prefix + join_table + table_name_suffix
+ end
+
+ def associate_identification(association_id, association_class_name, foreign_key, plural = true)
+ if association_class_name !~ /::/
+ association_class_name = type_name_with_module(
+ association_class_name ||
+ Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name)
+ )
+ end
+
+ primary_key_name = foreign_key || Inflector.underscore(Inflector.demodulize(name)) + "_id"
+
+ return association_id.id2name, association_class_name, primary_key_name
+ end
+
+ def association_comparison_method(association_name, association_class_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}?(comparison_object, force_reload = false)
+ if comparison_object.kind_of?(#{association_class_name})
+ #{association_name}(force_reload) == comparison_object
+ else
+ raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
+ end
+ end
+ end_eval
+ end
+
+ def association_reader_method(association_name, association_finder)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}(force_reload = false)
+ if @#{association_name}.nil? || force_reload
+ begin
+ @#{association_name} = #{association_finder}
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
+ nil
+ end
+ end
+
+ return @#{association_name}
+ end
+ end_eval
+ end
+
+ def has_one_writer_method(association_name, association_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}=(association)
+ if association.nil?
+ @#{association_name}.#{class_primary_key_name} = nil
+ @#{association_name}.save(false)
+ @#{association_name} = nil
+ else
+ raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
+ association.#{class_primary_key_name} = id
+ association.save(false)
+ @#{association_name} = association
+ end
+ end
+ end_eval
+ end
+
+ def belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}=(association)
+ if association.nil?
+ @#{association_name} = self.#{association_class_primary_key_name} = nil
+ else
+ raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
+ @#{association_name} = association
+ self.#{association_class_primary_key_name} = association.id
+ end
+ end
+ end_eval
+ end
+
+ def has_association_method(association_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def has_#{association_name}?(force_reload = false)
+ !#{association_name}(force_reload).nil?
+ end
+ end_eval
+ end
+
+ def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{method_prefix + collection_name}(attributes = {})
+ association = #{collection_class_name}.new
+ association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
+ association
+ end
+ end_eval
+ end
+
+ def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{method_prefix + collection_name}(attributes = nil)
+ #{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
+ end
+ end_eval
+ end
+
+ def require_association_class(class_name)
+ begin
+ require(Inflector.underscore(class_name))
+ rescue LoadError
+ if logger
+ logger.info "#{self.to_s} failed to require #{class_name}"
+ else
+ STDERR << "#{self.to_s} failed to require #{class_name}\n"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
new file mode 100644
index 0000000000..a60b9ddab5
--- /dev/null
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -0,0 +1,129 @@
+module ActiveRecord
+ module Associations
+ class AssociationCollection #:nodoc:
+ alias_method :proxy_respond_to?, :respond_to?
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }
+
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ @owner = owner
+ @options = options
+ @association_name = association_name
+ @association_class = eval(association_class_name)
+ @association_class_primary_key_name = association_class_primary_key_name
+ end
+
+ def method_missing(symbol, *args, &block)
+ load_collection
+ @collection.send(symbol, *args, &block)
+ end
+
+ def to_ary
+ load_collection
+ @collection.to_ary
+ end
+
+ def respond_to?(symbol)
+ proxy_respond_to?(symbol) || [].respond_to?(symbol)
+ end
+
+ def loaded?
+ !@collection.nil?
+ end
+
+ def reload
+ @collection = nil
+ end
+
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
+ def <<(*records)
+ flatten_deeper(records).each do |record|
+ raise_on_type_mismatch(record)
+ insert_record(record)
+ @collection << record if loaded?
+ end
+ self
+ end
+
+ alias_method :push, :<<
+ alias_method :concat, :<<
+
+ # Remove +records+ from this association. Does not destroy +records+.
+ def delete(*records)
+ records = flatten_deeper(records)
+ records.each { |record| raise_on_type_mismatch(record) }
+ delete_records(records)
+ records.each { |record| @collection.delete(record) } if loaded?
+ end
+
+ def destroy_all
+ each { |record| record.destroy }
+ @collection = []
+ end
+
+ def size
+ if loaded? then @collection.size else count_records end
+ end
+
+ def empty?
+ size == 0
+ end
+
+ def uniq(collection = self)
+ collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
+ end
+
+ alias_method :length, :size
+
+ protected
+ def loaded?
+ not @collection.nil?
+ end
+
+ def quoted_record_ids(records)
+ records.map { |record| "'#{@association_class.send(:sanitize, record.id)}'" }.join(',')
+ end
+
+ def interpolate_sql_options!(options, *keys)
+ keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
+ end
+
+ def interpolate_sql(sql, record = nil)
+ @owner.send(:interpolate_sql, sql, record)
+ end
+
+ private
+ def load_collection
+ begin
+ @collection = find_all_records unless loaded?
+ rescue ActiveRecord::RecordNotFound
+ @collection = []
+ end
+ end
+
+ def raise_on_type_mismatch(record)
+ raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
+ end
+
+
+ def load_collection_to_array
+ return unless @collection_array.nil?
+ begin
+ @collection_array = find_all_records
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
+ @collection_array = []
+ end
+ end
+
+ def duplicated_records_array(records)
+ records = [records] unless records.is_a?(Array) || records.is_a?(ActiveRecord::Associations::AssociationCollection)
+ records.dup
+ end
+
+ # Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems.
+ def flatten_deeper(array)
+ array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
new file mode 100644
index 0000000000..946f238f21
--- /dev/null
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -0,0 +1,107 @@
+module ActiveRecord
+ module Associations
+ class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
+ super(owner, association_name, association_class_name, association_class_primary_key_name, options)
+
+ @association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name.downcase)) + "_id"
+ association_table_name = options[:table_name] || @association_class.table_name(association_class_name)
+ @join_table = join_table
+ @order = options[:order] || "t.#{@owner.class.primary_key}"
+
+ interpolate_sql_options!(options, :finder_sql, :delete_sql)
+ @finder_sql = options[:finder_sql] ||
+ "SELECT t.*, j.* FROM #{association_table_name} t, #{@join_table} j " +
+ "WHERE t.#{@owner.class.primary_key} = j.#{@association_foreign_key} AND " +
+ "j.#{association_class_primary_key_name} = '#{@owner.id}' " +
+ (options[:conditions] ? " AND " + options[:conditions] : "") + " " +
+ "ORDER BY #{@order}"
+ end
+
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
+ def clear
+ return self if size == 0 # forces load_collection if hasn't happened already
+
+ if sql = @options[:delete_sql]
+ each { |record| @owner.connection.execute(sql) }
+ elsif @options[:conditions]
+ sql =
+ "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' " +
+ "AND #{@association_foreign_key} IN (#{collect { |record| record.id }.join(", ")})"
+ @owner.connection.execute(sql)
+ else
+ sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}'"
+ @owner.connection.execute(sql)
+ end
+
+ @collection = []
+ self
+ end
+
+ def find(association_id = nil, &block)
+ if block_given? || @options[:finder_sql]
+ load_collection
+ @collection.find(&block)
+ else
+ if loaded?
+ find_all { |record| record.id == association_id.to_i }.first
+ else
+ find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} = '#{association_id}' ORDER BY")).first
+ end
+ end
+ end
+
+ def push_with_attributes(record, join_attributes = {})
+ raise_on_type_mismatch(record)
+ insert_record_with_join_attributes(record, join_attributes)
+ join_attributes.each { |key, value| record.send(:write_attribute, key, value) }
+ @collection << record if loaded?
+ self
+ end
+
+ alias :concat_with_attributes :push_with_attributes
+
+ def size
+ @options[:uniq] ? count_records : super
+ end
+
+ protected
+ def find_all_records(sql = @finder_sql)
+ records = @association_class.find_by_sql(sql)
+ @options[:uniq] ? uniq(records) : records
+ end
+
+ def count_records
+ load_collection
+ @collection.size
+ end
+
+ def insert_record(record)
+ if @options[:insert_sql]
+ @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
+ else
+ sql = "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) VALUES ('#{@owner.id}','#{record.id}')"
+ @owner.connection.execute(sql)
+ end
+ end
+
+ def insert_record_with_join_attributes(record, join_attributes)
+ attributes = { @association_class_primary_key_name => @owner.id, @association_foreign_key => record.id }.update(join_attributes)
+ sql =
+ "INSERT INTO #{@join_table} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
+ "VALUES (#{attributes.values.collect { |value| @owner.send(:quote, value) }.join(', ')})"
+ @owner.connection.execute(sql)
+ end
+
+ def delete_records(records)
+ if sql = @options[:delete_sql]
+ records.each { |record| @owner.connection.execute(sql) }
+ else
+ ids = quoted_record_ids(records)
+ sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_foreign_key} IN (#{ids})"
+ @owner.connection.execute(sql)
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
new file mode 100644
index 0000000000..947862ad37
--- /dev/null
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -0,0 +1,102 @@
+module ActiveRecord
+ module Associations
+ class HasManyAssociation < AssociationCollection #:nodoc:
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ super(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ @conditions = @association_class.send(:sanitize_conditions, options[:conditions])
+
+ if options[:finder_sql]
+ @finder_sql = interpolate_sql(options[:finder_sql])
+ @counter_sql = @finder_sql.gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
+ else
+ @finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
+ @counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
+ end
+ end
+
+ def create(attributes = {})
+ # Can't use Base.create since the foreign key may be a protected attribute.
+ record = build(attributes)
+ record.save
+ @collection << record if loaded?
+ record
+ end
+
+ def build(attributes = {})
+ record = @association_class.new(attributes)
+ record[@association_class_primary_key_name] = @owner.id
+ record
+ end
+
+ def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block)
+ if block_given? || @options[:finder_sql]
+ load_collection
+ @collection.find_all(&block)
+ else
+ @association_class.find_all(
+ "#{@association_class_primary_key_name} = '#{@owner.id}' " +
+ "#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + @association_class.send(:sanitize_conditions, runtime_conditions) : ""}",
+ orderings,
+ limit,
+ joins
+ )
+ end
+ end
+
+ def find(association_id = nil, &block)
+ if block_given? || @options[:finder_sql]
+ load_collection
+ @collection.find(&block)
+ else
+ @association_class.find_on_conditions(association_id,
+ "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
+ )
+ end
+ end
+
+ # Removes all records from this association. Returns +self+ so
+ # method calls may be chained.
+ def clear
+ @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}'")
+ @collection = []
+ self
+ end
+
+ protected
+ def find_all_records
+ if @options[:finder_sql]
+ @association_class.find_by_sql(@finder_sql)
+ else
+ @association_class.find_all(@finder_sql, @options[:order] ? @options[:order] : nil)
+ end
+ end
+
+ def count_records
+ if has_cached_counter?
+ @owner.send(:read_attribute, cached_counter_attribute_name)
+ elsif @options[:finder_sql]
+ @association_class.count_by_sql(@counter_sql)
+ else
+ @association_class.count(@counter_sql)
+ end
+ end
+
+ def has_cached_counter?
+ @owner.attribute_present?(cached_counter_attribute_name)
+ end
+
+ def cached_counter_attribute_name
+ "#{@association_name}_count"
+ end
+
+ def insert_record(record)
+ record.update_attribute(@association_class_primary_key_name, @owner.id)
+ end
+
+ def delete_records(records)
+ ids = quoted_record_ids(records)
+ @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_class.primary_key} IN (#{ids})")
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
new file mode 100755
index 0000000000..3312d41d06
--- /dev/null
+++ b/activerecord/lib/active_record/base.rb
@@ -0,0 +1,1051 @@
+require 'active_record/support/class_attribute_accessors'
+require 'active_record/support/class_inheritable_attributes'
+require 'active_record/support/inflector'
+require 'yaml'
+
+module ActiveRecord #:nodoc:
+ class ActiveRecordError < StandardError #:nodoc:
+ end
+ class AssociationTypeMismatch < ActiveRecordError #:nodoc:
+ end
+ class SerializationTypeMismatch < ActiveRecordError #:nodoc:
+ end
+ class AdapterNotSpecified < ActiveRecordError # :nodoc:
+ end
+ class AdapterNotFound < ActiveRecordError # :nodoc:
+ end
+ class ConnectionNotEstablished < ActiveRecordError #:nodoc:
+ end
+ class ConnectionFailed < ActiveRecordError #:nodoc:
+ end
+ class RecordNotFound < ActiveRecordError #:nodoc:
+ end
+ class StatementInvalid < ActiveRecordError #:nodoc:
+ end
+
+ # Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with
+ # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
+ # is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain
+ # database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
+ #
+ # See the mapping rules in table_name and the full example in link:files/README.html for more insight.
+ #
+ # == Creation
+ #
+ # Active Records accepts constructor parameters either in a hash or as a block. The hash method is especially useful when
+ # you're receiving the data from somewhere else, like a HTTP request. It works like this:
+ #
+ # user = User.new("name" => "David", "occupation" => "Code Artist")
+ # user.name # => "David"
+ #
+ # You can also use block initialization:
+ #
+ # user = User.new do |u|
+ # u.name = "David"
+ # u.occupation = "Code Artist"
+ # end
+ #
+ # And of course you can just create a bare object and specify the attributes after the fact:
+ #
+ # user = User.new
+ # user.name = "David"
+ # user.occupation = "Code Artist"
+ #
+ # == Conditions
+ #
+ # Conditions can either be specified as a string or an array representing the WHERE-part of an SQL statement.
+ # The array form is to be used when the condition input is tainted and requires sanitization. The string form can
+ # be used for statements that doesn't involve tainted data. Examples:
+ #
+ # User < ActiveRecord::Base
+ # def self.authenticate_unsafely(user_name, password)
+ # find_first("user_name = '#{user_name}' AND password = '#{password}'")
+ # end
+ #
+ # def self.authenticate_safely(user_name, password)
+ # find_first([ "user_name = '%s' AND password = '%s'", user_name, password ])
+ # end
+ # end
+ #
+ # The +authenticate_unsafely+ method inserts the parameters directly into the query and is thus susceptible to SQL-injection
+ # attacks if the +user_name+ and +password+ parameters come directly from a HTTP request. The +authenticate_safely+ method, on
+ # the other hand, will sanitize the +user_name+ and +password+ before inserting them in the query, which will ensure that
+ # an attacker can't escape the query and fake the login (or worse).
+ #
+ # == Overwriting default accessors
+ #
+ # All column values are automatically available through basic accessors on the Active Record object, but some times you
+ # want to specialize this behavior. This can be done by either by overwriting the default accessors (using the same
+ # name as the attribute) calling read_attribute(attr_name) and write_attribute(attr_name, value) to actually change things.
+ # Example:
+ #
+ # class Song < ActiveRecord::Base
+ # # Uses an integer of seconds to hold the length of the song
+ #
+ # def length=(minutes)
+ # write_attribute("length", minutes * 60)
+ # end
+ #
+ # def length
+ # read_attribute("length") / 60
+ # end
+ # end
+ #
+ # == Saving arrays, hashes, and other non-mappeable objects in text columns
+ #
+ # Active Record can serialize any object in text columns using YAML. To do so, you must specify this with a call to the class method +serialize+.
+ # This makes it possible to store arrays, hashes, and other non-mappeable objects without doing any additional work. Example:
+ #
+ # class User < ActiveRecord::Base
+ # serialize :preferences
+ # end
+ #
+ # user = User.create("preferences" => { "background" => "black", "display" => large })
+ # User.find(user.id).preferences # => { "background" => "black", "display" => large }
+ #
+ # You can also specify an optional :class_name option that'll raise an exception if a serialized object is retrieved as a
+ # descendent of a class not in the hierarchy. Example:
+ #
+ # class User < ActiveRecord::Base
+ # serialize :preferences, :class_name => "Hash"
+ # end
+ #
+ # user = User.create("preferences" => %w( one two three ))
+ # User.find(user.id).preferences # raises SerializationTypeMismatch
+ #
+ # == Single table inheritance
+ #
+ # Active Record allows inheritance by storing the name of the class in a column that by default is called "type" (can be changed
+ # by overwriting <tt>Base.inheritance_column</tt>). This means that an inheritance looking like this:
+ #
+ # class Company < ActiveRecord::Base; end
+ # class Firm < Company; end
+ # class Client < Company; end
+ # class PriorityClient < Client; end
+ #
+ # When you do Firm.create("name" => "37signals"), this record with be saved in the companies table with type = "Firm". You can then
+ # fetch this row again using Company.find_first "name = '37signals'" and it will return a Firm object.
+ #
+ # Note, all the attributes for all the cases are kept in the same table. Read more:
+ # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ #
+ # == Connection to multiple databases in different models
+ #
+ # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved by ActiveRecord::Base.connection.
+ # All classes inheriting from ActiveRecord::Base will use this connection. But you can also set a class-specific connection.
+ # For example, if Course is a ActiveRecord::Base, but resides in a different database you can just say Course.establish_connection
+ # and Course *and all its subclasses* will use this connection instead.
+ #
+ # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is a Hash indexed by the class. If a connection is
+ # requested, the retrieve_connection method will go up the class-hierarchy until a connection is found in the connection pool.
+ #
+ # == Exceptions
+ #
+ # * +ActiveRecordError+ -- generic error class and superclass of all other errors raised by Active Record
+ # * +AdapterNotSpecified+ -- the configuration hash used in <tt>establish_connection</tt> didn't include a
+ # <tt>:adapter</tt> key.
+ # * +AdapterNotSpecified+ -- the <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified an unexisting adapter
+ # (or a bad spelling of an existing one).
+ # * +AssociationTypeMismatch+ -- the object assigned to the association wasn't of the type specified in the association definition.
+ # * +SerializationTypeMismatch+ -- the object serialized wasn't of the class specified in the <tt>:class_name</tt> option of
+ # the serialize definition.
+ # * +ConnectionNotEstablished+ -- no connection has been established. Use <tt>establish_connection</tt> before querying.
+ # * +RecordNotFound+ -- no record responded to the find* method.
+ # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions.
+ # * +StatementInvalid+ -- the database server rejected the SQL statement. The precise error is added in the message.
+ # Either the record with the given ID doesn't exist or the record didn't meet the additional restrictions.
+ #
+ # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
+ # So it's possible to assign a logger to the class through Base.logger= which will then be used by all
+ # instances in the current object space.
+ class Base
+ include ClassInheritableAttributes
+
+ # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed
+ # on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+.
+ cattr_accessor :logger
+
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work unrelated
+ # to any of the specific Active Records.
+ def self.connection
+ retrieve_connection
+ end
+
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work that isn't
+ # easily done without going straight to SQL.
+ def connection
+ self.class.connection
+ end
+
+ def self.inherited(child) #:nodoc:
+ @@subclasses[self] ||= []
+ @@subclasses[self] << child
+ super
+ end
+
+ @@subclasses = {}
+
+ cattr_accessor :configurations
+ @@primary_key_prefix_type = {}
+
+ # Accessor for the prefix type that will be prepended to every primary key column name. The options are :table_name and
+ # :table_name_with_underscore. If the first is specified, the Product class will look for "productid" instead of "id" as
+ # the primary column. If the latter is specified, the Product class will look for "product_id" instead of "id". Remember
+ # that this is a global setting for all Active Records.
+ cattr_accessor :primary_key_prefix_type
+ @@primary_key_prefix_type = nil
+
+ # Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all
+ # table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convinient way of creating a namespace
+ # for tables in a shared database. By default, the prefix is the empty string.
+ cattr_accessor :table_name_prefix
+ @@table_name_prefix = ""
+
+ # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
+ # "people_basecamp"). By default, the suffix is the empty string.
+ cattr_accessor :table_name_suffix
+ @@table_name_suffix = ""
+
+ # Indicate whether or not table names should be the pluralized versions of the corresponding class names.
+ # If true, this the default table name for a +Product+ class will be +products+. If false, it would just be +product+.
+ # See table_name for the full rules on table/class naming. This is true, by default.
+ cattr_accessor :pluralize_table_names
+ @@pluralize_table_names = true
+
+ # When turned on (which is default), all associations are included using "load". This mean that any change is instant in cached
+ # environments like mod_ruby or FastCGI. When set to false, "require" is used, which is faster but requires server restart to
+ # be effective.
+ @@reload_associations = true
+ cattr_accessor :reload_associations
+
+ @@associations_loaded = []
+ cattr_accessor :associations_loaded
+
+ class << self # Class methods
+ # Returns objects for the records responding to either a specific id (1), a list of ids (1, 5, 6) or an array of ids.
+ # If only one ID is specified, that object is returned directly. If more than one ID is specified, an array is returned.
+ # Examples:
+ # Person.find(1) # returns the object for ID = 1
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
+ # +RecordNotFound+ is raised if no record can be found.
+ def find(*ids)
+ ids = ids.flatten.compact.uniq
+
+ if ids.length > 1
+ ids_list = ids.map{ |id| "'#{sanitize(id)}'" }.join(", ")
+ objects = find_all("#{primary_key} IN (#{ids_list})", primary_key)
+
+ if objects.length == ids.length
+ return objects
+ else
+ raise RecordNotFound, "Couldn't find #{name} with ID in (#{ids_list})"
+ end
+ elsif ids.length == 1
+ id = ids.first
+ sql = "SELECT * FROM #{table_name} WHERE #{primary_key} = '#{sanitize(id)}'"
+ sql << " AND #{type_condition}" unless descends_from_active_record?
+
+ if record = connection.select_one(sql, "#{name} Find")
+ instantiate(record)
+ else
+ raise RecordNotFound, "Couldn't find #{name} with ID = #{id}"
+ end
+ else
+ raise RecordNotFound, "Couldn't find #{name} without an ID"
+ end
+ end
+
+ # Works like find, but the record matching +id+ must also meet the +conditions+.
+ # +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition.
+ # Example:
+ # Person.find_on_conditions 5, "first_name LIKE '%dav%' AND last_name = 'heinemeier'"
+ def find_on_conditions(id, conditions)
+ find_first("#{primary_key} = '#{sanitize(id)}' AND #{sanitize_conditions(conditions)}") ||
+ raise(RecordNotFound, "Couldn't find #{name} with #{primary_key} = #{id} on the condition of #{conditions}")
+ end
+
+ # Returns an array of all the objects that could be instantiated from the associated
+ # table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part),
+ # such as by "color = 'red'", and arrangement of the selection can be done through +orderings+ (ORDER BY-part),
+ # such as by "last_name, first_name DESC". A maximum of returned objects can be specified in +limit+. Example:
+ # Project.find_all "category = 'accounts'", "last_accessed DESC", 15
+ def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)
+ sql = "SELECT * FROM #{table_name} "
+ sql << "#{joins} " if joins
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+ sql << "LIMIT #{limit} " unless limit.nil?
+
+ find_by_sql(sql)
+ end
+
+ # Works like find_all, but requires a complete SQL string. Example:
+ # Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"
+ def find_by_sql(sql)
+ connection.select_all(sql, "#{name} Load").inject([]) { |objects, record| objects << instantiate(record) }
+ end
+
+ # Returns the object for the first record responding to the conditions in +conditions+,
+ # such as "group = 'master'". If more than one record is returned from the query, it's the first that'll
+ # be used to create the object. In such cases, it might be beneficial to also specify
+ # +orderings+, like "income DESC, name", to control exactly which record is to be used. Example:
+ # Employee.find_first "income > 50000", "income DESC, name"
+ def find_first(conditions = nil, orderings = nil)
+ sql = "SELECT * FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+ sql << "LIMIT 1"
+
+ record = connection.select_one(sql, "#{name} Load First")
+ instantiate(record) unless record.nil?
+ end
+
+ # Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
+ # fail under validations, the unsaved object is still returned.
+ def create(attributes = nil)
+ object = new(attributes)
+ object.save
+ object
+ end
+
+ # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
+ # and returns it. If the save fail under validations, the unsaved object is still returned.
+ def update(id, attributes)
+ object = find(id)
+ object.attributes = attributes
+ object.save
+ object
+ end
+
+ # Updates all records with the SET-part of an SQL update statement in +updates+. A subset of the records can be selected
+ # by specifying +conditions+. Example:
+ # Billing.update_all "category = 'authorized', approved = 1", "author = 'David'"
+ def update_all(updates, conditions = nil)
+ sql = "UPDATE #{table_name} SET #{updates} "
+ add_conditions!(sql, conditions)
+ connection.update(sql, "#{name} Update")
+ end
+
+ # Destroys the objects for all the records that matches the +condition+ by instantiating each object and calling
+ # the destroy method. Example:
+ # Person.destroy_all "last_login < '2004-04-04'"
+ def destroy_all(conditions = nil)
+ find_all(conditions).each { |object| object.destroy }
+ end
+
+ # Deletes all the records that matches the +condition+ without instantiating the objects first (and hence not
+ # calling the destroy method). Example:
+ # Post.destroy_all "person_id = 5 AND (category = 'Something' OR category = 'Else')"
+ def delete_all(conditions = nil)
+ sql = "DELETE FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ connection.delete(sql, "#{name} Delete all")
+ end
+
+ # Returns the number of records that meets the +conditions+. Zero is returned if no records match. Example:
+ # Product.count "sales > 1"
+ def count(conditions = nil)
+ sql = "SELECT COUNT(*) FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ count_by_sql(sql)
+ end
+
+ # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
+ # Product.count "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
+ def count_by_sql(sql)
+ count = connection.select_one(sql, "#{name} Count").values.first
+ return count ? count.to_i : 0
+ end
+
+ # Increments the specified counter by one. So <tt>DiscussionBoard.increment_counter("post_count",
+ # discussion_board_id)</tt> would increment the "post_count" counter on the board responding to discussion_board_id.
+ # This is used for caching aggregate values, so that they doesn't need to be computed every time. Especially important
+ # for looping over a collection where each element require a number of aggregate values. Like the DiscussionBoard
+ # that needs to list both the number of posts and comments.
+ def increment_counter(counter_name, id)
+ update_all "#{counter_name} = #{counter_name} + 1", "#{primary_key} = #{id}"
+ end
+
+ # Works like increment_counter, but decrements instead.
+ def decrement_counter(counter_name, id)
+ update_all "#{counter_name} = #{counter_name} - 1", "#{primary_key} = #{id}"
+ end
+
+ # Attributes named in this macro are protected from mass-assignment, such as <tt>new(attributes)</tt> and
+ # <tt>attributes=(attributes)</tt>. Their assignment will simply be ignored. Instead, you can use the direct writer
+ # methods to do assignment. This is meant to protect sensitive attributes to be overwritten by URL/form hackers. Example:
+ #
+ # class Customer < ActiveRecord::Base
+ # attr_protected :credit_rating
+ # end
+ #
+ # customer = Customer.new("name" => David, "credit_rating" => "Excellent")
+ # customer.credit_rating # => nil
+ # customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
+ # customer.credit_rating # => nil
+ #
+ # customer.credit_rating = "Average"
+ # customer.credit_rating # => "Average"
+ def attr_protected(*attributes)
+ write_inheritable_array("attr_protected", attributes)
+ end
+
+ # Returns an array of all the attributes that have been protected from mass-assigment.
+ def protected_attributes # :nodoc:
+ read_inheritable_attribute("attr_protected")
+ end
+
+ # If this macro is used, only those attributed named in it will be accessible for mass-assignment, such as
+ # <tt>new(attributes)</tt> and <tt>attributes=(attributes)</tt>. This is the more conservative choice for mass-assignment
+ # protection. If you'd rather start from an all-open default and restrict attributes as needed, have a look at
+ # attr_protected.
+ def attr_accessible(*attributes)
+ write_inheritable_array("attr_accessible", attributes)
+ end
+
+ # Returns an array of all the attributes that have been made accessible to mass-assigment.
+ def accessible_attributes # :nodoc:
+ read_inheritable_attribute("attr_accessible")
+ end
+
+ # Specifies that the attribute by the name of +attr_name+ should be serialized before saving to the database and unserialized
+ # after loading from the database. The serialization is done through YAML. If +class_name+ is specified, the serialized
+ # object must be of that class on retrival or +SerializationTypeMismatch+ will be raised.
+ def serialize(attr_name, class_name = Object)
+ write_inheritable_attribute("attr_serialized", serialized_attributes.update(attr_name.to_s => class_name))
+ end
+
+ # Returns a hash of all the attributes that have been specified for serialization as keys and their class restriction as values.
+ def serialized_attributes
+ read_inheritable_attribute("attr_serialized") || { }
+ end
+
+ # Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
+ # directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used
+ # to guess the table name from even when called on Reply. The guessing rules are as follows:
+ #
+ # * Class name ends in "x", "ch" or "ss": "es" is appended, so a Search class becomes a searches table.
+ # * Class name ends in "y" preceded by a consonant or "qu": The "y" is replaced with "ies", so a Category class becomes a categories table.
+ # * Class name ends in "fe": The "fe" is replaced with "ves", so a Wife class becomes a wives table.
+ # * Class name ends in "lf" or "rf": The "f" is replaced with "ves", so a Half class becomes a halves table.
+ # * Class name ends in "person": The "person" is replaced with "people", so a Salesperson class becomes a salespeople table.
+ # * Class name ends in "man": The "man" is replaced with "men", so a Spokesman class becomes a spokesmen table.
+ # * Class name ends in "sis": The "i" is replaced with an "e", so a Basis class becomes a bases table.
+ # * Class name ends in "tum" or "ium": The "um" is replaced with an "a", so a Datum class becomes a data table.
+ # * Class name ends in "child": The "child" is replaced with "children", so a NodeChild class becomes a node_children table.
+ # * Class name ends in an "s": No additional characters are added or removed.
+ # * Class name doesn't end in "s": An "s" is appended, so a Comment class becomes a comments table.
+ # * Class name with word compositions: Compositions are underscored, so CreditCard class becomes a credit_cards table.
+ #
+ # Additionally, the class-level table_name_prefix is prepended to the table_name and the table_name_suffix is appended.
+ # So if you have "myapp_" as a prefix, the table name guess for an Account class becomes "myapp_accounts".
+ #
+ # You can also overwrite this class method to allow for unguessable links, such as a Mouse class with a link to a
+ # "mice" table. Example:
+ #
+ # class Mouse < ActiveRecord::Base
+ # def self.table_name() "mice" end
+ # end
+ def table_name(class_name = nil)
+ if class_name.nil?
+ class_name = class_name_of_active_record_descendant(self)
+ table_name_prefix + undecorated_table_name(class_name) + table_name_suffix
+ else
+ table_name_prefix + undecorated_table_name(class_name) + table_name_suffix
+ end
+ end
+
+ # Defines the primary key field -- can be overridden in subclasses. Overwritting will negate any effect of the
+ # primary_key_prefix_type setting, though.
+ def primary_key
+ case primary_key_prefix_type
+ when :table_name
+ Inflector.foreign_key(class_name_of_active_record_descendant(self), false)
+ when :table_name_with_underscore
+ Inflector.foreign_key(class_name_of_active_record_descendant(self))
+ else
+ "id"
+ end
+ end
+
+ # Defines the column name for use with single table inheritance -- can be overridden in subclasses.
+ def inheritance_column
+ "type"
+ end
+
+ # Turns the +table_name+ back into a class name following the reverse rules of +table_name+.
+ def class_name(table_name = table_name) # :nodoc:
+ # remove any prefix and/or suffix from the table name
+ class_name = Inflector.camelize(table_name[table_name_prefix.length..-(table_name_suffix.length + 1)])
+ class_name = Inflector.singularize(class_name) if pluralize_table_names
+ return class_name
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns
+ @columns ||= connection.columns(table_name, "#{name} Columns")
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns_hash
+ @columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash }
+ end
+
+ # Returns an array of columns objects where the primary id, all columns ending in "_id" or "_count",
+ # and columns used for single table inheritance has been removed.
+ def content_columns
+ @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
+ end
+
+ # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
+ # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
+ # is available.
+ def column_methods_hash
+ @dynamic_methods_hash ||= columns_hash.keys.inject(Hash.new(false)) do |methods, attr|
+ methods[attr.to_sym] = true
+ methods["#{attr}=".to_sym] = true
+ methods["#{attr}?".to_sym] = true
+ methods
+ end
+ end
+
+ # Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
+ # Person.human_attribute_name("first_name") # => "First name"
+ def human_attribute_name(attribute_key_name)
+ attribute_key_name.gsub(/_/, " ").capitalize unless attribute_key_name.nil?
+ end
+
+ def descends_from_active_record? # :nodoc:
+ superclass == Base
+ end
+
+ # Used to sanitize objects before they're used in an SELECT SQL-statement.
+ def sanitize(object) # :nodoc:
+ return object if Fixnum === object
+ object.to_s.gsub(/([;:])/, "").gsub('##', '\#\#').gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ # Used to aggregate logging and benchmark, so you can measure and represent multiple statements in a single block.
+ # Usage (hides all the SQL calls for the individual actions and calculates total runtime for them all):
+ #
+ # Project.benchmark("Creating project") do
+ # project = Project.create("name" => "stuff")
+ # project.create_manager("name" => "David")
+ # project.milestones << Milestone.find_all
+ # end
+ def benchmark(title)
+ result = nil
+ logger.level = Logger::ERROR
+ bm = Benchmark.measure { result = yield }
+ logger.level = Logger::DEBUG
+ logger.info "#{title} (#{sprintf("%f", bm.real)})"
+ return result
+ end
+
+ # Loads the <tt>file_name</tt> if reload_associations is true or requires if it's false.
+ def require_or_load(file_name)
+ if !associations_loaded.include?(file_name)
+ associations_loaded << file_name
+ reload_associations ? load("#{file_name}.rb") : require(file_name)
+ end
+ end
+
+ # Resets the list of dependencies loaded (typically to be called by the end of a request), so when require_or_load is
+ # called for that dependency it'll be loaded anew.
+ def reset_associations_loaded
+ associations_loaded = []
+ end
+
+ private
+ # Finder methods must instantiate through this method to work with the single-table inheritance model
+ # that makes it possible to create objects of different types from the same table.
+ def instantiate(record)
+ object = record_with_type?(record) ? compute_type(record[inheritance_column]).allocate : allocate
+ object.instance_variable_set("@attributes", record)
+ return object
+ end
+
+ # Returns true if the +record+ has a single table inheritance column and is using it.
+ def record_with_type?(record)
+ record.include?(inheritance_column) && !record[inheritance_column].nil? &&
+ !record[inheritance_column].empty?
+ end
+
+ # Returns the name of the type of the record using the current module as a prefix. So descendents of
+ # MyApp::Business::Account would be appear as "MyApp::Business::AccountSubclass".
+ def type_name_with_module(type_name)
+ self.name =~ /::/ ? self.name.scan(/(.*)::/).first.first + "::" + type_name : type_name
+ end
+
+ # Adds a sanitized version of +conditions+ to the +sql+ string. Note that it's the passed +sql+ string is changed.
+ def add_conditions!(sql, conditions)
+ sql << "WHERE #{sanitize_conditions(conditions)} " unless conditions.nil?
+ sql << (conditions.nil? ? "WHERE " : " AND ") + type_condition unless descends_from_active_record?
+ end
+
+ def type_condition
+ " (" + subclasses.inject("#{inheritance_column} = '#{Inflector.demodulize(name)}' ") do |condition, subclass|
+ condition << "OR #{inheritance_column} = '#{Inflector.demodulize(subclass.name)}'"
+ end + ") "
+ end
+
+ # Guesses the table name, but does not decorate it with prefix and suffix information.
+ def undecorated_table_name(class_name = class_name_of_active_record_descendant(self))
+ table_name = Inflector.underscore(Inflector.demodulize(class_name))
+ table_name = Inflector.pluralize(table_name) if pluralize_table_names
+ return table_name
+ end
+
+
+ protected
+ def subclasses
+ @@subclasses[self] ||= []
+ @@subclasses[self] + extra = @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses }
+ end
+
+ # Returns the class type of the record using the current module as a prefix. So descendents of
+ # MyApp::Business::Account would be appear as MyApp::Business::AccountSubclass.
+ def compute_type(type_name)
+ type_name_with_module(type_name).split("::").inject(Object) do |final_type, part|
+ final_type = final_type.const_get(part)
+ end
+ end
+
+ # Returns the name of the class descending directly from ActiveRecord in the inheritance hierarchy.
+ def class_name_of_active_record_descendant(klass)
+ if klass.superclass == Base
+ return klass.name
+ elsif klass.superclass.nil?
+ raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
+ else
+ class_name_of_active_record_descendant(klass.superclass)
+ end
+ end
+
+ # Accepts either a condition array or string. The string is returned untouched, but the array has each of
+ # the condition values sanitized.
+ def sanitize_conditions(conditions)
+ if Array === conditions
+ statement, values = conditions[0], conditions[1..-1]
+ values.collect! { |value| sanitize(value) }
+ conditions = statement % values
+ end
+
+ return conditions
+ end
+ end
+
+ public
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
+ # attributes but not yet saved (pass a hash with key names matching the associated table column names).
+ # In both instances, valid attribute keys are determined by the column names of the associated table --
+ # hence you can't have attributes that aren't part of the table columns.
+ def initialize(attributes = nil)
+ @attributes = attributes_from_column_definition
+ @new_record = true
+ ensure_proper_type
+ self.attributes = attributes unless attributes.nil?
+ yield self if block_given?
+ end
+
+ # Every Active Record class must use "id" as their primary ID. This getter overwrites the native
+ # id method, which isn't being used in this context.
+ def id
+ read_attribute(self.class.primary_key)
+ end
+
+ # Sets the primary ID.
+ def id=(value)
+ write_attribute(self.class.primary_key, value)
+ end
+
+ # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
+ def new_record?
+ @new_record
+ end
+
+ # * No record exists: Creates a new record with values matching those of the object attributes.
+ # * A record does exist: Updates the record with values matching those of the object attributes.
+ def save
+ create_or_update
+ return true
+ end
+
+ # Deletes the record in the database and freezes this instance to reflect that no changes should
+ # be made (since they can't be persisted).
+ def destroy
+ unless new_record?
+ connection.delete(
+ "DELETE FROM #{self.class.table_name} " +
+ "WHERE #{self.class.primary_key} = '#{id}'",
+ "#{self.class.name} Destroy"
+ )
+ end
+
+ freeze
+ end
+
+ # Returns a clone of the record that hasn't been assigned an id yet and is treated as a new record.
+ def clone
+ attr = Hash.new
+
+ self.attribute_names.each do |name|
+ begin
+ attr[name] = read_attribute(name).clone
+ rescue TypeError
+ attr[name] = read_attribute(name)
+ end
+ end
+
+ cloned_record = self.class.new(attr)
+ cloned_record.instance_variable_set "@new_record", true
+ cloned_record.id = nil
+ cloned_record
+ end
+
+ # Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
+ def update_attribute(name, value)
+ self[name] = value
+ save
+ end
+
+ # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ # (Alias for the protected read_attribute method).
+ def [](attr_name)
+ read_attribute(attr_name)
+ end
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
+ # (Alias for the protected write_attribute method).
+ def []= (attr_name, value)
+ write_attribute(attr_name, value)
+ end
+
+ # Allows you to set all the attributes at once by passing in a hash with keys
+ # matching the attribute names (which again matches the column names). Sensitive attributes can be protected
+ # from this form of mass-assignment by using the +attr_protected+ macro. Or you can alternatively
+ # specify which attributes *can* be accessed in with the +attr_accessible+ macro. Then all the
+ # attributes not included in that won't be allowed to be mass-assigned.
+ def attributes=(attributes)
+ return if attributes.nil?
+
+ multi_parameter_attributes = []
+ remove_attributes_protected_from_mass_assignment(attributes).each do |k, v|
+ k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
+ end
+ assign_multiparameter_attributes(multi_parameter_attributes)
+ end
+
+ # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
+ # nil nor empty? (the latter only applies to objects that responds to empty?, most notably Strings).
+ def attribute_present?(attribute)
+ is_empty = read_attribute(attribute).respond_to?("empty?") ? read_attribute(attribute).empty? : false
+ @attributes.include?(attribute) && !@attributes[attribute].nil? && !is_empty
+ end
+
+ # Returns an array of names for the attributes available on this object sorted alphabetically.
+ def attribute_names
+ @attributes.keys.sort
+ end
+
+ # Returns the column object for the named attribute.
+ def column_for_attribute(name)
+ self.class.columns_hash[name]
+ end
+
+ # Returns true if the +comparison_object+ is of the same type and has the same id.
+ def ==(comparison_object)
+ comparison_object.instance_of?(self.class) && comparison_object.id == id
+ end
+
+ # Delegates to ==
+ def eql?(comparison_object)
+ self == (comparison_object)
+ end
+
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
+ def hash
+ id
+ end
+
+ # For checking respond_to? without searching the attributes (which is faster).
+ alias_method :respond_to_without_attributes?, :respond_to?
+
+ # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
+ # person.respond_to?("name?") which will all return true.
+ def respond_to?(method)
+ self.class.column_methods_hash[method.to_sym] || respond_to_without_attributes?(method)
+ end
+
+ def require_or_load(file_name)
+ self.class.require_or_load(file_name)
+ end
+
+ private
+ def create_or_update
+ if new_record? then create else update end
+ end
+
+ # Updates the associated record with values matching those of the instant attributes.
+ def update
+ connection.update(
+ "UPDATE #{self.class.table_name} " +
+ "SET #{quoted_comma_pair_list(connection, attributes_with_quotes)} " +
+ "WHERE #{self.class.primary_key} = '#{id}'",
+ "#{self.class.name} Update"
+ )
+ end
+
+ # Creates a new record with values matching those of the instant attributes.
+ def create
+ self.id = connection.insert(
+ "INSERT INTO #{self.class.table_name} " +
+ "(#{quoted_column_names.join(', ')}) " +
+ "VALUES(#{attributes_with_quotes.values.join(', ')})",
+ "#{self.class.name} Create",
+ self.class.primary_key, self.id
+ )
+
+ @new_record = false
+ end
+
+ # Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord descendant.
+ # Considering the hierarchy Reply < Message < ActiveRecord, this makes it possible to do Reply.new without having to
+ # set Reply[Reply.inheritance_column] = "Reply" yourself. No such attribute would be set for objects of the
+ # Message class in that example.
+ def ensure_proper_type
+ unless self.class.descends_from_active_record?
+ write_attribute(self.class.inheritance_column, Inflector.demodulize(self.class.name))
+ end
+ end
+
+ # Allows access to the object attributes, which are held in the @attributes hash, as were
+ # they first-class methods. So a Person class with a name attribute can use Person#name and
+ # Person#name= and never directly use the attributes hash -- except for multiple assigns with
+ # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
+ # the completed attribute is not nil or 0.
+ #
+ # It's also possible to instantiate related objects, so a Client class belonging to the clients
+ # table with a master_id foreign key can instantiate master through Client#master.
+ def method_missing(method_id, *arguments)
+ method_name = method_id.id2name
+
+
+
+ if method_name =~ read_method? && @attributes.include?($1)
+ return read_attribute($1)
+ elsif method_name =~ write_method? && @attributes.include?($1)
+ write_attribute($1, arguments[0])
+ elsif method_name =~ query_method? && @attributes.include?($1)
+ return query_attribute($1)
+ else
+ super
+ end
+ end
+
+ def read_method?() /^([a-zA-Z][-_\w]*)[^=?]*$/ end
+ def write_method?() /^([a-zA-Z][-_\w]*)=.*$/ end
+ def query_method?() /^([a-zA-Z][-_\w]*)\?$/ end
+
+ # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ def read_attribute(attr_name) #:doc:
+ if @attributes.keys.include? attr_name
+ if column = column_for_attribute(attr_name)
+ @attributes[attr_name] = unserializable_attribute?(attr_name, column) ?
+ unserialize_attribute(attr_name) : column.type_cast(@attributes[attr_name])
+ end
+
+ @attributes[attr_name]
+ else
+ nil
+ end
+ end
+
+ # Returns true if the attribute is of a text column and marked for serialization.
+ def unserializable_attribute?(attr_name, column)
+ @attributes[attr_name] && column.send(:type) == :text && @attributes[attr_name].is_a?(String) && self.class.serialized_attributes[attr_name]
+ end
+
+ # Returns the unserialized object of the attribute.
+ def unserialize_attribute(attr_name)
+ unserialized_object = object_from_yaml(@attributes[attr_name])
+
+ if unserialized_object.is_a?(self.class.serialized_attributes[attr_name])
+ @attributes[attr_name] = unserialized_object
+ else
+ raise(
+ SerializationTypeMismatch,
+ "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, " +
+ "but was a #{unserialized_object.class.to_s}"
+ )
+ end
+ end
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
+ # columns are turned into nil.
+ def write_attribute(attr_name, value) #:doc:
+ @attributes[attr_name] = empty_string_for_number_column?(attr_name, value) ? nil : value
+ end
+
+ def empty_string_for_number_column?(attr_name, value)
+ column = column_for_attribute(attr_name)
+ column && (column.klass == Fixnum || column.klass == Float) && value == ""
+ end
+
+ def query_attribute(attr_name)
+ attribute = @attributes[attr_name]
+ if attribute.kind_of?(Fixnum) && attribute == 0
+ false
+ elsif attribute.kind_of?(String) && attribute == "0"
+ false
+ elsif attribute.kind_of?(String) && attribute.empty?
+ false
+ elsif attribute.nil?
+ false
+ elsif attribute == false
+ false
+ elsif attribute == "f"
+ false
+ elsif attribute == "false"
+ false
+ else
+ true
+ end
+ end
+
+ def remove_attributes_protected_from_mass_assignment(attributes)
+ if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
+ attributes.reject { |key, value| key == self.class.primary_key }
+ elsif self.class.protected_attributes.nil?
+ attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.intern) || key == self.class.primary_key }
+ elsif self.class.accessible_attributes.nil?
+ attributes.reject { |key, value| self.class.protected_attributes.include?(key.intern) || key == self.class.primary_key }
+ end
+ end
+
+ # Returns copy of the attributes hash where all the values have been safely quoted for use in
+ # an SQL statement.
+ def attributes_with_quotes
+ columns_hash = self.class.columns_hash
+ @attributes.inject({}) do |attrs_quoted, pair|
+ attrs_quoted[pair.first] = quote(pair.last, columns_hash[pair.first])
+ attrs_quoted
+ end
+ end
+
+ # Quote strings appropriately for SQL statements.
+ def quote(value, column = nil)
+ connection.quote(value, column)
+ end
+
+ # Interpolate custom sql string in instance context.
+ # Optional record argument is meant for custom insert_sql.
+ def interpolate_sql(sql, record = nil)
+ instance_eval("%(#{sql})")
+ end
+
+ # Initializes the attributes array with keys matching the columns from the linked table and
+ # the values matching the corresponding default value of that column, so
+ # that a new instance, or one populated from a passed-in Hash, still has all the attributes
+ # that instances loaded from the database would.
+ def attributes_from_column_definition
+ connection.columns(self.class.table_name, "#{self.class.name} Columns").inject({}) do |attributes, column|
+ attributes[column.name] = column.default unless column.name == self.class.primary_key
+ attributes
+ end
+ end
+
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
+ # parenteses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float,
+ # s for String, and a for Array. If all the values for a given attribute is empty, the attribute will be set to nil.
+ def assign_multiparameter_attributes(pairs)
+ execute_callstack_for_multiparameter_attributes(
+ extract_callstack_for_multiparameter_attributes(pairs)
+ )
+ end
+
+ # Includes an ugly hack for Time.local instead of Time.new because the latter is reserved by Time itself.
+ def execute_callstack_for_multiparameter_attributes(callstack)
+ callstack.each do |name, values|
+ klass = (self.class.reflect_on_aggregation(name) || column_for_attribute(name)).klass
+ if values.empty?
+ send(name + "=", nil)
+ else
+ send(name + "=", Time == klass ? klass.local(*values) : klass.new(*values))
+ end
+ end
+ end
+
+ def extract_callstack_for_multiparameter_attributes(pairs)
+ attributes = { }
+
+ for pair in pairs
+ multiparameter_name, value = pair
+ attribute_name = multiparameter_name.split("(").first
+ attributes[attribute_name] = [] unless attributes.include?(attribute_name)
+
+ unless value.empty?
+ attributes[attribute_name] <<
+ [find_parameter_position(multiparameter_name), type_cast_attribute_value(multiparameter_name, value)]
+ end
+ end
+
+ attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
+ end
+
+ def type_cast_attribute_value(multiparameter_name, value)
+ multiparameter_name =~ /\([0-9]*([a-z])\)/ ? value.send("to_" + $1) : value
+ end
+
+ def find_parameter_position(multiparameter_name)
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
+ end
+
+ # Returns a comma-separated pair list, like "key1 = val1, key2 = val2".
+ def comma_pair_list(hash)
+ hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ")
+ end
+
+ def quoted_column_names(attributes = attributes_with_quotes)
+ attributes.keys.collect { |column_name| connection.quote_column_name(column_name) }
+ end
+
+ def quote_columns(column_quoter, hash)
+ hash.inject({}) {|list, pair|
+ list[column_quoter.quote_column_name(pair.first)] = pair.last
+ list
+ }
+ end
+
+ def quoted_comma_pair_list(column_quoter, hash)
+ comma_pair_list(quote_columns(column_quoter, hash))
+ end
+
+ def object_from_yaml(string)
+ return string unless String === string
+ if has_yaml_encoding_header?(string)
+ begin
+ YAML::load(string)
+ rescue Object
+ # Apparently wasn't YAML anyway
+ string
+ end
+ else
+ string
+ end
+ end
+
+ def has_yaml_encoding_header?(string)
+ string[0..3] == "--- "
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
new file mode 100755
index 0000000000..fc013ba743
--- /dev/null
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -0,0 +1,337 @@
+require 'observer'
+
+module ActiveRecord
+ # Callbacks are hooks into the lifecycle of an Active Record object that allows you to trigger logic
+ # before or after an alteration of the object state. This can be used to make sure that associated and
+ # dependent objects are deleted when destroy is called (by overwriting before_destroy) or to massage attributes
+ # before they're validated (by overwriting before_validation). As an example of the callbacks initiated, consider
+ # the Base#save call:
+ #
+ # * (-) save
+ # * (-) valid?
+ # * (1) before_validation
+ # * (2) before_validation_on_create
+ # * (-) validate
+ # * (-) validate_on_create
+ # * (4) after_validation
+ # * (5) after_validation_on_create
+ # * (6) before_save
+ # * (7) before_create
+ # * (-) create
+ # * (8) after_create
+ # * (9) after_save
+ #
+ # That's a total of nine callbacks, which gives you immense power to react and prepare for each state in the
+ # Active Record lifecyle.
+ #
+ # Examples:
+ # class CreditCard < ActiveRecord::Base
+ # # Strip everything but digits, so the user can specify "555 234 34" or
+ # # "5552-3434" or both will mean "55523434"
+ # def before_validation_on_create
+ # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
+ # end
+ # end
+ #
+ # class Subscription < ActiveRecord::Base
+ # # Automatically assign the signup date
+ # def before_create
+ # self.signed_up_on = Date.today
+ # end
+ # end
+ #
+ # class Firm < ActiveRecord::Base
+ # # Destroys the associated clients and people when the firm is destroyed
+ # def before_destroy
+ # Client.destroy_all "client_of = #{id}"
+ # Person.destroy_all "firm_id = #{id}"
+ # end
+ #
+ # == Inheritable callback queues
+ #
+ # Besides the overwriteable callback methods, it's also possible to register callbacks through the use of the callback macros.
+ # Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance
+ # hierarchy. Example:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy :destroy_author
+ # end
+ #
+ # class Reply < Topic
+ # before_destroy :destroy_readers
+ # end
+ #
+ # Now, when Topic#destroy is run only +destroy_author+ is called. When Reply#destroy is run both +destroy_author+ and
+ # +destroy_readers+ is called. Contrast this to the situation where we've implemented the save behavior through overwriteable
+ # methods:
+ #
+ # class Topic < ActiveRecord::Base
+ # def before_destroy() destroy_author end
+ # end
+ #
+ # class Reply < Topic
+ # def before_destroy() destroy_readers end
+ # end
+ #
+ # In that case, Reply#destroy would only run +destroy_readers+ and _not_ +destroy_author+. So use the callback macros when
+ # you want to ensure that a certain callback is called for the entire hierarchy and the regular overwriteable methods when you
+ # want to leave it up to each descendent to decide whether they want to call +super+ and trigger the inherited callbacks.
+ #
+ # == Types of callbacks
+ #
+ # There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
+ # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects are the
+ # recommended approaches, inline methods using a proc is some times appropriate (such as for creating mix-ins), and inline
+ # eval methods are deprecated.
+ #
+ # The method reference callbacks work by specifying a protected or private method available in the object, like this:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy :delete_parents
+ #
+ # private
+ # def delete_parents
+ # self.class.delete_all "parent_id = #{id}"
+ # end
+ # end
+ #
+ # The callback objects have methods named after the callback called with the record as the only parameter, such as:
+ #
+ # class BankAccount < ActiveRecord::Base
+ # before_save EncryptionWrapper.new("credit_card_number")
+ # after_save EncryptionWrapper.new("credit_card_number")
+ # after_initialize EncryptionWrapper.new("credit_card_number")
+ # end
+ #
+ # class EncryptionWrapper
+ # def initialize(attribute)
+ # @attribute = attribute
+ # end
+ #
+ # def before_save(record)
+ # record.credit_card_number = encrypt(record.credit_card_number)
+ # end
+ #
+ # def after_save(record)
+ # record.credit_card_number = decrypt(record.credit_card_number)
+ # end
+ #
+ # alias_method :after_initialize, :after_save
+ #
+ # private
+ # def encrypt(value)
+ # # Secrecy is committed
+ # end
+ #
+ # def decrypt(value)
+ # # Secrecy is unvieled
+ # end
+ # end
+ #
+ # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
+ # a method by the name of the callback messaged.
+ #
+ # The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string",
+ # which will then be evaluated within the binding of the callback. Example:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy 'self.class.delete_all "parent_id = #{id}"'
+ # end
+ #
+ # Notice that single plings (') are used so the #{id} part isn't evaluated until the callback is triggered. Also note that these
+ # inline callbacks can be stacked just like the regular ones:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy 'self.class.delete_all "parent_id = #{id}"',
+ # 'puts "Evaluated after parents are destroyed"'
+ # end
+ #
+ # == The after_find and after_initialize exceptions
+ #
+ # Because after_find and after_initialize is called for each object instantiated found by a finder, such as Base.find_all, we've had
+ # to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, after_find and
+ # after_initialize can only be declared using an explicit implementation. So using the inheritable callback queue for after_find and
+ # after_initialize won't work.
+ module Callbacks
+ CALLBACKS = %w(
+ after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
+ after_validation before_validation_on_create after_validation_on_create before_validation_on_update
+ after_validation_on_update before_destroy after_destroy
+ )
+
+ def self.append_features(base) #:nodoc:
+ super
+
+ base.extend(ClassMethods)
+ base.class_eval do
+ class << self
+ include Observable
+ alias_method :instantiate_without_callbacks, :instantiate
+ alias_method :instantiate, :instantiate_with_callbacks
+ end
+ end
+
+ base.class_eval do
+ alias_method :initialize_without_callbacks, :initialize
+ alias_method :initialize, :initialize_with_callbacks
+
+ alias_method :create_or_update_without_callbacks, :create_or_update
+ alias_method :create_or_update, :create_or_update_with_callbacks
+
+ alias_method :valid_without_callbacks, :valid?
+ alias_method :valid?, :valid_with_callbacks
+
+ alias_method :create_without_callbacks, :create
+ alias_method :create, :create_with_callbacks
+
+ alias_method :update_without_callbacks, :update
+ alias_method :update, :update_with_callbacks
+
+ alias_method :destroy_without_callbacks, :destroy
+ alias_method :destroy, :destroy_with_callbacks
+ end
+
+ CALLBACKS.each { |cb| base.class_eval("def self.#{cb}(*methods) write_inheritable_array(\"#{cb}\", methods) end") }
+ end
+
+ module ClassMethods #:nodoc:
+ def instantiate_with_callbacks(record)
+ object = instantiate_without_callbacks(record)
+ object.callback(:after_find) if object.respond_to_without_attributes?(:after_find)
+ object.callback(:after_initialize) if object.respond_to_without_attributes?(:after_initialize)
+ object
+ end
+ end
+
+ # Is called when the object was instantiated by one of the finders, like Base.find.
+ # def after_find() end
+
+ # Is called after the object has been instantiated by a call to Base.new.
+ # def after_initialize() end
+ def initialize_with_callbacks(attributes = nil) #:nodoc:
+ initialize_without_callbacks(attributes)
+ yield self if block_given?
+ after_initialize if respond_to_without_attributes?(:after_initialize)
+ end
+
+ # Is called _before_ Base.save (regardless of whether it's a create or update save).
+ def before_save() end
+
+ # Is called _after_ Base.save (regardless of whether it's a create or update save).
+ def after_save() end
+ def create_or_update_with_callbacks #:nodoc:
+ callback(:before_save)
+ create_or_update_without_callbacks
+ callback(:after_save)
+ end
+
+ # Is called _before_ Base.save on new objects that haven't been saved yet (no record exists).
+ def before_create() end
+
+ # Is called _after_ Base.save on new objects that haven't been saved yet (no record exists).
+ def after_create() end
+ def create_with_callbacks #:nodoc:
+ callback(:before_create)
+ create_without_callbacks
+ callback(:after_create)
+ end
+
+ # Is called _before_ Base.save on existing objects that has a record.
+ def before_update() end
+
+ # Is called _after_ Base.save on existing objects that has a record.
+ def after_update() end
+
+ def update_with_callbacks #:nodoc:
+ callback(:before_update)
+ update_without_callbacks
+ callback(:after_update)
+ end
+
+ # Is called _before_ Validations.validate (which is part of the Base.save call).
+ def before_validation() end
+
+ # Is called _after_ Validations.validate (which is part of the Base.save call).
+ def after_validation() end
+
+ # Is called _before_ Validations.validate (which is part of the Base.save call) on new objects
+ # that haven't been saved yet (no record exists).
+ def before_validation_on_create() end
+
+ # Is called _after_ Validations.validate (which is part of the Base.save call) on new objects
+ # that haven't been saved yet (no record exists).
+ def after_validation_on_create() end
+
+ # Is called _before_ Validations.validate (which is part of the Base.save call) on
+ # existing objects that has a record.
+ def before_validation_on_update() end
+
+ # Is called _after_ Validations.validate (which is part of the Base.save call) on
+ # existing objects that has a record.
+ def after_validation_on_update() end
+
+ def valid_with_callbacks #:nodoc:
+ callback(:before_validation)
+ if new_record? then callback(:before_validation_on_create) else callback(:before_validation_on_update) end
+
+ result = valid_without_callbacks
+
+ callback(:after_validation)
+ if new_record? then callback(:after_validation_on_create) else callback(:after_validation_on_update) end
+
+ return result
+ end
+
+ # Is called _before_ Base.destroy.
+ def before_destroy() end
+
+ # Is called _after_ Base.destroy (and all the attributes have been frozen).
+ def after_destroy() end
+ def destroy_with_callbacks #:nodoc:
+ callback(:before_destroy)
+ destroy_without_callbacks
+ callback(:after_destroy)
+ end
+
+ def callback(callback_method) #:nodoc:
+ run_callbacks(callback_method)
+ send(callback_method)
+ notify(callback_method)
+ end
+
+ def run_callbacks(callback_method)
+ filters = self.class.read_inheritable_attribute(callback_method.to_s)
+ if filters.nil? then return end
+ filters.each do |filter|
+ if Symbol === filter
+ self.send(filter)
+ elsif String === filter
+ eval(filter, binding)
+ elsif filter_block?(filter)
+ filter.call(self)
+ elsif filter_class?(filter, callback_method)
+ filter.send(callback_method, self)
+ else
+ raise(
+ ActiveRecordError,
+ "Filters need to be either a symbol, string (to be eval'ed), proc/method, or " +
+ "class implementing a static filter method"
+ )
+ end
+ end
+ end
+
+ def filter_block?(filter)
+ filter.respond_to?("call") && (filter.arity == 1 || filter.arity == -1)
+ end
+
+ def filter_class?(filter, callback_method)
+ filter.respond_to?(callback_method)
+ end
+
+ def notify(callback_method) #:nodoc:
+ self.class.changed
+ self.class.notify_observers(callback_method, self)
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
new file mode 100755
index 0000000000..54fdfd25cd
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -0,0 +1,371 @@
+require 'benchmark'
+require 'date'
+
+# Method that requires a library, ensuring that rubygems is loaded
+# This is used in the database adaptors to require DB drivers. Reasons:
+# (1) database drivers are the only third-party library that Rails depend upon
+# (2) they are often installed as gems
+def require_library_or_gem(library_name)
+ begin
+ require library_name
+ rescue LoadError => cannot_require
+ # 1. Requiring the module is unsuccessful, maybe it's a gem and nobody required rubygems yet. Try.
+ begin
+ require 'rubygems'
+ rescue LoadError => rubygems_not_installed
+ raise cannot_require
+ end
+ # 2. Rubygems is installed and loaded. Try to load the library again
+ begin
+ require library_name
+ rescue LoadError => gem_not_installed
+ raise cannot_require
+ end
+ end
+end
+
+module ActiveRecord
+ class Base
+ class ConnectionSpecification #:nodoc:
+ attr_reader :config, :adapter_method
+ def initialize (config, adapter_method)
+ @config, @adapter_method = config, adapter_method
+ end
+ end
+
+ # The class -> [adapter_method, config] map
+ @@defined_connections = {}
+
+ # Establishes the connection to the database. Accepts a hash as input where
+ # the :adapter key must be specified with the name of a database adapter (in lower-case)
+ # example for regular databases (MySQL, Postgresql, etc):
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "mysql",
+ # :host => "localhost",
+ # :username => "myuser",
+ # :password => "mypass",
+ # :database => "somedatabase"
+ # )
+ #
+ # Example for SQLite database:
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "sqlite",
+ # :dbfile => "path/to/dbfile"
+ # )
+ #
+ # Also accepts keys as strings (for parsing from yaml for example):
+ # ActiveRecord::Base.establish_connection(
+ # "adapter" => "sqlite",
+ # "dbfile" => "path/to/dbfile"
+ # )
+ #
+ # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
+ # may be returned on an error.
+ #
+ # == Connecting to another database for a single model
+ #
+ # To support different connections for different classes, you can
+ # simply call establish_connection with the classes you wish to have
+ # different connections for:
+ #
+ # class Courses < ActiveRecord::Base
+ # ...
+ # end
+ #
+ # Courses.establish_connection( ... )
+ def self.establish_connection(spec)
+ if spec.instance_of? ConnectionSpecification
+ @@defined_connections[self] = spec
+ elsif spec.is_a?(Symbol)
+ establish_connection(configurations[spec.to_s])
+ else
+ if spec.nil? then raise AdapterNotSpecified end
+ symbolize_strings_in_hash(spec)
+ unless spec.key?(:adapter) then raise AdapterNotSpecified end
+
+ adapter_method = "#{spec[:adapter]}_connection"
+ unless methods.include?(adapter_method) then raise AdapterNotFound end
+ remove_connection
+ @@defined_connections[self] = ConnectionSpecification.new(spec, adapter_method)
+ end
+ end
+
+ # Locate the connection of the nearest super class. This can be an
+ # active or defined connections: if it is the latter, it will be
+ # opened and set as the active connection for the class it was defined
+ # for (not necessarily the current class).
+ def self.retrieve_connection #:nodoc:
+ klass = self
+ until klass == ActiveRecord::Base.superclass
+ Thread.current['active_connections'] ||= {}
+ if Thread.current['active_connections'][klass]
+ return Thread.current['active_connections'][klass]
+ elsif @@defined_connections[klass]
+ klass.connection = @@defined_connections[klass]
+ return self.connection
+ end
+ klass = klass.superclass
+ end
+ raise ConnectionNotEstablished
+ end
+
+ # Returns true if a connection that's accessible to this class have already been opened.
+ def self.connected?
+ klass = self
+ until klass == ActiveRecord::Base.superclass
+ if Thread.current['active_connections'].is_a?(Hash) && Thread.current['active_connections'][klass]
+ return true
+ else
+ klass = klass.superclass
+ end
+ end
+ return false
+ end
+
+ # Remove the connection for this class. This will close the active
+ # connection and the defined connection (if they exist). The result
+ # can be used as argument for establish_connection, for easy
+ # re-establishing of the connection.
+ def self.remove_connection(klass=self)
+ conn = @@defined_connections[klass]
+ @@defined_connections.delete(klass)
+ Thread.current['active_connections'] ||= {}
+ Thread.current['active_connections'][klass] = nil
+ conn.config if conn
+ end
+
+ # Set the connection for the class.
+ def self.connection=(spec)
+ raise ConnectionNotEstablished unless spec
+ conn = self.send(spec.adapter_method, spec.config)
+ Thread.current['active_connections'] ||= {}
+ Thread.current['active_connections'][self] = conn
+ end
+
+ # Converts all strings in a hash to symbols.
+ def self.symbolize_strings_in_hash(hash)
+ hash.each do |key, value|
+ if key.class == String
+ hash.delete key
+ hash[key.intern] = value
+ end
+ end
+ end
+ end
+
+ module ConnectionAdapters # :nodoc:
+ class Column # :nodoc:
+ attr_reader :name, :default, :type, :limit
+ # The name should contain the name of the column, such as "name" in "name varchar(250)"
+ # The default should contain the type-casted default of the column, such as 1 in "count int(11) DEFAULT 1"
+ # The type parameter should either contain :integer, :float, :datetime, :date, :text, or :string
+ # The sql_type is just used for extracting the limit, such as 10 in "varchar(10)"
+ def initialize(name, default, sql_type = nil)
+ @name, @default, @type = name, default, simplified_type(sql_type)
+ @limit = extract_limit(sql_type) unless sql_type.nil?
+ end
+
+ def default
+ type_cast(@default)
+ end
+
+ def klass
+ case type
+ when :integer then Fixnum
+ when :float then Float
+ when :datetime then Time
+ when :date then Date
+ when :text, :string then String
+ when :boolean then Object
+ end
+ end
+
+ def type_cast(value)
+ if value.nil? then return nil end
+ case type
+ when :string then value
+ when :text then value
+ when :integer then value.to_i
+ when :float then value.to_f
+ when :datetime then string_to_time(value)
+ when :date then string_to_date(value)
+ when :boolean then (value == "t" or value == true ? true : false)
+ else value
+ end
+ end
+
+ def human_name
+ Base.human_attribute_name(@name)
+ end
+
+ private
+ def string_to_date(string)
+ return string if Date === string
+ date_array = ParseDate.parsedate(string)
+ # treat 0000-00-00 as nil
+ Date.new(date_array[0], date_array[1], date_array[2]) rescue nil
+ end
+
+ def string_to_time(string)
+ return string if Time === string
+ time_array = ParseDate.parsedate(string).compact
+ # treat 0000-00-00 00:00:00 as nil
+ Time.local(*time_array) rescue nil
+ end
+
+ def extract_limit(sql_type)
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
+ def simplified_type(field_type)
+ case field_type
+ when /int/i
+ :integer
+ when /float|double|decimal|numeric/i
+ :float
+ when /time/i
+ :datetime
+ when /date/i
+ :date
+ when /(c|b)lob/i, /text/i
+ :text
+ when /char/i, /string/i
+ :string
+ when /boolean/i
+ :boolean
+ end
+ end
+ end
+
+ # All the concrete database adapters follow the interface laid down in this class.
+ # You can use this interface directly by borrowing the database connection from the Base with
+ # Base.connection.
+ class AbstractAdapter
+ @@row_even = true
+
+ include Benchmark
+
+ def initialize(connection, logger = nil) # :nodoc:
+ @connection, @logger = connection, logger
+ @runtime = 0
+ end
+
+ # Returns an array of record hashes with the column names as a keys and fields as values.
+ def select_all(sql, name = nil) end
+
+ # Returns a record hash with the column names as a keys and fields as values.
+ def select_one(sql, name = nil) end
+
+ # Returns an array of column objects for the table specified by +table_name+.
+ def columns(table_name, name = nil) end
+
+ # Returns the last auto-generated ID from the affected table.
+ def insert(sql, name = nil, pk = nil, id_value = nil) end
+
+ # Executes the update statement.
+ def update(sql, name = nil) end
+
+ # Executes the delete statement.
+ def delete(sql, name = nil) end
+
+ def reset_runtime # :nodoc:
+ rt = @runtime
+ @runtime = 0
+ return rt
+ end
+
+ # Wrap a block in a transaction. Returns result of block.
+ def transaction
+ begin
+ if block_given?
+ begin_db_transaction
+ result = yield
+ commit_db_transaction
+ result
+ end
+ rescue Exception => database_transaction_rollback
+ rollback_db_transaction
+ raise
+ end
+ end
+
+ # Begins the transaction (and turns off auto-committing).
+ def begin_db_transaction() end
+
+ # Commits the transaction (and turns on auto-committing).
+ def commit_db_transaction() end
+
+ # Rollsback the transaction (and turns on auto-committing). Must be done if the transaction block
+ # raises an exception or returns false.
+ def rollback_db_transaction() end
+
+ def quote(value, column = nil)
+ case value
+ when String then "'#{quote_string(value)}'" # ' (for ruby-mode)
+ when NilClass then "NULL"
+ when TrueClass then (column && column.type == :boolean ? "'t'" : "1")
+ when FalseClass then (column && column.type == :boolean ? "'f'" : "0")
+ when Float, Fixnum, Bignum, Date then "'#{value.to_s}'"
+ when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
+ else "'#{quote_string(value.to_yaml)}'"
+ end
+ end
+
+ def quote_string(s)
+ s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ def quote_column_name(name)
+ return name
+ end
+
+ # Returns a string of the CREATE TABLE SQL statements for recreating the entire structure of the database.
+ def structure_dump() end
+
+ protected
+ def log(sql, name, connection, &action)
+ begin
+ if @logger.nil?
+ action.call(connection)
+ else
+ result = nil
+ bm = measure { result = action.call(connection) }
+ @runtime += bm.real
+ log_info(sql, name, bm.real)
+ result
+ end
+ rescue => e
+ log_info("#{e.message}: #{sql}", name, 0)
+ raise ActiveRecord::StatementInvalid, "#{e.message}: #{sql}"
+ end
+ end
+
+ def log_info(sql, name, runtime)
+ if @logger.nil? then return end
+
+ @logger.info(
+ format_log_entry(
+ "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})",
+ sql.gsub(/ +/, " ")
+ )
+ )
+ end
+
+ def format_log_entry(message, dump = nil)
+ if @@row_even then
+ @@row_even = false; caller_color = "1;32"; message_color = "4;33"; dump_color = "1;37"
+ else
+ @@row_even = true; caller_color = "1;36"; message_color = "4;35"; dump_color = "0;37"
+ end
+
+ log_entry = " \e[#{message_color}m#{message}\e[m"
+ log_entry << " \e[#{dump_color}m%s\e[m" % dump if dump.kind_of?(String) && !dump.nil?
+ log_entry << " \e[#{dump_color}m%p\e[m" % dump if !dump.kind_of?(String) && !dump.nil?
+ log_entry
+ end
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
new file mode 100755
index 0000000000..5dcdded5bc
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -0,0 +1,131 @@
+require 'active_record/connection_adapters/abstract_adapter'
+require 'parsedate'
+
+module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by all Active Record objects
+ def self.mysql_connection(config) # :nodoc:
+ unless self.class.const_defined?(:Mysql)
+ begin
+ # Only include the MySQL driver if one hasn't already been loaded
+ require_library_or_gem 'mysql'
+ rescue LoadError => cannot_require_mysql
+ # Only use the supplied backup Ruby/MySQL driver if no driver is already in place
+ begin
+ require 'active_record/vendor/mysql'
+ rescue LoadError
+ raise cannot_require_mysql
+ end
+ end
+ end
+ symbolize_strings_in_hash(config)
+ host = config[:host]
+ port = config[:port]
+ socket = config[:socket]
+ username = config[:username] ? config[:username].to_s : 'root'
+ password = config[:password].to_s
+
+ if config.has_key?(:database)
+ database = config[:database]
+ else
+ raise ArgumentError, "No database specified. Missing argument: database."
+ end
+
+ ConnectionAdapters::MysqlAdapter.new(
+ Mysql::real_connect(host, username, password, database, port, socket), logger
+ )
+ end
+ end
+
+ module ConnectionAdapters
+ class MysqlAdapter < AbstractAdapter # :nodoc:
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ sql = "SHOW FIELDS FROM #{table_name}"
+ result = nil
+ log(sql, name, @connection) { |connection| result = connection.query(sql) }
+
+ columns = []
+ result.each { |field| columns << Column.new(field[0], field[4], field[1]) }
+ columns
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ execute(sql, name = nil)
+ return id_value || @connection.insert_id
+ end
+
+ def execute(sql, name = nil)
+ log(sql, name, @connection) { |connection| connection.query(sql) }
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction
+ begin
+ execute "BEGIN"
+ rescue Exception
+ # Transactions aren't supported
+ end
+ end
+
+ def commit_db_transaction
+ begin
+ execute "COMMIT"
+ rescue Exception
+ # Transactions aren't supported
+ end
+ end
+
+ def rollback_db_transaction
+ begin
+ execute "ROLLBACK"
+ rescue Exception
+ # Transactions aren't supported
+ end
+ end
+
+ def quote_column_name(name)
+ return "`#{name}`"
+ end
+
+ def structure_dump
+ select_all("SHOW TABLES").inject("") do |structure, table|
+ structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
+ end
+ end
+
+ def recreate_database(name)
+ drop_database(name)
+ create_database(name)
+ end
+
+ def drop_database(name)
+ execute "DROP DATABASE IF EXISTS #{name}"
+ end
+
+ def create_database(name)
+ execute "CREATE DATABASE #{name}"
+ end
+
+ private
+ def select(sql, name = nil)
+ result = nil
+ log(sql, name, @connection) { |connection| connection.query_with_result = true; result = connection.query(sql) }
+ rows = []
+ all_fields_initialized = result.fetch_fields.inject({}) { |all_fields, f| all_fields[f.name] = nil; all_fields }
+ result.each_hash { |row| rows << all_fields_initialized.dup.update(row) }
+ rows
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
new file mode 100644
index 0000000000..fb54642d3a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -0,0 +1,170 @@
+
+# postgresql_adaptor.rb
+# author: Luke Holden <lholden@cablelan.net>
+# notes: Currently this adaptor does not pass the test_zero_date_fields
+# and test_zero_datetime_fields unit tests in the BasicsTest test
+# group.
+#
+# This is due to the fact that, in postgresql you can not have a
+# totally zero timestamp. Instead null/nil should be used to
+# represent no value.
+#
+
+require 'active_record/connection_adapters/abstract_adapter'
+require 'parsedate'
+
+module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by all Active Record objects
+ def self.postgresql_connection(config) # :nodoc:
+ require_library_or_gem 'postgres' unless self.class.const_defined?(:PGconn)
+ symbolize_strings_in_hash(config)
+ host = config[:host]
+ port = config[:port] || 5432 unless host.nil?
+ username = config[:username].to_s
+ password = config[:password].to_s
+
+ if config.has_key?(:database)
+ database = config[:database]
+ else
+ raise ArgumentError, "No database specified. Missing argument: database."
+ end
+
+ ConnectionAdapters::PostgreSQLAdapter.new(
+ PGconn.connect(host, port, "", "", database, username, password), logger
+ )
+ end
+ end
+
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter # :nodoc:
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ table_structure(table_name).inject([]) do |columns, field|
+ columns << Column.new(field[0], field[2], field[1])
+ columns
+ end
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ execute(sql, name = nil)
+ table = sql.split(" ", 4)[2]
+ return id_value || last_insert_id(table, pk)
+ end
+
+ def execute(sql, name = nil)
+ log(sql, name, @connection) { |connection| connection.query(sql) }
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction() execute "BEGIN" end
+ def commit_db_transaction() execute "COMMIT" end
+ def rollback_db_transaction() execute "ROLLBACK" end
+
+ def quote_column_name(name)
+ return "\"#{name}\""
+ end
+
+ private
+ def last_insert_id(table, column = "id")
+ sequence_name = "#{table}_#{column || 'id'}_seq"
+ @connection.exec("SELECT currval('#{sequence_name}')")[0][0].to_i
+ end
+
+ def select(sql, name = nil)
+ res = nil
+ log(sql, name, @connection) { |connection| res = connection.exec(sql) }
+
+ results = res.result
+ rows = []
+ if results.length > 0
+ fields = res.fields
+ results.each do |row|
+ hashed_row = {}
+ row.each_index { |cel_index| hashed_row[fields[cel_index]] = row[cel_index] }
+ rows << hashed_row
+ end
+ end
+ return rows
+ end
+
+ def split_table_schema(table_name)
+ schema_split = table_name.split('.')
+ schema_name = "public"
+ if schema_split.length > 1
+ schema_name = schema_split.first.strip
+ table_name = schema_split.last.strip
+ end
+ return [schema_name, table_name]
+ end
+
+ def table_structure(table_name)
+ database_name = @connection.db
+ schema_name, table_name = split_table_schema(table_name)
+
+ # Grab a list of all the default values for the columns.
+ sql = "SELECT column_name, column_default, character_maximum_length, data_type "
+ sql << " FROM information_schema.columns "
+ sql << " WHERE table_catalog = '#{database_name}' "
+ sql << " AND table_schema = '#{schema_name}' "
+ sql << " AND table_name = '#{table_name}';"
+
+ column_defaults = nil
+ log(sql, nil, @connection) { |connection| column_defaults = connection.query(sql) }
+ column_defaults.collect do |row|
+ field = row[0]
+ type = type_as_string(row[3], row[2])
+ default = default_value(row[1])
+ length = row[2]
+
+ [field, type, default, length]
+ end
+ end
+
+ def type_as_string(field_type, field_length)
+ type = case field_type
+ when 'numeric', 'real', 'money' then 'float'
+ when 'character varying', 'interval' then 'string'
+ when 'timestamp without time zone' then 'datetime'
+ else field_type
+ end
+
+ size = field_length.nil? ? "" : "(#{field_length})"
+
+ return type + size
+ end
+
+ def default_value(value)
+ # Boolean types
+ return "t" if value =~ /true/i
+ return "f" if value =~ /false/i
+
+ # Char/String type values
+ return $1 if value =~ /^'(.*)'::(bpchar|text|character varying)$/
+
+ # Numeric values
+ return value if value =~ /^[0-9]+(\.[0-9]*)?/
+
+ # Date / Time magic values
+ return Time.now.to_s if value =~ /^\('now'::text\)::(date|timestamp)/
+
+ # Fixed dates / times
+ return $1 if value =~ /^'(.+)'::(date|timestamp)/
+
+ # Anything else is blank, some user type, or some function
+ # and we can't know the value of that, so return nil.
+ return nil
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
new file mode 100644
index 0000000000..1f3845e6a8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -0,0 +1,105 @@
+# sqlite_adapter.rb
+# author: Luke Holden <lholden@cablelan.net>
+
+require 'active_record/connection_adapters/abstract_adapter'
+
+module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by all Active Record objects
+ def self.sqlite_connection(config) # :nodoc:
+ require_library_or_gem('sqlite') unless self.class.const_defined?(:SQLite)
+ symbolize_strings_in_hash(config)
+ unless config.has_key?(:dbfile)
+ raise ArgumentError, "No database file specified. Missing argument: dbfile"
+ end
+
+ db = SQLite::Database.new(config[:dbfile], 0)
+
+ db.show_datatypes = "ON" if !defined? SQLite::Version
+ db.results_as_hash = true if defined? SQLite::Version
+ db.type_translation = false
+
+ ConnectionAdapters::SQLiteAdapter.new(db, logger)
+ end
+ end
+
+ module ConnectionAdapters
+ class SQLiteAdapter < AbstractAdapter # :nodoc:
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ table_structure(table_name).inject([]) do |columns, field|
+ columns << Column.new(field['name'], field['dflt_value'], field['type'])
+ columns
+ end
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ execute(sql, name = nil)
+ id_value || @connection.send( defined?( SQLite::Version ) ? :last_insert_row_id : :last_insert_rowid )
+ end
+
+ def execute(sql, name = nil)
+ log(sql, name, @connection) do |connection|
+ if defined?( SQLite::Version )
+ case sql
+ when "BEGIN" then connection.transaction
+ when "COMMIT" then connection.commit
+ when "ROLLBACK" then connection.rollback
+ else connection.execute(sql)
+ end
+ else
+ connection.execute( sql )
+ end
+ end
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction() execute "BEGIN" end
+ def commit_db_transaction() execute "COMMIT" end
+ def rollback_db_transaction() execute "ROLLBACK" end
+
+ def quote_string(s)
+ SQLite::Database.quote(s)
+ end
+
+ def quote_column_name(name)
+ return "'#{name}'"
+ end
+
+ private
+ def select(sql, name = nil)
+ results = nil
+ log(sql, name, @connection) { |connection| results = connection.execute(sql) }
+
+ rows = []
+
+ results.each do |row|
+ hash_only_row = {}
+ row.each_key do |key|
+ hash_only_row[key.sub(/\w+\./, "")] = row[key] unless key.class == Fixnum
+ end
+ rows << hash_only_row
+ end
+
+ return rows
+ end
+
+ def table_structure(table_name)
+ sql = "PRAGMA table_info(#{table_name});"
+ results = nil
+ log(sql, nil, @connection) { |connection| results = connection.execute(sql) }
+ return results
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb
new file mode 100644
index 0000000000..5cd5f5a0be
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb
@@ -0,0 +1,298 @@
+require 'active_record/connection_adapters/abstract_adapter'
+
+# sqlserver_adapter.rb -- ActiveRecord adapter for Microsoft SQL Server
+#
+# Author: Joey Gibson <joey@joeygibson.com>
+# Date: 10/14/2004
+#
+# REQUIREMENTS:
+#
+# This adapter will ONLY work on Windows systems, since it relies on Win32OLE, which,
+# to my knowledge, is only available on Window.
+#
+# It relies on the ADO support in the DBI module. If you are using the
+# one-click installer of Ruby, then you already have DBI installed, but
+# the ADO module is *NOT* installed. You will need to get the latest
+# source distribution of Ruby-DBI from http://ruby-dbi.sourceforge.net/
+# unzip it, and copy the file src/lib/dbd_ado/ADO.rb to
+# X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb (you will need to create
+# the ADO directory). Once you've installed that file, you are ready to go.
+#
+# This module uses the ADO-style DSNs for connection. For example:
+# "DBI:ADO:Provider=SQLOLEDB;Data Source=(local);Initial Catalog=test;User Id=sa;Password=password;"
+# with User Id replaced with your proper login, and Password with your
+# password.
+#
+# I have tested this code on a WindowsXP Pro SP1 system,
+# ruby 1.8.2 (2004-07-29) [i386-mswin32], SQL Server 2000.
+#
+module ActiveRecord
+ class Base
+ def self.sqlserver_connection(config)
+ require_library_or_gem 'dbi' unless self.class.const_defined?(:DBI)
+ class_eval { include ActiveRecord::SQLServerBaseExtensions }
+
+ symbolize_strings_in_hash(config)
+
+ if config.has_key? :dsn
+ dsn = config[:dsn]
+ else
+ raise ArgumentError, "No DSN specified"
+ end
+
+ conn = DBI.connect(dsn)
+ conn["AutoCommit"] = true
+
+ ConnectionAdapters::SQLServerAdapter.new(conn, logger)
+ end
+ end
+
+ module SQLServerBaseExtensions #:nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def find_first(conditions = nil, orderings = nil)
+ sql = "SELECT TOP 1 * FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+
+ record = connection.select_one(sql, "#{name} Load First")
+ instantiate(record) unless record.nil?
+ end
+
+ def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)
+ sql = "SELECT "
+ sql << "TOP #{limit} " unless limit.nil?
+ sql << " * FROM #{table_name} "
+ sql << "#{joins} " if joins
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+
+ find_by_sql(sql)
+ end
+ end
+
+ def attributes_with_quotes
+ columns_hash = self.class.columns_hash
+
+ attrs = @attributes.dup
+
+ attrs = attrs.reject do |name, value|
+ columns_hash[name].identity
+ end
+
+ attrs.inject({}) do |attrs_quoted, pair|
+ attrs_quoted[pair.first] = quote(pair.last, columns_hash[pair.first])
+ attrs_quoted
+ end
+ end
+ end
+
+ module ConnectionAdapters
+ class ColumnWithIdentity < Column
+ attr_reader :identity
+
+ def initialize(name, default, sql_type = nil, is_identity = false)
+ super(name, default, sql_type)
+
+ @identity = is_identity
+ end
+ end
+
+ class SQLServerAdapter < AbstractAdapter # :nodoc:
+ def quote_column_name(name)
+ " [#{name}] "
+ end
+
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ sql = <<EOL
+SELECT s.name AS TableName, c.id AS ColId, c.name AS ColName, t.name AS ColType, c.length AS Length,
+c.AutoVal AS IsIdentity,
+c.cdefault AS DefaultId, com.text AS DefaultValue
+FROM syscolumns AS c
+JOIN systypes AS t ON (c.xtype = t.xtype AND c.usertype = t.usertype)
+JOIN sysobjects AS s ON (c.id = s.id)
+LEFT OUTER JOIN syscomments AS com ON (c.cdefault = com.id)
+WHERE s.name = '#{table_name}'
+EOL
+
+ columns = []
+
+ log(sql, name, @connection) do |conn|
+ conn.select_all(sql) do |row|
+ default_value = row[:DefaultValue]
+
+ if default_value =~ /null/i
+ default_value = nil
+ else
+ default_value =~ /\(([^)]+)\)/
+ default_value = $1
+ end
+
+ col = ColumnWithIdentity.new(row[:ColName], default_value, "#{row[:ColType]}(#{row[:Length]})", row[:IsIdentity] != nil)
+
+ columns << col
+ end
+ end
+
+ columns
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ begin
+ table_name = get_table_name(sql)
+
+ col = get_identity_column(table_name)
+
+ ii_enabled = false
+
+ if col != nil
+ if query_contains_identity_column(sql, col)
+ begin
+ execute enable_identity_insert(table_name, true)
+ ii_enabled = true
+ rescue Exception => e
+ # Coulnd't turn on IDENTITY_INSERT
+ end
+ end
+ end
+
+ log(sql, name, @connection) do |conn|
+ conn.execute(sql)
+
+ select_one("SELECT @@IDENTITY AS Ident")["Ident"]
+ end
+ ensure
+ if ii_enabled
+ begin
+ execute enable_identity_insert(table_name, false)
+
+ rescue Exception => e
+ # Couldn't turn off IDENTITY_INSERT
+ end
+ end
+ end
+ end
+
+ def execute(sql, name = nil)
+ if sql =~ /^INSERT/i
+ insert(sql, name)
+ else
+ log(sql, name, @connection) do |conn|
+ conn.execute(sql)
+ end
+ end
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction
+ begin
+ @connection["AutoCommit"] = false
+ rescue Exception => e
+ @connection["AutoCommit"] = true
+ end
+ end
+
+ def commit_db_transaction
+ begin
+ @connection.commit
+ ensure
+ @connection["AutoCommit"] = true
+ end
+ end
+
+ def rollback_db_transaction
+ begin
+ @connection.rollback
+ ensure
+ @connection["AutoCommit"] = true
+ end
+ end
+
+ def recreate_database(name)
+ drop_database(name)
+ create_database(name)
+ end
+
+ def drop_database(name)
+ execute "DROP DATABASE #{name}"
+ end
+
+ def create_database(name)
+ execute "CREATE DATABASE #{name}"
+ end
+
+ private
+ def select(sql, name = nil)
+ rows = []
+
+ log(sql, name, @connection) do |conn|
+ conn.select_all(sql) do |row|
+ record = {}
+
+ row.column_names.each do |col|
+ record[col] = row[col]
+ end
+
+ rows << record
+ end
+ end
+
+ rows
+ end
+
+ def enable_identity_insert(table_name, enable = true)
+ if has_identity_column(table_name)
+ "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
+ end
+ end
+
+ def get_table_name(sql)
+ if sql =~ /into\s*([^\s]+)\s*/i or
+ sql =~ /update\s*([^\s]+)\s*/i
+ $1
+ else
+ nil
+ end
+ end
+
+ def has_identity_column(table_name)
+ return get_identity_column(table_name) != nil
+ end
+
+ def get_identity_column(table_name)
+ if not @table_columns
+ @table_columns = {}
+ end
+
+ if @table_columns[table_name] == nil
+ @table_columns[table_name] = columns(table_name)
+ end
+
+ @table_columns[table_name].each do |col|
+ return col.name if col.identity
+ end
+
+ return nil
+ end
+
+ def query_contains_identity_column(sql, col)
+ return sql =~ /[\(\.\,]\s*#{col}/
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/deprecated_associations.rb b/activerecord/lib/active_record/deprecated_associations.rb
new file mode 100644
index 0000000000..481b66bf0a
--- /dev/null
+++ b/activerecord/lib/active_record/deprecated_associations.rb
@@ -0,0 +1,70 @@
+module ActiveRecord
+ module Associations # :nodoc:
+ module ClassMethods
+ def deprecated_collection_count_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{collection_name}_count(force_reload = false)
+ #{collection_name}.reload if force_reload
+ #{collection_name}.size
+ end
+ end_eval
+ end
+
+ def deprecated_add_association_relation(association_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def add_#{association_name}(*items)
+ #{association_name}.concat(items)
+ end
+ end_eval
+ end
+
+ def deprecated_remove_association_relation(association_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def remove_#{association_name}(*items)
+ #{association_name}.delete(items)
+ end
+ end_eval
+ end
+
+ def deprecated_has_collection_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def has_#{collection_name}?(force_reload = false)
+ !#{collection_name}(force_reload).empty?
+ end
+ end_eval
+ end
+
+ def deprecated_find_in_collection_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def find_in_#{collection_name}(association_id)
+ #{collection_name}.find(association_id)
+ end
+ end_eval
+ end
+
+ def deprecated_find_all_in_collection_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def find_all_in_#{collection_name}(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil)
+ #{collection_name}.find_all(runtime_conditions, orderings, limit, joins)
+ end
+ end_eval
+ end
+
+ def deprecated_create_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def create_in_#{collection_name}(attributes = {})
+ #{collection_name}.create(attributes)
+ end
+ end_eval
+ end
+
+ def deprecated_build_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def build_to_#{collection_name}(attributes = {})
+ #{collection_name}.build(attributes)
+ end
+ end_eval
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
new file mode 100755
index 0000000000..f17768e1f2
--- /dev/null
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -0,0 +1,208 @@
+require 'erb'
+require 'yaml'
+require 'active_record/support/class_inheritable_attributes'
+require 'active_record/support/inflector'
+
+# Fixtures are a way of organizing data that you want to test against. You normally have one YAML file with fixture
+# definitions per model. They're just hashes of hashes with the first-level key being the name of fixture (try to keep
+# that name unique across all fixtures in the system for reasons that will follow). The value to that key is a hash
+# where the keys are column names and the values the fixture data you want to insert into it. Example for developers.yml:
+#
+# david:
+# id: 1
+# name: David Heinemeier Hansson
+# birthday: 1979-10-15
+# profession: Systems development
+#
+# steve:
+# id: 2
+# name: Steve Ross Kellock
+# birthday: 1974-09-27
+# profession: guy with keyboard
+#
+# So this YAML file includes two fixtures. T
+#
+# Now when we call <tt>@developers = Fixtures.create_fixtures(".", "developers")</tt> both developers will get inserted into
+# the "developers" table through the active Active Record connection (that must be setup before-hand). And we can now query
+# the fixture data through the <tt>@developers</tt> hash, so <tt>@developers["david"]["name"]</tt> will return
+# <tt>"David Heinemeier Hansson"</tt> and <tt>@developers["david"]["birthday"]</tt> will return <tt>Date.new(1979, 10, 15)</tt>.
+#
+# In addition to getting the raw data, we can also get the Developer object by doing @developers["david"].find. This can then
+# be used for comparison in a unit test. Something like:
+#
+# def test_find
+# assert_equal @developers["david"]["name"], @developers["david"].find.name
+# end
+#
+# Comparing that the data we have on the name is also what the object returns when we ask for it.
+#
+# == Automatic fixture setup and instance variable availability
+#
+# Fixtures can also be automatically instantiated in instance variables relating to their names using the following style:
+#
+# class FixturesTest < Test::Unit::TestCase
+# fixtures :developers # you can add more with comma separation
+#
+# def test_developers
+# assert_equal 3, @developers.size # the container for all the fixtures is automatically set
+# assert_kind_of Developer, @david # works like @developers["david"].find
+# assert_equal "David Heinemeier Hansson", @david.name
+# end
+# end
+class Fixtures < Hash
+ def self.instantiate_fixtures(object, fixtures_directory, *table_names)
+ [ create_fixtures(fixtures_directory, *table_names) ].flatten.each_with_index do |fixtures, idx|
+ object.instance_variable_set "@#{table_names[idx]}", fixtures
+ fixtures.each { |name, fixture| object.instance_variable_set "@#{name}", fixture.find }
+ end
+ end
+
+ def self.create_fixtures(fixtures_directory, *table_names)
+ connection = block_given? ? yield : ActiveRecord::Base.connection
+ old_logger_level = ActiveRecord::Base.logger.level
+
+ begin
+ ActiveRecord::Base.logger.level = Logger::ERROR
+ fixtures = connection.transaction do
+ table_names.flatten.map do |table_name|
+ Fixtures.new(connection, table_name.to_s, File.join(fixtures_directory, table_name.to_s))
+ end
+ end
+ return fixtures.size > 1 ? fixtures : fixtures.first
+ ensure
+ ActiveRecord::Base.logger.level = old_logger_level
+ end
+ end
+
+ def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yml/)
+ @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
+ @class_name = Inflector.classify(@table_name)
+
+ read_fixture_files
+ delete_existing_fixtures
+ insert_fixtures
+ end
+
+ private
+ def read_fixture_files
+ if File.exists?(yaml_file_path)
+ YAML::load(erb_render(IO.read(yaml_file_path))).each do |name, data|
+ self[name] = Fixture.new(data, @class_name)
+ end
+ else
+ Dir.entries(@fixture_path).each do |file|
+ self[file] = Fixture.new(File.join(@fixture_path, file), @class_name) unless file =~ @file_filter
+ end
+ end
+ end
+
+ def delete_existing_fixtures
+ @connection.delete "DELETE FROM #{@table_name}"
+ end
+
+ def insert_fixtures
+ values.each do |fixture|
+ @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
+ end
+ end
+
+ def yaml_file_path
+ @fixture_path + ".yml"
+ end
+
+ def yaml_fixtures_key(path)
+ File.basename(@fixture_path).split(".").first
+ end
+
+ def erb_render(fixture_content)
+ ERB.new(fixture_content).result
+ end
+end
+
+class Fixture #:nodoc:
+ include Enumerable
+ class FixtureError < StandardError; end
+ class FormatError < FixtureError; end
+
+ def initialize(fixture, class_name)
+ @fixture = fixture.is_a?(Hash) ? fixture : read_fixture_file(fixture)
+ @class_name = class_name
+ end
+
+ def each
+ @fixture.each { |item| yield item }
+ end
+
+ def [](key)
+ @fixture[key]
+ end
+
+ def to_hash
+ @fixture
+ end
+
+ def key_list
+ @fixture.keys.join(", ")
+ end
+
+ def value_list
+ @fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
+ end
+
+ def find
+ Object.const_get(@class_name).find(self["id"])
+ end
+
+ private
+ def read_fixture_file(fixture_file_path)
+ IO.readlines(fixture_file_path).inject({}) do |fixture, line|
+ # Mercifully skip empty lines.
+ next if line.empty?
+
+ # Use the same regular expression for attributes as Active Record.
+ unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
+ raise FormatError, "#{path}: fixture format error at '#{line}'. Expecting 'key => value'."
+ end
+ key, value = md.captures
+
+ # Disallow duplicate keys to catch typos.
+ raise FormatError, "#{path}: duplicate '#{key}' in fixture." if fixture[key]
+ fixture[key] = value.strip
+ fixture
+ end
+ end
+end
+
+class Test::Unit::TestCase #:nodoc:
+ include ClassInheritableAttributes
+
+ cattr_accessor :fixture_path
+ cattr_accessor :fixture_table_names
+
+ def self.fixtures(*table_names)
+ write_inheritable_attribute("fixture_table_names", table_names)
+ end
+
+ def setup
+ instantiate_fixtures(*fixture_table_names) if fixture_table_names
+ end
+
+ def self.method_added(method_symbol)
+ if method_symbol == :setup && !method_defined?(:setup_without_fixtures)
+ alias_method :setup_without_fixtures, :setup
+ define_method(:setup) do
+ instantiate_fixtures(*fixture_table_names) if fixture_table_names
+ setup_without_fixtures
+ end
+ end
+ end
+
+ private
+ def instantiate_fixtures(*table_names)
+ Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
+ end
+
+ def fixture_table_names
+ self.class.read_inheritable_attribute("fixture_table_names")
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb
new file mode 100644
index 0000000000..ceba78b043
--- /dev/null
+++ b/activerecord/lib/active_record/observer.rb
@@ -0,0 +1,71 @@
+require 'singleton'
+
+module ActiveRecord
+ # Observers can be programmed to react to lifecycle callbacks in another class to implement
+ # trigger-like behavior outside the original class. This is a great way to reduce the clutter that
+ # normally comes when the model class is burdened with excess responsibility that doesn't pertain to
+ # the core and nature of the class. Example:
+ #
+ # class CommentObserver < ActiveRecord::Observer
+ # def after_save(comment)
+ # Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)
+ # end
+ # end
+ #
+ # This Observer is triggered when a Comment#save is finished and sends a notification about it to the administrator.
+ #
+ # == Observing a class that can't be infered
+ #
+ # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
+ # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
+ # something else than the class you're interested in observing, you can implement the observed_class class method. Like this:
+ #
+ # class AuditObserver < ActiveRecord::Observer
+ # def self.observed_class() Account end
+ # def after_update(account)
+ # AuditTrail.new(account, "UPDATED")
+ # end
+ # end
+ #
+ # == Observing multiple classes at once
+ #
+ # If the audit observer needs to watch more than one kind of object, this can be specified in an array, like this:
+ #
+ # class AuditObserver < ActiveRecord::Observer
+ # def self.observed_class() [ Account, Balance ] end
+ # def after_update(record)
+ # AuditTrail.new(record, "UPDATED")
+ # end
+ # end
+ #
+ # The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
+ #
+ # The observer can implement callback methods for each of the methods described in the Callbacks module.
+ class Observer
+ include Singleton
+
+ def initialize
+ [ observed_class ].flatten.each do |klass|
+ klass.add_observer(self)
+ klass.send(:define_method, :after_find) unless klass.respond_to?(:after_find)
+ end
+ end
+
+ def update(callback_method, object)
+ send(callback_method, object) if respond_to?(callback_method)
+ end
+
+ private
+ def observed_class
+ if self.class.respond_to? "observed_class"
+ self.class.observed_class
+ else
+ Object.const_get(infer_observed_class_name)
+ end
+ end
+
+ def infer_observed_class_name
+ self.class.name.scan(/(.*)Observer/)[0][0]
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
new file mode 100644
index 0000000000..036200a200
--- /dev/null
+++ b/activerecord/lib/active_record/reflection.rb
@@ -0,0 +1,126 @@
+module ActiveRecord
+ module Reflection # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+
+ base.class_eval do
+ class << self
+ alias_method :composed_of_without_reflection, :composed_of
+
+ def composed_of_with_reflection(part_id, options = {})
+ composed_of_without_reflection(part_id, options)
+ write_inheritable_array "aggregations", [ AggregateReflection.new(part_id, options, self) ]
+ end
+
+ alias_method :composed_of, :composed_of_with_reflection
+ end
+ end
+
+ for association_type in %w( belongs_to has_one has_many has_and_belongs_to_many )
+ base.module_eval <<-"end_eval"
+ class << self
+ alias_method :#{association_type}_without_reflection, :#{association_type}
+
+ def #{association_type}_with_reflection(association_id, options = {})
+ #{association_type}_without_reflection(association_id, options)
+ write_inheritable_array "associations", [ AssociationReflection.new(association_id, options, self) ]
+ end
+
+ alias_method :#{association_type}, :#{association_type}_with_reflection
+ end
+ end_eval
+ end
+ end
+
+ # Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
+ # This information can for example be used in a form builder that took an Active Record object and created input
+ # fields for all of the attributes depending on their type and displayed the associations to other objects.
+ #
+ # You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
+ module ClassMethods
+ # Returns an array of AggregateReflection objects for all the aggregations in the class.
+ def reflect_on_all_aggregations
+ read_inheritable_attribute "aggregations"
+ end
+
+ # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
+ # Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
+ def reflect_on_aggregation(aggregation)
+ reflect_on_all_aggregations.find { |reflection| reflection.name == aggregation } unless reflect_on_all_aggregations.nil?
+ end
+
+ # Returns an array of AssociationReflection objects for all the aggregations in the class.
+ def reflect_on_all_associations
+ read_inheritable_attribute "associations"
+ end
+
+ # Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
+ def reflect_on_association(association)
+ reflect_on_all_associations.find { |reflection| reflection.name == association } unless reflect_on_all_associations.nil?
+ end
+ end
+
+
+ # Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of
+ # those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
+ class MacroReflection
+ attr_reader :active_record
+ def initialize(name, options, active_record)
+ @name, @options, @active_record = name, options, active_record
+ end
+
+ # Returns the name of the macro, so it would return :balance for "composed_of :balance, :class_name => 'Money'" or
+ # :clients for "has_many :clients".
+ def name
+ @name
+ end
+
+ # Returns the hash of options used for the macro, so it would return { :class_name => "Money" } for
+ # "composed_of :balance, :class_name => 'Money'" or {} for "has_many :clients".
+ def options
+ @options
+ end
+
+ # Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" would return the Money class and
+ # "has_many :clients" would return the Client class.
+ def klass() end
+
+ def ==(other_aggregation)
+ name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
+ end
+ end
+
+
+ # Holds all the meta-data about an aggregation as it was specified in the Active Record class.
+ class AggregateReflection < MacroReflection #:nodoc:
+ def klass
+ Object.const_get(options[:class_name] || name_to_class_name(name.id2name))
+ end
+
+ private
+ def name_to_class_name(name)
+ name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
+ end
+ end
+
+ # Holds all the meta-data about an association as it was specified in the Active Record class.
+ class AssociationReflection < MacroReflection #:nodoc:
+ def klass
+ active_record.send(:compute_type, (name_to_class_name(name.id2name)))
+ end
+
+ private
+ def name_to_class_name(name)
+ if name !~ /::/
+ class_name = active_record.send(
+ :type_name_with_module,
+ (options[:class_name] || active_record.class_name(active_record.table_name_prefix + name + active_record.table_name_suffix))
+ )
+ end
+ return class_name || name
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/support/class_attribute_accessors.rb b/activerecord/lib/active_record/support/class_attribute_accessors.rb
new file mode 100644
index 0000000000..0e269165a6
--- /dev/null
+++ b/activerecord/lib/active_record/support/class_attribute_accessors.rb
@@ -0,0 +1,43 @@
+# attr_* style accessors for class-variables that can accessed both on an instance and class level.
+class Class #:nodoc:
+ def cattr_reader(*syms)
+ syms.each do |sym|
+ class_eval <<-EOS
+ if ! defined? @@#{sym.id2name}
+ @@#{sym.id2name} = nil
+ end
+
+ def self.#{sym.id2name}
+ @@#{sym}
+ end
+
+ def #{sym.id2name}
+ self.class.#{sym.id2name}
+ end
+ EOS
+ end
+ end
+
+ def cattr_writer(*syms)
+ syms.each do |sym|
+ class_eval <<-EOS
+ if ! defined? @@#{sym.id2name}
+ @@#{sym.id2name} = nil
+ end
+
+ def self.#{sym.id2name}=(obj)
+ @@#{sym.id2name} = obj
+ end
+
+ def #{sym.id2name}=(obj)
+ self.class.#{sym.id2name}=(obj)
+ end
+ EOS
+ end
+ end
+
+ def cattr_accessor(*syms)
+ cattr_reader(*syms)
+ cattr_writer(*syms)
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/support/class_inheritable_attributes.rb b/activerecord/lib/active_record/support/class_inheritable_attributes.rb
new file mode 100644
index 0000000000..ee69646da0
--- /dev/null
+++ b/activerecord/lib/active_record/support/class_inheritable_attributes.rb
@@ -0,0 +1,37 @@
+# Allows attributes to be shared within an inheritance hierarchy, but where each descentent gets a copy of
+# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
+# to, for example, an array without those additions being shared with either their parent, siblings, or
+# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
+module ClassInheritableAttributes # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods # :nodoc:
+ @@classes ||= {}
+
+ def inheritable_attributes
+ @@classes[self] ||= {}
+ end
+
+ def write_inheritable_attribute(key, value)
+ inheritable_attributes[key] = value
+ end
+
+ def write_inheritable_array(key, elements)
+ write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
+ write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
+ end
+
+ def read_inheritable_attribute(key)
+ inheritable_attributes[key]
+ end
+
+ private
+ def inherited(child)
+ @@classes[child] = inheritable_attributes.dup
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/support/clean_logger.rb b/activerecord/lib/active_record/support/clean_logger.rb
new file mode 100644
index 0000000000..1a36562892
--- /dev/null
+++ b/activerecord/lib/active_record/support/clean_logger.rb
@@ -0,0 +1,10 @@
+require 'logger'
+
+class Logger #:nodoc:
+ private
+ remove_const "Format"
+ Format = "%s\n"
+ def format_message(severity, timestamp, msg, progname)
+ Format % [msg]
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/support/inflector.rb b/activerecord/lib/active_record/support/inflector.rb
new file mode 100644
index 0000000000..05ff4fede9
--- /dev/null
+++ b/activerecord/lib/active_record/support/inflector.rb
@@ -0,0 +1,78 @@
+# The Inflector transforms words from singular to plural, class names to table names, modulized class names to ones without,
+# and class names to foreign keys.
+module Inflector
+ extend self
+
+ def pluralize(word)
+ result = word.dup
+ plural_rules.each do |(rule, replacement)|
+ break if result.gsub!(rule, replacement)
+ end
+ return result
+ end
+
+ def singularize(word)
+ result = word.dup
+ singular_rules.each do |(rule, replacement)|
+ break if result.gsub!(rule, replacement)
+ end
+ return result
+ end
+
+ def camelize(lower_case_and_underscored_word)
+ lower_case_and_underscored_word.gsub(/(^|_)(.)/){$2.upcase}
+ end
+
+ def underscore(camel_cased_word)
+ camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
+ end
+
+ def demodulize(class_name_in_module)
+ class_name_in_module.gsub(/^.*::/, '')
+ end
+
+ def tableize(class_name)
+ pluralize(underscore(class_name))
+ end
+
+ def classify(table_name)
+ camelize(singularize(table_name))
+ end
+
+ def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
+ Inflector.underscore(Inflector.demodulize(class_name)) +
+ (separate_class_name_and_id_with_underscore ? "_id" : "id")
+ end
+
+ private
+ def plural_rules #:doc:
+ [
+ [/(x|ch|ss)$/, '\1es'], # search, switch, fix, box, process, address
+ [/([^aeiouy]|qu)y$/, '\1ies'], # query, ability, agency
+ [/(?:([^f])fe|([lr])f)$/, '\1\2ves'], # half, safe, wife
+ [/sis$/, 'ses'], # basis, diagnosis
+ [/([ti])um$/, '\1a'], # datum, medium
+ [/person$/, 'people'], # person, salesperson
+ [/man$/, 'men'], # man, woman, spokesman
+ [/child$/, 'children'], # child
+ [/s$/, 's'], # no change (compatibility)
+ [/$/, 's']
+ ]
+ end
+
+ def singular_rules #:doc:
+ [
+ [/(x|ch|ss)es$/, '\1'],
+ [/([^aeiouy]|qu)ies$/, '\1y'],
+ [/([lr])ves$/, '\1f'],
+ [/([^f])ves$/, '\1fe'],
+ [/(analy|ba|diagno|parenthe|progno|synop|the)ses$/, '\1sis'],
+ [/([ti])a$/, '\1um'],
+ [/people$/, 'person'],
+ [/men$/, 'man'],
+ [/status$/, 'status'],
+ [/children$/, 'child'],
+ [/s$/, '']
+ ]
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
new file mode 100644
index 0000000000..d440e74346
--- /dev/null
+++ b/activerecord/lib/active_record/transactions.rb
@@ -0,0 +1,119 @@
+require 'active_record/vendor/simple.rb'
+require 'thread'
+
+module ActiveRecord
+ module Transactions # :nodoc:
+ TRANSACTION_MUTEX = Mutex.new
+
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+
+ base.class_eval do
+ alias_method :destroy_without_transactions, :destroy
+ alias_method :destroy, :destroy_with_transactions
+
+ alias_method :save_without_transactions, :save
+ alias_method :save, :save_with_transactions
+ end
+ end
+
+ # Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action.
+ # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succedded and
+ # vice versa. Transaction enforce the integrity of the database and guards the data against program errors or database break-downs.
+ # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or
+ # not at all. Example:
+ #
+ # transaction do
+ # david.withdrawal(100)
+ # mary.deposit(100)
+ # end
+ #
+ # This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception.
+ # Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though,
+ # that the objects by default will _not_ have their instance data returned to their pre-transactional state.
+ #
+ # == Transactions are not distributed across database connections
+ #
+ # A transaction acts on a single database connection. If you have
+ # multiple class-specific databases, the transaction will not protect
+ # interaction among them. One workaround is to begin a transaction
+ # on each class whose models you alter:
+ #
+ # Student.transaction do
+ # Course.transaction do
+ # course.enroll(student)
+ # student.units += course.units
+ # end
+ # end
+ #
+ # This is a poor solution, but full distributed transactions are beyond
+ # the scope of Active Record.
+ #
+ # == Save and destroy are automatically wrapped in a transaction
+ #
+ # Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks
+ # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
+ # depend on or you can raise exceptions in the callbacks to rollback.
+ #
+ # == Object-level transactions
+ #
+ # You can enable object-level transactions for Active Record objects, though. You do this by naming the each of the Active Records
+ # that you want to enable object-level transactions for, like this:
+ #
+ # Account.transaction(david, mary) do
+ # david.withdrawal(100)
+ # mary.deposit(100)
+ # end
+ #
+ # If the transaction fails, David and Mary will be returned to their pre-transactional state. No money will have changed hands in
+ # neither object nor database.
+ #
+ # == Exception handling
+ #
+ # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
+ # should be ready to catch those in your application code.
+ #
+ # Tribute: Object-level transactions are implemented by Transaction::Simple by Austin Ziegler.
+ module ClassMethods
+ def transaction(*objects, &block)
+ TRANSACTION_MUTEX.lock
+
+ begin
+ objects.each { |o| o.extend(Transaction::Simple) }
+ objects.each { |o| o.start_transaction }
+
+ result = connection.transaction(&block)
+
+ objects.each { |o| o.commit_transaction }
+ return result
+ rescue Exception => object_transaction_rollback
+ objects.each { |o| o.abort_transaction }
+ raise
+ ensure
+ TRANSACTION_MUTEX.unlock
+ end
+ end
+ end
+
+ def transaction(*objects, &block)
+ self.class.transaction(*objects, &block)
+ end
+
+ def destroy_with_transactions #:nodoc:
+ if TRANSACTION_MUTEX.locked?
+ destroy_without_transactions
+ else
+ transaction { destroy_without_transactions }
+ end
+ end
+
+ def save_with_transactions(perform_validation = true) #:nodoc:
+ if TRANSACTION_MUTEX.locked?
+ save_without_transactions(perform_validation)
+ else
+ transaction { save_without_transactions(perform_validation) }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
new file mode 100755
index 0000000000..07bc7b99b5
--- /dev/null
+++ b/activerecord/lib/active_record/validations.rb
@@ -0,0 +1,205 @@
+module ActiveRecord
+ # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
+ # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
+ # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
+ #
+ # Example:
+ #
+ # class Person < ActiveRecord::Base
+ # protected
+ # def validate
+ # errors.add_on_empty %w( first_name last_name )
+ # errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
+ # end
+ #
+ # def validate_on_create # is only run the first time a new object is saved
+ # unless valid_discount?(membership_discount)
+ # errors.add("membership_discount", "has expired")
+ # end
+ # end
+ #
+ # def validate_on_update
+ # errors.add_to_base("No changes have occured") if unchanged_attributes?
+ # end
+ # end
+ #
+ # person = Person.new("first_name" => "David", "phone_number" => "what?")
+ # person.save # => false (and doesn't do the save)
+ # person.errors.empty? # => false
+ # person.count # => 2
+ # person.errors.on "last_name" # => "can't be empty"
+ # person.errors.on "phone_number" # => "has invalid format"
+ # person.each_full { |msg| puts msg } # => "Last name can't be empty\n" +
+ # "Phone number has invalid format"
+ #
+ # person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
+ # person.save # => true (and person is now saved in the database)
+ #
+ # An +Errors+ object is automatically created for every Active Record.
+ module Validations
+ def self.append_features(base) # :nodoc:
+ super
+
+ base.class_eval do
+ alias_method :save_without_validation, :save
+ alias_method :save, :save_with_validation
+
+ alias_method :update_attribute_without_validation_skipping, :update_attribute
+ alias_method :update_attribute, :update_attribute_with_validation_skipping
+ end
+ end
+
+ # The validation process on save can be skipped by passing false. The regular Base#save method is
+ # replaced with this when the validations module is mixed in, which it is by default.
+ def save_with_validation(perform_validation = true)
+ if perform_validation && valid? || !perform_validation then save_without_validation else false end
+ end
+
+ # Updates a single attribute and saves the record without going through the normal validation procedure.
+ # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
+ # in Base is replaced with this when the validations module is mixed in, which it is by default.
+ def update_attribute_with_validation_skipping(name, value)
+ @attributes[name] = value
+ save(false)
+ end
+
+ # Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false.
+ def valid?
+ errors.clear
+ validate
+ if new_record? then validate_on_create else validate_on_update end
+ errors.empty?
+ end
+
+ # Returns the Errors object that holds all information about attribute error messages.
+ def errors
+ @errors = Errors.new(self) if @errors.nil?
+ @errors
+ end
+
+ protected
+ # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes.
+ def validate #:doc:
+ end
+
+ # Overwrite this method for validation checks used only on creation.
+ def validate_on_create #:doc:
+ end
+
+ # Overwrite this method for validation checks used only on updates.
+ def validate_on_update # :doc:
+ end
+ end
+
+ # Active Record validation is reported to and from this object, which is used by Base#save to
+ # determine whether the object in a valid state to be saved. See usage example in Validations.
+ class Errors
+ def initialize(base) # :nodoc:
+ @base, @errors = base, {}
+ end
+
+ # Adds an error to the base object instead of any particular attribute. This is used
+ # to report errors that doesn't tie to any specific attribute, but rather to the object
+ # as a whole. These error messages doesn't get prepended with any field name when iterating
+ # with each_full, so they should be complete sentences.
+ def add_to_base(msg)
+ add(:base, msg)
+ end
+
+ # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
+ # for the same attribute and ensure that this error object returns false when asked if +empty?+. More than one
+ # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
+ # If no +msg+ is supplied, "invalid" is assumed.
+ def add(attribute, msg = "invalid")
+ @errors[attribute] = [] if @errors[attribute].nil?
+ @errors[attribute] << msg
+ end
+
+ # Will add an error message to each of the attributes in +attributes+ that is empty (defined by <tt>attribute_present?</tt>).
+ def add_on_empty(attributes, msg = "can't be empty")
+ [attributes].flatten.each { |attr| add(attr, msg) unless @base.attribute_present?(attr) }
+ end
+
+ # Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
+ # If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
+ def add_on_boundary_breaking(attributes, range, too_long_msg = "is too long (max is %d characters)", too_short_msg = "is too short (min is %d characters)")
+ for attr in [attributes].flatten
+ add(attr, too_short_msg % range.begin) if @base.attribute_present?(attr) && @base.send(attr).length < range.begin
+ add(attr, too_long_msg % range.end) if @base.attribute_present?(attr) && @base.send(attr).length > range.end
+ end
+ end
+
+ alias :add_on_boundry_breaking :add_on_boundary_breaking
+
+ # Returns true if the specified +attribute+ has errors associated with it.
+ def invalid?(attribute)
+ !@errors[attribute].nil?
+ end
+
+ # * Returns nil, if no errors are associated with the specified +attribute+.
+ # * Returns the error message, if one error is associated with the specified +attribute+.
+ # * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
+ def on(attribute)
+ if @errors[attribute].nil?
+ nil
+ elsif @errors[attribute].length == 1
+ @errors[attribute].first
+ else
+ @errors[attribute]
+ end
+ end
+
+ alias :[] :on
+
+ # Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
+ def on_base
+ on(:base)
+ end
+
+ # Yields each attribute and associated message per error added.
+ def each
+ @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
+ end
+
+ # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
+ # through iteration as "First name can't be empty".
+ def each_full
+ full_messages.each { |msg| yield msg }
+ end
+
+ # Returns all the full error messages in an array.
+ def full_messages
+ full_messages = []
+
+ @errors.each_key do |attr|
+ @errors[attr].each do |msg|
+ if attr == :base
+ full_messages << msg
+ else
+ full_messages << @base.class.human_attribute_name(attr) + " " + msg
+ end
+ end
+ end
+
+ return full_messages
+ end
+
+ # Returns true if no errors have been added.
+ def empty?
+ return @errors.empty?
+ end
+
+ # Removes all the errors that have been added.
+ def clear
+ @errors = {}
+ end
+
+ # Returns the total number of errors added. Two errors added to the same attribute will be counted as such
+ # with this as well.
+ def count
+ error_count = 0
+ @errors.each_value { |attribute| error_count += attribute.length }
+ error_count
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/vendor/mysql.rb b/activerecord/lib/active_record/vendor/mysql.rb
new file mode 100644
index 0000000000..4970f77bd3
--- /dev/null
+++ b/activerecord/lib/active_record/vendor/mysql.rb
@@ -0,0 +1,1117 @@
+# $Id: mysql.rb,v 1.1 2004/02/24 15:42:29 webster132 Exp $
+#
+# Copyright (C) 2003 TOMITA Masahiro
+# tommy@tmtm.org
+#
+
+class Mysql
+
+ VERSION = "4.0-ruby-0.2.4"
+
+ require "socket"
+
+ MAX_PACKET_LENGTH = 256*256*256-1
+ MAX_ALLOWED_PACKET = 1024*1024*1024
+
+ MYSQL_UNIX_ADDR = "/tmp/mysql.sock"
+ MYSQL_PORT = 3306
+ PROTOCOL_VERSION = 10
+
+ # Command
+ COM_SLEEP = 0
+ COM_QUIT = 1
+ COM_INIT_DB = 2
+ COM_QUERY = 3
+ COM_FIELD_LIST = 4
+ COM_CREATE_DB = 5
+ COM_DROP_DB = 6
+ COM_REFRESH = 7
+ COM_SHUTDOWN = 8
+ COM_STATISTICS = 9
+ COM_PROCESS_INFO = 10
+ COM_CONNECT = 11
+ COM_PROCESS_KILL = 12
+ COM_DEBUG = 13
+ COM_PING = 14
+ COM_TIME = 15
+ COM_DELAYED_INSERT = 16
+ COM_CHANGE_USER = 17
+ COM_BINLOG_DUMP = 18
+ COM_TABLE_DUMP = 19
+ COM_CONNECT_OUT = 20
+ COM_REGISTER_SLAVE = 21
+
+ # Client flag
+ CLIENT_LONG_PASSWORD = 1
+ CLIENT_FOUND_ROWS = 1 << 1
+ CLIENT_LONG_FLAG = 1 << 2
+ CLIENT_CONNECT_WITH_DB= 1 << 3
+ CLIENT_NO_SCHEMA = 1 << 4
+ CLIENT_COMPRESS = 1 << 5
+ CLIENT_ODBC = 1 << 6
+ CLIENT_LOCAL_FILES = 1 << 7
+ CLIENT_IGNORE_SPACE = 1 << 8
+ CLIENT_INTERACTIVE = 1 << 10
+ CLIENT_SSL = 1 << 11
+ CLIENT_IGNORE_SIGPIPE = 1 << 12
+ CLIENT_TRANSACTIONS = 1 << 13
+ CLIENT_CAPABILITIES = CLIENT_LONG_PASSWORD|CLIENT_LONG_FLAG|CLIENT_TRANSACTIONS
+
+ # Connection Option
+ OPT_CONNECT_TIMEOUT = 0
+ OPT_COMPRESS = 1
+ OPT_NAMED_PIPE = 2
+ INIT_COMMAND = 3
+ READ_DEFAULT_FILE = 4
+ READ_DEFAULT_GROUP = 5
+ SET_CHARSET_DIR = 6
+ SET_CHARSET_NAME = 7
+ OPT_LOCAL_INFILE = 8
+
+ # Server Status
+ SERVER_STATUS_IN_TRANS = 1
+ SERVER_STATUS_AUTOCOMMIT = 2
+
+ # Refresh parameter
+ REFRESH_GRANT = 1
+ REFRESH_LOG = 2
+ REFRESH_TABLES = 4
+ REFRESH_HOSTS = 8
+ REFRESH_STATUS = 16
+ REFRESH_THREADS = 32
+ REFRESH_SLAVE = 64
+ REFRESH_MASTER = 128
+
+ def initialize(*args)
+ @client_flag = 0
+ @max_allowed_packet = MAX_ALLOWED_PACKET
+ @query_with_result = true
+ @status = :STATUS_READY
+ if args[0] != :INIT then
+ real_connect(*args)
+ end
+ end
+
+ def real_connect(host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil)
+ @server_status = SERVER_STATUS_AUTOCOMMIT
+ if (host == nil or host == "localhost") and defined? UNIXSocket then
+ unix_socket = socket || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_ADDR
+ sock = UNIXSocket::new(unix_socket)
+ @host_info = Error::err(Error::CR_LOCALHOST_CONNECTION)
+ @unix_socket = unix_socket
+ else
+ sock = TCPSocket::new(host, port||ENV["MYSQL_TCP_PORT"]||(Socket::getservbyname("mysql","tcp") rescue MYSQL_PORT))
+ @host_info = sprintf Error::err(Error::CR_TCP_CONNECTION), host
+ end
+ @host = host ? host.dup : nil
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true
+ @net = Net::new sock
+
+ a = read
+ @protocol_version = a.slice!(0)
+ @server_version, a = a.split(/\0/,2)
+ @thread_id, @scramble_buff = a.slice!(0,13).unpack("La8")
+ if a.size >= 2 then
+ @server_capabilities, = a.slice!(0,2).unpack("v")
+ end
+ if a.size >= 16 then
+ @server_language, @server_status = a.unpack("cv")
+ end
+
+ flag = 0 if flag == nil
+ flag |= @client_flag | CLIENT_CAPABILITIES
+ flag |= CLIENT_CONNECT_WITH_DB if db
+ data = Net::int2str(flag)+Net::int3str(@max_allowed_packet)+(user||"")+"\0"+scramble(passwd, @scramble_buff, @protocol_version==9)
+ if db and @server_capabilities & CLIENT_CONNECT_WITH_DB != 0 then
+ data << "\0"+db
+ @db = db.dup
+ end
+ write data
+ read
+ self
+ end
+ alias :connect :real_connect
+
+ def escape_string(str)
+ Mysql::escape_string str
+ end
+ alias :quote :escape_string
+
+ def get_client_info()
+ VERSION
+ end
+ alias :client_info :get_client_info
+
+ def options(option, arg=nil)
+ if option == OPT_LOCAL_INFILE then
+ if arg == false or arg == 0 then
+ @client_flag &= ~CLIENT_LOCAL_FILES
+ else
+ @client_flag |= CLIENT_LOCAL_FILES
+ end
+ else
+ raise "not implemented"
+ end
+ end
+
+ def real_query(query)
+ command COM_QUERY, query, true
+ read_query_result
+ self
+ end
+
+ def use_result()
+ if @status != :STATUS_GET_RESULT then
+ error Error::CR_COMMANDS_OUT_OF_SYNC
+ end
+ res = Result::new self, @fields, @field_count
+ @status = :STATUS_USE_RESULT
+ res
+ end
+
+ def store_result()
+ if @status != :STATUS_GET_RESULT then
+ error Error::CR_COMMANDS_OUT_OF_SYNC
+ end
+ @status = :STATUS_READY
+ data = read_rows @field_count
+ res = Result::new self, @fields, @field_count, data
+ @fields = nil
+ @affected_rows = data.length
+ res
+ end
+
+ def change_user(user="", passwd="", db="")
+ data = user+"\0"+scramble(passwd, @scramble_buff, @protocol_version==9)+"\0"+db
+ command COM_CHANGE_USER, data
+ @user = user
+ @passwd = passwd
+ @db = db
+ end
+
+ def character_set_name()
+ raise "not implemented"
+ end
+
+ def close()
+ @status = :STATUS_READY
+ command COM_QUIT, nil, true
+ @net.close
+ self
+ end
+
+ def create_db(db)
+ command COM_CREATE_DB, db
+ self
+ end
+
+ def drop_db(db)
+ command COM_DROP_DB, db
+ self
+ end
+
+ def dump_debug_info()
+ command COM_DEBUG
+ self
+ end
+
+ def get_host_info()
+ @host_info
+ end
+ alias :host_info :get_host_info
+
+ def get_proto_info()
+ @protocol_version
+ end
+ alias :proto_info :get_proto_info
+
+ def get_server_info()
+ @server_version
+ end
+ alias :server_info :get_server_info
+
+ def kill(id)
+ command COM_PROCESS_KILL, Net::int4str(id)
+ self
+ end
+
+ def list_dbs(db=nil)
+ real_query "show databases #{db}"
+ @status = :STATUS_READY
+ read_rows(1).flatten
+ end
+
+ def list_fields(table, field=nil)
+ command COM_FIELD_LIST, "#{table}\0#{field}", true
+ f = read_rows 6
+ fields = unpack_fields(f, @server_capabilities & CLIENT_LONG_FLAG != 0)
+ res = Result::new self, fields, f.length
+ res.eof = true
+ res
+ end
+
+ def list_processes()
+ data = command COM_PROCESS_INFO
+ @field_count = get_length data
+ fields = read_rows 5
+ @fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0)
+ @status = :STATUS_GET_RESULT
+ store_result
+ end
+
+ def list_tables(table=nil)
+ real_query "show tables #{table}"
+ @status = :STATUS_READY
+ read_rows(1).flatten
+ end
+
+ def ping()
+ command COM_PING
+ self
+ end
+
+ def query(query)
+ real_query query
+ if not @query_with_result then
+ return self
+ end
+ if @field_count == 0 then
+ return nil
+ end
+ store_result
+ end
+
+ def refresh(r)
+ command COM_REFRESH, r.chr
+ self
+ end
+
+ def reload()
+ refresh REFRESH_GRANT
+ self
+ end
+
+ def select_db(db)
+ command COM_INIT_DB, db
+ @db = db
+ self
+ end
+
+ def shutdown()
+ command COM_SHUTDOWN
+ self
+ end
+
+ def stat()
+ command COM_STATISTICS
+ end
+
+ attr_reader :info, :insert_id, :affected_rows, :field_count, :thread_id
+ attr_accessor :query_with_result, :status
+
+ def read_one_row(field_count)
+ data = read
+ return if data[0] == 254 and data.length == 1
+ rec = []
+ field_count.times do
+ len = get_length data
+ if len == nil then
+ rec << len
+ else
+ rec << data.slice!(0,len)
+ end
+ end
+ rec
+ end
+
+ def skip_result()
+ if @status == :STATUS_USE_RESULT then
+ loop do
+ data = read
+ break if data[0] == 254 and data.length == 1
+ end
+ @status = :STATUS_READY
+ end
+ end
+
+ def inspect()
+ "#<#{self.class}>"
+ end
+
+ private
+
+ def read_query_result()
+ data = read
+ @field_count = get_length(data)
+ if @field_count == nil then # LOAD DATA LOCAL INFILE
+ File::open(data) do |f|
+ write f.read
+ end
+ write "" # mark EOF
+ data = read
+ @field_count = get_length(data)
+ end
+ if @field_count == 0 then
+ @affected_rows = get_length(data, true)
+ @insert_id = get_length(data, true)
+ if @server_capabilities & CLIENT_TRANSACTIONS != 0 then
+ a = data.slice!(0,2)
+ @server_status = a[0]+a[1]*256
+ end
+ if data.size > 0 and get_length(data) then
+ @info = data
+ end
+ else
+ @extra_info = get_length(data, true)
+ fields = read_rows 5
+ @fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0)
+ @status = :STATUS_GET_RESULT
+ end
+ self
+ end
+
+ def unpack_fields(data, long_flag_protocol)
+ ret = []
+ data.each do |f|
+ table = org_table = f[0]
+ name = f[1]
+ length = f[2][0]+f[2][1]*256+f[2][2]*256*256
+ type = f[3][0]
+ if long_flag_protocol then
+ flags = f[4][0]+f[4][1]*256
+ decimals = f[4][2]
+ else
+ flags = f[4][0]
+ decimals = f[4][1]
+ end
+ def_value = f[5]
+ max_length = 0
+ ret << Field::new(table, org_table, name, length, type, flags, decimals, def_value, max_length)
+ end
+ ret
+ end
+
+ def read_rows(field_count)
+ ret = []
+ while rec = read_one_row(field_count) do
+ ret << rec
+ end
+ ret
+ end
+
+ def get_length(data, longlong=nil)
+ return if data.length == 0
+ c = data.slice!(0)
+ case c
+ when 251
+ return nil
+ when 252
+ a = data.slice!(0,2)
+ return a[0]+a[1]*256
+ when 253
+ a = data.slice!(0,3)
+ return a[0]+a[1]*256+a[2]*256**2
+ when 254
+ a = data.slice!(0,8)
+ if longlong then
+ return a[0]+a[1]*256+a[2]*256**2+a[3]*256**3+
+ a[4]*256**4+a[5]*256**5+a[6]*256**6+a[7]*256**7
+ else
+ return a[0]+a[1]*256+a[2]*256**2+a[3]*256**3
+ end
+ else
+ c
+ end
+ end
+
+ def command(cmd, arg=nil, skip_check=nil)
+ unless @net then
+ error Error::CR_SERVER_GONE_ERROR
+ end
+ if @status != :STATUS_READY then
+ error Error::CR_COMMANDS_OUT_OF_SYNC
+ end
+ @net.clear
+ write cmd.chr+(arg||"")
+ read unless skip_check
+ end
+
+ def read()
+ unless @net then
+ error Error::CR_SERVER_GONE_ERROR
+ end
+ a = @net.read
+ if a[0] == 255 then
+ if a.length > 3 then
+ @errno = a[1]+a[2]*256
+ @error = a[3 .. -1]
+ else
+ @errno = Error::CR_UNKNOWN_ERROR
+ @error = Error::err @errno
+ end
+ raise Error::new(@errno, @error)
+ end
+ a
+ end
+
+ def write(arg)
+ unless @net then
+ error Error::CR_SERVER_GONE_ERROR
+ end
+ @net.write arg
+ end
+
+ def hash_password(password)
+ nr = 1345345333
+ add = 7
+ nr2 = 0x12345671
+ password.each_byte do |i|
+ next if i == 0x20 or i == 9
+ nr ^= (((nr & 63) + add) * i) + (nr << 8)
+ nr2 += (nr2 << 8) ^ nr
+ add += i
+ end
+ [nr & ((1 << 31) - 1), nr2 & ((1 << 31) - 1)]
+ end
+
+ def scramble(password, message, old_ver)
+ return "" if password == nil or password == ""
+ raise "old version password is not implemented" if old_ver
+ hash_pass = hash_password password
+ hash_message = hash_password message
+ rnd = Random::new hash_pass[0] ^ hash_message[0], hash_pass[1] ^ hash_message[1]
+ to = []
+ 1.upto(message.length) do
+ to << ((rnd.rnd*31)+64).floor
+ end
+ extra = (rnd.rnd*31).floor
+ to.map! do |t| (t ^ extra).chr end
+ to.join
+ end
+
+ def error(errno)
+ @errno = errno
+ @error = Error::err errno
+ raise Error::new(@errno, @error)
+ end
+
+ class Result
+ def initialize(mysql, fields, field_count, data=nil)
+ @handle = mysql
+ @fields = fields
+ @field_count = field_count
+ @data = data
+ @current_field = 0
+ @current_row = 0
+ @eof = false
+ @row_count = 0
+ end
+ attr_accessor :eof
+
+ def data_seek(n)
+ @current_row = n
+ end
+
+ def fetch_field()
+ return if @current_field >= @field_count
+ f = @fields[@current_field]
+ @current_field += 1
+ f
+ end
+
+ def fetch_fields()
+ @fields
+ end
+
+ def fetch_field_direct(n)
+ @fields[n]
+ end
+
+ def fetch_lengths()
+ @data ? @data[@current_row].map{|i| i ? i.length : 0} : @lengths
+ end
+
+ def fetch_row()
+ if @data then
+ if @current_row >= @data.length then
+ @handle.status = :STATUS_READY
+ return
+ end
+ ret = @data[@current_row]
+ @current_row += 1
+ else
+ return if @eof
+ ret = @handle.read_one_row @field_count
+ if ret == nil then
+ @eof = true
+ return
+ end
+ @lengths = ret.map{|i| i ? i.length : 0}
+ @row_count += 1
+ end
+ ret
+ end
+
+ def fetch_hash(with_table=nil)
+ row = fetch_row
+ return if row == nil
+ hash = {}
+ @fields.each_index do |i|
+ f = with_table ? @fields[i].table+"."+@fields[i].name : @fields[i].name
+ hash[f] = row[i]
+ end
+ hash
+ end
+
+ def field_seek(n)
+ @current_field = n
+ end
+
+ def field_tell()
+ @current_field
+ end
+
+ def free()
+ @handle.skip_result
+ @handle = @fields = @data = nil
+ GC::start
+ end
+
+ def num_fields()
+ @field_count
+ end
+
+ def num_rows()
+ @data ? @data.length : @row_count
+ end
+
+ def row_seek(n)
+ @current_row = n
+ end
+
+ def row_tell()
+ @current_row
+ end
+
+ def each()
+ while row = fetch_row do
+ yield row
+ end
+ end
+
+ def each_hash(with_table=nil)
+ while hash = fetch_hash(with_table) do
+ yield hash
+ end
+ end
+
+ def inspect()
+ "#<#{self.class}>"
+ end
+
+ end
+
+ class Field
+ # Field type
+ TYPE_DECIMAL = 0
+ TYPE_TINY = 1
+ TYPE_SHORT = 2
+ TYPE_LONG = 3
+ TYPE_FLOAT = 4
+ TYPE_DOUBLE = 5
+ TYPE_NULL = 6
+ TYPE_TIMESTAMP = 7
+ TYPE_LONGLONG = 8
+ TYPE_INT24 = 9
+ TYPE_DATE = 10
+ TYPE_TIME = 11
+ TYPE_DATETIME = 12
+ TYPE_YEAR = 13
+ TYPE_NEWDATE = 14
+ TYPE_ENUM = 247
+ TYPE_SET = 248
+ TYPE_TINY_BLOB = 249
+ TYPE_MEDIUM_BLOB = 250
+ TYPE_LONG_BLOB = 251
+ TYPE_BLOB = 252
+ TYPE_VAR_STRING = 253
+ TYPE_STRING = 254
+ TYPE_GEOMETRY = 255
+ TYPE_CHAR = TYPE_TINY
+ TYPE_INTERVAL = TYPE_ENUM
+
+ # Flag
+ NOT_NULL_FLAG = 1
+ PRI_KEY_FLAG = 2
+ UNIQUE_KEY_FLAG = 4
+ MULTIPLE_KEY_FLAG = 8
+ BLOB_FLAG = 16
+ UNSIGNED_FLAG = 32
+ ZEROFILL_FLAG = 64
+ BINARY_FLAG = 128
+ ENUM_FLAG = 256
+ AUTO_INCREMENT_FLAG = 512
+ TIMESTAMP_FLAG = 1024
+ SET_FLAG = 2048
+ NUM_FLAG = 32768
+ PART_KEY_FLAG = 16384
+ GROUP_FLAG = 32768
+ UNIQUE_FLAG = 65536
+
+ def initialize(table, org_table, name, length, type, flags, decimals, def_value, max_length)
+ @table = table
+ @org_table = org_table
+ @name = name
+ @length = length
+ @type = type
+ @flags = flags
+ @decimals = decimals
+ @def = def_value
+ @max_length = max_length
+ if (type <= TYPE_INT24 and (type != TYPE_TIMESTAMP or length == 14 or length == 8)) or type == TYPE_YEAR then
+ @flags |= NUM_FLAG
+ end
+ end
+ attr_reader :table, :org_table, :name, :length, :type, :flags, :decimals, :def, :max_length
+
+ def inspect()
+ "#<#{self.class}:#{@name}>"
+ end
+ end
+
+ class Error < StandardError
+ # Server Error
+ ER_HASHCHK = 1000
+ ER_NISAMCHK = 1001
+ ER_NO = 1002
+ ER_YES = 1003
+ ER_CANT_CREATE_FILE = 1004
+ ER_CANT_CREATE_TABLE = 1005
+ ER_CANT_CREATE_DB = 1006
+ ER_DB_CREATE_EXISTS = 1007
+ ER_DB_DROP_EXISTS = 1008
+ ER_DB_DROP_DELETE = 1009
+ ER_DB_DROP_RMDIR = 1010
+ ER_CANT_DELETE_FILE = 1011
+ ER_CANT_FIND_SYSTEM_REC = 1012
+ ER_CANT_GET_STAT = 1013
+ ER_CANT_GET_WD = 1014
+ ER_CANT_LOCK = 1015
+ ER_CANT_OPEN_FILE = 1016
+ ER_FILE_NOT_FOUND = 1017
+ ER_CANT_READ_DIR = 1018
+ ER_CANT_SET_WD = 1019
+ ER_CHECKREAD = 1020
+ ER_DISK_FULL = 1021
+ ER_DUP_KEY = 1022
+ ER_ERROR_ON_CLOSE = 1023
+ ER_ERROR_ON_READ = 1024
+ ER_ERROR_ON_RENAME = 1025
+ ER_ERROR_ON_WRITE = 1026
+ ER_FILE_USED = 1027
+ ER_FILSORT_ABORT = 1028
+ ER_FORM_NOT_FOUND = 1029
+ ER_GET_ERRNO = 1030
+ ER_ILLEGAL_HA = 1031
+ ER_KEY_NOT_FOUND = 1032
+ ER_NOT_FORM_FILE = 1033
+ ER_NOT_KEYFILE = 1034
+ ER_OLD_KEYFILE = 1035
+ ER_OPEN_AS_READONLY = 1036
+ ER_OUTOFMEMORY = 1037
+ ER_OUT_OF_SORTMEMORY = 1038
+ ER_UNEXPECTED_EOF = 1039
+ ER_CON_COUNT_ERROR = 1040
+ ER_OUT_OF_RESOURCES = 1041
+ ER_BAD_HOST_ERROR = 1042
+ ER_HANDSHAKE_ERROR = 1043
+ ER_DBACCESS_DENIED_ERROR = 1044
+ ER_ACCESS_DENIED_ERROR = 1045
+ ER_NO_DB_ERROR = 1046
+ ER_UNKNOWN_COM_ERROR = 1047
+ ER_BAD_NULL_ERROR = 1048
+ ER_BAD_DB_ERROR = 1049
+ ER_TABLE_EXISTS_ERROR = 1050
+ ER_BAD_TABLE_ERROR = 1051
+ ER_NON_UNIQ_ERROR = 1052
+ ER_SERVER_SHUTDOWN = 1053
+ ER_BAD_FIELD_ERROR = 1054
+ ER_WRONG_FIELD_WITH_GROUP = 1055
+ ER_WRONG_GROUP_FIELD = 1056
+ ER_WRONG_SUM_SELECT = 1057
+ ER_WRONG_VALUE_COUNT = 1058
+ ER_TOO_LONG_IDENT = 1059
+ ER_DUP_FIELDNAME = 1060
+ ER_DUP_KEYNAME = 1061
+ ER_DUP_ENTRY = 1062
+ ER_WRONG_FIELD_SPEC = 1063
+ ER_PARSE_ERROR = 1064
+ ER_EMPTY_QUERY = 1065
+ ER_NONUNIQ_TABLE = 1066
+ ER_INVALID_DEFAULT = 1067
+ ER_MULTIPLE_PRI_KEY = 1068
+ ER_TOO_MANY_KEYS = 1069
+ ER_TOO_MANY_KEY_PARTS = 1070
+ ER_TOO_LONG_KEY = 1071
+ ER_KEY_COLUMN_DOES_NOT_EXITS = 1072
+ ER_BLOB_USED_AS_KEY = 1073
+ ER_TOO_BIG_FIELDLENGTH = 1074
+ ER_WRONG_AUTO_KEY = 1075
+ ER_READY = 1076
+ ER_NORMAL_SHUTDOWN = 1077
+ ER_GOT_SIGNAL = 1078
+ ER_SHUTDOWN_COMPLETE = 1079
+ ER_FORCING_CLOSE = 1080
+ ER_IPSOCK_ERROR = 1081
+ ER_NO_SUCH_INDEX = 1082
+ ER_WRONG_FIELD_TERMINATORS = 1083
+ ER_BLOBS_AND_NO_TERMINATED = 1084
+ ER_TEXTFILE_NOT_READABLE = 1085
+ ER_FILE_EXISTS_ERROR = 1086
+ ER_LOAD_INFO = 1087
+ ER_ALTER_INFO = 1088
+ ER_WRONG_SUB_KEY = 1089
+ ER_CANT_REMOVE_ALL_FIELDS = 1090
+ ER_CANT_DROP_FIELD_OR_KEY = 1091
+ ER_INSERT_INFO = 1092
+ ER_INSERT_TABLE_USED = 1093
+ ER_NO_SUCH_THREAD = 1094
+ ER_KILL_DENIED_ERROR = 1095
+ ER_NO_TABLES_USED = 1096
+ ER_TOO_BIG_SET = 1097
+ ER_NO_UNIQUE_LOGFILE = 1098
+ ER_TABLE_NOT_LOCKED_FOR_WRITE = 1099
+ ER_TABLE_NOT_LOCKED = 1100
+ ER_BLOB_CANT_HAVE_DEFAULT = 1101
+ ER_WRONG_DB_NAME = 1102
+ ER_WRONG_TABLE_NAME = 1103
+ ER_TOO_BIG_SELECT = 1104
+ ER_UNKNOWN_ERROR = 1105
+ ER_UNKNOWN_PROCEDURE = 1106
+ ER_WRONG_PARAMCOUNT_TO_PROCEDURE = 1107
+ ER_WRONG_PARAMETERS_TO_PROCEDURE = 1108
+ ER_UNKNOWN_TABLE = 1109
+ ER_FIELD_SPECIFIED_TWICE = 1110
+ ER_INVALID_GROUP_FUNC_USE = 1111
+ ER_UNSUPPORTED_EXTENSION = 1112
+ ER_TABLE_MUST_HAVE_COLUMNS = 1113
+ ER_RECORD_FILE_FULL = 1114
+ ER_UNKNOWN_CHARACTER_SET = 1115
+ ER_TOO_MANY_TABLES = 1116
+ ER_TOO_MANY_FIELDS = 1117
+ ER_TOO_BIG_ROWSIZE = 1118
+ ER_STACK_OVERRUN = 1119
+ ER_WRONG_OUTER_JOIN = 1120
+ ER_NULL_COLUMN_IN_INDEX = 1121
+ ER_CANT_FIND_UDF = 1122
+ ER_CANT_INITIALIZE_UDF = 1123
+ ER_UDF_NO_PATHS = 1124
+ ER_UDF_EXISTS = 1125
+ ER_CANT_OPEN_LIBRARY = 1126
+ ER_CANT_FIND_DL_ENTRY = 1127
+ ER_FUNCTION_NOT_DEFINED = 1128
+ ER_HOST_IS_BLOCKED = 1129
+ ER_HOST_NOT_PRIVILEGED = 1130
+ ER_PASSWORD_ANONYMOUS_USER = 1131
+ ER_PASSWORD_NOT_ALLOWED = 1132
+ ER_PASSWORD_NO_MATCH = 1133
+ ER_UPDATE_INFO = 1134
+ ER_CANT_CREATE_THREAD = 1135
+ ER_WRONG_VALUE_COUNT_ON_ROW = 1136
+ ER_CANT_REOPEN_TABLE = 1137
+ ER_INVALID_USE_OF_NULL = 1138
+ ER_REGEXP_ERROR = 1139
+ ER_MIX_OF_GROUP_FUNC_AND_FIELDS = 1140
+ ER_NONEXISTING_GRANT = 1141
+ ER_TABLEACCESS_DENIED_ERROR = 1142
+ ER_COLUMNACCESS_DENIED_ERROR = 1143
+ ER_ILLEGAL_GRANT_FOR_TABLE = 1144
+ ER_GRANT_WRONG_HOST_OR_USER = 1145
+ ER_NO_SUCH_TABLE = 1146
+ ER_NONEXISTING_TABLE_GRANT = 1147
+ ER_NOT_ALLOWED_COMMAND = 1148
+ ER_SYNTAX_ERROR = 1149
+ ER_DELAYED_CANT_CHANGE_LOCK = 1150
+ ER_TOO_MANY_DELAYED_THREADS = 1151
+ ER_ABORTING_CONNECTION = 1152
+ ER_NET_PACKET_TOO_LARGE = 1153
+ ER_NET_READ_ERROR_FROM_PIPE = 1154
+ ER_NET_FCNTL_ERROR = 1155
+ ER_NET_PACKETS_OUT_OF_ORDER = 1156
+ ER_NET_UNCOMPRESS_ERROR = 1157
+ ER_NET_READ_ERROR = 1158
+ ER_NET_READ_INTERRUPTED = 1159
+ ER_NET_ERROR_ON_WRITE = 1160
+ ER_NET_WRITE_INTERRUPTED = 1161
+ ER_TOO_LONG_STRING = 1162
+ ER_TABLE_CANT_HANDLE_BLOB = 1163
+ ER_TABLE_CANT_HANDLE_AUTO_INCREMENT = 1164
+ ER_DELAYED_INSERT_TABLE_LOCKED = 1165
+ ER_WRONG_COLUMN_NAME = 1166
+ ER_WRONG_KEY_COLUMN = 1167
+ ER_WRONG_MRG_TABLE = 1168
+ ER_DUP_UNIQUE = 1169
+ ER_BLOB_KEY_WITHOUT_LENGTH = 1170
+ ER_PRIMARY_CANT_HAVE_NULL = 1171
+ ER_TOO_MANY_ROWS = 1172
+ ER_REQUIRES_PRIMARY_KEY = 1173
+ ER_NO_RAID_COMPILED = 1174
+ ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE = 1175
+ ER_KEY_DOES_NOT_EXITS = 1176
+ ER_CHECK_NO_SUCH_TABLE = 1177
+ ER_CHECK_NOT_IMPLEMENTED = 1178
+ ER_CANT_DO_THIS_DURING_AN_TRANSACTION = 1179
+ ER_ERROR_DURING_COMMIT = 1180
+ ER_ERROR_DURING_ROLLBACK = 1181
+ ER_ERROR_DURING_FLUSH_LOGS = 1182
+ ER_ERROR_DURING_CHECKPOINT = 1183
+ ER_NEW_ABORTING_CONNECTION = 1184
+ ER_DUMP_NOT_IMPLEMENTED = 1185
+ ER_FLUSH_MASTER_BINLOG_CLOSED = 1186
+ ER_INDEX_REBUILD = 1187
+ ER_MASTER = 1188
+ ER_MASTER_NET_READ = 1189
+ ER_MASTER_NET_WRITE = 1190
+ ER_FT_MATCHING_KEY_NOT_FOUND = 1191
+ ER_LOCK_OR_ACTIVE_TRANSACTION = 1192
+ ER_UNKNOWN_SYSTEM_VARIABLE = 1193
+ ER_CRASHED_ON_USAGE = 1194
+ ER_CRASHED_ON_REPAIR = 1195
+ ER_WARNING_NOT_COMPLETE_ROLLBACK = 1196
+ ER_TRANS_CACHE_FULL = 1197
+ ER_SLAVE_MUST_STOP = 1198
+ ER_SLAVE_NOT_RUNNING = 1199
+ ER_BAD_SLAVE = 1200
+ ER_MASTER_INFO = 1201
+ ER_SLAVE_THREAD = 1202
+ ER_TOO_MANY_USER_CONNECTIONS = 1203
+ ER_SET_CONSTANTS_ONLY = 1204
+ ER_LOCK_WAIT_TIMEOUT = 1205
+ ER_LOCK_TABLE_FULL = 1206
+ ER_READ_ONLY_TRANSACTION = 1207
+ ER_DROP_DB_WITH_READ_LOCK = 1208
+ ER_CREATE_DB_WITH_READ_LOCK = 1209
+ ER_WRONG_ARGUMENTS = 1210
+ ER_NO_PERMISSION_TO_CREATE_USER = 1211
+ ER_UNION_TABLES_IN_DIFFERENT_DIR = 1212
+ ER_LOCK_DEADLOCK = 1213
+ ER_TABLE_CANT_HANDLE_FULLTEXT = 1214
+ ER_CANNOT_ADD_FOREIGN = 1215
+ ER_NO_REFERENCED_ROW = 1216
+ ER_ROW_IS_REFERENCED = 1217
+ ER_CONNECT_TO_MASTER = 1218
+ ER_QUERY_ON_MASTER = 1219
+ ER_ERROR_WHEN_EXECUTING_COMMAND = 1220
+ ER_WRONG_USAGE = 1221
+ ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT = 1222
+ ER_CANT_UPDATE_WITH_READLOCK = 1223
+ ER_MIXING_NOT_ALLOWED = 1224
+ ER_DUP_ARGUMENT = 1225
+ ER_USER_LIMIT_REACHED = 1226
+ ER_SPECIFIC_ACCESS_DENIED_ERROR = 1227
+ ER_LOCAL_VARIABLE = 1228
+ ER_GLOBAL_VARIABLE = 1229
+ ER_NO_DEFAULT = 1230
+ ER_WRONG_VALUE_FOR_VAR = 1231
+ ER_WRONG_TYPE_FOR_VAR = 1232
+ ER_VAR_CANT_BE_READ = 1233
+ ER_CANT_USE_OPTION_HERE = 1234
+ ER_NOT_SUPPORTED_YET = 1235
+ ER_MASTER_FATAL_ERROR_READING_BINLOG = 1236
+ ER_SLAVE_IGNORED_TABLE = 1237
+ ER_ERROR_MESSAGES = 238
+
+ # Client Error
+ CR_MIN_ERROR = 2000
+ CR_MAX_ERROR = 2999
+ CR_UNKNOWN_ERROR = 2000
+ CR_SOCKET_CREATE_ERROR = 2001
+ CR_CONNECTION_ERROR = 2002
+ CR_CONN_HOST_ERROR = 2003
+ CR_IPSOCK_ERROR = 2004
+ CR_UNKNOWN_HOST = 2005
+ CR_SERVER_GONE_ERROR = 2006
+ CR_VERSION_ERROR = 2007
+ CR_OUT_OF_MEMORY = 2008
+ CR_WRONG_HOST_INFO = 2009
+ CR_LOCALHOST_CONNECTION = 2010
+ CR_TCP_CONNECTION = 2011
+ CR_SERVER_HANDSHAKE_ERR = 2012
+ CR_SERVER_LOST = 2013
+ CR_COMMANDS_OUT_OF_SYNC = 2014
+ CR_NAMEDPIPE_CONNECTION = 2015
+ CR_NAMEDPIPEWAIT_ERROR = 2016
+ CR_NAMEDPIPEOPEN_ERROR = 2017
+ CR_NAMEDPIPESETSTATE_ERROR = 2018
+ CR_CANT_READ_CHARSET = 2019
+ CR_NET_PACKET_TOO_LARGE = 2020
+ CR_EMBEDDED_CONNECTION = 2021
+ CR_PROBE_SLAVE_STATUS = 2022
+ CR_PROBE_SLAVE_HOSTS = 2023
+ CR_PROBE_SLAVE_CONNECT = 2024
+ CR_PROBE_MASTER_CONNECT = 2025
+ CR_SSL_CONNECTION_ERROR = 2026
+ CR_MALFORMED_PACKET = 2027
+
+ CLIENT_ERRORS = [
+ "Unknown MySQL error",
+ "Can't create UNIX socket (%d)",
+ "Can't connect to local MySQL server through socket '%-.64s' (%d)",
+ "Can't connect to MySQL server on '%-.64s' (%d)",
+ "Can't create TCP/IP socket (%d)",
+ "Unknown MySQL Server Host '%-.64s' (%d)",
+ "MySQL server has gone away",
+ "Protocol mismatch. Server Version = %d Client Version = %d",
+ "MySQL client run out of memory",
+ "Wrong host info",
+ "Localhost via UNIX socket",
+ "%-.64s via TCP/IP",
+ "Error in server handshake",
+ "Lost connection to MySQL server during query",
+ "Commands out of sync; You can't run this command now",
+ "%-.64s via named pipe",
+ "Can't wait for named pipe to host: %-.64s pipe: %-.32s (%lu)",
+ "Can't open named pipe to host: %-.64s pipe: %-.32s (%lu)",
+ "Can't set state of named pipe to host: %-.64s pipe: %-.32s (%lu)",
+ "Can't initialize character set %-.64s (path: %-.64s)",
+ "Got packet bigger than 'max_allowed_packet'",
+ "Embedded server",
+ "Error on SHOW SLAVE STATUS:",
+ "Error on SHOW SLAVE HOSTS:",
+ "Error connecting to slave:",
+ "Error connecting to master:",
+ "SSL connection error",
+ "Malformed packet"
+ ]
+
+ def initialize(errno, error)
+ @errno = errno
+ @error = error
+ super error
+ end
+ attr_reader :errno, :error
+
+ def Error::err(errno)
+ CLIENT_ERRORS[errno - Error::CR_MIN_ERROR]
+ end
+ end
+
+ class Net
+ def initialize(sock)
+ @sock = sock
+ @pkt_nr = 0
+ end
+
+ def clear()
+ @pkt_nr = 0
+ end
+
+ def read()
+ buf = []
+ len = nil
+ @sock.sync = false
+ while len == nil or len == MAX_PACKET_LENGTH do
+ a = @sock.read(4)
+ len = a[0]+a[1]*256+a[2]*256*256
+ pkt_nr = a[3]
+ if @pkt_nr != pkt_nr then
+ raise "Packets out of order: #{@pkt_nr}<>#{pkt_nr}"
+ end
+ @pkt_nr = @pkt_nr + 1 & 0xff
+ buf << @sock.read(len)
+ end
+ @sock.sync = true
+ buf.join
+ end
+
+ def write(data)
+ if data.is_a? Array then
+ data = data.join
+ end
+ @sock.sync = false
+ ptr = 0
+ while data.length >= MAX_PACKET_LENGTH do
+ @sock.write Net::int3str(MAX_PACKET_LENGTH)+@pkt_nr.chr+data[ptr, MAX_PACKET_LENGTH]
+ @pkt_nr = @pkt_nr + 1 & 0xff
+ ptr += MAX_PACKET_LENGTH
+ end
+ @sock.write Net::int3str(data.length-ptr)+@pkt_nr.chr+data[ptr .. -1]
+ @pkt_nr = @pkt_nr + 1 & 0xff
+ @sock.sync = true
+ @sock.flush
+ end
+
+ def close()
+ @sock.close
+ end
+
+ def Net::int2str(n)
+ [n].pack("v")
+ end
+
+ def Net::int3str(n)
+ [n%256, n>>8].pack("cv")
+ end
+
+ def Net::int4str(n)
+ [n].pack("V")
+ end
+
+ end
+
+ class Random
+ def initialize(seed1, seed2)
+ @max_value = 0x3FFFFFFF
+ @seed1 = seed1 % @max_value
+ @seed2 = seed2 % @max_value
+ end
+
+ def rnd()
+ @seed1 = (@seed1*3+@seed2) % @max_value
+ @seed2 = (@seed1+@seed2+33) % @max_value
+ @seed1.to_f / @max_value
+ end
+ end
+
+end
+
+class << Mysql
+ def init()
+ Mysql::new :INIT
+ end
+
+ def real_connect(*args)
+ Mysql::new(*args)
+ end
+ alias :connect :real_connect
+
+ def escape_string(str)
+ str.gsub(/([\0\n\r\032\'\"\\])/) do
+ case $1
+ when "\0" then "\\0"
+ when "\n" then "\\n"
+ when "\r" then "\\r"
+ when "\032" then "\Z"
+ else "\\"+$1
+ end
+ end
+ end
+ alias :quote :escape_string
+
+ def get_client_info()
+ Mysql::VERSION
+ end
+ alias :client_info :get_client_info
+
+ def debug(str)
+ raise "not implemented"
+ end
+end
+
+#
+# for compatibility
+#
+
+MysqlRes = Mysql::Result
+MysqlField = Mysql::Field
+MysqlError = Mysql::Error
diff --git a/activerecord/lib/active_record/vendor/simple.rb b/activerecord/lib/active_record/vendor/simple.rb
new file mode 100644
index 0000000000..1bd332c882
--- /dev/null
+++ b/activerecord/lib/active_record/vendor/simple.rb
@@ -0,0 +1,702 @@
+# :title: Transaction::Simple
+#
+# == Licence
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#--
+# Transaction::Simple
+# Simple object transaction support for Ruby
+# Version 1.11
+#
+# Copyright (c) 2003 Austin Ziegler
+#
+# $Id: simple.rb,v 1.2 2004/08/20 13:56:37 webster132 Exp $
+#
+# ==========================================================================
+# Revision History ::
+# YYYY.MM.DD Change ID Developer
+# Description
+# --------------------------------------------------------------------------
+# 2003.07.29 Austin Ziegler
+# Added debugging capabilities and VERSION string.
+# 2003.08.21 Austin Ziegler
+# Added named transactions.
+#
+# ==========================================================================
+#++
+require 'thread'
+
+ # The "Transaction" namespace can be used for additional transactional
+ # support objects and modules.
+module Transaction
+
+ # A standard exception for transactional errors.
+ class TransactionError < StandardError; end
+ # A standard exception for transactional errors involving the acquisition
+ # of locks for Transaction::Simple::ThreadSafe.
+ class TransactionThreadError < StandardError; end
+
+ # = Transaction::Simple for Ruby
+ # Simple object transaction support for Ruby
+ #
+ # == Introduction
+ #
+ # Transaction::Simple provides a generic way to add active transactional
+ # support to objects. The transaction methods added by this module will
+ # work with most objects, excluding those that cannot be <i>Marshal</i>ed
+ # (bindings, procedure objects, IO instances, or singleton objects).
+ #
+ # The transactions supported by Transaction::Simple are not backed
+ # transactions; that is, they have nothing to do with any sort of data
+ # store. They are "live" transactions occurring in memory and in the
+ # object itself. This is to allow "test" changes to be made to an object
+ # before making the changes permanent.
+ #
+ # Transaction::Simple can handle an "infinite" number of transactional
+ # levels (limited only by memory). If I open two transactions, commit the
+ # first, but abort the second, the object will revert to the original
+ # version.
+ #
+ # Transaction::Simple supports "named" transactions, so that multiple
+ # levels of transactions can be committed, aborted, or rewound by
+ # referring to the appropriate name of the transaction. Names may be any
+ # object *except* +nil+.
+ #
+ # Copyright:: Copyright © 2003 by Austin Ziegler
+ # Version:: 1.1
+ # Licence:: MIT-Style
+ #
+ # Thanks to David Black for help with the initial concept that led to this
+ # library.
+ #
+ # == Usage
+ # include 'transaction/simple'
+ #
+ # v = "Hello, you." # => "Hello, you."
+ # v.extend(Transaction::Simple) # => "Hello, you."
+ #
+ # v.start_transaction # => ... (a Marshal string)
+ # v.transaction_open? # => true
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ #
+ # v.rewind_transaction # => "Hello, you."
+ # v.transaction_open? # => true
+ #
+ # v.gsub!(/you/, "HAL") # => "Hello, HAL."
+ # v.abort_transaction # => "Hello, you."
+ # v.transaction_open? # => false
+ #
+ # v.start_transaction # => ... (a Marshal string)
+ # v.start_transaction # => ... (a Marshal string)
+ #
+ # v.transaction_open? # => true
+ # v.gsub!(/you/, "HAL") # => "Hello, HAL."
+ #
+ # v.commit_transaction # => "Hello, HAL."
+ # v.transaction_open? # => true
+ # v.abort_transaction # => "Hello, you."
+ # v.transaction_open? # => false
+ #
+ # == Named Transaction Usage
+ # v = "Hello, you." # => "Hello, you."
+ # v.extend(Transaction::Simple) # => "Hello, you."
+ #
+ # v.start_transaction(:first) # => ... (a Marshal string)
+ # v.transaction_open? # => true
+ # v.transaction_open?(:first) # => true
+ # v.transaction_open?(:second) # => false
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ #
+ # v.start_transaction(:second) # => ... (a Marshal string)
+ # v.gsub!(/world/, "HAL") # => "Hello, HAL."
+ # v.rewind_transaction(:first) # => "Hello, you."
+ # v.transaction_open? # => true
+ # v.transaction_open?(:first) # => true
+ # v.transaction_open?(:second) # => false
+ #
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ # v.start_transaction(:second) # => ... (a Marshal string)
+ # v.gsub!(/world/, "HAL") # => "Hello, HAL."
+ # v.transaction_name # => :second
+ # v.abort_transaction(:first) # => "Hello, you."
+ # v.transaction_open? # => false
+ #
+ # v.start_transaction(:first) # => ... (a Marshal string)
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ # v.start_transaction(:second) # => ... (a Marshal string)
+ # v.gsub!(/world/, "HAL") # => "Hello, HAL."
+ #
+ # v.commit_transaction(:first) # => "Hello, HAL."
+ # v.transaction_open? # => false
+ #
+ # == Contraindications
+ #
+ # While Transaction::Simple is very useful, it has some severe limitations
+ # that must be understood. Transaction::Simple:
+ #
+ # * uses Marshal. Thus, any object which cannot be <i>Marshal</i>ed cannot
+ # use Transaction::Simple.
+ # * does not manage resources. Resources external to the object and its
+ # instance variables are not managed at all. However, all instance
+ # variables and objects "belonging" to those instance variables are
+ # managed. If there are object reference counts to be handled,
+ # Transaction::Simple will probably cause problems.
+ # * is not inherently thread-safe. In the ACID ("atomic, consistent,
+ # isolated, durable") test, Transaction::Simple provides CD, but it is
+ # up to the user of Transaction::Simple to provide isolation and
+ # atomicity. Transactions should be considered "critical sections" in
+ # multi-threaded applications. If thread safety and atomicity is
+ # absolutely required, use Transaction::Simple::ThreadSafe, which uses a
+ # Mutex object to synchronize the accesses on the object during the
+ # transactional operations.
+ # * does not necessarily maintain Object#__id__ values on rewind or abort.
+ # This may change for future versions that will be Ruby 1.8 or better
+ # *only*. Certain objects that support #replace will maintain
+ # Object#__id__.
+ # * Can be a memory hog if you use many levels of transactions on many
+ # objects.
+ #
+ module Simple
+ VERSION = '1.1.1.0';
+
+ # Sets the Transaction::Simple debug object. It must respond to #<<.
+ # Sets the transaction debug object. Debugging will be performed
+ # automatically if there's a debug object. The generic transaction error
+ # class.
+ def self.debug_io=(io)
+ raise TransactionError, "Transaction Error: the transaction debug object must respond to #<<" unless io.respond_to?(:<<)
+ @tdi = io
+ end
+
+ # Returns the Transaction::Simple debug object. It must respond to #<<.
+ def self.debug_io
+ @tdi
+ end
+
+ # If +name+ is +nil+ (default), then returns +true+ if there is
+ # currently a transaction open.
+ #
+ # If +name+ is specified, then returns +true+ if there is currently a
+ # transaction that responds to +name+ open.
+ def transaction_open?(name = nil)
+ if name.nil?
+ Transaction::Simple.debug_io << "Transaction [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil?
+ return (not @__transaction_checkpoint__.nil?)
+ else
+ Transaction::Simple.debug_io << "Transaction(#{name.inspect}) [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil?
+ return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name))
+ end
+ end
+
+ # Returns the current name of the transaction. Transactions not
+ # explicitly named are named +nil+.
+ def transaction_name
+ raise TransactionError, "Transaction Error: No transaction open." if @__transaction_checkpoint__.nil?
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Transaction Name: #{@__transaction_names__[-1].inspect}\n" unless Transaction::Simple.debug_io.nil?
+ @__transaction_names__[-1]
+ end
+
+ # Starts a transaction. Stores the current object state. If a
+ # transaction name is specified, the transaction will be named.
+ # Transaction names must be unique. Transaction names of +nil+ will be
+ # treated as unnamed transactions.
+ def start_transaction(name = nil)
+ @__transaction_level__ ||= 0
+ @__transaction_names__ ||= []
+
+ if name.nil?
+ @__transaction_names__ << nil
+ s = ""
+ else
+ raise TransactionError, "Transaction Error: Named transactions must be unique." if @__transaction_names__.include?(name)
+ @__transaction_names__ << name
+ s = "(#{name.inspect})"
+ end
+
+ @__transaction_level__ += 1
+
+ Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} Start Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+
+ @__transaction_checkpoint__ = Marshal.dump(self)
+ end
+
+ # Rewinds the transaction. If +name+ is specified, then the intervening
+ # transactions will be aborted and the named transaction will be
+ # rewound. Otherwise, only the current transaction is rewound.
+ def rewind_transaction(name = nil)
+ raise TransactionError, "Transaction Error: Cannot rewind. There is no current transaction." if @__transaction_checkpoint__.nil?
+ if name.nil?
+ __rewind_this_transaction
+ s = ""
+ else
+ raise TransactionError, "Transaction Error: Cannot rewind to transaction #{name.inspect} because it does not exist." unless @__transaction_names__.include?(name)
+ s = "(#{name})"
+
+ while @__transaction_names__[-1] != name
+ @__transaction_checkpoint__ = __rewind_this_transaction
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ @__transaction_level__ -= 1
+ @__transaction_names__.pop
+ end
+ __rewind_this_transaction
+ end
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ self
+ end
+
+ # Aborts the transaction. Resets the object state to what it was before
+ # the transaction was started and closes the transaction. If +name+ is
+ # specified, then the intervening transactions and the named transaction
+ # will be aborted. Otherwise, only the current transaction is aborted.
+ def abort_transaction(name = nil)
+ raise TransactionError, "Transaction Error: Cannot abort. There is no current transaction." if @__transaction_checkpoint__.nil?
+ if name.nil?
+ __abort_transaction(name)
+ else
+ raise TransactionError, "Transaction Error: Cannot abort nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name)
+
+ __abort_transaction(name) while @__transaction_names__.include?(name)
+ end
+ self
+ end
+
+ # If +name+ is +nil+ (default), the current transaction level is closed
+ # out and the changes are committed.
+ #
+ # If +name+ is specified and +name+ is in the list of named
+ # transactions, then all transactions are closed and committed until the
+ # named transaction is reached.
+ def commit_transaction(name = nil)
+ raise TransactionError, "Transaction Error: Cannot commit. There is no current transaction." if @__transaction_checkpoint__.nil?
+
+ if name.nil?
+ s = ""
+ __commit_transaction
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ else
+ raise TransactionError, "Transaction Error: Cannot commit nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name)
+ s = "(#{name})"
+
+ while @__transaction_names__[-1] != name
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ __commit_transaction
+ end
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ __commit_transaction
+ end
+ self
+ end
+
+ # Alternative method for calling the transaction methods. An optional
+ # name can be specified for named transaction support.
+ #
+ # #transaction(:start):: #start_transaction
+ # #transaction(:rewind):: #rewind_transaction
+ # #transaction(:abort):: #abort_transaction
+ # #transaction(:commit):: #commit_transaction
+ # #transaction(:name):: #transaction_name
+ # #transaction:: #transaction_open?
+ def transaction(action = nil, name = nil)
+ case action
+ when :start
+ start_transaction(name)
+ when :rewind
+ rewind_transaction(name)
+ when :abort
+ abort_transaction(name)
+ when :commit
+ commit_transaction(name)
+ when :name
+ transaction_name
+ when nil
+ transaction_open?(name)
+ end
+ end
+
+ def __abort_transaction(name = nil) #:nodoc:
+ @__transaction_checkpoint__ = __rewind_this_transaction
+
+ if name.nil?
+ s = ""
+ else
+ s = "(#{name.inspect})"
+ end
+
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Abort Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ @__transaction_level__ -= 1
+ @__transaction_names__.pop
+ if @__transaction_level__ < 1
+ @__transaction_level__ = 0
+ @__transaction_names__ = []
+ end
+ end
+
+ TRANSACTION_CHECKPOINT = "@__transaction_checkpoint__" #:nodoc:
+ SKIP_TRANSACTION_VARS = [TRANSACTION_CHECKPOINT, "@__transaction_level__"] #:nodoc:
+
+ def __rewind_this_transaction #:nodoc:
+ r = Marshal.restore(@__transaction_checkpoint__)
+
+ begin
+ self.replace(r) if respond_to?(:replace)
+ rescue
+ nil
+ end
+
+ r.instance_variables.each do |i|
+ next if SKIP_TRANSACTION_VARS.include?(i)
+ if respond_to?(:instance_variable_get)
+ instance_variable_set(i, r.instance_variable_get(i))
+ else
+ instance_eval(%q|#{i} = r.instance_eval("#{i}")|)
+ end
+ end
+
+ if respond_to?(:instance_variable_get)
+ return r.instance_variable_get(TRANSACTION_CHECKPOINT)
+ else
+ return r.instance_eval(TRANSACTION_CHECKPOINT)
+ end
+ end
+
+ def __commit_transaction #:nodoc:
+ if respond_to?(:instance_variable_get)
+ @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_variable_get(TRANSACTION_CHECKPOINT)
+ else
+ @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_eval(TRANSACTION_CHECKPOINT)
+ end
+
+ @__transaction_level__ -= 1
+ @__transaction_names__.pop
+ if @__transaction_level__ < 1
+ @__transaction_level__ = 0
+ @__transaction_names__ = []
+ end
+ end
+
+ private :__abort_transaction, :__rewind_this_transaction, :__commit_transaction
+
+ # = Transaction::Simple::ThreadSafe
+ # Thread-safe simple object transaction support for Ruby.
+ # Transaction::Simple::ThreadSafe is used in the same way as
+ # Transaction::Simple. Transaction::Simple::ThreadSafe uses a Mutex
+ # object to ensure atomicity at the cost of performance in threaded
+ # applications.
+ #
+ # Transaction::Simple::ThreadSafe will not wait to obtain a lock; if the
+ # lock cannot be obtained immediately, a
+ # Transaction::TransactionThreadError will be raised.
+ #
+ # Thanks to Mauricio Fernández for help with getting this part working.
+ module ThreadSafe
+ VERSION = '1.1.1.0';
+
+ include Transaction::Simple
+
+ SKIP_TRANSACTION_VARS = Transaction::Simple::SKIP_TRANSACTION_VARS.dup #:nodoc:
+ SKIP_TRANSACTION_VARS << "@__transaction_mutex__"
+
+ Transaction::Simple.instance_methods(false) do |meth|
+ next if meth == "transaction"
+ arg = "(name = nil)" unless meth == "transaction_name"
+ module_eval <<-EOS
+ def #{meth}#{arg}
+ if (@__transaction_mutex__ ||= Mutex.new).try_lock
+ result = super
+ @__transaction_mutex__.unlock
+ return result
+ else
+ raise TransactionThreadError, "Transaction Error: Cannot obtain lock for ##{meth}"
+ end
+ ensure
+ @__transaction_mutex__.unlock
+ end
+ EOS
+ end
+ end
+ end
+end
+
+if $0 == __FILE__
+ require 'test/unit'
+
+ class Test__Transaction_Simple < Test::Unit::TestCase #:nodoc:
+ VALUE = "Now is the time for all good men to come to the aid of their country."
+
+ def setup
+ @value = VALUE.dup
+ @value.extend(Transaction::Simple)
+ end
+
+ def test_extended
+ assert_respond_to(@value, :start_transaction)
+ end
+
+ def test_started
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ end
+
+ def test_rewind
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.rewind_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_abort
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_commit
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_not_equal(VALUE, @value)
+ end
+
+ def test_multilevel
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_nothing_raised { @value.gsub!(/country/, 'nation-state') }
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(VALUE, @value)
+ end
+
+ def test_multilevel_named
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.transaction_name }
+ assert_nothing_raised { @value.start_transaction(:first) } # 1
+ assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(:first, @value.transaction_name)
+ assert_nothing_raised { @value.start_transaction } # 2
+ assert_not_equal(:first, @value.transaction_name)
+ assert_equal(nil, @value.transaction_name)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) }
+ assert_nothing_raised { @value.abort_transaction(:first) }
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised do
+ @value.start_transaction(:first)
+ @value.gsub!(/men/, 'women')
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_nothing_raised { @value.abort_transaction(:second) }
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) }
+ assert_nothing_raised { @value.rewind_transaction(:second) }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) }
+ assert_nothing_raised { @value.commit_transaction(:first) }
+ assert_equal(VALUE.gsub(/men/, 'sentients'), @value)
+ assert_equal(false, @value.transaction_open?)
+ end
+
+ def test_array
+ assert_nothing_raised do
+ @orig = ["first", "second", "third"]
+ @value = ["first", "second", "third"]
+ @value.extend(Transaction::Simple)
+ end
+ assert_equal(@orig, @value)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value[1].gsub!(/second/, "fourth") }
+ assert_not_equal(@orig, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(@orig, @value)
+ end
+ end
+
+ class Test__Transaction_Simple_ThreadSafe < Test::Unit::TestCase #:nodoc:
+ VALUE = "Now is the time for all good men to come to the aid of their country."
+
+ def setup
+ @value = VALUE.dup
+ @value.extend(Transaction::Simple::ThreadSafe)
+ end
+
+ def test_extended
+ assert_respond_to(@value, :start_transaction)
+ end
+
+ def test_started
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ end
+
+ def test_rewind
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.rewind_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_abort
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_commit
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_not_equal(VALUE, @value)
+ end
+
+ def test_multilevel
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_nothing_raised { @value.gsub!(/country/, 'nation-state') }
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(VALUE, @value)
+ end
+
+ def test_multilevel_named
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.transaction_name }
+ assert_nothing_raised { @value.start_transaction(:first) } # 1
+ assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(:first, @value.transaction_name)
+ assert_nothing_raised { @value.start_transaction } # 2
+ assert_not_equal(:first, @value.transaction_name)
+ assert_equal(nil, @value.transaction_name)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) }
+ assert_nothing_raised { @value.abort_transaction(:first) }
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised do
+ @value.start_transaction(:first)
+ @value.gsub!(/men/, 'women')
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_nothing_raised { @value.abort_transaction(:second) }
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) }
+ assert_nothing_raised { @value.rewind_transaction(:second) }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) }
+ assert_nothing_raised { @value.commit_transaction(:first) }
+ assert_equal(VALUE.gsub(/men/, 'sentients'), @value)
+ assert_equal(false, @value.transaction_open?)
+ end
+
+ def test_array
+ assert_nothing_raised do
+ @orig = ["first", "second", "third"]
+ @value = ["first", "second", "third"]
+ @value.extend(Transaction::Simple::ThreadSafe)
+ end
+ assert_equal(@orig, @value)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value[1].gsub!(/second/, "fourth") }
+ assert_not_equal(@orig, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(@orig, @value)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/wrappers/yaml_wrapper.rb b/activerecord/lib/active_record/wrappers/yaml_wrapper.rb
new file mode 100644
index 0000000000..74f40a507c
--- /dev/null
+++ b/activerecord/lib/active_record/wrappers/yaml_wrapper.rb
@@ -0,0 +1,15 @@
+require 'yaml'
+
+module ActiveRecord
+ module Wrappings #:nodoc:
+ class YamlWrapper < AbstractWrapper #:nodoc:
+ def wrap(attribute) attribute.to_yaml end
+ def unwrap(attribute) YAML::load(attribute) end
+ end
+
+ module ClassMethods #:nodoc:
+ # Wraps the attribute in Yaml encoding
+ def wrap_in_yaml(*attributes) wrap_with(YamlWrapper, attributes) end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/wrappings.rb b/activerecord/lib/active_record/wrappings.rb
new file mode 100644
index 0000000000..43e5e3151d
--- /dev/null
+++ b/activerecord/lib/active_record/wrappings.rb
@@ -0,0 +1,59 @@
+module ActiveRecord
+ # A plugin framework for wrapping attribute values before they go in and unwrapping them after they go out of the database.
+ # This was intended primarily for YAML wrapping of arrays and hashes, but this behavior is now native in the Base class.
+ # So for now this framework is laying dorment until a need pops up.
+ module Wrappings #:nodoc:
+ module ClassMethods #:nodoc:
+ def wrap_with(wrapper, *attributes)
+ [ attributes ].flat.each { |attribute| wrapper.wrap(attribute) }
+ end
+ end
+
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ class AbstractWrapper #:nodoc:
+ def self.wrap(attribute, record_binding) #:nodoc:
+ %w( before_save after_save after_initialize ).each do |callback|
+ eval "#{callback} #{name}.new('#{attribute}')", record_binding
+ end
+ end
+
+ def initialize(attribute) #:nodoc:
+ @attribute = attribute
+ end
+
+ def save_wrapped_attribute(record) #:nodoc:
+ if record.attribute_present?(@attribute)
+ record.send(
+ "write_attribute",
+ @attribute,
+ wrap(record.send("read_attribute", @attribute))
+ )
+ end
+ end
+
+ def load_wrapped_attribute(record) #:nodoc:
+ if record.attribute_present?(@attribute)
+ record.send(
+ "write_attribute",
+ @attribute,
+ unwrap(record.send("read_attribute", @attribute))
+ )
+ end
+ end
+
+ alias_method :before_save, :save_wrapped_attribute #:nodoc:
+ alias_method :after_save, :load_wrapped_attribute #:nodoc:
+ alias_method :after_initialize, :after_save #:nodoc:
+
+ # Overwrite to implement the logic that'll take the regular attribute and wrap it.
+ def wrap(attribute) end
+
+ # Overwrite to implement the logic that'll take the wrapped attribute and unwrap it.
+ def unwrap(attribute) end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/abstract_unit.rb b/activerecord/test/abstract_unit.rb
new file mode 100755
index 0000000000..1b33579206
--- /dev/null
+++ b/activerecord/test/abstract_unit.rb
@@ -0,0 +1,22 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')#.unshift(File.dirname(__FILE__))
+
+# Make rubygems available for testing if possible
+begin require('rubygems'); rescue LoadError; end
+begin require('dev-utils/debug'); rescue LoadError; end
+
+require 'test/unit'
+require 'active_record'
+require 'active_record/fixtures'
+require 'connection'
+
+class Test::Unit::TestCase #:nodoc:
+ def create_fixtures(*table_names)
+ if block_given?
+ Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names) { yield }
+ else
+ Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names)
+ end
+ end
+end
+
+Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" \ No newline at end of file
diff --git a/activerecord/test/aggregations_test.rb b/activerecord/test/aggregations_test.rb
new file mode 100644
index 0000000000..2eff36dc73
--- /dev/null
+++ b/activerecord/test/aggregations_test.rb
@@ -0,0 +1,34 @@
+require 'abstract_unit'
+# require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'fixtures/customer'
+
+class AggregationsTest < Test::Unit::TestCase
+ def setup
+ @customers = create_fixtures "customers"
+ @david = Customer.find(1)
+ end
+
+ def test_find_single_value_object
+ assert_equal 50, @david.balance.amount
+ assert_kind_of Money, @david.balance
+ assert_equal 300, @david.balance.exchange_to("DKK").amount
+ end
+
+ def test_find_multiple_value_object
+ assert_equal @customers["david"]["address_street"], @david.address.street
+ assert(
+ @david.address.close_to?(Address.new("Different Street", @customers["david"]["address_city"], @customers["david"]["address_country"]))
+ )
+ end
+
+ def test_change_single_value_object
+ @david.balance = Money.new(100)
+ @david.save
+ assert_equal 100, Customer.find(1).balance.amount
+ end
+
+ def test_immutable_value_objects
+ @david.balance = Money.new(100)
+ assert_raises(TypeError) { @david.balance.instance_eval { @amount = 20 } }
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/all.sh b/activerecord/test/all.sh
new file mode 100755
index 0000000000..a6712cc48e
--- /dev/null
+++ b/activerecord/test/all.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 connections/<db_library>" 1>&2
+ exit 1
+fi
+
+ruby -I $1 -e 'Dir.foreach(".") { |file| require file if file =~ /_test.rb$/ }'
diff --git a/activerecord/test/associations_test.rb b/activerecord/test/associations_test.rb
new file mode 100755
index 0000000000..2eb6ba267e
--- /dev/null
+++ b/activerecord/test/associations_test.rb
@@ -0,0 +1,549 @@
+require 'abstract_unit'
+require 'fixtures/developer'
+require 'fixtures/project'
+# require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'fixtures/company'
+require 'fixtures/topic'
+require 'fixtures/reply'
+
+# Can't declare new classes in test case methods, so tests before that
+bad_collection_keys = false
+begin
+ class Car < ActiveRecord::Base; has_many :wheels, :name => "wheels"; end
+rescue ActiveRecord::ActiveRecordError
+ bad_collection_keys = true
+end
+raise "ActiveRecord should have barked on bad collection keys" unless bad_collection_keys
+
+
+class AssociationsTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts", "companies", "accounts", "developers", "projects", "developers_projects"
+ @signals37 = Firm.find(1)
+ end
+
+ def test_force_reload
+ firm = Firm.new
+ firm.save
+ firm.clients.each {|c|} # forcing to load all clients
+ assert firm.clients.empty?, "New firm shouldn't have client objects"
+ assert !firm.has_clients?, "New firm shouldn't have clients"
+ assert_equal 0, firm.clients.size, "New firm should have 0 clients"
+
+ client = Client.new("firm_id" => firm.id)
+ client.save
+
+ assert firm.clients.empty?, "New firm should have cached no client objects"
+ assert !firm.has_clients?, "New firm should have cached a no-clients response"
+ assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count"
+
+ assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
+ assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
+ end
+
+ def test_storing_in_pstore
+ require "tmpdir"
+ store_filename = File.join(Dir.tmpdir, "ar-pstore-association-test")
+ File.delete(store_filename) if File.exists?(store_filename)
+ require "pstore"
+ apple = Firm.create("name" => "Apple")
+ natural = Client.new("name" => "Natural Company")
+ apple.clients << natural
+
+ db = PStore.new(store_filename)
+ db.transaction do
+ db["apple"] = apple
+ end
+
+ db = PStore.new(store_filename)
+ db.transaction do
+ assert_equal "Natural Company", db["apple"].clients.first.name
+ end
+ end
+end
+
+class HasOneAssociationsTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts", "companies", "accounts", "developers", "projects", "developers_projects"
+ @signals37 = Firm.find(1)
+ end
+
+ def test_has_one
+ assert_equal @signals37.account, Account.find(1)
+ assert_equal Account.find(1).credit_limit, @signals37.account.credit_limit
+ assert @signals37.has_account?, "37signals should have an account"
+ assert Account.find(1).firm?(@signals37), "37signals account should be able to backtrack"
+ assert Account.find(1).has_firm?, "37signals account should be able to backtrack"
+
+ assert !Account.find(2).has_firm?, "Unknown isn't linked"
+ assert !Account.find(2).firm?(@signals37), "Unknown isn't linked"
+ end
+
+ def test_type_mismatch
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { @signals37.account = 1 }
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { @signals37.account = Project.find(1) }
+ end
+
+ def test_natural_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ apple.account = citibank
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_to_nil
+ old_account_id = @signals37.account.id
+ @signals37.account = nil
+ @signals37.save
+ assert_nil @signals37.account
+ assert_nil Account.find(old_account_id).firm_id
+ end
+
+ def test_build
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account("credit_limit" => 1000)
+ assert account.save
+ assert_equal account, firm.account
+ end
+
+ def test_failing_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account
+ assert !account.save
+ assert_equal "can't be empty", account.errors.on("credit_limit")
+ end
+
+ def test_create
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+ assert_equal firm.create_account("credit_limit" => 1000), firm.account
+ end
+
+ def test_dependence
+ firm = Firm.find(1)
+ assert !firm.account.nil?
+ firm.destroy
+ assert_equal 1, Account.find_all.length
+ end
+
+ def test_dependence_with_missing_association
+ Account.destroy_all
+ firm = Firm.find(1)
+ assert !firm.has_account?
+ firm.destroy
+ end
+end
+
+
+class HasManyAssociationsTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts", "companies", "accounts", "developers", "projects", "developers_projects", "topics"
+ @signals37 = Firm.find(1)
+ end
+
+ def force_signal37_to_load_all_clients_of_firm
+ @signals37.clients_of_firm.each {|f| }
+ end
+
+ def test_finding
+ assert_equal 2, Firm.find_first.clients.length
+ end
+
+ def test_finding_default_orders
+ assert_equal "Summit", Firm.find_first.clients.first.name
+ end
+
+ def test_finding_with_different_class_name_and_order
+ assert_equal "Microsoft", Firm.find_first.clients_sorted_desc.first.name
+ end
+
+ def test_finding_with_foreign_key
+ assert_equal "Microsoft", Firm.find_first.clients_of_firm.first.name
+ end
+
+ def test_finding_with_condition
+ assert_equal "Microsoft", Firm.find_first.clients_like_ms.first.name
+ end
+
+ def test_finding_using_sql
+ firm = Firm.find_first
+ first_client = firm.clients_using_sql.first
+ assert_not_nil first_client
+ assert_equal "Microsoft", first_client.name
+ assert_equal 1, firm.clients_using_sql.size
+ assert_equal 1, Firm.find_first.clients_using_sql.size
+ end
+
+ def test_find_all
+ assert_equal 2, Firm.find_first.clients.find_all("type = 'Client'").length
+ assert_equal 1, Firm.find_first.clients.find_all("name = 'Summit'").length
+ end
+
+ def test_find_all_sanitized
+ firm = Firm.find_first
+ assert_equal firm.clients.find_all("name = 'Summit'"), firm.clients.find_all(["name = '%s'", "Summit"])
+ end
+
+ def test_find_in_collection
+ assert_equal Client.find(2).name, @signals37.clients.find(2).name
+ assert_equal Client.find(2).name, @signals37.clients.find {|c| c.name == @signals37.clients.find(2).name }.name
+ assert_raises(ActiveRecord::RecordNotFound) { @signals37.clients.find(6) }
+ end
+
+ def test_adding
+ force_signal37_to_load_all_clients_of_firm
+ natural = Client.new("name" => "Natural Company")
+ @signals37.clients_of_firm << natural
+ assert_equal 2, @signals37.clients_of_firm.size # checking via the collection
+ assert_equal 2, @signals37.clients_of_firm(true).size # checking using the db
+ assert_equal natural, @signals37.clients_of_firm.last
+ end
+
+ def test_adding_a_mismatch_class
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { @signals37.clients_of_firm << nil }
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { @signals37.clients_of_firm << 1 }
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { @signals37.clients_of_firm << Topic.find(1) }
+ end
+
+ def test_adding_a_collection
+ force_signal37_to_load_all_clients_of_firm
+ @signals37.clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
+ assert_equal 3, @signals37.clients_of_firm.size
+ assert_equal 3, @signals37.clients_of_firm(true).size
+ end
+
+ def test_build
+ new_client = @signals37.clients_of_firm.build("name" => "Another Client")
+ assert_equal "Another Client", new_client.name
+ assert new_client.save
+ assert_equal 2, @signals37.clients_of_firm(true).size
+ end
+
+ def test_create
+ force_signal37_to_load_all_clients_of_firm
+ new_client = @signals37.clients_of_firm.create("name" => "Another Client")
+ assert_equal new_client, @signals37.clients_of_firm.last
+ assert_equal new_client, @signals37.clients_of_firm(true).last
+ end
+
+ def test_deleting
+ force_signal37_to_load_all_clients_of_firm
+ @signals37.clients_of_firm.delete(@signals37.clients_of_firm.first)
+ assert_equal 0, @signals37.clients_of_firm.size
+ assert_equal 0, @signals37.clients_of_firm(true).size
+ end
+
+ def test_deleting_a_collection
+ force_signal37_to_load_all_clients_of_firm
+ @signals37.clients_of_firm.create("name" => "Another Client")
+ assert_equal 2, @signals37.clients_of_firm.size
+ #@signals37.clients_of_firm.clear
+ @signals37.clients_of_firm.delete([@signals37.clients_of_firm[0], @signals37.clients_of_firm[1]])
+ assert_equal 0, @signals37.clients_of_firm.size
+ assert_equal 0, @signals37.clients_of_firm(true).size
+ end
+
+ def test_deleting_a_association_collection
+ force_signal37_to_load_all_clients_of_firm
+ @signals37.clients_of_firm.create("name" => "Another Client")
+ assert_equal 2, @signals37.clients_of_firm.size
+ @signals37.clients_of_firm.clear
+ assert_equal 0, @signals37.clients_of_firm.size
+ assert_equal 0, @signals37.clients_of_firm(true).size
+ end
+
+ def test_deleting_a_item_which_is_not_in_the_collection
+ force_signal37_to_load_all_clients_of_firm
+ summit = Client.find_first("name = 'Summit'")
+ @signals37.clients_of_firm.delete(summit)
+ assert_equal 1, @signals37.clients_of_firm.size
+ assert_equal 1, @signals37.clients_of_firm(true).size
+ assert_equal 2, summit.client_of
+ end
+
+ def test_deleting_type_mismatch
+ david = Developer.find(1)
+ david.projects.id
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) }
+ end
+
+ def test_deleting_self_type_mismatch
+ david = Developer.find(1)
+ david.projects.id
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
+ end
+
+ def test_destroy_all
+ force_signal37_to_load_all_clients_of_firm
+ assert !@signals37.clients_of_firm.empty?, "37signals has clients after load"
+ @signals37.clients_of_firm.destroy_all
+ assert @signals37.clients_of_firm.empty?, "37signals has no clients after destroy all"
+ assert @signals37.clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh"
+ end
+
+ def test_dependence
+ assert_equal 2, Client.find_all.length
+ Firm.find_first.destroy
+ assert_equal 0, Client.find_all.length
+ end
+
+ def test_dependence_with_transaction_support_on_failure
+ assert_equal 2, Client.find_all.length
+ firm = Firm.find_first
+ clients = firm.clients
+ clients.last.instance_eval { def before_destroy() raise "Trigger rollback" end }
+
+ firm.destroy rescue "do nothing"
+
+ assert_equal 2, Client.find_all.length
+ end
+
+ def test_dependence_on_account
+ assert_equal 2, Account.find_all.length
+ @signals37.destroy
+ assert_equal 1, Account.find_all.length
+ end
+
+ def test_included_in_collection
+ assert @signals37.clients.include?(Client.find(2))
+ end
+
+ def test_adding_array_and_collection
+ assert_nothing_raised { Firm.find_first.clients + Firm.find_all.last.clients }
+ end
+end
+
+class BelongsToAssociationsTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts", "companies", "accounts", "developers", "projects", "developers_projects", "topics"
+ @signals37 = Firm.find(1)
+ end
+
+ def test_belongs_to
+ Client.find(3).firm.name
+ assert_equal @signals37.name, Client.find(3).firm.name
+ assert !Client.find(3).firm.nil?, "Microsoft should have a firm"
+ end
+
+ def test_type_mismatch
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = 1 }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) }
+ end
+
+ def test_natural_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ citibank.firm = apple
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_to_nil
+ client = Client.find(3)
+ client.firm = nil
+ client.save
+ assert_nil client.firm(true)
+ assert_nil client.client_of
+ end
+
+ def test_with_different_class_name
+ assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
+ assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm"
+ end
+
+ def test_with_condition
+ assert_equal Company.find(1).name, Company.find(3).firm_with_condition.name
+ assert_not_nil Company.find(3).firm_with_condition, "Microsoft should have a firm"
+ end
+
+ def test_belongs_to_counter
+ debate = Topic.create("title" => "debate")
+ assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet"
+
+ trash = debate.replies.create("title" => "blah!", "content" => "world around!")
+ assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created"
+
+ trash.destroy
+ assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
+ end
+
+ def xtest_counter_cache
+ apple = Firm.create("name" => "Apple")
+ final_cut = apple.clients.create("name" => "Final Cut")
+
+ apple.clients.to_s
+ assert_equal 1, apple.clients.size, "Created one client"
+
+ apple.companies_count = 2
+ apple.save
+
+ apple = Firm.find_first("name = 'Apple'")
+ assert_equal 2, apple.clients.size, "Should use the new cached number"
+
+ apple.clients.to_s
+ assert_equal 1, apple.clients.size, "Should not use the cached number, but go to the database"
+ end
+end
+
+
+class HasAndBelongsToManyAssociationsTest < Test::Unit::TestCase
+ def setup
+ @accounts, @companies, @developers, @projects, @developers_projects =
+ create_fixtures "accounts", "companies", "developers", "projects", "developers_projects"
+
+ @signals37 = Firm.find(1)
+ end
+
+ def test_has_and_belongs_to_many
+ david = Developer.find(1)
+
+ assert !david.projects.empty?
+ assert_equal 2, david.projects.size
+
+ active_record = Project.find(1)
+ assert !active_record.developers.empty?
+ assert_equal 2, active_record.developers.size
+ assert_equal david.name, active_record.developers.first.name
+ end
+
+ def test_adding_single
+ jamis = Developer.find(2)
+ jamis.projects.id # causing the collection to load
+ action_controller = Project.find(2)
+ assert_equal 1, jamis.projects.size
+ assert_equal 1, action_controller.developers.size
+
+ jamis.projects << action_controller
+
+ assert_equal 2, jamis.projects.size
+ assert_equal 2, jamis.projects(true).size
+ assert_equal 2, action_controller.developers(true).size
+ end
+
+ def test_adding_type_mismatch
+ jamis = Developer.find(2)
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << nil }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << 1 }
+ end
+
+ def test_adding_from_the_project
+ jamis = Developer.find(2)
+ action_controller = Project.find(2)
+ action_controller.developers.id
+ assert_equal 1, jamis.projects.size
+ assert_equal 1, action_controller.developers.size
+
+ action_controller.developers << jamis
+
+ assert_equal 2, jamis.projects(true).size
+ assert_equal 2, action_controller.developers.size
+ assert_equal 2, action_controller.developers(true).size
+ end
+
+ def test_adding_multiple
+ aridridel = Developer.new("name" => "Aridridel")
+ aridridel.save
+ aridridel.projects.id
+ aridridel.projects.push(Project.find(1), Project.find(2))
+ assert_equal 2, aridridel.projects.size
+ assert_equal 2, aridridel.projects(true).size
+ end
+
+ def test_adding_a_collection
+ aridridel = Developer.new("name" => "Aridridel")
+ aridridel.save
+ aridridel.projects.id
+ aridridel.projects.concat([Project.find(1), Project.find(2)])
+ assert_equal 2, aridridel.projects.size
+ assert_equal 2, aridridel.projects(true).size
+ end
+
+ def test_uniq_after_the_fact
+ @developers["jamis"].find.projects << @projects["active_record"].find
+ @developers["jamis"].find.projects << @projects["active_record"].find
+ assert_equal 3, @developers["jamis"].find.projects.size
+ assert_equal 1, @developers["jamis"].find.projects.uniq.size
+ end
+
+ def test_uniq_before_the_fact
+ @projects["active_record"].find.developers << @developers["jamis"].find
+ @projects["active_record"].find.developers << @developers["david"].find
+ assert_equal 2, @projects["active_record"].find.developers.size
+ end
+
+ def test_deleting
+ david = Developer.find(1)
+ active_record = Project.find(1)
+ david.projects.id
+ assert_equal 2, david.projects.size
+ assert_equal 2, active_record.developers.size
+
+ david.projects.delete(active_record)
+
+ assert_equal 1, david.projects.size
+ assert_equal 1, david.projects(true).size
+ assert_equal 1, active_record.developers(true).size
+ end
+
+ def test_deleting_array
+ david = Developer.find(1)
+ david.projects.id
+ david.projects.delete(Project.find_all)
+ assert_equal 0, david.projects.size
+ assert_equal 0, david.projects(true).size
+ end
+
+ def test_deleting_all
+ david = Developer.find(1)
+ david.projects.id
+ david.projects.clear
+ assert_equal 0, david.projects.size
+ assert_equal 0, david.projects(true).size
+ end
+
+ def test_removing_associations_on_destroy
+ Developer.find(1).destroy
+ assert Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = '1'").empty?
+ end
+
+ def test_additional_columns_from_join_table
+ assert_equal Date.new(2004, 10, 10).to_s, Developer.find(1).projects.first.joined_on.to_s
+ end
+
+ def test_destroy_all
+ david = Developer.find(1)
+ david.projects.id
+ assert !david.projects.empty?
+ david.projects.destroy_all
+ assert david.projects.empty?
+ assert david.projects(true).empty?
+ end
+
+ def test_rich_association
+ @jamis = @developers["jamis"].find
+ @jamis.projects.push_with_attributes(@projects["action_controller"].find, :joined_on => Date.today)
+ assert_equal Date.today.to_s, @jamis.projects.select { |p| p.name == @projects["action_controller"]["name"] }.first.joined_on.to_s
+ assert_equal Date.today.to_s, @developers["jamis"].find.projects.select { |p| p.name == @projects["action_controller"]["name"] }.first.joined_on.to_s
+ end
+
+ def test_associations_with_conditions
+ assert_equal 2, @projects["active_record"].find.developers.size
+ assert_equal 1, @projects["active_record"].find.developers_named_david.size
+
+ @projects["active_record"].find.developers_named_david.clear
+ assert_equal 1, @projects["active_record"].find.developers.size
+ end
+
+ def test_find_in_association
+ # Using sql
+ assert_equal @developers["david"].find, @projects["active_record"].find.developers.find(@developers["david"]["id"]), "SQL find"
+
+ # Using ruby
+ @active_record = @projects["active_record"].find
+ @active_record.developers.reload
+ assert_equal @developers["david"].find, @active_record.developers.find(@developers["david"]["id"]), "Ruby find"
+ end
+end
diff --git a/activerecord/test/base_test.rb b/activerecord/test/base_test.rb
new file mode 100755
index 0000000000..0d2278eb58
--- /dev/null
+++ b/activerecord/test/base_test.rb
@@ -0,0 +1,544 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/company'
+require 'fixtures/default'
+require 'fixtures/auto_id'
+require 'fixtures/column_name'
+
+class Category < ActiveRecord::Base; end
+class Smarts < ActiveRecord::Base; end
+class CreditCard < ActiveRecord::Base; end
+class MasterCreditCard < ActiveRecord::Base; end
+
+class LoosePerson < ActiveRecord::Base
+ attr_protected :credit_rating, :administrator
+end
+
+class TightPerson < ActiveRecord::Base
+ attr_accessible :name, :address
+end
+
+class TightDescendent < TightPerson
+ attr_accessible :phone_number
+end
+
+class Booleantest < ActiveRecord::Base; end
+
+class BasicsTest < Test::Unit::TestCase
+ def setup
+ @topic_fixtures, @companies = create_fixtures "topics", "companies"
+ end
+
+ def test_set_attributes
+ topic = Topic.find(1)
+ topic.attributes = { "title" => "Budget", "author_name" => "Jason" }
+ topic.save
+ assert_equal("Budget", topic.title)
+ assert_equal("Jason", topic.author_name)
+ assert_equal(@topic_fixtures["first"]["author_email_address"], Topic.find(1).author_email_address)
+ end
+
+ def test_integers_as_nil
+ Topic.update(1, "approved" => "")
+ assert_nil Topic.find(1).approved
+ end
+
+ def test_set_attributes_with_block
+ topic = Topic.new do |t|
+ t.title = "Budget"
+ t.author_name = "Jason"
+ end
+
+ assert_equal("Budget", topic.title)
+ assert_equal("Jason", topic.author_name)
+ end
+
+ def test_respond_to?
+ topic = Topic.find(1)
+ assert topic.respond_to?("title")
+ assert topic.respond_to?("title?")
+ assert topic.respond_to?("title=")
+ assert topic.respond_to?(:title)
+ assert topic.respond_to?(:title?)
+ assert topic.respond_to?(:title=)
+ assert topic.respond_to?("author_name")
+ assert topic.respond_to?("attribute_names")
+ assert !topic.respond_to?("nothingness")
+ assert !topic.respond_to?(:nothingness)
+ end
+
+ def test_array_content
+ topic = Topic.new
+ topic.content = %w( one two three )
+ topic.save
+
+ assert_equal(%w( one two three ), Topic.find(topic.id).content)
+ end
+
+ def test_hash_content
+ topic = Topic.new
+ topic.content = { "one" => 1, "two" => 2 }
+ topic.save
+
+ assert_equal 2, Topic.find(topic.id).content["two"]
+
+ topic.content["three"] = 3
+ topic.save
+
+ assert_equal 3, Topic.find(topic.id).content["three"]
+ end
+
+ def test_update_array_content
+ topic = Topic.new
+ topic.content = %w( one two three )
+
+ topic.content.push "four"
+ assert_equal(%w( one two three four ), topic.content)
+
+ topic.save
+
+ topic = Topic.find(topic.id)
+ topic.content << "five"
+ assert_equal(%w( one two three four five ), topic.content)
+ end
+
+ def test_create
+ topic = Topic.new
+ topic.title = "New Topic"
+ topic.save
+ id = topic.id
+ topicReloaded = Topic.find(id)
+ assert_equal("New Topic", topicReloaded.title)
+ end
+
+ def test_create_through_factory
+ topic = Topic.create("title" => "New Topic")
+ topicReloaded = Topic.find(topic.id)
+ assert_equal(topic, topicReloaded)
+ end
+
+ def test_update
+ topic = Topic.new
+ topic.title = "Another New Topic"
+ topic.written_on = "2003-12-12 23:23"
+ topic.save
+ id = topic.id
+ assert_equal(id, topic.id)
+
+ topicReloaded = Topic.find(id)
+ assert_equal("Another New Topic", topicReloaded.title)
+
+ topicReloaded.title = "Updated topic"
+ topicReloaded.save
+
+ topicReloadedAgain = Topic.find(id)
+
+ assert_equal("Updated topic", topicReloadedAgain.title)
+ end
+
+ def test_preserving_date_objects
+ # SQL Server doesn't have a separate column type just for dates, so all are returned as time
+ if ActiveRecord::ConnectionAdapters.const_defined? :SQLServerAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
+ end
+
+ assert_kind_of(
+ Date, Topic.find(1).last_read,
+ "The last_read attribute should be of the Date class"
+ )
+ end
+
+ def test_preserving_time_objects
+ assert_kind_of(
+ Time, Topic.find(1).written_on,
+ "The written_on attribute should be of the Time class"
+ )
+ end
+
+ def test_destroy
+ topic = Topic.new
+ topic.title = "Yet Another New Topic"
+ topic.written_on = "2003-12-12 23:23"
+ topic.save
+ id = topic.id
+ topic.destroy
+
+ assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(id) }
+ end
+
+ def test_record_not_found_exception
+ assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(id) }
+ end
+
+ def test_initialize_with_attributes
+ topic = Topic.new({
+ "title" => "initialized from attributes", "written_on" => "2003-12-12 23:23"
+ })
+
+ assert_equal("initialized from attributes", topic.title)
+ end
+
+ def test_load
+ topics = Topic.find_all nil, "id"
+ assert_equal(2, topics.size)
+ assert_equal(@topic_fixtures["first"]["title"], topics.first.title)
+ end
+
+ def test_load_with_condition
+ topics = Topic.find_all "author_name = 'Mary'"
+
+ assert_equal(1, topics.size)
+ assert_equal(@topic_fixtures["second"]["title"], topics.first.title)
+ end
+
+ def test_table_name_guesses
+ assert_equal "topics", Topic.table_name
+
+ assert_equal "categories", Category.table_name
+ assert_equal "smarts", Smarts.table_name
+ assert_equal "credit_cards", CreditCard.table_name
+ assert_equal "master_credit_cards", MasterCreditCard.table_name
+
+ ActiveRecord::Base.pluralize_table_names = false
+ assert_equal "category", Category.table_name
+ assert_equal "smarts", Smarts.table_name
+ assert_equal "credit_card", CreditCard.table_name
+ assert_equal "master_credit_card", MasterCreditCard.table_name
+ ActiveRecord::Base.pluralize_table_names = true
+
+ ActiveRecord::Base.table_name_prefix = "test_"
+ assert_equal "test_categories", Category.table_name
+ ActiveRecord::Base.table_name_suffix = "_test"
+ assert_equal "test_categories_test", Category.table_name
+ ActiveRecord::Base.table_name_prefix = ""
+ assert_equal "categories_test", Category.table_name
+ ActiveRecord::Base.table_name_suffix = ""
+ assert_equal "categories", Category.table_name
+
+ ActiveRecord::Base.pluralize_table_names = false
+ ActiveRecord::Base.table_name_prefix = "test_"
+ assert_equal "test_category", Category.table_name
+ ActiveRecord::Base.table_name_suffix = "_test"
+ assert_equal "test_category_test", Category.table_name
+ ActiveRecord::Base.table_name_prefix = ""
+ assert_equal "category_test", Category.table_name
+ ActiveRecord::Base.table_name_suffix = ""
+ assert_equal "category", Category.table_name
+ ActiveRecord::Base.pluralize_table_names = true
+ end
+
+ def test_destroy_all
+ assert_equal(2, Topic.find_all.size)
+
+ Topic.destroy_all "author_name = 'Mary'"
+ assert_equal(1, Topic.find_all.size)
+ end
+
+ def test_boolean_attributes
+ assert ! Topic.find(1).approved?
+ assert Topic.find(2).approved?
+ end
+
+ def test_increment_counter
+ Topic.increment_counter("replies_count", 1)
+ assert_equal 1, Topic.find(1).replies_count
+
+ Topic.increment_counter("replies_count", 1)
+ assert_equal 2, Topic.find(1).replies_count
+ end
+
+ def test_decrement_counter
+ Topic.decrement_counter("replies_count", 2)
+ assert_equal 1, Topic.find(2).replies_count
+
+ Topic.decrement_counter("replies_count", 2)
+ assert_equal 0, Topic.find(1).replies_count
+ end
+
+ def test_update_all
+ Topic.update_all "content = 'bulk updated!'"
+ assert_equal "bulk updated!", Topic.find(1).content
+ assert_equal "bulk updated!", Topic.find(2).content
+ end
+
+ def test_update_by_condition
+ Topic.update_all "content = 'bulk updated!'", "approved = 1"
+ assert_equal "Have a nice day", Topic.find(1).content
+ assert_equal "bulk updated!", Topic.find(2).content
+ end
+
+ def test_attribute_present
+ t = Topic.new
+ t.title = "hello there!"
+ t.written_on = Time.now
+ assert t.attribute_present?("title")
+ assert t.attribute_present?("written_on")
+ assert !t.attribute_present?("content")
+ end
+
+ def test_attribute_keys_on_new_instance
+ t = Topic.new
+ assert_equal nil, t.title, "The topics table has a title column, so it should be nil"
+ assert_raises(NoMethodError) { t.title2 }
+ end
+
+ def test_class_name
+ assert_equal "Firm", ActiveRecord::Base.class_name("firms")
+ assert_equal "Category", ActiveRecord::Base.class_name("categories")
+ assert_equal "AccountHolder", ActiveRecord::Base.class_name("account_holder")
+
+ ActiveRecord::Base.pluralize_table_names = false
+ assert_equal "Firms", ActiveRecord::Base.class_name( "firms" )
+ ActiveRecord::Base.pluralize_table_names = true
+
+ ActiveRecord::Base.table_name_prefix = "test_"
+ assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms" )
+ ActiveRecord::Base.table_name_suffix = "_tests"
+ assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms_tests" )
+ ActiveRecord::Base.table_name_prefix = ""
+ assert_equal "Firm", ActiveRecord::Base.class_name( "firms_tests" )
+ ActiveRecord::Base.table_name_suffix = ""
+ assert_equal "Firm", ActiveRecord::Base.class_name( "firms" )
+ end
+
+ def test_null_fields
+ assert_nil Topic.find(1).parent_id
+ assert_nil Topic.create("title" => "Hey you").parent_id
+ end
+
+ def test_default_values
+ topic = Topic.new
+ assert_equal 1, topic.approved
+ assert_nil topic.written_on
+ assert_nil topic.last_read
+
+ topic.save
+
+ topic = Topic.find(topic.id)
+ assert_equal 1, topic.approved
+ assert_nil topic.last_read
+ end
+
+ def test_default_values_on_empty_strings
+ topic = Topic.new
+ topic.approved = nil
+ topic.last_read = nil
+
+ topic.save
+
+ topic = Topic.find(topic.id)
+ assert_nil topic.last_read
+ assert_nil topic.approved
+ end
+
+ def test_equality
+ assert_equal Topic.find(1), Topic.find(2).parent
+ end
+
+ def test_hashing
+ assert_equal [ Topic.find(1) ], [ Topic.find(2).parent ] & [ Topic.find(1) ]
+ end
+
+ def test_destroy_new_record
+ client = Client.new
+ client.destroy
+ assert client.frozen?
+ end
+
+ def test_update_attribute
+ assert !Topic.find(1).approved?
+ Topic.find(1).update_attribute("approved", true)
+ assert Topic.find(1).approved?
+ end
+
+ def test_mass_assignment_protection
+ firm = Firm.new
+ firm.attributes = { "name" => "Next Angle", "rating" => 5 }
+ assert_equal 1, firm.rating
+ end
+
+ def test_mass_assignment_accessible
+ reply = Reply.new("title" => "hello", "content" => "world", "approved" => 0)
+ reply.save
+
+ assert_equal 1, reply.approved
+
+ reply.approved = 0
+ reply.save
+
+ assert_equal 0, reply.approved
+ end
+
+ def test_mass_assignment_protection_inheritance
+ assert_equal [ :credit_rating, :administrator ], LoosePerson.protected_attributes
+ assert_nil TightPerson.protected_attributes
+ end
+
+ def test_multiparameter_attributes_on_date
+ # SQL Server doesn't have a separate column type just for dates, so all are returned as time
+ if ActiveRecord::ConnectionAdapters.const_defined? :SQLServerAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
+ end
+
+ attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Date.new(2004, 6, 24).to_s, topic.last_read.to_s
+ end
+
+ def test_multiparameter_attributes_on_date_with_empty_date
+ # SQL Server doesn't have a separate column type just for dates, so all are returned as time
+ if ActiveRecord::ConnectionAdapters.const_defined? :SQLServerAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
+ end
+
+ attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Date.new(2004, 6, 1).to_s, topic.last_read.to_s
+ end
+
+ def test_multiparameter_attributes_on_date_with_all_empty
+ attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_nil topic.last_read
+ end
+
+ def test_multiparameter_attributes_on_time
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
+
+ def test_multiparameter_attributes_on_time_with_empty_seconds
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
+
+ def test_boolean
+ b_false = Booleantest.create({ "value" => false })
+ false_id = b_false.id
+ b_true = Booleantest.create({ "value" => true })
+ true_id = b_true.id
+
+ b_false = Booleantest.find(false_id)
+ assert !b_false.value?
+ b_true = Booleantest.find(true_id)
+ assert b_true.value?
+ end
+
+ def test_clone
+ topic = Topic.find(1)
+ cloned_topic = topic.clone
+ assert_equal topic.title, cloned_topic.title
+ assert cloned_topic.new_record?
+
+ # test if the attributes have been cloned
+ topic.title = "a"
+ cloned_topic.title = "b"
+ assert_equal "a", topic.title
+ assert_equal "b", cloned_topic.title
+
+ # test if the attribute values have been cloned
+ topic.title = {"a" => "b"}
+ cloned_topic = topic.clone
+ cloned_topic.title["a"] = "c"
+ assert_equal "b", topic.title["a"]
+ end
+
+ def test_bignum
+ company = Company.find(1)
+ company.rating = 2147483647
+ company.save
+ company = Company.find(1)
+ assert_equal 2147483647, company.rating
+ end
+
+ def test_default
+ if Default.connection.class.name == 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
+ default = Default.new
+
+ # dates / timestampts
+ time_format = "%m/%d/%Y %H:%M"
+ assert_equal Time.now.strftime(time_format), default.modified_time.strftime(time_format)
+ assert_equal Date.today, default.modified_date
+
+ # fixed dates / times
+ assert_equal Date.new(2004, 1, 1), default.fixed_date
+ assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
+
+ # char types
+ assert_equal 'Y', default.char1
+ assert_equal 'a varchar field', default.char2
+ assert_equal 'a text field', default.char3
+ end
+ end
+
+ def test_auto_id
+ auto = AutoId.new
+ auto.save
+ assert (auto.id > 0)
+ end
+
+ def quote_column_name(name)
+ "<#{name}>"
+ end
+
+ def test_quote_keys
+ ar = AutoId.new
+ source = {"foo" => "bar", "baz" => "quux"}
+ actual = ar.send(:quote_columns, self, source)
+ inverted = actual.invert
+ assert_equal("<foo>", inverted["bar"])
+ assert_equal("<baz>", inverted["quux"])
+ end
+
+ def test_column_name_properly_quoted
+ col_record = ColumnName.new
+ col_record.references = 40
+ col_record.save
+ col_record.references = 41
+ col_record.save
+ c2 = ColumnName.find(col_record.id)
+ assert_equal(41, c2.references)
+ end
+
+ MyObject = Struct.new :attribute1, :attribute2
+
+ def test_serialized_attribute
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.create("content" => myobj)
+ Topic.serialize("content", MyObject)
+ assert_equal(myobj, topic.content)
+ end
+
+ def test_serialized_attribute_with_class_constraint
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.create("content" => myobj)
+ Topic.serialize(:content, Hash)
+
+ assert_raises(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content }
+
+ settings = { "color" => "blue" }
+ Topic.find(topic.id).update_attribute("content", settings)
+ assert_equal(settings, Topic.find(topic.id).content)
+ Topic.serialize(:content)
+ end
+
+ def test_quote
+ content = "\\ \001 ' \n \\n \""
+ topic = Topic.create('content' => content)
+ assert_equal content, Topic.find(topic.id).content
+ end
+end
diff --git a/activerecord/test/class_inheritable_attributes_test.rb b/activerecord/test/class_inheritable_attributes_test.rb
new file mode 100644
index 0000000000..00a6945a66
--- /dev/null
+++ b/activerecord/test/class_inheritable_attributes_test.rb
@@ -0,0 +1,33 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+
+require 'test/unit'
+require 'active_record/support/class_inheritable_attributes'
+
+class A
+ include ClassInheritableAttributes
+end
+
+class B < A
+ write_inheritable_array "first", [ :one, :two ]
+end
+
+class C < A
+ write_inheritable_array "first", [ :three ]
+end
+
+class D < B
+ write_inheritable_array "first", [ :four ]
+end
+
+
+class ClassInheritableAttributesTest < Test::Unit::TestCase
+ def test_first_level
+ assert_equal [ :one, :two ], B.read_inheritable_attribute("first")
+ assert_equal [ :three ], C.read_inheritable_attribute("first")
+ end
+
+ def test_second_level
+ assert_equal [ :one, :two, :four ], D.read_inheritable_attribute("first")
+ assert_equal [ :one, :two ], B.read_inheritable_attribute("first")
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/connections/native_mysql/connection.rb b/activerecord/test/connections/native_mysql/connection.rb
new file mode 100644
index 0000000000..b663106d1f
--- /dev/null
+++ b/activerecord/test/connections/native_mysql/connection.rb
@@ -0,0 +1,24 @@
+print "Using native MySQL\n"
+require 'fixtures/course'
+require 'logger'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+db1 = 'activerecord_unittest'
+db2 = 'activerecord_unittest2'
+
+ActiveRecord::Base.establish_connection(
+ :adapter => "mysql",
+ :host => "localhost",
+ :username => "root",
+ :password => "",
+ :database => db1
+)
+
+Course.establish_connection(
+ :adapter => "mysql",
+ :host => "localhost",
+ :username => "root",
+ :password => "",
+ :database => db2
+)
diff --git a/activerecord/test/connections/native_postgresql/connection.rb b/activerecord/test/connections/native_postgresql/connection.rb
new file mode 100644
index 0000000000..c9b00447f9
--- /dev/null
+++ b/activerecord/test/connections/native_postgresql/connection.rb
@@ -0,0 +1,24 @@
+print "Using native PostgreSQL\n"
+require 'fixtures/course'
+require 'logger'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+db1 = 'activerecord_unittest'
+db2 = 'activerecord_unittest2'
+
+ActiveRecord::Base.establish_connection(
+ :adapter => "postgresql",
+ :host => nil,
+ :username => "postgres",
+ :password => "postgres",
+ :database => db1
+)
+
+Course.establish_connection(
+ :adapter => "postgresql",
+ :host => nil,
+ :username => "postgres",
+ :password => "postgres",
+ :database => db2
+) \ No newline at end of file
diff --git a/activerecord/test/connections/native_sqlite/connection.rb b/activerecord/test/connections/native_sqlite/connection.rb
new file mode 100644
index 0000000000..db688bdb70
--- /dev/null
+++ b/activerecord/test/connections/native_sqlite/connection.rb
@@ -0,0 +1,34 @@
+print "Using native SQlite\n"
+require 'fixtures/course'
+require 'logger'
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+BASE_DIR = File.expand_path(File.dirname(__FILE__) + '/../../fixtures')
+sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite"
+sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite"
+
+def make_connection(clazz, db_file, db_definitions_file)
+ unless File.exist?(db_file)
+ puts "SQLite database not found at #{db_file}. Rebuilding it."
+ sqlite_command = "sqlite #{db_file} 'create table a (a integer); drop table a;'"
+ puts "Executing '#{sqlite_command}'"
+ `#{sqlite_command}`
+ clazz.establish_connection(
+ :adapter => "sqlite",
+ :dbfile => db_file)
+ script = File.read("#{BASE_DIR}/db_definitions/#{db_definitions_file}")
+ # SQLite-Ruby has problems with semi-colon separated commands, so split and execute one at a time
+ script.split(';').each do
+ |command|
+ clazz.connection.execute(command) unless command.strip.empty?
+ end
+ else
+ clazz.establish_connection(
+ :adapter => "sqlite",
+ :dbfile => db_file)
+ end
+end
+
+make_connection(ActiveRecord::Base, sqlite_test_db, 'sqlite.sql')
+make_connection(Course, sqlite_test_db2, 'sqlite2.sql')
+
diff --git a/activerecord/test/connections/native_sqlserver/connection.rb b/activerecord/test/connections/native_sqlserver/connection.rb
new file mode 100644
index 0000000000..b198f21c4b
--- /dev/null
+++ b/activerecord/test/connections/native_sqlserver/connection.rb
@@ -0,0 +1,15 @@
+print "Using native SQLServer\n"
+require 'fixtures/course'
+require 'logger'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+ActiveRecord::Base.establish_connection(
+ :adapter => "sqlserver",
+ :dsn => "DBI:ADO:Provider=SQLOLEDB;Data Source=(local);Initial Catalog=test;User Id=sa;Password=password;"
+)
+
+Course.establish_connection(
+ :adapter => "sqlserver",
+ :dsn => "DBI:ADO:Provider=SQLOLEDB;Data Source=(local);Initial Catalog=test2;User Id=sa;Password=password;"
+)
diff --git a/activerecord/test/deprecated_associations_test.rb b/activerecord/test/deprecated_associations_test.rb
new file mode 100755
index 0000000000..cb3d1aec8a
--- /dev/null
+++ b/activerecord/test/deprecated_associations_test.rb
@@ -0,0 +1,335 @@
+require 'abstract_unit'
+require 'fixtures/developer'
+require 'fixtures/project'
+require 'fixtures/company'
+require 'fixtures/topic'
+# require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'fixtures/reply'
+
+# Can't declare new classes in test case methods, so tests before that
+bad_collection_keys = false
+begin
+ class Car < ActiveRecord::Base; has_many :wheels, :name => "wheels"; end
+rescue ActiveRecord::ActiveRecordError
+ bad_collection_keys = true
+end
+raise "ActiveRecord should have barked on bad collection keys" unless bad_collection_keys
+
+
+class DeprecatedAssociationsTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts", "companies", "accounts", "developers", "projects", "developers_projects", "topics"
+ @signals37 = Firm.find(1)
+ end
+
+ def test_has_many_find
+ assert_equal 2, Firm.find_first.clients.length
+ end
+
+ def test_has_many_orders
+ assert_equal "Summit", Firm.find_first.clients.first.name
+ end
+
+ def test_has_many_class_name
+ assert_equal "Microsoft", Firm.find_first.clients_sorted_desc.first.name
+ end
+
+ def test_has_many_foreign_key
+ assert_equal "Microsoft", Firm.find_first.clients_of_firm.first.name
+ end
+
+ def test_has_many_conditions
+ assert_equal "Microsoft", Firm.find_first.clients_like_ms.first.name
+ end
+
+ def test_has_many_sql
+ firm = Firm.find_first
+ assert_equal "Microsoft", firm.clients_using_sql.first.name
+ assert_equal 1, firm.clients_using_sql_count
+ assert_equal 1, Firm.find_first.clients_using_sql_count
+ end
+
+ def test_has_many_queries
+ assert Firm.find_first.has_clients?
+ firm = Firm.find_first
+ assert_equal 2, firm.clients_count # tests using class count
+ firm.clients
+ assert firm.has_clients?
+ assert_equal 2, firm.clients_count # tests using collection length
+ end
+
+ def test_has_many_dependence
+ assert_equal 2, Client.find_all.length
+ Firm.find_first.destroy
+ assert_equal 0, Client.find_all.length
+ end
+
+ def test_has_many_dependence_with_transaction_support_on_failure
+ assert_equal 2, Client.find_all.length
+
+ firm = Firm.find_first
+ clients = firm.clients
+ clients.last.instance_eval { def before_destroy() raise "Trigger rollback" end }
+
+ firm.destroy rescue "do nothing"
+
+ assert_equal 2, Client.find_all.length
+ end
+
+ def test_has_one_dependence
+ firm = Firm.find(1)
+ assert firm.has_account?
+ firm.destroy
+ assert_equal 1, Account.find_all.length
+ end
+
+ def test_has_one_dependence_with_missing_association
+ Account.destroy_all
+ firm = Firm.find(1)
+ assert !firm.has_account?
+ firm.destroy
+ end
+
+ def test_belongs_to
+ assert_equal @signals37.name, Client.find(3).firm.name
+ assert Client.find(3).has_firm?, "Microsoft should have a firm"
+ # assert !Company.find(1).has_firm?, "37signals shouldn't have a firm"
+ end
+
+ def test_belongs_to_with_different_class_name
+ assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
+ assert Company.find(3).has_firm_with_other_name?, "Microsoft should have a firm"
+ end
+
+ def test_belongs_to_with_condition
+ assert_equal Company.find(1).name, Company.find(3).firm_with_condition.name
+ assert Company.find(3).has_firm_with_condition?, "Microsoft should have a firm"
+ end
+
+
+ def test_belongs_to_equality
+ assert Company.find(3).firm?(Company.find(1)), "Microsoft should have 37signals as firm"
+ assert_raises(RuntimeError) { !Company.find(3).firm?(Company.find(3)) } # "Summit shouldn't have itself as firm"
+ end
+
+ def test_has_one
+ assert @signals37.account?(Account.find(1))
+ assert_equal Account.find(1).credit_limit, @signals37.account.credit_limit
+ assert @signals37.has_account?, "37signals should have an account"
+ assert Account.find(1).firm?(@signals37), "37signals account should be able to backtrack"
+ assert Account.find(1).has_firm?, "37signals account should be able to backtrack"
+
+ assert !Account.find(2).has_firm?, "Unknown isn't linked"
+ assert !Account.find(2).firm?(@signals37), "Unknown isn't linked"
+ end
+
+ def test_has_many_dependence_on_account
+ assert_equal 2, Account.find_all.length
+ @signals37.destroy
+ assert_equal 1, Account.find_all.length
+ end
+
+ def test_find_in
+ assert_equal Client.find(2).name, @signals37.find_in_clients(2).name
+ assert_raises(ActiveRecord::RecordNotFound) { @signals37.find_in_clients(6) }
+ end
+
+ def test_force_reload
+ firm = Firm.new
+ firm.save
+ firm.clients.each {|c|} # forcing to load all clients
+ assert firm.clients.empty?, "New firm shouldn't have client objects"
+ assert !firm.has_clients?, "New firm shouldn't have clients"
+ assert_equal 0, firm.clients_count, "New firm should have 0 clients"
+
+ client = Client.new("firm_id" => firm.id)
+ client.save
+
+ assert firm.clients.empty?, "New firm should have cached no client objects"
+ assert !firm.has_clients?, "New firm should have cached a no-clients response"
+ assert_equal 0, firm.clients_count, "New firm should have cached 0 clients count"
+
+ assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
+ assert firm.has_clients?(true), "New firm should have reloaded with a have-clients response"
+ assert_equal 1, firm.clients_count(true), "New firm should have reloaded clients count"
+ end
+
+ def test_included_in_collection
+ assert @signals37.clients.include?(Client.find(2))
+ end
+
+ def test_build_to_collection
+ assert_equal 1, @signals37.clients_of_firm_count
+ new_client = @signals37.build_to_clients_of_firm("name" => "Another Client")
+ assert_equal "Another Client", new_client.name
+ assert new_client.save
+
+ assert new_client.firm?(@signals37)
+ assert_equal 2, @signals37.clients_of_firm_count(true)
+ end
+
+ def test_create_in_collection
+ assert_equal @signals37.create_in_clients_of_firm("name" => "Another Client"), @signals37.clients_of_firm(true).last
+ end
+
+ def test_succesful_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account("credit_limit" => 1000)
+ assert account.save
+ assert_equal account, firm.account
+ end
+
+ def test_failing_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account
+ assert !account.save
+ assert_equal "can't be empty", account.errors.on("credit_limit")
+ end
+
+ def test_create_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+ assert_equal firm.create_account("credit_limit" => 1000), firm.account
+ end
+
+ def test_has_and_belongs_to_many
+ david = Developer.find(1)
+ assert david.has_projects?
+ assert_equal 2, david.projects_count
+
+ active_record = Project.find(1)
+ assert active_record.has_developers?
+ assert_equal 2, active_record.developers_count
+ assert_equal david.name, active_record.developers.first.name
+ end
+
+ def test_has_and_belongs_to_many_removing
+ david = Developer.find(1)
+ active_record = Project.find(1)
+
+ david.remove_projects(active_record)
+
+ assert_equal 1, david.projects_count
+ assert_equal 1, active_record.developers_count
+ end
+
+ def test_has_and_belongs_to_many_zero
+ david = Developer.find(1)
+ david.remove_projects(Project.find_all)
+
+ assert_equal 0, david.projects_count
+ assert !david.has_projects?
+ end
+
+ def test_has_and_belongs_to_many_adding
+ jamis = Developer.find(2)
+ action_controller = Project.find(2)
+
+ jamis.add_projects(action_controller)
+
+ assert_equal 2, jamis.projects_count
+ assert_equal 2, action_controller.developers_count
+ end
+
+ def test_has_and_belongs_to_many_adding_from_the_project
+ jamis = Developer.find(2)
+ action_controller = Project.find(2)
+
+ action_controller.add_developers(jamis)
+
+ assert_equal 2, jamis.projects_count
+ assert_equal 2, action_controller.developers_count
+ end
+
+ def test_has_and_belongs_to_many_adding_a_collection
+ aridridel = Developer.new("name" => "Aridridel")
+ aridridel.save
+
+ aridridel.add_projects([ Project.find(1), Project.find(2) ])
+ assert_equal 2, aridridel.projects_count
+ end
+
+ def test_belongs_to_counter
+ topic = Topic.create("title" => "Apple", "content" => "hello world")
+ assert_equal 0, topic.send(:read_attribute, "replies_count"), "No replies yet"
+
+ reply = topic.create_in_replies("title" => "I'm saying no!", "content" => "over here")
+ assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count"), "First reply created"
+
+ reply.destroy
+ assert_equal 0, Topic.find(topic.id).send(:read_attribute, "replies_count"), "First reply deleted"
+ end
+
+ def test_natural_assignment_of_has_one
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ apple.account = citibank
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_of_belongs_to
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ citibank.firm = apple
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_of_has_many
+ apple = Firm.create("name" => "Apple")
+ natural = Client.new("name" => "Natural Company")
+ apple.clients << natural
+ assert_equal apple.id, natural.firm_id
+ assert_equal Client.find(natural.id), Firm.find(apple.id).clients.find { |c| c.id == natural.id }
+ apple.clients.delete natural
+ assert_nil Firm.find(apple.id).clients.find { |c| c.id == natural.id }
+ end
+
+
+ def test_natural_adding_of_has_and_belongs_to_many
+ rails = Project.create("name" => "Rails")
+ ap = Project.create("name" => "Action Pack")
+ john = Developer.create("name" => "John")
+ mike = Developer.create("name" => "Mike")
+ rails.developers << john
+ rails.developers << mike
+
+ assert_equal Developer.find(john.id), Project.find(rails.id).developers.find { |d| d.id == john.id }
+ assert_equal Developer.find(mike.id), Project.find(rails.id).developers.find { |d| d.id == mike.id }
+ assert_equal Project.find(rails.id), Developer.find(mike.id).projects.find { |p| p.id == rails.id }
+ assert_equal Project.find(rails.id), Developer.find(john.id).projects.find { |p| p.id == rails.id }
+ ap.developers << john
+ assert_equal Developer.find(john.id), Project.find(ap.id).developers.find { |d| d.id == john.id }
+ assert_equal Project.find(ap.id), Developer.find(john.id).projects.find { |p| p.id == ap.id }
+
+ ap.developers.delete john
+ assert_nil Project.find(ap.id).developers.find { |d| d.id == john.id }
+ assert_nil Developer.find(john.id).projects.find { |p| p.id == ap.id }
+ end
+
+ def test_storing_in_pstore
+ require "pstore"
+ require "tmpdir"
+ apple = Firm.create("name" => "Apple")
+ natural = Client.new("name" => "Natural Company")
+ apple.clients << natural
+
+ db = PStore.new(File.join(Dir.tmpdir, "ar-pstore-association-test"))
+ db.transaction do
+ db["apple"] = apple
+ end
+
+ db = PStore.new(File.join(Dir.tmpdir, "ar-pstore-association-test"))
+ db.transaction do
+ assert_equal "Natural Company", db["apple"].clients.first.name
+ end
+ end
+
+ def test_has_many_find_all
+ assert_equal 2, Firm.find_first.find_all_in_clients("type = 'Client'").length
+ assert_equal 1, Firm.find_first.find_all_in_clients("name = 'Summit'").length
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/finder_test.rb b/activerecord/test/finder_test.rb
new file mode 100755
index 0000000000..d369f6b033
--- /dev/null
+++ b/activerecord/test/finder_test.rb
@@ -0,0 +1,67 @@
+require 'abstract_unit'
+require 'fixtures/company'
+require 'fixtures/topic'
+
+class FinderTest < Test::Unit::TestCase
+ def setup
+ @company_fixtures = create_fixtures("companies")
+ @topic_fixtures = create_fixtures("topics")
+ end
+
+ def test_find
+ assert_equal(@topic_fixtures["first"]["title"], Topic.find(1).title)
+ end
+
+ def test_find_by_ids
+ assert_equal(2, Topic.find(1, 2).length)
+ assert_equal(@topic_fixtures["second"]["title"], Topic.find([ 2 ]).title)
+ end
+
+ def test_find_by_ids_missing_one
+ assert_raises(ActiveRecord::RecordNotFound) {
+ Topic.find(1, 2, 45)
+ }
+ end
+
+ def test_find_with_entire_select_statement
+ topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'"
+
+ assert_equal(1, topics.size)
+ assert_equal(@topic_fixtures["second"]["title"], topics.first.title)
+ end
+
+ def test_find_first
+ first = Topic.find_first "title = 'The First Topic'"
+ assert_equal(@topic_fixtures["first"]["title"], first.title)
+ end
+
+ def test_find_first_failing
+ first = Topic.find_first "title = 'The First Topic!'"
+ assert_nil(first)
+ end
+
+ def test_unexisting_record_exception_handling
+ assert_raises(ActiveRecord::RecordNotFound) {
+ Topic.find(1).parent
+ }
+
+ Topic.find(2).parent
+ end
+
+ def test_find_on_conditions
+ assert Topic.find_on_conditions(1, "approved = 0")
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find_on_conditions(1, "approved = 1") }
+ end
+
+ def test_condition_interpolation
+ assert_kind_of Firm, Company.find_first(["name = '%s'", "37signals"])
+ assert_nil Company.find_first(["name = '%s'", "37signals!"])
+ assert_nil Company.find_first(["name = '%s'", "37signals!' OR 1=1"])
+ assert_kind_of Time, Topic.find_first(["id = %d", 1]).written_on
+ end
+
+ def test_string_sanitation
+ assert_equal "something '' 1=1", ActiveRecord::Base.sanitize("something ' 1=1")
+ assert_equal "something select table", ActiveRecord::Base.sanitize("something; select table")
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/accounts.yml b/activerecord/test/fixtures/accounts.yml
new file mode 100644
index 0000000000..21a0aab52a
--- /dev/null
+++ b/activerecord/test/fixtures/accounts.yml
@@ -0,0 +1,8 @@
+signals37:
+ id: 1
+ firm_id: 1
+ credit_limit: 50
+
+unknown:
+ id: 2
+ credit_limit: 50
diff --git a/activerecord/test/fixtures/auto_id.rb b/activerecord/test/fixtures/auto_id.rb
new file mode 100644
index 0000000000..d720e2be5e
--- /dev/null
+++ b/activerecord/test/fixtures/auto_id.rb
@@ -0,0 +1,4 @@
+class AutoId < ActiveRecord::Base
+ def self.table_name () "auto_id_tests" end
+ def self.primary_key () "auto_id" end
+end
diff --git a/activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char b/activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char
new file mode 100644
index 0000000000..ef27947f27
--- /dev/null
+++ b/activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char
@@ -0,0 +1 @@
+1b => 1
diff --git a/activerecord/test/fixtures/bad_fixtures/attr_with_spaces b/activerecord/test/fixtures/bad_fixtures/attr_with_spaces
new file mode 100644
index 0000000000..46fd6f2fe9
--- /dev/null
+++ b/activerecord/test/fixtures/bad_fixtures/attr_with_spaces
@@ -0,0 +1 @@
+a b => 1
diff --git a/activerecord/test/fixtures/bad_fixtures/blank_line b/activerecord/test/fixtures/bad_fixtures/blank_line
new file mode 100644
index 0000000000..3ea1f71743
--- /dev/null
+++ b/activerecord/test/fixtures/bad_fixtures/blank_line
@@ -0,0 +1,3 @@
+a => 1
+
+b => 2
diff --git a/activerecord/test/fixtures/bad_fixtures/duplicate_attributes b/activerecord/test/fixtures/bad_fixtures/duplicate_attributes
new file mode 100644
index 0000000000..cc0236f26f
--- /dev/null
+++ b/activerecord/test/fixtures/bad_fixtures/duplicate_attributes
@@ -0,0 +1,3 @@
+a => 1
+b => 2
+a => 3
diff --git a/activerecord/test/fixtures/bad_fixtures/missing_value b/activerecord/test/fixtures/bad_fixtures/missing_value
new file mode 100644
index 0000000000..fb59ec33e8
--- /dev/null
+++ b/activerecord/test/fixtures/bad_fixtures/missing_value
@@ -0,0 +1 @@
+a =>
diff --git a/activerecord/test/fixtures/column_name.rb b/activerecord/test/fixtures/column_name.rb
new file mode 100644
index 0000000000..ec07205a3a
--- /dev/null
+++ b/activerecord/test/fixtures/column_name.rb
@@ -0,0 +1,3 @@
+class ColumnName < ActiveRecord::Base
+ def self.table_name () "colnametests" end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/companies/first_client b/activerecord/test/fixtures/companies/first_client
new file mode 100755
index 0000000000..800c694eeb
--- /dev/null
+++ b/activerecord/test/fixtures/companies/first_client
@@ -0,0 +1,6 @@
+id => 2
+type => Client
+firm_id => 1
+client_of => 2
+name => Summit
+ruby_type => Client
diff --git a/activerecord/test/fixtures/companies/first_firm b/activerecord/test/fixtures/companies/first_firm
new file mode 100755
index 0000000000..22d876dad1
--- /dev/null
+++ b/activerecord/test/fixtures/companies/first_firm
@@ -0,0 +1,4 @@
+id => 1
+type => Firm
+name => 37signals
+ruby_type => Firm
diff --git a/activerecord/test/fixtures/companies/second_client b/activerecord/test/fixtures/companies/second_client
new file mode 100755
index 0000000000..69f8adc49c
--- /dev/null
+++ b/activerecord/test/fixtures/companies/second_client
@@ -0,0 +1,6 @@
+id => 3
+type => Client
+firm_id => 1
+client_of => 1
+name => Microsoft
+ruby_type => Client
diff --git a/activerecord/test/fixtures/company.rb b/activerecord/test/fixtures/company.rb
new file mode 100755
index 0000000000..b5ee055948
--- /dev/null
+++ b/activerecord/test/fixtures/company.rb
@@ -0,0 +1,37 @@
+class Company < ActiveRecord::Base
+ attr_protected :rating
+end
+
+
+class Firm < Company
+ has_many :clients, :order => "id", :dependent => true
+ has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC"
+ has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id"
+ has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id"
+ has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}'
+
+ has_one :account, :dependent => true
+end
+
+class Client < Company
+ belongs_to :firm, :foreign_key => "client_of"
+ belongs_to :firm_with_basic_id, :class_name => "Firm", :foreign_key => "firm_id"
+ belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of"
+ belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => "1 = 1"
+end
+
+
+class SpecialClient < Client
+end
+
+class VerySpecialClient < SpecialClient
+end
+
+class Account < ActiveRecord::Base
+ belongs_to :firm
+
+ protected
+ def validate
+ errors.add_on_empty "credit_limit"
+ end
+end
diff --git a/activerecord/test/fixtures/company_in_module.rb b/activerecord/test/fixtures/company_in_module.rb
new file mode 100644
index 0000000000..a484ed5eaf
--- /dev/null
+++ b/activerecord/test/fixtures/company_in_module.rb
@@ -0,0 +1,47 @@
+module MyApplication
+ module Business
+ class Company < ActiveRecord::Base
+ attr_protected :rating
+ end
+
+ class Firm < Company
+ has_many :clients, :order => "id", :dependent => true
+ has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC"
+ has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id"
+ has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id"
+ has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}'
+
+ has_one :account, :dependent => true
+ end
+
+ class Client < Company
+ belongs_to :firm, :foreign_key => "client_of"
+ belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of"
+ end
+
+ class Developer < ActiveRecord::Base
+ has_and_belongs_to_many :projects
+
+ protected
+ def validate
+ errors.add_on_boundry_breaking("name", 3..20)
+ end
+ end
+
+ class Project < ActiveRecord::Base
+ has_and_belongs_to_many :developers
+ end
+
+ end
+
+ module Billing
+ class Account < ActiveRecord::Base
+ belongs_to :firm, :class_name => "MyApplication::Business::Firm"
+
+ protected
+ def validate
+ errors.add_on_empty "credit_limit"
+ end
+ end
+ end
+end
diff --git a/activerecord/test/fixtures/course.rb b/activerecord/test/fixtures/course.rb
new file mode 100644
index 0000000000..8a40fa740d
--- /dev/null
+++ b/activerecord/test/fixtures/course.rb
@@ -0,0 +1,3 @@
+class Course < ActiveRecord::Base
+ has_many :entrants
+end
diff --git a/activerecord/test/fixtures/courses/java b/activerecord/test/fixtures/courses/java
new file mode 100644
index 0000000000..84b10d390b
--- /dev/null
+++ b/activerecord/test/fixtures/courses/java
@@ -0,0 +1,2 @@
+id => 2
+name => Java Development
diff --git a/activerecord/test/fixtures/courses/ruby b/activerecord/test/fixtures/courses/ruby
new file mode 100644
index 0000000000..db42f96d27
--- /dev/null
+++ b/activerecord/test/fixtures/courses/ruby
@@ -0,0 +1,2 @@
+id => 1
+name => Ruby Development
diff --git a/activerecord/test/fixtures/customer.rb b/activerecord/test/fixtures/customer.rb
new file mode 100644
index 0000000000..5275a5209d
--- /dev/null
+++ b/activerecord/test/fixtures/customer.rb
@@ -0,0 +1,30 @@
+class Customer < ActiveRecord::Base
+ composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ]
+ composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
+end
+
+class Address
+ attr_reader :street, :city, :country
+
+ def initialize(street, city, country)
+ @street, @city, @country = street, city, country
+ end
+
+ def close_to?(other_address)
+ city == other_address.city && country == other_address.country
+ end
+end
+
+class Money
+ attr_reader :amount, :currency
+
+ EXCHANGE_RATES = { "USD_TO_DKK" => 6, "DKK_TO_USD" => 0.6 }
+
+ def initialize(amount, currency = "USD")
+ @amount, @currency = amount, currency
+ end
+
+ def exchange_to(other_currency)
+ Money.new((amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor, other_currency)
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/customers/david b/activerecord/test/fixtures/customers/david
new file mode 100644
index 0000000000..69b9c32376
--- /dev/null
+++ b/activerecord/test/fixtures/customers/david
@@ -0,0 +1,6 @@
+id => 1
+name => David
+balance => 50
+address_street => Funny Street
+address_city => Scary Town
+address_country => Loony Land \ No newline at end of file
diff --git a/activerecord/test/fixtures/db_definitions/mysql.sql b/activerecord/test/fixtures/db_definitions/mysql.sql
new file mode 100755
index 0000000000..766c0ec71f
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/mysql.sql
@@ -0,0 +1,97 @@
+CREATE TABLE `accounts` (
+ `id` int(11) NOT NULL auto_increment,
+ `firm_id` int(11) default NULL,
+ `credit_limit` int(5) default NULL,
+ PRIMARY KEY (`id`)
+) TYPE=InnoDB;
+
+CREATE TABLE `companies` (
+ `id` int(11) NOT NULL auto_increment,
+ `type` varchar(50) default NULL,
+ `ruby_type` varchar(50) default NULL,
+ `firm_id` int(11) default NULL,
+ `name` varchar(50) default NULL,
+ `client_of` int(11) default NULL,
+ `rating` int(11) default NULL default 1,
+ PRIMARY KEY (`id`)
+) TYPE=InnoDB;
+
+
+CREATE TABLE `topics` (
+ `id` int(11) NOT NULL auto_increment,
+ `title` varchar(255) default NULL,
+ `author_name` varchar(255) default NULL,
+ `author_email_address` varchar(255) default NULL,
+ `written_on` datetime default NULL,
+ `last_read` date default NULL,
+ `content` text,
+ `approved` tinyint(1) default 1,
+ `replies_count` int(11) default 0,
+ `parent_id` int(11) default NULL,
+ `type` varchar(50) default NULL,
+ PRIMARY KEY (`id`)
+) TYPE=InnoDB;
+
+CREATE TABLE `developers` (
+ `id` int(11) NOT NULL auto_increment,
+ `name` varchar(100) default NULL,
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `projects` (
+ `id` int(11) NOT NULL auto_increment,
+ `name` varchar(100) default NULL,
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `developers_projects` (
+ `developer_id` int(11) NOT NULL,
+ `project_id` int(11) NOT NULL,
+ `joined_on` date default NULL
+);
+
+CREATE TABLE `customers` (
+ `id` int(11) NOT NULL auto_increment,
+ `name` varchar(100) default NULL,
+ `balance` int(6) default 0,
+ `address_street` varchar(100) default NULL,
+ `address_city` varchar(100) default NULL,
+ `address_country` varchar(100) default NULL,
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `movies` (
+ `movieid` int(11) NOT NULL auto_increment,
+ `name` varchar(100) default NULL,
+ PRIMARY KEY (`movieid`)
+);
+
+CREATE TABLE `subscribers` (
+ `nick` varchar(100) NOT NULL,
+ `name` varchar(100) default NULL,
+ PRIMARY KEY (`nick`)
+);
+
+CREATE TABLE `booleantests` (
+ `id` int(11) NOT NULL auto_increment,
+ `value` integer default NULL,
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `auto_id_tests` (
+ `auto_id` int(11) NOT NULL auto_increment,
+ `value` integer default NULL,
+ PRIMARY KEY (`auto_id`)
+);
+
+CREATE TABLE `entrants` (
+ `id` INTEGER NOT NULL PRIMARY KEY,
+ `name` VARCHAR(255) NOT NULL,
+ `course_id` INTEGER NOT NULL
+);
+
+CREATE TABLE `colnametests` (
+ `id` int(11) NOT NULL auto_increment,
+ `references` int(11) NOT NULL,
+ PRIMARY KEY (`id`)
+);
diff --git a/activerecord/test/fixtures/db_definitions/mysql2.sql b/activerecord/test/fixtures/db_definitions/mysql2.sql
new file mode 100644
index 0000000000..0a16a0a2f9
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/mysql2.sql
@@ -0,0 +1,4 @@
+CREATE TABLE `courses` (
+ `id` INTEGER NOT NULL PRIMARY KEY,
+ `name` VARCHAR(255) NOT NULL
+);
diff --git a/activerecord/test/fixtures/db_definitions/postgresql.sql b/activerecord/test/fixtures/db_definitions/postgresql.sql
new file mode 100644
index 0000000000..e83356627b
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/postgresql.sql
@@ -0,0 +1,114 @@
+SET search_path = public, pg_catalog;
+
+CREATE TABLE accounts (
+ id serial,
+ firm_id integer,
+ credit_limit integer,
+ PRIMARY KEY (id)
+);
+SELECT setval('accounts_id_seq', 100);
+
+CREATE TABLE companies (
+ id serial,
+ "type" character varying(50),
+ "ruby_type" character varying(50),
+ firm_id integer,
+ name character varying(50),
+ client_of integer,
+ rating integer default 1,
+ PRIMARY KEY (id)
+);
+SELECT setval('companies_id_seq', 100);
+
+CREATE TABLE developers_projects (
+ developer_id integer NOT NULL,
+ project_id integer NOT NULL,
+ joined_on date
+);
+
+CREATE TABLE developers (
+ id serial,
+ name character varying(100),
+ PRIMARY KEY (id)
+);
+SELECT setval('developers_id_seq', 100);
+
+CREATE TABLE projects (
+ id serial,
+ name character varying(100),
+ PRIMARY KEY (id)
+);
+SELECT setval('projects_id_seq', 100);
+
+CREATE TABLE topics (
+ id serial,
+ title character varying(255),
+ author_name character varying(255),
+ author_email_address character varying(255),
+ written_on timestamp without time zone,
+ last_read date,
+ content text,
+ replies_count integer default 0,
+ parent_id integer,
+ "type" character varying(50),
+ approved smallint DEFAULT 1,
+ PRIMARY KEY (id)
+);
+SELECT setval('topics_id_seq', 100);
+
+CREATE TABLE customers (
+ id serial,
+ name character varying,
+ balance integer default 0,
+ address_street character varying,
+ address_city character varying,
+ address_country character varying,
+ PRIMARY KEY (id)
+);
+SELECT setval('customers_id_seq', 100);
+
+CREATE TABLE movies (
+ movieid serial,
+ name text,
+ PRIMARY KEY (movieid)
+);
+
+CREATE TABLE subscribers (
+ nick text NOT NULL,
+ name text,
+ PRIMARY KEY (nick)
+);
+
+CREATE TABLE booleantests (
+ id serial,
+ value boolean,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE defaults (
+ id serial,
+ modified_date date default CURRENT_DATE,
+ fixed_date date default '2004-01-01',
+ modified_time timestamp default CURRENT_TIMESTAMP,
+ fixed_time timestamp default '2004-01-01 00:00:00.000000-00',
+ char1 char(1) default 'Y',
+ char2 character varying(50) default 'a varchar field',
+ char3 text default 'a text field'
+);
+
+CREATE TABLE auto_id_tests (
+ auto_id serial,
+ value integer,
+ PRIMARY KEY (auto_id)
+);
+
+CREATE TABLE entrants (
+ id serial,
+ name text,
+ course_id integer
+);
+
+CREATE TABLE colnametests (
+ id serial,
+ "references" integer NOT NULL
+);
diff --git a/activerecord/test/fixtures/db_definitions/postgresql2.sql b/activerecord/test/fixtures/db_definitions/postgresql2.sql
new file mode 100644
index 0000000000..b58a45eff7
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/postgresql2.sql
@@ -0,0 +1,4 @@
+CREATE TABLE courses (
+ id serial,
+ name text
+); \ No newline at end of file
diff --git a/activerecord/test/fixtures/db_definitions/sqlite.sql b/activerecord/test/fixtures/db_definitions/sqlite.sql
new file mode 100644
index 0000000000..cb617305dc
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/sqlite.sql
@@ -0,0 +1,86 @@
+CREATE TABLE 'accounts' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'firm_id' INTEGER DEFAULT NULL,
+ 'credit_limit' INTEGER DEFAULT NULL
+);
+
+CREATE TABLE 'companies' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'type' VARCHAR(255) DEFAULT NULL,
+ 'ruby_type' VARCHAR(255) DEFAULT NULL,
+ 'firm_id' INTEGER DEFAULT NULL,
+ 'name' TEXT DEFAULT NULL,
+ 'client_of' INTEGER DEFAULT NULL,
+ 'rating' INTEGER DEFAULT 1
+);
+
+
+CREATE TABLE 'topics' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'title' VARCHAR(255) DEFAULT NULL,
+ 'author_name' VARCHAR(255) DEFAULT NULL,
+ 'author_email_address' VARCHAR(255) DEFAULT NULL,
+ 'written_on' DATETIME DEFAULT NULL,
+ 'last_read' DATE DEFAULT NULL,
+ 'content' TEXT,
+ 'approved' INTEGER DEFAULT 1,
+ 'replies_count' INTEGER DEFAULT 0,
+ 'parent_id' INTEGER DEFAULT NULL,
+ 'type' VARCHAR(255) DEFAULT NULL
+);
+
+CREATE TABLE 'developers' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'name' TEXT DEFAULT NULL
+);
+
+CREATE TABLE 'projects' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'name' TEXT DEFAULT NULL
+);
+
+CREATE TABLE 'developers_projects' (
+ 'developer_id' INTEGER NOT NULL,
+ 'project_id' INTEGER NOT NULL,
+ 'joined_on' DATE DEFAULT NULL
+);
+
+CREATE TABLE 'customers' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'name' VARCHAR(255) DEFAULT NULL,
+ 'balance' INTEGER DEFAULT 0,
+ 'address_street' TEXT DEFAULT NULL,
+ 'address_city' TEXT DEFAULT NULL,
+ 'address_country' TEXT DEFAULT NULL
+);
+
+CREATE TABLE 'movies' (
+ 'movieid' INTEGER PRIMARY KEY NOT NULL,
+ 'name' VARCHAR(255) DEFAULT NULL
+);
+
+CREATE TABLE subscribers (
+ 'nick' VARCHAR(255) PRIMARY KEY NOT NULL,
+ 'name' VARCHAR(255) DEFAULT NULL
+);
+
+CREATE TABLE 'booleantests' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'value' INTEGER DEFAULT NULL
+);
+
+CREATE TABLE 'auto_id_tests' (
+ 'auto_id' INTEGER PRIMARY KEY NOT NULL,
+ 'value' INTEGER DEFAULT NULL
+);
+
+CREATE TABLE 'entrants' (
+ 'id' INTEGER NOT NULL PRIMARY KEY,
+ 'name' VARCHAR(255) NOT NULL,
+ 'course_id' INTEGER NOT NULL
+);
+
+CREATE TABLE 'colnametests' (
+ 'id' INTEGER NOT NULL PRIMARY KEY,
+ 'references' INTEGER NOT NULL
+);
diff --git a/activerecord/test/fixtures/db_definitions/sqlite2.sql b/activerecord/test/fixtures/db_definitions/sqlite2.sql
new file mode 100644
index 0000000000..19b123968a
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/sqlite2.sql
@@ -0,0 +1,4 @@
+CREATE TABLE 'courses' (
+ 'id' INTEGER NOT NULL PRIMARY KEY,
+ 'name' VARCHAR(255) NOT NULL
+);
diff --git a/activerecord/test/fixtures/db_definitions/sqlserver.sql b/activerecord/test/fixtures/db_definitions/sqlserver.sql
new file mode 100644
index 0000000000..0ae9780273
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/sqlserver.sql
@@ -0,0 +1,96 @@
+CREATE TABLE accounts (
+ id int NOT NULL IDENTITY(1, 1),
+ firm_id int default NULL,
+ credit_limit int default NULL,
+ PRIMARY KEY (id)
+)
+
+CREATE TABLE companies (
+ id int NOT NULL IDENTITY(1, 1),
+ type varchar(50) default NULL,
+ ruby_type varchar(50) default NULL,
+ firm_id int default NULL,
+ name varchar(50) default NULL,
+ client_of int default NULL,
+ companies_count int default 0,
+ rating int default 1,
+ PRIMARY KEY (id)
+)
+
+CREATE TABLE topics (
+ id int NOT NULL IDENTITY(1, 1),
+ title varchar(255) default NULL,
+ author_name varchar(255) default NULL,
+ author_email_address varchar(255) default NULL,
+ written_on datetime default NULL,
+ last_read datetime default NULL,
+ content text,
+ approved tinyint default 1,
+ replies_count int default 0,
+ parent_id int default NULL,
+ type varchar(50) default NULL,
+ PRIMARY KEY (id)
+)
+
+CREATE TABLE developers (
+ id int NOT NULL IDENTITY(1, 1),
+ name varchar(100) default NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE projects (
+ id int NOT NULL IDENTITY(1, 1),
+ name varchar(100) default NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE developers_projects (
+ developer_id int NOT NULL,
+ project_id int NOT NULL
+);
+
+CREATE TABLE customers (
+ id int NOT NULL IDENTITY(1, 1),
+ name varchar(100) default NULL,
+ balance int default 0,
+ address_street varchar(100) default NULL,
+ address_city varchar(100) default NULL,
+ address_country varchar(100) default NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE movies (
+ movieid int NOT NULL IDENTITY(1, 1),
+ name varchar(100) default NULL,
+ PRIMARY KEY (movieid)
+);
+
+CREATE TABLE subscribers (
+ nick varchar(100) NOT NULL,
+ name varchar(100) default NULL,
+ PRIMARY KEY (nick)
+);
+
+CREATE TABLE booleantests (
+ id int NOT NULL IDENTITY(1, 1),
+ value integer default NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE auto_id_tests (
+ auto_id int NOT NULL IDENTITY(1, 1),
+ value int default NULL,
+ PRIMARY KEY (auto_id)
+);
+
+CREATE TABLE entrants (
+ id int NOT NULL PRIMARY KEY,
+ name varchar(255) NOT NULL,
+ course_id int NOT NULL
+);
+
+CREATE TABLE colnametests (
+ id int NOT NULL IDENTITY(1, 1),
+ [references] int NOT NULL,
+ PRIMARY KEY (id)
+); \ No newline at end of file
diff --git a/activerecord/test/fixtures/db_definitions/sqlserver2.sql b/activerecord/test/fixtures/db_definitions/sqlserver2.sql
new file mode 100644
index 0000000000..dc4f9ed364
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/sqlserver2.sql
@@ -0,0 +1,4 @@
+CREATE TABLE courses (
+ id int NOT NULL PRIMARY KEY,
+ name varchar(255) NOT NULL
+);
diff --git a/activerecord/test/fixtures/default.rb b/activerecord/test/fixtures/default.rb
new file mode 100644
index 0000000000..887e9cc999
--- /dev/null
+++ b/activerecord/test/fixtures/default.rb
@@ -0,0 +1,2 @@
+class Default < ActiveRecord::Base
+end
diff --git a/activerecord/test/fixtures/developer.rb b/activerecord/test/fixtures/developer.rb
new file mode 100644
index 0000000000..737fc3824b
--- /dev/null
+++ b/activerecord/test/fixtures/developer.rb
@@ -0,0 +1,8 @@
+class Developer < ActiveRecord::Base
+ has_and_belongs_to_many :projects
+
+ protected
+ def validate
+ errors.add_on_boundry_breaking("name", 3..20)
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/developers.yml b/activerecord/test/fixtures/developers.yml
new file mode 100644
index 0000000000..733455f789
--- /dev/null
+++ b/activerecord/test/fixtures/developers.yml
@@ -0,0 +1,13 @@
+david:
+ id: 1
+ name: David
+
+jamis:
+ id: 2
+ name: Jamis
+
+<% for digit in 3..10 %>
+dev_<%= digit %>:
+ id: <%= digit %>
+ name: fixture_<%= digit %>
+<% end %> \ No newline at end of file
diff --git a/activerecord/test/fixtures/developers_projects/david_action_controller b/activerecord/test/fixtures/developers_projects/david_action_controller
new file mode 100644
index 0000000000..e6e9d0e59b
--- /dev/null
+++ b/activerecord/test/fixtures/developers_projects/david_action_controller
@@ -0,0 +1,3 @@
+developer_id => 1
+project_id => 2
+joined_on => 2004-10-10 \ No newline at end of file
diff --git a/activerecord/test/fixtures/developers_projects/david_active_record b/activerecord/test/fixtures/developers_projects/david_active_record
new file mode 100644
index 0000000000..2ef474c10d
--- /dev/null
+++ b/activerecord/test/fixtures/developers_projects/david_active_record
@@ -0,0 +1,3 @@
+developer_id => 1
+project_id => 1
+joined_on => 2004-10-10 \ No newline at end of file
diff --git a/activerecord/test/fixtures/developers_projects/jamis_active_record b/activerecord/test/fixtures/developers_projects/jamis_active_record
new file mode 100644
index 0000000000..91beb80797
--- /dev/null
+++ b/activerecord/test/fixtures/developers_projects/jamis_active_record
@@ -0,0 +1,2 @@
+developer_id => 2
+project_id => 1 \ No newline at end of file
diff --git a/activerecord/test/fixtures/entrant.rb b/activerecord/test/fixtures/entrant.rb
new file mode 100644
index 0000000000..4682ce48c8
--- /dev/null
+++ b/activerecord/test/fixtures/entrant.rb
@@ -0,0 +1,3 @@
+class Entrant < ActiveRecord::Base
+ belongs_to :course
+end
diff --git a/activerecord/test/fixtures/entrants/first b/activerecord/test/fixtures/entrants/first
new file mode 100644
index 0000000000..e45cd6c1c2
--- /dev/null
+++ b/activerecord/test/fixtures/entrants/first
@@ -0,0 +1,3 @@
+id => 1
+course_id => 1
+name => Ruby Developer
diff --git a/activerecord/test/fixtures/entrants/second b/activerecord/test/fixtures/entrants/second
new file mode 100644
index 0000000000..38cd702476
--- /dev/null
+++ b/activerecord/test/fixtures/entrants/second
@@ -0,0 +1,3 @@
+id => 2
+course_id => 1
+name => Ruby Guru
diff --git a/activerecord/test/fixtures/entrants/third b/activerecord/test/fixtures/entrants/third
new file mode 100644
index 0000000000..594ac77af0
--- /dev/null
+++ b/activerecord/test/fixtures/entrants/third
@@ -0,0 +1,3 @@
+id => 3
+course_id => 2
+name => Java Lover
diff --git a/activerecord/test/fixtures/movie.rb b/activerecord/test/fixtures/movie.rb
new file mode 100644
index 0000000000..6384b4c801
--- /dev/null
+++ b/activerecord/test/fixtures/movie.rb
@@ -0,0 +1,5 @@
+class Movie < ActiveRecord::Base
+ def self.primary_key
+ "movieid"
+ end
+end
diff --git a/activerecord/test/fixtures/movies/first b/activerecord/test/fixtures/movies/first
new file mode 100644
index 0000000000..0feaeac7b0
--- /dev/null
+++ b/activerecord/test/fixtures/movies/first
@@ -0,0 +1,2 @@
+movieid => 1
+name => Terminator
diff --git a/activerecord/test/fixtures/movies/second b/activerecord/test/fixtures/movies/second
new file mode 100644
index 0000000000..b3c506b7da
--- /dev/null
+++ b/activerecord/test/fixtures/movies/second
@@ -0,0 +1,2 @@
+movieid => 2
+name => Gladiator
diff --git a/activerecord/test/fixtures/project.rb b/activerecord/test/fixtures/project.rb
new file mode 100644
index 0000000000..1ccf39d7cf
--- /dev/null
+++ b/activerecord/test/fixtures/project.rb
@@ -0,0 +1,4 @@
+class Project < ActiveRecord::Base
+ has_and_belongs_to_many :developers, :uniq => true
+ has_and_belongs_to_many :developers_named_david, :class_name => "Developer", :conditions => "name = 'David'", :uniq => true
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/projects/action_controller b/activerecord/test/fixtures/projects/action_controller
new file mode 100644
index 0000000000..b3f00ae727
--- /dev/null
+++ b/activerecord/test/fixtures/projects/action_controller
@@ -0,0 +1,2 @@
+id => 2
+name => Active Controller \ No newline at end of file
diff --git a/activerecord/test/fixtures/projects/active_record b/activerecord/test/fixtures/projects/active_record
new file mode 100644
index 0000000000..31131a7f30
--- /dev/null
+++ b/activerecord/test/fixtures/projects/active_record
@@ -0,0 +1,2 @@
+id => 1
+name => Active Record \ No newline at end of file
diff --git a/activerecord/test/fixtures/reply.rb b/activerecord/test/fixtures/reply.rb
new file mode 100755
index 0000000000..51dfe21d2d
--- /dev/null
+++ b/activerecord/test/fixtures/reply.rb
@@ -0,0 +1,21 @@
+class Reply < Topic
+ belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true
+
+ attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read
+
+ def validate
+ errors.add("title", "Empty") unless attribute_present? "title"
+ errors.add("content", "Empty") unless attribute_present? "content"
+ end
+
+ def validate_on_create
+ errors.add("title", "is Wrong Create") if attribute_present?("title") && title == "Wrong Create"
+ if attribute_present?("title") && attribute_present?("content") && content == "Mismatch"
+ errors.add("title", "is Content Mismatch")
+ end
+ end
+
+ def validate_on_update
+ errors.add("title", "is Wrong Update") if attribute_present?("title") && title == "Wrong Update"
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/subscriber.rb b/activerecord/test/fixtures/subscriber.rb
new file mode 100644
index 0000000000..3f1ade0d83
--- /dev/null
+++ b/activerecord/test/fixtures/subscriber.rb
@@ -0,0 +1,5 @@
+class Subscriber < ActiveRecord::Base
+ def self.primary_key
+ "nick"
+ end
+end
diff --git a/activerecord/test/fixtures/subscribers/first b/activerecord/test/fixtures/subscribers/first
new file mode 100644
index 0000000000..5287e26e4d
--- /dev/null
+++ b/activerecord/test/fixtures/subscribers/first
@@ -0,0 +1,2 @@
+nick => alterself
+name => Luke Holden
diff --git a/activerecord/test/fixtures/subscribers/second b/activerecord/test/fixtures/subscribers/second
new file mode 100644
index 0000000000..2345e4475a
--- /dev/null
+++ b/activerecord/test/fixtures/subscribers/second
@@ -0,0 +1,2 @@
+nick => webster132
+name => David Heinemeier Hansson
diff --git a/activerecord/test/fixtures/topic.rb b/activerecord/test/fixtures/topic.rb
new file mode 100755
index 0000000000..55c94e9e88
--- /dev/null
+++ b/activerecord/test/fixtures/topic.rb
@@ -0,0 +1,20 @@
+class Topic < ActiveRecord::Base
+ has_many :replies, :foreign_key => "parent_id"
+ serialize :content
+
+ before_create :default_written_on
+ before_destroy :destroy_children #'self.class.delete_all "parent_id = #{id}"'
+
+ def parent
+ self.class.find(parent_id)
+ end
+
+ protected
+ def default_written_on
+ self.written_on = Time.now unless attribute_present?("written_on")
+ end
+
+ def destroy_children
+ self.class.delete_all "parent_id = #{id}"
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/topics/first b/activerecord/test/fixtures/topics/first
new file mode 100755
index 0000000000..9972a578c8
--- /dev/null
+++ b/activerecord/test/fixtures/topics/first
@@ -0,0 +1,9 @@
+id => 1
+title => The First Topic
+author_name => David
+author_email_address => david@loudthinking.com
+written_on => 2003-07-16 15:28
+last_read => 2004-04-15
+content => Have a nice day
+approved => 0
+replies_count => 0 \ No newline at end of file
diff --git a/activerecord/test/fixtures/topics/second b/activerecord/test/fixtures/topics/second
new file mode 100755
index 0000000000..f669b4fef4
--- /dev/null
+++ b/activerecord/test/fixtures/topics/second
@@ -0,0 +1,8 @@
+id => 2
+title => The Second Topic's of the day
+author_name => Mary
+written_on => 2003-07-15 15:28
+content => Have a great day!
+approved => 1
+replies_count => 2
+parent_id => 1 \ No newline at end of file
diff --git a/activerecord/test/fixtures_test.rb b/activerecord/test/fixtures_test.rb
new file mode 100755
index 0000000000..015b6ababe
--- /dev/null
+++ b/activerecord/test/fixtures_test.rb
@@ -0,0 +1,84 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/developer'
+require 'fixtures/company'
+
+class FixturesTest < Test::Unit::TestCase
+ fixtures :topics, :developers, :accounts, :developers
+
+ FIXTURES = %w( accounts companies customers
+ developers developers_projects entrants
+ movies projects subscribers topics )
+ MATCH_ATTRIBUTE_NAME = /[a-zA-Z][-_\w]*/
+
+ def test_clean_fixtures
+ FIXTURES.each do |name|
+ fixtures = nil
+ assert_nothing_raised { fixtures = create_fixtures(name) }
+ assert_kind_of(Fixtures, fixtures)
+ fixtures.each { |name, fixture|
+ fixture.each { |key, value|
+ assert_match(MATCH_ATTRIBUTE_NAME, key)
+ }
+ }
+ end
+ end
+
+ def test_multiple_clean_fixtures
+ fixtures_array = nil
+ assert_nothing_raised { fixtures_array = create_fixtures(*FIXTURES) }
+ assert_kind_of(Array, fixtures_array)
+ fixtures_array.each { |fixtures| assert_kind_of(Fixtures, fixtures) }
+ end
+
+ def test_attributes
+ topics = create_fixtures("topics")
+ assert_equal("The First Topic", topics["first"]["title"])
+ assert_nil(topics["second"]["author_email_address"])
+ end
+
+ def test_inserts
+ topics = create_fixtures("topics")
+ firstRow = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'David'")
+ assert_equal("The First Topic", firstRow["title"])
+
+ secondRow = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'Mary'")
+ assert_nil(secondRow["author_email_address"])
+ end
+
+ def test_bad_format
+ path = File.join(File.dirname(__FILE__), 'fixtures', 'bad_fixtures')
+ Dir.entries(path).each do |file|
+ next unless File.file?(file) and file !~ %r(^.|.yaml$)
+ assert_raise(Fixture::FormatError) {
+ Fixture.new(bad_fixtures_path, file)
+ }
+ end
+ end
+
+ def test_logger_level_invariant
+ level = ActiveRecord::Base.logger.level
+ create_fixtures('topics')
+ assert_equal level, ActiveRecord::Base.logger.level
+ end
+
+ def test_instantiation
+ topics = create_fixtures("topics")
+ assert_kind_of Topic, topics["first"].find
+ end
+
+ def test_complete_instantiation
+ assert_equal 2, @topics.size
+ assert_equal "The First Topic", @first.title
+ end
+
+ def test_fixtures_from_root_yml_with_instantiation
+ # assert_equal 2, @accounts.size
+ assert_equal 50, @unknown.credit_limit
+ end
+
+ def test_erb_in_fixtures
+ assert_equal 10, @developers.size
+ assert_equal "fixture_5", @dev_5.name
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/inflector_test.rb b/activerecord/test/inflector_test.rb
new file mode 100644
index 0000000000..4665558e74
--- /dev/null
+++ b/activerecord/test/inflector_test.rb
@@ -0,0 +1,121 @@
+require 'abstract_unit'
+
+class InflectorTest < Test::Unit::TestCase
+ SingularToPlural = {
+ "search" => "searches",
+ "switch" => "switches",
+ "fix" => "fixes",
+ "box" => "boxes",
+ "process" => "processes",
+ "address" => "addresses",
+ "case" => "cases",
+ "stack" => "stacks",
+
+ "category" => "categories",
+ "query" => "queries",
+ "ability" => "abilities",
+ "agency" => "agencies",
+
+ "wife" => "wives",
+ "safe" => "saves",
+ "half" => "halves",
+
+ "salesperson" => "salespeople",
+ "person" => "people",
+
+ "spokesman" => "spokesmen",
+ "man" => "men",
+ "woman" => "women",
+
+ "basis" => "bases",
+ "diagnosis" => "diagnoses",
+
+ "datum" => "data",
+ "medium" => "media",
+
+ "node_child" => "node_children",
+ "child" => "children",
+
+ "experience" => "experiences",
+ "day" => "days",
+
+ "comment" => "comments",
+ "foobar" => "foobars"
+ }
+
+ CamelToUnderscore = {
+ "Product" => "product",
+ "SpecialGuest" => "special_guest",
+ "AbstractApplicationController" => "abstract_application_controller"
+ }
+
+ ClassNameToForeignKeyWithUnderscore = {
+ "Person" => "person_id",
+ "MyApplication::Billing::Account" => "account_id"
+ }
+
+ ClassNameToForeignKeyWithoutUnderscore = {
+ "Person" => "personid",
+ "MyApplication::Billing::Account" => "accountid"
+ }
+
+ ClassNameToTableName = {
+ "PrimarySpokesman" => "primary_spokesmen",
+ "NodeChild" => "node_children"
+ }
+
+ def test_pluralize
+ SingularToPlural.each do |singular, plural|
+ assert_equal(plural, Inflector.pluralize(singular))
+ end
+
+ assert_equal("plurals", Inflector.pluralize("plurals"))
+ end
+
+ def test_singularize
+ SingularToPlural.each do |singular, plural|
+ assert_equal(singular, Inflector.singularize(plural))
+ end
+ end
+
+ def test_camelize
+ CamelToUnderscore.each do |camel, underscore|
+ assert_equal(camel, Inflector.camelize(underscore))
+ end
+ end
+
+ def test_underscore
+ CamelToUnderscore.each do |camel, underscore|
+ assert_equal(underscore, Inflector.underscore(camel))
+ end
+
+ assert_equal "html_tidy", Inflector.underscore("HTMLTidy")
+ assert_equal "html_tidy_generator", Inflector.underscore("HTMLTidyGenerator")
+ end
+
+ def test_demodulize
+ assert_equal "Account", Inflector.demodulize("MyApplication::Billing::Account")
+ end
+
+ def test_foreign_key
+ ClassNameToForeignKeyWithUnderscore.each do |klass, foreign_key|
+ assert_equal(foreign_key, Inflector.foreign_key(klass))
+ end
+
+ ClassNameToForeignKeyWithoutUnderscore.each do |klass, foreign_key|
+ assert_equal(foreign_key, Inflector.foreign_key(klass, false))
+ end
+ end
+
+ def test_tableize
+ ClassNameToTableName.each do |class_name, table_name|
+ assert_equal(table_name, Inflector.tableize(class_name))
+ end
+ end
+
+ def test_classify
+ ClassNameToTableName.each do |class_name, table_name|
+ assert_equal(class_name, Inflector.classify(table_name))
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/inheritance_test.rb b/activerecord/test/inheritance_test.rb
new file mode 100755
index 0000000000..6f8175801d
--- /dev/null
+++ b/activerecord/test/inheritance_test.rb
@@ -0,0 +1,125 @@
+require 'abstract_unit'
+require 'fixtures/company'
+
+
+class InheritanceTest < Test::Unit::TestCase
+ def setup
+ @company_fixtures = create_fixtures "companies"
+ end
+
+ def switch_to_alt_inheritance_column
+ # we don't want misleading test results, so get rid of the values in the type column
+ Company.find_all(nil, "id").each do |c|
+ c['type'] = nil
+ c.save
+ end
+
+ def Company.inheritance_column() "ruby_type" end
+ end
+
+ def test_inheritance_find
+ assert Company.find(1).kind_of?(Firm), "37signals should be a firm"
+ assert Firm.find(1).kind_of?(Firm), "37signals should be a firm"
+ assert Company.find(2).kind_of?(Client), "Summit should be a client"
+ assert Client.find(2).kind_of?(Client), "Summit should be a client"
+ end
+
+ def test_alt_inheritance_find
+ switch_to_alt_inheritance_column
+ test_inheritance_find
+ end
+
+ def test_inheritance_find_all
+ companies = Company.find_all(nil, "id")
+ assert companies[0].kind_of?(Firm), "37signals should be a firm"
+ assert companies[1].kind_of?(Client), "Summit should be a client"
+ end
+
+ def test_alt_inheritance_find_all
+ switch_to_alt_inheritance_column
+ test_inheritance_find_all
+ end
+
+ def test_inheritance_save
+ firm = Firm.new
+ firm.name = "Next Angle"
+ firm.save
+
+ next_angle = Company.find(firm.id)
+ assert next_angle.kind_of?(Firm), "Next Angle should be a firm"
+ end
+
+ def test_alt_inheritance_save
+ switch_to_alt_inheritance_column
+ test_inheritance_save
+ end
+
+ def test_inheritance_condition
+ assert_equal 3, Company.find_all.length
+ assert_equal 1, Firm.find_all.length
+ assert_equal 2, Client.find_all.length
+ end
+
+ def test_alt_inheritance_condition
+ switch_to_alt_inheritance_column
+ test_inheritance_condition
+ end
+
+ def test_finding_incorrect_type_data
+ assert_raises(ActiveRecord::RecordNotFound) { Firm.find(2) }
+ assert_nothing_raised { Firm.find(1) }
+ end
+
+ def test_alt_finding_incorrect_type_data
+ switch_to_alt_inheritance_column
+ test_finding_incorrect_type_data
+ end
+
+ def test_update_all_within_inheritance
+ Client.update_all "name = 'I am a client'"
+ assert_equal "I am a client", Client.find_all.first.name
+ assert_equal "37signals", Firm.find_all.first.name
+ end
+
+ def test_alt_update_all_within_inheritance
+ switch_to_alt_inheritance_column
+ test_update_all_within_inheritance
+ end
+
+ def test_destroy_all_within_inheritance
+ Client.destroy_all
+ assert_equal 0, Client.find_all.length
+ assert_equal 1, Firm.find_all.length
+ end
+
+ def test_alt_destroy_all_within_inheritance
+ switch_to_alt_inheritance_column
+ test_destroy_all_within_inheritance
+ end
+
+ def test_find_first_within_inheritance
+ assert_kind_of Firm, Company.find_first("name = '37signals'")
+ assert_kind_of Firm, Firm.find_first("name = '37signals'")
+ assert_nil Client.find_first("name = '37signals'")
+ end
+
+ def test_alt_find_first_within_inheritance
+ switch_to_alt_inheritance_column
+ test_find_first_within_inheritance
+ end
+
+ def test_complex_inheritance
+ very_special_client = VerySpecialClient.create("name" => "veryspecial")
+ assert_equal very_special_client, VerySpecialClient.find_first("name = 'veryspecial'")
+ assert_equal very_special_client, SpecialClient.find_first("name = 'veryspecial'")
+ assert_equal very_special_client, Company.find_first("name = 'veryspecial'")
+ assert_equal very_special_client, Client.find_first("name = 'veryspecial'")
+ assert_equal 1, Client.find_all("name = 'Summit'").size
+ assert_equal very_special_client, Client.find(very_special_client.id)
+ end
+
+ def test_alt_complex_inheritance
+ switch_to_alt_inheritance_column
+ test_complex_inheritance
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/lifecycle_test.rb b/activerecord/test/lifecycle_test.rb
new file mode 100755
index 0000000000..8b34c8c24c
--- /dev/null
+++ b/activerecord/test/lifecycle_test.rb
@@ -0,0 +1,110 @@
+# require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/developer'
+
+class Topic; def after_find() end end
+class Developer; def after_find() end end
+
+class TopicManualObserver
+ include Singleton
+
+ attr_reader :action, :object, :callbacks
+
+ def initialize
+ Topic.add_observer(self)
+ @callbacks = []
+ end
+
+ def update(callback_method, object)
+ @callbacks << { "callback_method" => callback_method, "object" => object }
+ end
+
+ def has_been_notified?
+ !@callbacks.empty?
+ end
+end
+
+class TopicaObserver < ActiveRecord::Observer
+ def self.observed_class() Topic end
+
+ attr_reader :topic
+
+ def after_find(topic)
+ @topic = topic
+ end
+end
+
+class TopicObserver < ActiveRecord::Observer
+ attr_reader :topic
+
+ def after_find(topic)
+ @topic = topic
+ end
+end
+
+class MultiObserver < ActiveRecord::Observer
+ attr_reader :record
+
+ def self.observed_class() [ Topic, Developer ] end
+
+ def after_find(record)
+ @record = record
+ end
+
+end
+
+class LifecycleTest < Test::Unit::TestCase
+ def setup
+ @topics, @developers = create_fixtures("topics", "developers")
+ end
+
+ def test_before_destroy
+ assert_equal 2, Topic.count
+ Topic.find(1).destroy
+ assert_equal 0, Topic.count
+ end
+
+ def test_after_save
+ topic_observer = TopicManualObserver.instance
+
+ topic = Topic.find(1)
+ topic.title = "hello"
+ topic.save
+
+ assert topic_observer.has_been_notified?
+ assert_equal :after_save, topic_observer.callbacks.last["callback_method"]
+ end
+
+ def test_observer_update_on_save
+ topic_observer = TopicManualObserver.instance
+
+ topic = Topic.find(1)
+ assert topic_observer.has_been_notified?
+ assert_equal :after_find, topic_observer.callbacks.first["callback_method"]
+ end
+
+ def test_auto_observer
+ topic_observer = TopicaObserver.instance
+
+ topic = Topic.find(1)
+ assert_equal topic_observer.topic.title, topic.title
+ end
+
+ def test_infered_auto_observer
+ topic_observer = TopicObserver.instance
+
+ topic = Topic.find(1)
+ assert_equal topic_observer.topic.title, topic.title
+ end
+
+ def test_observing_two_classes
+ multi_observer = MultiObserver.instance
+
+ topic = Topic.find(1)
+ assert_equal multi_observer.record.title, topic.title
+
+ developer = Developer.find(1)
+ assert_equal multi_observer.record.name, developer.name
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/modules_test.rb b/activerecord/test/modules_test.rb
new file mode 100644
index 0000000000..f43bb1d077
--- /dev/null
+++ b/activerecord/test/modules_test.rb
@@ -0,0 +1,29 @@
+require 'abstract_unit'
+# require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'fixtures/company_in_module'
+
+class ModulesTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts"
+ create_fixtures "companies"
+ create_fixtures "projects"
+ create_fixtures "developers"
+ end
+
+ def test_module_spanning_associations
+ assert MyApplication::Business::Firm.find_first.has_clients?, "Firm should have clients"
+ firm = MyApplication::Business::Firm.find_first
+ assert_nil firm.class.table_name.match('::'), "Firm shouldn't have the module appear in its table name"
+ assert_equal 2, firm.clients_count, "Firm should have two clients"
+ end
+
+ def test_module_spanning_has_and_belongs_to_many_associations
+ project = MyApplication::Business::Project.find_first
+ project.developers << MyApplication::Business::Developer.create("name" => "John")
+ assert "John", project.developers.last.name
+ end
+
+ def test_associations_spanning_cross_modules
+ assert MyApplication::Billing::Account.find(1).has_firm?, "37signals account should be able to backtrack"
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/multiple_db_test.rb b/activerecord/test/multiple_db_test.rb
new file mode 100644
index 0000000000..f2f73c0dda
--- /dev/null
+++ b/activerecord/test/multiple_db_test.rb
@@ -0,0 +1,46 @@
+require 'abstract_unit'
+require 'fixtures/course'
+require 'fixtures/entrant'
+
+class MultipleDbTest < Test::Unit::TestCase
+ def setup
+ @courses = create_fixtures("courses") { Course.retrieve_connection }
+ @entrants = create_fixtures("entrants")
+ end
+
+ def test_connected
+ assert_not_nil Entrant.connection
+ assert_not_nil Course.connection
+ end
+
+ def test_proper_connection
+ assert_not_equal(Entrant.connection, Course.connection)
+ assert_equal(Entrant.connection, Entrant.retrieve_connection)
+ assert_equal(Course.connection, Course.retrieve_connection)
+ assert_equal(ActiveRecord::Base.connection, Entrant.connection)
+ end
+
+ def test_find
+ c1 = Course.find(1)
+ assert_equal "Ruby Development", c1.name
+ c2 = Course.find(2)
+ assert_equal "Java Development", c2.name
+ e1 = Entrant.find(1)
+ assert_equal "Ruby Developer", e1.name
+ e2 = Entrant.find(2)
+ assert_equal "Ruby Guru", e2.name
+ e3 = Entrant.find(3)
+ assert_equal "Java Lover", e3.name
+ end
+
+ def test_associations
+ c1 = Course.find(1)
+ assert_equal 2, c1.entrants_count
+ e1 = Entrant.find(1)
+ assert_equal e1.course.id, c1.id
+ c2 = Course.find(2)
+ assert_equal 1, c2.entrants_count
+ e3 = Entrant.find(3)
+ assert_equal e3.course.id, c2.id
+ end
+end
diff --git a/activerecord/test/pk_test.rb b/activerecord/test/pk_test.rb
new file mode 100644
index 0000000000..aefaebde6e
--- /dev/null
+++ b/activerecord/test/pk_test.rb
@@ -0,0 +1,59 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/subscriber'
+require 'fixtures/movie'
+
+class PrimaryKeysTest < Test::Unit::TestCase
+ def setup
+ @topics = create_fixtures "topics"
+ @subscribers = create_fixtures "subscribers"
+ @movies = create_fixtures "movies"
+ end
+
+ def test_integer_key
+ topic = Topic.find(1)
+ assert_equal(@topics["first"]["author_name"], topic.author_name)
+ topic = Topic.find(2)
+ assert_equal(@topics["second"]["author_name"], topic.author_name)
+
+ topic = Topic.new
+ topic.title = "New Topic"
+ assert_equal(nil, topic.id)
+ assert_nothing_raised{ topic.save }
+ id = topic.id
+
+ topicReloaded = Topic.find(id)
+ assert_equal("New Topic", topicReloaded.title)
+ end
+
+ def test_string_key
+ subscriber = Subscriber.find(@subscribers["first"]["nick"])
+ assert_equal(@subscribers["first"]["name"], subscriber.name)
+ subscriber = Subscriber.find(@subscribers["second"]["nick"])
+ assert_equal(@subscribers["second"]["name"], subscriber.name)
+
+ subscriber = Subscriber.new
+ subscriber.id = "jdoe"
+ assert_equal("jdoe", subscriber.id)
+ subscriber.name = "John Doe"
+ assert_nothing_raised{ subscriber.save }
+
+ subscriberReloaded = Subscriber.find("jdoe")
+ assert_equal("John Doe", subscriberReloaded.name)
+ end
+
+ def test_find_with_more_than_one_string_key
+ assert_equal 2, Subscriber.find(@subscribers["first"]["nick"], @subscribers["second"]["nick"]).length
+ end
+
+ def test_primary_key_prefix
+ ActiveRecord::Base.primary_key_prefix_type = :table_name
+ assert_equal "topicid", Topic.primary_key
+
+ ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
+ assert_equal "topic_id", Topic.primary_key
+
+ ActiveRecord::Base.primary_key_prefix_type = nil
+ assert_equal "id", Topic.primary_key
+ end
+end
diff --git a/activerecord/test/reflection_test.rb b/activerecord/test/reflection_test.rb
new file mode 100644
index 0000000000..5d7e9d1197
--- /dev/null
+++ b/activerecord/test/reflection_test.rb
@@ -0,0 +1,78 @@
+#require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/customer'
+require 'fixtures/company'
+require 'fixtures/company_in_module'
+
+class ReflectionTest < Test::Unit::TestCase
+ def setup
+ @topics = create_fixtures "topics"
+ @customers = create_fixtures "customers"
+ @companies = create_fixtures "companies"
+ @first = Topic.find(1)
+ end
+
+ def test_read_attribute_names
+ assert_equal(
+ %w( id title author_name author_email_address written_on last_read content approved replies_count parent_id type ).sort,
+ @first.attribute_names
+ )
+ end
+
+ def test_columns
+ assert_equal 11, Topic.columns.length
+ end
+
+ def test_content_columns
+ assert_equal 7, Topic.content_columns.length
+ end
+
+ def test_column_string_type_and_limit
+ assert_equal :string, @first.column_for_attribute("title").type
+ assert_equal 255, @first.column_for_attribute("title").limit
+ end
+
+ def test_human_name_for_column
+ assert_equal "Author name", @first.column_for_attribute("author_name").human_name
+ end
+
+ def test_integer_columns
+ assert_equal :integer, @first.column_for_attribute("id").type
+ end
+
+ def test_aggregation_reflection
+ reflection_for_address = ActiveRecord::Reflection::AggregateReflection.new(
+ :address, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
+ )
+
+ reflection_for_balance = ActiveRecord::Reflection::AggregateReflection.new(
+ :balance, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
+ )
+
+ assert_equal(
+ [ reflection_for_address, reflection_for_balance ],
+ Customer.reflect_on_all_aggregations
+ )
+
+ assert_equal reflection_for_address, Customer.reflect_on_aggregation(:address)
+
+ assert_equal Address, Customer.reflect_on_aggregation(:address).klass
+ end
+
+ def test_association_reflection
+ reflection_for_clients = ActiveRecord::Reflection::AssociationReflection.new(
+ :clients, { :order => "id", :dependent => true }, Firm
+ )
+
+ assert_equal reflection_for_clients, Firm.reflect_on_association(:clients)
+
+ assert_equal Client, Firm.reflect_on_association(:clients).klass
+ assert_equal Client, Firm.reflect_on_association(:clients_of_firm).klass
+ end
+
+ def test_association_reflection_in_modules
+ assert_equal MyApplication::Business::Client, MyApplication::Business::Firm.reflect_on_association(:clients_of_firm).klass
+ assert_equal MyApplication::Business::Firm, MyApplication::Billing::Account.reflect_on_association(:firm).klass
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/thread_safety_test.rb b/activerecord/test/thread_safety_test.rb
new file mode 100644
index 0000000000..635240c6af
--- /dev/null
+++ b/activerecord/test/thread_safety_test.rb
@@ -0,0 +1,33 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+
+class ThreadSafetyTest < Test::Unit::TestCase
+ def setup
+ @topics = create_fixtures "topics"
+ @threads = []
+ end
+
+ def test_threading_on_transactions
+ # SQLite breaks down under thread banging
+ # Jamis Buck (author of SQLite-ruby): "I know that sqlite itself is not designed for concurrent access"
+ if ActiveRecord::ConnectionAdapters.const_defined? :SQLiteAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLiteAdapter)
+ end
+
+ 5.times do |thread_number|
+ @threads << Thread.new(thread_number) do |thread_number|
+ first, second = Topic.find(1, 2)
+ Topic.transaction(first, second) do
+ Topic.logger.info "started #{thread_number}"
+ first.approved = 1
+ second.approved = 0
+ first.save
+ second.save
+ Topic.logger.info "ended #{thread_number}"
+ end
+ end
+ end
+
+ @threads.each { |t| t.join }
+ end
+end
diff --git a/activerecord/test/transactions_test.rb b/activerecord/test/transactions_test.rb
new file mode 100644
index 0000000000..18b2ea3e65
--- /dev/null
+++ b/activerecord/test/transactions_test.rb
@@ -0,0 +1,110 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+
+
+class TransactionTest < Test::Unit::TestCase
+ def setup
+ @topics = create_fixtures "topics"
+ @first, @second = Topic.find(1, 2)
+ end
+
+ def test_successful
+ Topic.transaction do
+ @first.approved = 1
+ @second.approved = 0
+ @first.save
+ @second.save
+ end
+
+ assert Topic.find(1).approved?, "First should have been approved"
+ assert !Topic.find(2).approved?, "Second should have been unapproved"
+ end
+
+ def test_successful_with_instance_method
+ @first.transaction do
+ @first.approved = 1
+ @second.approved = 0
+ @first.save
+ @second.save
+ end
+
+ assert Topic.find(1).approved?, "First should have been approved"
+ assert !Topic.find(2).approved?, "Second should have been unapproved"
+ end
+
+ def test_failing_on_exception
+ begin
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ raise "Bad things!"
+ end
+ rescue
+ # caught it
+ end
+
+ assert @first.approved?, "First should still be changed in the objects"
+ assert !@second.approved?, "Second should still be changed in the objects"
+
+ assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert Topic.find(2).approved?, "Second should still be approved"
+ end
+
+ def test_failing_with_object_rollback
+ begin
+ Topic.transaction(@first, @second) do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ raise "Bad things!"
+ end
+ rescue
+ # caught it
+ end
+
+ assert !@first.approved?, "First shouldn't have been approved"
+ assert @second.approved?, "Second should still be approved"
+ end
+
+ def test_callback_rollback_in_save
+ add_exception_raising_after_save_callback_to_topic
+
+ begin
+ @first.approved = true
+ @first.save
+ flunk
+ rescue => e
+ assert_equal "Make the transaction rollback", e.message
+ assert !Topic.find(1).approved?
+ ensure
+ remove_exception_raising_after_save_callback_to_topic
+ end
+ end
+
+ def xtest_nested_explicit_transactions
+ Topic.transaction do
+ Topic.transaction do
+ @first.approved = 1
+ @second.approved = 0
+ @first.save
+ @second.save
+ end
+ end
+
+ assert Topic.find(1).approved?, "First should have been approved"
+ assert !Topic.find(2).approved?, "Second should have been unapproved"
+ end
+
+
+ private
+ def add_exception_raising_after_save_callback_to_topic
+ Topic.class_eval { def after_save() raise "Make the transaction rollback" end }
+ end
+
+ def remove_exception_raising_after_save_callback_to_topic
+ Topic.class_eval { remove_method :after_save }
+ end
+end
diff --git a/activerecord/test/unconnected_test.rb b/activerecord/test/unconnected_test.rb
new file mode 100755
index 0000000000..0966dd9b06
--- /dev/null
+++ b/activerecord/test/unconnected_test.rb
@@ -0,0 +1,24 @@
+require 'abstract_unit'
+
+class TestRecord < ActiveRecord::Base
+end
+
+class TestUnconnectedAdaptor < Test::Unit::TestCase
+
+ def setup
+ @connection = ActiveRecord::Base.remove_connection
+ end
+
+ def teardown
+ ActiveRecord::Base.establish_connection(@connection)
+ end
+
+ def test_unconnected
+ assert_raise(ActiveRecord::ConnectionNotEstablished) do
+ TestRecord.find(1)
+ end
+ assert_raise(ActiveRecord::ConnectionNotEstablished) do
+ TestRecord.new.save
+ end
+ end
+end
diff --git a/activerecord/test/validations_test.rb b/activerecord/test/validations_test.rb
new file mode 100755
index 0000000000..27a9b21c7d
--- /dev/null
+++ b/activerecord/test/validations_test.rb
@@ -0,0 +1,126 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/developer'
+
+
+class ValidationsTest < Test::Unit::TestCase
+ def setup
+ @topic_fixtures = create_fixtures("topics")
+ @developers = create_fixtures("developers")
+ end
+
+ def test_single_field_validation
+ r = Reply.new
+ r.title = "There's no content!"
+ assert !r.save, "A reply without content shouldn't be saveable"
+
+ r.content = "Messa content!"
+ assert r.save, "A reply with content should be saveable"
+ end
+
+ def test_single_attr_validation_and_error_msg
+ r = Reply.new
+ r.title = "There's no content!"
+ r.save
+ assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid"
+ assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error"
+ assert_equal 1, r.errors.count
+ end
+
+ def test_double_attr_validation_and_error_msg
+ r = Reply.new
+ assert !r.save
+
+ assert r.errors.invalid?("title"), "A reply without title should mark that attribute as invalid"
+ assert_equal "Empty", r.errors.on("title"), "A reply without title should contain an error"
+
+ assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid"
+ assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error"
+
+ assert_equal 2, r.errors.count
+ end
+
+ def test_error_on_create
+ r = Reply.new
+ r.title = "Wrong Create"
+ assert !r.save
+ assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid"
+ assert_equal "is Wrong Create", r.errors.on("title"), "A reply with a bad content should contain an error"
+ end
+
+
+ def test_error_on_update
+ r = Reply.new
+ r.title = "Bad"
+ r.content = "Good"
+
+ assert r.save, "First save should be successful"
+
+ r.title = "Wrong Update"
+ assert !r.save, "Second save should fail"
+
+ assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid"
+ assert_equal "is Wrong Update", r.errors.on("title"), "A reply with a bad content should contain an error"
+ end
+
+ def test_single_error_per_attr_iteration
+ r = Reply.new
+ r.save
+
+ errors = []
+ r.errors.each { |attr, msg| errors << [attr, msg] }
+
+ assert errors.include?(["title", "Empty"])
+ assert errors.include?(["content", "Empty"])
+ end
+
+ def test_multiple_errors_per_attr_iteration_with_full_error_composition
+ r = Reply.new
+ r.title = "Wrong Create"
+ r.content = "Mismatch"
+ r.save
+
+ errors = []
+ r.errors.each_full { |error| errors << error }
+
+ assert_equal "Title is Wrong Create", errors[0]
+ assert_equal "Title is Content Mismatch", errors[1]
+ assert_equal 2, r.errors.count
+ end
+
+ def test_errors_on_base
+ r = Reply.new
+ r.content = "Mismatch"
+ r.save
+ r.errors.add_to_base "Reply is not dignifying"
+
+ errors = []
+ r.errors.each_full { |error| errors << error }
+
+ assert_equal "Reply is not dignifying", r.errors.on_base
+
+ assert errors.include?("Title Empty")
+ assert errors.include?("Reply is not dignifying")
+ assert_equal 2, r.errors.count
+ end
+
+ def test_create_without_validation
+ reply = Reply.new
+ assert !reply.save
+ assert reply.save(false)
+ end
+
+ def test_errors_on_boundary_breaking
+ developer = Developer.new("name" => "xs")
+ assert !developer.save
+ assert_equal "is too short (min is 3 characters)", developer.errors.on("name")
+
+ developer.name = "All too very long for this boundary, it really is"
+ assert !developer.save
+ assert_equal "is too long (max is 20 characters)", developer.errors.on("name")
+
+ developer.name = "Just right"
+ assert developer.save
+ end
+end
diff --git a/railties/CHANGELOG b/railties/CHANGELOG
new file mode 100644
index 0000000000..60eb1c3e2a
--- /dev/null
+++ b/railties/CHANGELOG
@@ -0,0 +1,265 @@
+*CVS*
+
+* Added breakpoint support by default to the WEBrick dispatcher. This means that you can break out of execution at any point in
+ the code, investigate and change the model, AND then resume execution! Example:
+
+ class WeblogController < ActionController::Base
+ def index
+ @posts = Post.find_all
+ breakpoint "Breaking out from the list"
+ end
+ end
+
+ So the controller will accept the action, run the first line, then present you with a IRB prompt in the WEBrick window (you shouldn't
+ run as daemon when you want to use this). Here you can do things like:
+
+ Executing breakpoint "Breaking out from the list" at .../webrick_server.rb:16 in 'breakpoint'
+
+ >> @posts.inspect
+ => "[#<Post:0x14a6be8 @attributes={\"title\"=>nil, \"body\"=>nil, \"id\"=>\"1\"}>,
+ #<Post:0x14a6620 @attributes={\"title\"=>\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]"
+ >> @posts.first.title = "hello from a breakpoint"
+ => "hello from a breakpoint"
+
+ ...and even better is that you can examine how your runtime objects actually work:
+
+ >> f = @posts.first
+ => #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>
+ >> f.
+ Display all 152 possibilities? (y or n)
+
+ Finally, when you're ready to resume execution, you press CTRL-D
+
+* Changed environments to be configurable through an environment variable. By default, the environment is "development", but you
+ can change that and set your own by configuring the Apache vhost with a string like (mod_env must be available on the server):
+
+ SetEnv RAILS_ENV production
+
+ ...if you're using WEBrick, you can pick the environment to use with the command-line parameters -e/--environment, like this:
+
+ ruby public/dispatcher.servlet -e production
+
+* Added a new default environment called "development", which leaves the production environment to be tuned exclusively for that.
+
+* Added a start_server in the root of the Rails application to make it even easier to get started
+
+* Fixed public/.htaccess to use RewriteBase and share the same rewrite rules for all the dispatch methods
+
+* Fixed webrick_server to handle requests in a serialized manner (the Rails reloading infrastructure is not thread-safe)
+
+* Added support for controllers in directories. So you can have:
+
+ app/controllers/account_controller.rb # URL: /account/
+ app/controllers/admin/account_controller.rb # URL: /admin/account/
+
+ NOTE: You need to update your public/.htaccess with the new rules to pick it up
+
+* Added reloading for associations and dependencies under cached environments like FastCGI and mod_ruby. This makes it possible to use
+ those environments for development. This is turned on by default, but can be turned off with
+ ActiveRecord::Base.reload_associations = false and ActionController::Base.reload_dependencies = false in production environments.
+
+* Added support for sub-directories in app/models. So now you can have something like Basecamp with:
+
+ app/models/accounting
+ app/models/project
+ app/models/participants
+ app/models/settings
+
+ It's poor man's namespacing, but only for file-system organization. You still require files just like before.
+ Nothing changes inside the files themselves.
+
+
+* Fixed a few references in the tests generated by new_mailer [bitsweat]
+
+* Added support for mocks in testing with test/mocks
+
+* Cleaned up the environments a bit and added global constant RAILS_ROOT
+
+
+*0.8.5* (9)
+
+* Made dev-util available to all tests, so you can insert breakpoints in any test case to get an IRB prompt at that point [bitsweat]:
+
+ def test_complex_stuff
+ @david.projects << @new_project
+ breakpoint "Let's have a closer look at @david"
+ end
+
+ You need to install dev-utils yourself for this to work ("gem install dev-util").
+
+* Added shared generator behavior so future upgrades should be possible without manually copying over files [bitsweat]
+
+* Added the new helper style to both controller and helper templates [bitsweat]
+
+* Added new_crud generator for creating a model and controller at the same time with explicit scaffolding [bitsweat]
+
+* Added configuration of Test::Unit::TestCase.fixture_path to test_helper to concide with the new AR fixtures style
+
+* Fixed that new_model was generating singular table/fixture names
+
+* Upgraded to Action Mailer 0.4.0
+
+* Upgraded to Action Pack 0.9.5
+
+* Upgraded to Active Record 1.1.0
+
+
+*0.8.0 (15)*
+
+* Removed custom_table_name option for new_model now that the Inflector is as powerful as it is
+
+* Changed the default rake action to just do testing and separate API generation and coding statistics into a "doc" task.
+
+* Fixed WEBrick dispatcher to handle missing slashes in the URLs gracefully [alexey]
+
+* Added user option for all postgresql tool calls in the rakefile [elvstone]
+
+* Fixed problem with running "ruby public/dispatch.servlet" instead of "cd public; ruby dispatch.servlet" [alexey]
+
+* Fixed WEBrick server so that it no longer hardcodes the ruby interpreter used to "ruby" but will get the one used based
+ on the Ruby runtime configuration. [Marcel Molina Jr.]
+
+* Fixed Dispatcher so it'll route requests to magic_beans to MagicBeansController/magic_beans_controller.rb [Caio Chassot]
+
+* "new_controller MagicBeans" and "new_model SubscriptionPayments" will now both behave properly as they use the new Inflector.
+
+* Fixed problem with MySQL foreign key constraint checks in Rake :clone_production_structure_to_test target [Andreas Schwarz]
+
+* Changed WEBrick server to by default be auto-reloading, which is slower but makes source changes instant.
+ Class compilation cache can be turned on with "-c" or "--cache-classes".
+
+* Added "-b/--binding" option to WEBrick dispatcher to bind the server to a specific IP address (default: 127.0.0.1) [Kevin Temp]
+
+* dispatch.fcgi now DOESN'T set FCGI_PURE_RUBY as it was slowing things down for now reason [Andreas Schwarz]
+
+* Added new_mailer generator to work with Action Mailer
+
+* Included new framework: Action Mailer 0.3
+
+* Upgraded to Action Pack 0.9.0
+
+* Upgraded to Active Record 1.0.0
+
+
+*0.7.0*
+
+* Added an optional second argument to the new_model script that allows the programmer to specify the table name,
+ which will used to generate a custom table_name method in the model and will also be used in the creation of fixtures.
+ [Kevin Radloff]
+
+* script/new_model now turns AccountHolder into account_holder instead of accountholder [Kevin Radloff]
+
+* Fixed the faulty handleing of static files with WEBrick [Andreas Schwarz]
+
+* Unified function_test_helper and unit_test_helper into test_helper
+
+* Fixed bug with the automated production => test database dropping on PostgreSQL [dhawkins]
+
+* create_fixtures in both the functional and unit test helper now turns off the log during fixture generation
+ and can generate more than one fixture at a time. Which makes it possible for assignments like:
+
+ @people, @projects, @project_access, @companies, @accounts =
+ create_fixtures "people", "projects", "project_access", "companies", "accounts"
+
+* Upgraded to Action Pack 0.8.5 (locally-scoped variables, partials, advanced send_file)
+
+* Upgraded to Active Record 0.9.5 (better table_name guessing, cloning, find_all_in_collection)
+
+
+*0.6.5*
+
+* No longer specifies a template for rdoc, so it'll use whatever is default (you can change it in the rakefile)
+
+* The new_model generator will now use the same rules for plural wordings as Active Record
+ (so Category will give categories, not categorys) [Kevin Radloff]
+
+* dispatch.fcgi now sets FCGI_PURE_RUBY to true to ensure that it's the Ruby version that's loaded [danp]
+
+* Made the GEM work with Windows
+
+* Fixed bug where mod_ruby would "forget" the load paths added when switching between controllers
+
+* PostgreSQL are now supported for the automated production => test database dropping [Kevin Radloff]
+
+* Errors thrown by the dispatcher are now properly handled in FCGI.
+
+* Upgraded to Action Pack 0.8.0 (lots and lots and lots of fixes)
+
+* Upgraded to Active Record 0.9.4 (a bunch of fixes)
+
+
+*0.6.0*
+
+* Added AbstractionApplicationController as a superclass for all controllers generated. This class can be used
+ to carry filters and methods that are to be shared by all. It has an accompanying ApplicationHelper that all
+ controllers will also automatically have available.
+
+* Added environments that can be included from any script to get the full Active Record and Action Controller
+ context running. This can be used by maintenance scripts or to interact with the model through IRB. Example:
+
+ require 'config/environments/production'
+
+ for account in Account.find_all
+ account.recalculate_interests
+ end
+
+ A short migration script for an account model that had it's interest calculation strategy changed.
+
+* Accessing the index of a controller with "/weblog" will now redirect to "/weblog/" (only on Apache, not WEBrick)
+
+* Simplified the default Apache config so even remote requests are served off CGI as a default.
+ You'll now have to do something specific to activate mod_ruby and FCGI (like using the force urls).
+ This should make it easier for new comers that start on an external server.
+
+* Added more of the necessary Apache options to .htaccess to make it easier to setup
+
+* Upgraded to Action Pack 0.7.9 (lots of fixes)
+
+* Upgraded to Active Record 0.9.3 (lots of fixes)
+
+
+*0.5.7*
+
+* Fixed bug in the WEBrick dispatcher that prevented it from getting parameters from the URL
+ (through GET requests or otherwise)
+
+* Added lib in root as a place to store app specific libraries
+
+* Added lib and vendor to load_path, so anything store within can be loaded directly.
+ Hence lib/redcloth.rb can be loaded with require "redcloth"
+
+* Upgraded to Action Pack 0.7.8 (lots of fixes)
+
+* Upgraded to Active Record 0.9.2 (minor upgrade)
+
+
+*0.5.6*
+
+* Upgraded to Action Pack 0.7.7 (multipart form fix)
+
+* Updated the generated template stubs to valid XHTML files
+
+* Ensure that controllers generated are capitalized, so "new_controller TodoLists"
+ gives the same as "new_controller Todolists" and "new_controller todolists".
+
+
+*0.5.5*
+
+* Works on Windows out of the box! (Dropped symlinks)
+
+* Added webrick dispatcher: Try "ruby public/dispatch.servlet --help" [Florian Gross]
+
+* Report errors about initialization to browser (instead of attempting to use uninitialized logger)
+
+* Upgraded to Action Pack 0.7.6
+
+* Upgraded to Active Record 0.9.1
+
+* Added distinct 500.html instead of reusing 404.html
+
+* Added MIT license
+
+
+*0.5.0*
+
+* First public release
diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE
new file mode 100644
index 0000000000..5919c288e4
--- /dev/null
+++ b/railties/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2004 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/railties/README b/railties/README
new file mode 100644
index 0000000000..4702f8a6c8
--- /dev/null
+++ b/railties/README
@@ -0,0 +1,121 @@
+== Welcome to Rails
+
+Rails is a web-application and persistance framework that includes everything
+needed to create database-backed web-applications according to the
+Model-View-Control pattern of separation. This pattern splits the view (also
+called the presentation) into "dumb" templates that are primarily responsible
+for inserting pre-build data in between HTML tags. The model contains the
+"smart" domain objects (such as Account, Product, Person, Post) that holds all
+the business logic and knows how to persist themselves to a database. The
+controller handles the incoming requests (such as Save New Account, Update
+Product, Show Post) by manipulating the model and directing data to the view.
+
+In Rails, the model is handled by what's called a object-relational mapping
+layer entitled Active Record. This layer allows you to present the data from
+database rows as objects and embellish these data objects with business logic
+methods. You can read more about Active Record in
+link:files/vendor/activerecord/README.html.
+
+The controller and view is handled by the Action Pack, which handles both
+layers by its two parts: Action View and Action Controller. These two layers
+are bundled in a single package due to their heavy interdependence. This is
+unlike the relationship between the Active Record and Action Pack that is much
+more separate. Each of these packages can be used independently outside of
+Rails. You can read more about Action Pack in
+link:files/vendor/actionpack/README.html.
+
+
+== Requirements
+
+* Database and driver (MySQL, PostgreSQL, or SQLite)
+* Rake[http://rake.rubyforge.org] for running tests and the generating documentation
+
+== Optionals
+
+* Apache 1.3.x or 2.x (or any FastCGI-capable webserver with a
+ mod_rewrite-like module)
+* FastCGI (or mod_ruby) for production performance (CGI is used for
+ development)
+
+== Getting started
+
+1a. Setup Apache for the Rails application (see "Example for Apache conf")
+1b. Run the WEBrick servlet: <tt>ruby public/dispatch.servlet --help</tt>
+2. Go to http://rails/ (or whatever is your ServerName) and check
+ that you get the "Congratulations, you're on Rails!" screen
+3. Follow the guidelines on the "Congratulations, you're on Rails!" screen
+
+
+== Example for Apache conf
+
+ <VirtualHost *:80>
+ ServerName rails
+ DocumentRoot /path/tapplication/public/
+ ErrorLog /path/application/log/apache.log
+
+ <Directory /path/application/public/>
+ Options ExecCGI FollowSymLinks
+ AllowOverride all
+ Allow from all
+ Order allow,deny
+ </Directory>
+ </VirtualHost>
+
+NOTE: Be sure that CGIs can be executed in that directory as well. So ExecCGI
+should be on and ".cgi" should respond. All requests from 127.0.0.1 goes
+through CGI, so no Apache restart is necessary for changes. All other requests
+goes through FCGI (or mod_ruby) that requires restart to show changes.
+
+
+== Debugging Rails
+
+Have "tail -f" commands running on both the apache.log, production.log, and
+test.log files. Rails will automatically display debugging and runtime
+information to these files. Debugging info will also be shown in the browser
+on requests from 127.0.0.1.
+
+
+== Description of contents
+
+app
+ Holds all the code that's specific to this particular application.
+
+app/controllers
+ Holds controllers that should be named like weblog_controller.rb for
+ automated URL mapping. All controllers should descend from
+ ActionController::Base.
+
+app/models
+ Holds models that should be named like post.rb.
+ Most models will descent from ActiveRecord::Base.
+
+app/views
+ Holds the template files for the view that should be named like
+ weblog/index.rhtml for the WeblogController#index action. All views uses eRuby
+ syntax. This directory can also be used to keep stylesheets, images, and so on
+ that can be symlinked to public.
+
+app/helpers
+ Holds view helpers that should be named like weblog_helper.rb.
+
+config
+ Configuration files for Apache, database, and other dependencies.
+
+lib
+ Application specific libraries. Basically, any kind of custom code that doesn't
+ belong controllers, models, or helpers. This directory is in the load path.
+
+public
+ The directory available for Apache, which includes symbolic links to other
+ parts of the structure that are to be made available. Refrain from placing
+ actual files in here if you're using CVS and don't want to check in this
+ directory.
+
+script
+ Helper scripts for automation and generation.
+
+test
+ Unit and functional tests along with fixtures.
+
+vendor
+ External libraries that the application depend on. This directory is in the load path.
diff --git a/railties/Rakefile b/railties/Rakefile
new file mode 100644
index 0000000000..aef5a4e884
--- /dev/null
+++ b/railties/Rakefile
@@ -0,0 +1,279 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+
+require 'date'
+
+
+PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
+PKG_NAME = 'rails'
+PKG_VERSION = '0.8.5' + PKG_BUILD
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+PKG_DESTINATION = ENV["RAILS_PKG_DESTINATION"] || "../#{PKG_NAME}"
+
+desc "Default Task"
+task :default => [ :fresh_rails ]
+
+desc "Generates a fresh Rails package with documentation"
+task :fresh_rails => [ :make_dir_structure, :initialize_file_stubs, :copy_vendor_libraries, :copy_ties_content, :generate_documentation ]
+
+desc "Generates a fresh Rails package using GEMs with documentation"
+task :fresh_gem_rails => [ :make_dir_structure, :initialize_file_stubs, :copy_ties_content, :copy_gem_environment ]
+
+desc "Generates a fresh Rails package without documentation (faster)"
+task :fresh_rails_without_docs => [ :make_dir_structure, :initialize_file_stubs, :copy_vendor_libraries, :copy_ties_content ]
+
+desc "Packages the fresh Rails package with documentation"
+task :package => [ :clean, :fresh_rails ] do
+ system %{cd ..; tar -czvf #{PKG_NAME}-#{PKG_VERSION}.tgz #{PKG_NAME}}
+ system %{cd ..; zip -r #{PKG_NAME}-#{PKG_VERSION}.zip #{PKG_NAME}}
+end
+
+task :clean do
+ File.rm_rf "#{PKG_DESTINATION}"
+end
+
+
+# Make directory structure ----------------------------------------------------------------
+
+desc "Make the directory structure for the new Rails application"
+task :make_dir_structure => [ :make_base_dirs, :make_app_dirs, :make_public_dirs, :make_test_dirs ] do
+end
+
+task :make_base_dirs do
+ File.rm_rf PKG_DESTINATION
+ File.mkdir "#{PKG_DESTINATION}"
+ File.mkdir "#{PKG_DESTINATION}/app"
+ File.mkdir "#{PKG_DESTINATION}/config"
+ File.mkdir "#{PKG_DESTINATION}/config/environments"
+ File.mkdir "#{PKG_DESTINATION}/db"
+ File.mkdir "#{PKG_DESTINATION}/doc"
+ File.mkdir "#{PKG_DESTINATION}/log"
+ File.mkdir "#{PKG_DESTINATION}/lib"
+ File.mkdir "#{PKG_DESTINATION}/public"
+ File.mkdir "#{PKG_DESTINATION}/script"
+ File.mkdir "#{PKG_DESTINATION}/test"
+ File.mkdir "#{PKG_DESTINATION}/vendor"
+end
+
+task :make_app_dirs do
+ File.mkdir "#{PKG_DESTINATION}/app/models"
+ File.mkdir "#{PKG_DESTINATION}/app/controllers"
+ File.mkdir "#{PKG_DESTINATION}/app/helpers"
+ File.mkdir "#{PKG_DESTINATION}/app/views"
+ File.mkdir "#{PKG_DESTINATION}/app/views/layouts"
+end
+
+task :make_public_dirs do
+ File.mkdir "#{PKG_DESTINATION}/public/images"
+ File.mkdir "#{PKG_DESTINATION}/public/javascripts"
+ File.mkdir "#{PKG_DESTINATION}/public/stylesheets"
+ File.mkdir "#{PKG_DESTINATION}/public/_doc"
+end
+
+task :make_test_dirs do
+ File.mkdir "#{PKG_DESTINATION}/test/fixtures"
+ File.mkdir "#{PKG_DESTINATION}/test/unit"
+ File.mkdir "#{PKG_DESTINATION}/test/functional"
+ File.mkdir "#{PKG_DESTINATION}/test/mocks/development"
+ File.mkdir "#{PKG_DESTINATION}/test/mocks/testing"
+end
+
+
+# Initialize file stubs -------------------------------------------------------------------
+
+desc "Initialize empty file stubs (such as for logging)"
+task :initialize_file_stubs => [ :initialize_log_files ] do
+end
+
+task :initialize_log_files do
+ chmod 0777, "#{PKG_DESTINATION}/log"
+
+ File.touch "#{PKG_DESTINATION}/log/apache.log"
+ File.touch "#{PKG_DESTINATION}/log/production.log"
+
+ chmod 0777, "#{PKG_DESTINATION}/log/apache.log"
+ chmod 0777, "#{PKG_DESTINATION}/log/production.log"
+end
+
+
+# Copy Vendors ----------------------------------------------------------------------------
+
+desc "Copy in all the Rails packages to vendor"
+task :copy_vendor_libraries => [ :copy_action_pack, :copy_active_record, :copy_ties, :copy_action_mailer ]
+
+task :copy_action_pack do
+ File.cp_r "../actionpack", "#{PKG_DESTINATION}/vendor/actionpack"
+end
+
+task :copy_active_record do
+ File.cp_r "../activerecord", "#{PKG_DESTINATION}/vendor/activerecord"
+end
+
+task :copy_action_mailer do
+ File.cp_r "../actionmailer", "#{PKG_DESTINATION}/vendor/actionmailer"
+end
+
+task :copy_ties do
+ File.cp_r "../railties", "#{PKG_DESTINATION}/vendor/railties"
+end
+
+
+# Copy Ties Content -----------------------------------------------------------------------
+
+# :link_apache_config
+desc "Make copies of all the default content of ties"
+task :copy_ties_content => [
+ :copy_rootfiles, :copy_dispatches, :copy_html_files, :copy_abstract_application,
+ :copy_configs, :copy_generators, :copy_test_helpers, :copy_docs_in_public,
+ :copy_app_doc_readme ] do
+end
+
+task :copy_dispatches do
+ File.cp "dispatches/dispatch.rb", "#{PKG_DESTINATION}/public/dispatch.rb"
+ chmod 0755, "#{PKG_DESTINATION}/public/dispatch.rb"
+
+ File.cp "dispatches/dispatch.rb", "#{PKG_DESTINATION}/public/dispatch.cgi"
+ chmod 0755, "#{PKG_DESTINATION}/public/dispatch.cgi"
+
+ File.cp "dispatches/dispatch.fcgi", "#{PKG_DESTINATION}/public/dispatch.fcgi"
+ chmod 0755, "#{PKG_DESTINATION}/public/dispatch.fcgi"
+
+ File.cp "dispatches/dispatch.servlet", "#{PKG_DESTINATION}/public/dispatch.servlet"
+
+ File.cp "dispatches/start_server", "#{PKG_DESTINATION}/start_server"
+ chmod 0755, "#{PKG_DESTINATION}/start_server"
+end
+
+task :copy_html_files do
+ File.cp "html/404.html", "#{PKG_DESTINATION}/public/404.html"
+ File.cp "html/500.html", "#{PKG_DESTINATION}/public/500.html"
+ File.cp "html/index.html", "#{PKG_DESTINATION}/public/index.html"
+end
+
+task :copy_abstract_application do
+ File.cp "helpers/abstract_application.rb", "#{PKG_DESTINATION}/app/controllers/abstract_application.rb"
+ File.cp "helpers/application_helper.rb", "#{PKG_DESTINATION}/app/helpers/application_helper.rb"
+end
+
+task :copy_configs do
+ File.cp "configs/database.yml", "#{PKG_DESTINATION}/config/database.yml"
+
+ File.cp "configs/apache.conf", "#{PKG_DESTINATION}/public/.htaccess"
+
+ File.cp "environments/shared.rb", "#{PKG_DESTINATION}/config/environment.rb"
+ File.cp "environments/production.rb", "#{PKG_DESTINATION}/config/environments/production.rb"
+ File.cp "environments/development.rb", "#{PKG_DESTINATION}/config/environments/development.rb"
+ File.cp "environments/test.rb", "#{PKG_DESTINATION}/config/environments/test.rb"
+end
+
+task :copy_generators do
+ File.cp "generators/new_controller.rb", "#{PKG_DESTINATION}/script/new_controller"
+ File.cp "generators/new_model.rb", "#{PKG_DESTINATION}/script/new_model"
+ File.cp "generators/new_mailer.rb", "#{PKG_DESTINATION}/script/new_mailer"
+ File.cp "generators/new_crud.rb", "#{PKG_DESTINATION}/script/new_crud"
+ chmod 0755, "#{PKG_DESTINATION}/script/new_controller"
+ chmod 0755, "#{PKG_DESTINATION}/script/new_model"
+ chmod 0755, "#{PKG_DESTINATION}/script/new_mailer"
+ chmod 0755, "#{PKG_DESTINATION}/script/new_crud"
+end
+
+task :copy_rootfiles do
+ File.cp "fresh_rakefile", "#{PKG_DESTINATION}/Rakefile"
+ File.cp "README", "#{PKG_DESTINATION}/README"
+end
+
+task :copy_test_helpers do
+ File.cp "helpers/test_helper.rb", "#{PKG_DESTINATION}/test/test_helper.rb"
+end
+
+task :copy_docs_in_public do
+ File.cp "doc/index.html", "#{PKG_DESTINATION}/public/_doc/index.html"
+end
+
+task :copy_app_doc_readme do
+ File.cp "doc/README_FOR_APP", "#{PKG_DESTINATION}/doc/README_FOR_APP"
+end
+
+task :link_apache_config do
+ cd "#{PKG_DESTINATION}/config/"
+ ln_s "../public/.htaccess", "apache.conf"
+ cd "../../railties"
+end
+
+
+# Generate documentation ------------------------------------------------------------------
+
+desc "Generate documentation for the framework and for the empty application"
+task :generate_documentation => [ :generate_app_doc, :generate_rails_framework_doc ] do
+end
+
+task :generate_rails_framework_doc do
+ system %{cd #{PKG_DESTINATION}; rake apidoc}
+end
+
+task :generate_app_doc do
+ File.cp "doc/README_FOR_APP", "#{PKG_DESTINATION}/doc/README_FOR_APP"
+ system %{cd #{PKG_DESTINATION}; rake appdoc}
+end
+
+
+# Generate GEM ----------------------------------------------------------------------------
+
+task :copy_gem_environment do
+ File.cp "environments/shared_for_gem.rb", "#{PKG_DESTINATION}/config/environment.rb"
+end
+
+
+PKG_FILES = FileList[
+ '[a-zA-Z]*',
+ 'bin/**/*',
+ 'configs/**/*',
+ 'doc/**/*',
+ 'dispatches/**/*',
+ 'environments/**/*',
+ 'generators/**/*',
+ 'helpers/**/*',
+ 'html/**/*',
+ 'lib/**/*'
+]
+
+spec = Gem::Specification.new do |s|
+ s.name = 'rails'
+ s.version = PKG_VERSION
+ s.summary = "Web-application framework with template engine, control-flow layer, and ORM."
+ s.description = <<-EOF
+ Rails is a framework for building web-application using CGI, FCGI, mod_ruby, or WEBrick
+ on top of either MySQL, PostgreSQL, or SQLite with eRuby-based templates.
+ EOF
+
+ s.add_dependency('rake', '>= 0.4.11')
+ s.add_dependency('activerecord', '>= 1.1.0')
+ s.add_dependency('actionpack', '>= 0.9.5')
+ s.add_dependency('actionmailer', '>= 0.4.0')
+ s.add_dependency('dev-utils', '>= 1.0.1')
+
+ s.files = PKG_FILES.to_a
+ s.require_path = 'lib'
+
+ s.bindir = "bin" # Use these for applications.
+ s.executables = ["rails"]
+ s.default_executable = "rails"
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://www.rubyonrails.org"
+ s.rubyforge_project = "rails"
+end
+
+Rake::GemPackageTask.new(spec) do |pkg|
+end
+
+# Publish beta gem
+desc "Publish the API documentation"
+task :pgem => [:gem] do
+ Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
+ `ssh davidhh@one.textdrive.com './gemupdate.sh'`
+end \ No newline at end of file
diff --git a/railties/bin/rails b/railties/bin/rails
new file mode 100755
index 0000000000..846f02ac69
--- /dev/null
+++ b/railties/bin/rails
@@ -0,0 +1,28 @@
+if ARGV[0]
+ ENV["RAILS_PKG_DESTINATION"] = File.expand_path(ARGV[0])
+ if RUBY_PLATFORM =~ /mswin32/
+ Dir.chdir File.dirname(__FILE__)
+ system %{rake.cmd fresh_gem_rails}
+ else
+ system %{ cd #{File.dirname(__FILE__)}; rake fresh_gem_rails }
+ end
+else
+ puts <<-HELP
+
+NAME
+ rails - creates a new Rails installation
+
+SYNOPSIS
+ rails [full path]
+
+DESCRIPTION
+ This generator will create a suggested directory structure, lots of minor helper
+ files, and a default configuration for creating a new Rails application. Once the
+ generator is done, you're adviced to look at the README in the root of the folder.
+
+EXAMPLE
+ rails ~/Code/Ruby/weblog
+
+ This will generate a new Rails installation in the ~/Code/Ruby/weblog folder.
+HELP
+end \ No newline at end of file
diff --git a/railties/configs/apache.conf b/railties/configs/apache.conf
new file mode 100755
index 0000000000..feb2e32c4e
--- /dev/null
+++ b/railties/configs/apache.conf
@@ -0,0 +1,31 @@
+# General Apache options
+AddHandler fastcgi-script .fcgi
+AddHandler cgi-script .cgi
+Options +FollowSymLinks +ExecCGI
+  
+# Make sure that mod_ruby.c has been added and loaded as a module with Apache
+RewriteEngine On
+# Change extension from .cgi to .fcgi to switch to FCGI and to .rb to switch to mod_ruby
+RewriteBase /dispatch.cgi
+RewriteRule ^dispatch.servlet$ / [R]
+# Enable this rewrite rule to point to the controller/action that should serve root.
+# RewriteRule ^$ /controller/action
+# Add missing slash
+RewriteRule ^([-_a-zA-Z0-9]+)$ /$1/ [R]
+# Default rewriting rules.
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ ?controller=$1&action=$2&id=$3 [QSA,L]
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ ?controller=$1&action=$2 [QSA,L]
+RewriteRule ^([-_a-zA-Z0-9]+)/$ ?controller=$1&action=index [QSA,L]
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ ?module=$1&controller=$2&action=$3&id=$4 [QSA,L]
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ ?module=$1&controller=$2&action=$3 [QSA,L]
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/$ ?module=$1&controller=$2&action=index [QSA,L]
+# You can also point these error messages to a controller/action
+ErrorDocument 500 /500.html
+ErrorDocument 404 /404.html \ No newline at end of file
diff --git a/railties/configs/database.yml b/railties/configs/database.yml
new file mode 100644
index 0000000000..0f9a7eb20e
--- /dev/null
+++ b/railties/configs/database.yml
@@ -0,0 +1,20 @@
+development:
+ adapter: mysql
+ database: rails_development
+ host: localhost
+ username: root
+ password:
+
+test:
+ adapter: mysql
+ database: rails_test
+ host: localhost
+ username: root
+ password:
+
+production:
+ adapter: mysql
+ database: rails_production
+ host: localhost
+ username: root
+ password:
diff --git a/railties/dispatches/dispatch.fcgi b/railties/dispatches/dispatch.fcgi
new file mode 100755
index 0000000000..dc43f03b19
--- /dev/null
+++ b/railties/dispatches/dispatch.fcgi
@@ -0,0 +1,7 @@
+#!/usr/local/bin/ruby
+
+require File.dirname(__FILE__) + "/../config/environment"
+require 'dispatcher'
+require 'fcgi'
+
+FCGI.each_cgi { |cgi| Dispatcher.dispatch(cgi, Dispatcher::DEFAULT_SESSION_OPTIONS, File.dirname(__FILE__) + "/500.html") } \ No newline at end of file
diff --git a/railties/dispatches/dispatch.rb b/railties/dispatches/dispatch.rb
new file mode 100755
index 0000000000..eb2c95e813
--- /dev/null
+++ b/railties/dispatches/dispatch.rb
@@ -0,0 +1,10 @@
+#!/usr/local/bin/ruby
+
+require File.dirname(__FILE__) + "/../config/environment"
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
+require "dispatcher"
+
+ADDITIONAL_LOAD_PATHS.flatten.each { |dir| $:.unshift "#{RAILS_ROOT}/#{dir}" }
+Dispatcher.dispatch \ No newline at end of file
diff --git a/railties/dispatches/dispatch.servlet b/railties/dispatches/dispatch.servlet
new file mode 100644
index 0000000000..a1fa403a67
--- /dev/null
+++ b/railties/dispatches/dispatch.servlet
@@ -0,0 +1,49 @@
+#!/usr/local/bin/ruby
+
+require 'webrick'
+require 'optparse'
+
+OPTIONS = {
+ :port => 3000,
+ :ip => "127.0.0.1",
+ :environment => "development",
+ :server_root => File.expand_path(File.dirname(__FILE__)),
+ :server_type => WEBrick::SimpleServer
+}
+
+ARGV.options do |opts|
+ script_name = File.basename($0)
+ opts.banner = "Usage: ruby #{script_name} [options]"
+
+ opts.separator ""
+
+ opts.on("-p", "--port=port", Integer,
+ "Runs Rails on the specified port.",
+ "Default: 3000") { |OPTIONS[:port]| }
+ opts.on("-b", "--binding=ip", String,
+ "Binds Rails to the specified ip.",
+ "Default: 127.0.0.1") { |OPTIONS[:ip]| }
+ opts.on("-i", "--index=controller", String,
+ "Specifies an index controller that requests for root will go to (instead of congratulations screen)."
+ ) { |OPTIONS[:index_controller]| }
+ opts.on("-e", "--environment=name", String,
+ "Specifies the environment to run this server under (test/development/production).",
+ "Default: development") { |OPTIONS[:environment]| }
+ opts.on("-d", "--daemon",
+ "Make Rails run as a Daemon (only works if fork is available -- meaning on *nix)."
+ ) { OPTIONS[:server_type] = WEBrick::Daemon }
+
+ opts.separator ""
+
+ opts.on("-h", "--help",
+ "Show this help message.") { puts opts; exit }
+
+ opts.parse!
+end
+
+ENV["RAILS_ENV"] = OPTIONS[:environment]
+require File.dirname(__FILE__) + "/../config/environment"
+require 'webrick_server'
+
+puts "=> Rails application started on http://#{OPTIONS[:ip]}:#{OPTIONS[:port]}"
+DispatchServlet.dispatch(OPTIONS) \ No newline at end of file
diff --git a/railties/dispatches/start_server b/railties/dispatches/start_server
new file mode 100644
index 0000000000..c6ecb4e4fe
--- /dev/null
+++ b/railties/dispatches/start_server
@@ -0,0 +1 @@
+ruby public/dispatch.servlet \ No newline at end of file
diff --git a/railties/doc/README_FOR_APP b/railties/doc/README_FOR_APP
new file mode 100644
index 0000000000..ac6c149122
--- /dev/null
+++ b/railties/doc/README_FOR_APP
@@ -0,0 +1,2 @@
+Use this README file to introduce your application and point to useful places in the API for learning more.
+Run "rake appdoc" to generate API documentation for your models and controllers. \ No newline at end of file
diff --git a/railties/doc/apache_protection b/railties/doc/apache_protection
new file mode 100644
index 0000000000..37676c2c63
--- /dev/null
+++ b/railties/doc/apache_protection
@@ -0,0 +1,3 @@
+Order Deny,Allow
+Deny from all
+Allow from 127.0.0.1 \ No newline at end of file
diff --git a/railties/doc/index.html b/railties/doc/index.html
new file mode 100644
index 0000000000..57e25b75fa
--- /dev/null
+++ b/railties/doc/index.html
@@ -0,0 +1,94 @@
+<html>
+<head>
+ <title>Rails: Welcome on board</title>
+ <style>
+ body { background-color: #fff; color: #333; }
+
+ body, p, ol, ul, td {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 12px;
+ line-height: 18px;
+ }
+
+ li {
+ margin-bottom: 7px;
+ }
+
+ pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+ }
+
+ a { color: #000; }
+ a:visited { color: #666; }
+ a:hover { color: #fff; background-color:#000; }
+ </style>
+</head>
+<body>
+
+<h1>Congratulations, you're on Rails!</h1>
+
+<p>
+ <i>You've succesfully configured your web server to point at this Rails application.</i>
+</p>
+
+<p>Before you move on, verify that the following conditions have been met:</p>
+
+<ol>
+ <li>The log directory and the empty log files must be writable to the web server (<code>chmod -R 777 log</code>).
+ <li>
+ The shebang line in the public/dispatch* files must reference your Ruby installation. <br/>
+ You might need to change it to <code>#!/usr/bin/env ruby</code> or point directly at the installation.
+ </li>
+ <li>
+ Rails on Apache needs to have the cgi handler and mod_rewrite enabled. <br/>
+ Somewhere in your httpd.conf, you should have:<br/>
+ <code>AddHandler cgi-script .cgi</code><br/>
+ <code>LoadModule rewrite_module libexec/httpd/mod_rewrite.so</code><br/>
+ <code>AddModule mod_rewrite.c</code>
+ </li>
+</ol>
+
+<p>Take the following steps to get started:</p>
+
+<ol>
+ <li>Create empty production and test databases for your application.<br/>
+ <small>Warning: Don't point your test database at your production database, it'll destroy the latter on test runs!</small>
+ <li>Edit config/database.yml with your database settings.
+ <li>Create a new controller using the <code>script/new_controller</code> generator <br/>
+ <small>Help: Run with no arguments for documentation</small>
+ <li>Create a new model using the <code>script/new_model</code> generator <br/>
+ <small>Help: Run with no arguments for documentation</small>
+ <li>See all the tests run and fail by running <code>rake</code>.
+ <li>Develop your Rails application!
+ <li>Setup FastCGI or mod_ruby to get production-level performance
+</ol>
+
+<p>
+ Having problems getting up and running? First try debugging it yourself by looking at the log files. <br/> Then try the friendly Rails
+ community on IRC (<a href="http://www.rubyonrails.org/show/IRC">howto IRC</a>). It's on FreeNET in channel #rubyonrails.
+</p>
+
+<div style="float: left; margin-right: 20px">
+ <h2>Rails Online</h2>
+
+ <ul>
+ <li><a href="http://www.rubyonrails.org">Ruby on Rails</a></li>
+ <li><a href="http://activerecord.rubyonrails.org">Active Record</a></li>
+ <li><a href="http://actionpack.rubyonrails.org">Action Pack</a></li>
+ </ul>
+</div>
+
+<div style="float: left">
+ <h2>Beyond CGI</h2>
+
+ <ul>
+ <li><a href="http://www.fastcgi.com">FastCGI</a></li>
+ <li><a href="http://raa.ruby-lang.org/list.rhtml?name=fcgi">FastCGI bindings for Ruby</a></li>
+ <li><a href="http://modruby.net/en/">mod_ruby</a></li>
+ </ul>
+</div>
+
+</body>
+</html> \ No newline at end of file
diff --git a/railties/environments/development.rb b/railties/environments/development.rb
new file mode 100644
index 0000000000..81d5a73403
--- /dev/null
+++ b/railties/environments/development.rb
@@ -0,0 +1,2 @@
+ActiveRecord::Base.logger = ActionController::Base.logger = ActionMailer::Base.logger = Logger.new("#{RAILS_ROOT}/log/development.log")
+ActiveRecord::Base.establish_connection(:development) \ No newline at end of file
diff --git a/railties/environments/production.rb b/railties/environments/production.rb
new file mode 100644
index 0000000000..1ecda598de
--- /dev/null
+++ b/railties/environments/production.rb
@@ -0,0 +1,6 @@
+ActiveRecord::Base.logger = ActionController::Base.logger = ActionMailer::Base.logger = Logger.new("#{RAILS_ROOT}/log/production.log")
+ActiveRecord::Base.establish_connection(:production)
+
+ActionController::Base.consider_all_requests_local = false
+ActionController::Base.reload_dependencies = false
+ActiveRecord::Base.reload_associations = false \ No newline at end of file
diff --git a/railties/environments/shared.rb b/railties/environments/shared.rb
new file mode 100644
index 0000000000..f5d4771db6
--- /dev/null
+++ b/railties/environments/shared.rb
@@ -0,0 +1,35 @@
+RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + "/../")
+RAILS_ENV = ENV['RAILS_ENV'] || 'development'
+
+ADDITIONAL_LOAD_PATHS = [
+ "app/models",
+ "app/controllers",
+ "app/helpers",
+ "app",
+ "config",
+ "lib",
+ "vendor",
+ "vendor/railties",
+ "vendor/railties/lib",
+ "vendor/activerecord/lib",
+ "vendor/actionpack/lib",
+ "vendor/actionmailer/lib",
+]
+
+ADDITIONAL_LOAD_PATHS.unshift(Dir["#{RAILS_ROOT}/app/models/[a-z]*"].collect{ |dir| "app/models/#{File.basename(dir)}" })
+ADDITIONAL_LOAD_PATHS.unshift("test/mocks/#{RAILS_ENV}")
+
+ADDITIONAL_LOAD_PATHS.flatten.each { |dir| $: << "#{RAILS_ROOT}/#{dir}" }
+
+
+require 'active_record'
+require 'action_controller'
+require 'action_mailer'
+
+require 'yaml'
+
+ActionController::Base.template_root = ActionMailer::Base.template_root = "#{RAILS_ROOT}/app/views/"
+ActiveRecord::Base.configurations = YAML::load(File.open("#{RAILS_ROOT}/config/database.yml"))
+
+ActionController::Base.require_or_load 'abstract_application'
+ActionController::Base.require_or_load "environments/#{RAILS_ENV}" \ No newline at end of file
diff --git a/railties/environments/shared_for_gem.rb b/railties/environments/shared_for_gem.rb
new file mode 100644
index 0000000000..1ae6746122
--- /dev/null
+++ b/railties/environments/shared_for_gem.rb
@@ -0,0 +1,23 @@
+RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + "/../")
+RAILS_ENV = ENV['RAILS_ENV'] || 'development'
+
+ADDITIONAL_LOAD_PATHS = [ "app/models", "app/controllers", "app/helpers", "config", "lib", "vendor" ]
+ADDITIONAL_LOAD_PATHS.unshift(Dir["#{RAILS_ROOT}/app/models/[a-z]*"].collect{ |dir| "app/models/#{File.basename(dir)}" })
+ADDITIONAL_LOAD_PATHS.unshift("test/mocks/#{RAILS_ENV}")
+
+ADDITIONAL_LOAD_PATHS.flatten.each { |dir| $:.unshift "#{RAILS_ROOT}/#{dir}" }
+
+require 'rubygems'
+
+require_gem 'activerecord'
+require_gem 'actionpack'
+require_gem 'actionmailer'
+require_gem 'rails'
+
+require 'yaml'
+
+ActionController::Base.template_root = ActionMailer::Base.template_root = "#{RAILS_ROOT}/app/views/"
+ActiveRecord::Base.configurations = YAML::load(File.open("#{RAILS_ROOT}/config/database.yml"))
+
+ActionController::Base.require_or_load 'abstract_application'
+ActionController::Base.require_or_load "environments/#{RAILS_ENV}"
diff --git a/railties/environments/test.rb b/railties/environments/test.rb
new file mode 100644
index 0000000000..6ab6a1f50a
--- /dev/null
+++ b/railties/environments/test.rb
@@ -0,0 +1,2 @@
+ActiveRecord::Base.logger = ActionController::Base.logger = ActionMailer::Base.logger = Logger.new("#{RAILS_ROOT}/log/test.log")
+ActiveRecord::Base.establish_connection(:test)
diff --git a/railties/fresh_rakefile b/railties/fresh_rakefile
new file mode 100755
index 0000000000..3b746a49a5
--- /dev/null
+++ b/railties/fresh_rakefile
@@ -0,0 +1,104 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+$VERBOSE = nil
+
+require File.dirname(__FILE__) + '/config/environment'
+require 'code_statistics'
+
+desc "Run all the tests on a fresh test database"
+task :default => [ :clone_development_structure_to_test, :test_units, :test_functional ]
+
+desc "Generate API documentatio, show coding stats"
+task :doc => [ :appdoc, :stats ]
+
+
+desc "Run the unit tests in test/unit"
+Rake::TestTask.new("test_units") { |t|
+ t.libs << "test"
+ t.pattern = 'test/unit/*_test.rb'
+ t.verbose = true
+}
+
+desc "Run the functional tests in test/functional"
+Rake::TestTask.new("test_functional") { |t|
+ t.libs << "test"
+ t.pattern = 'test/functional/*_test.rb'
+ t.verbose = true
+}
+
+desc "Generate documentation for the application"
+Rake::RDocTask.new("appdoc") { |rdoc|
+ rdoc.rdoc_dir = 'doc/app'
+ rdoc.title = "Rails Application Documentation"
+ rdoc.options << '--line-numbers --inline-source'
+ rdoc.rdoc_files.include('doc/README_FOR_APP')
+ rdoc.rdoc_files.include('app/**/*.rb')
+}
+
+desc "Generate documentation for the Rails framework"
+Rake::RDocTask.new("apidoc") { |rdoc|
+ rdoc.rdoc_dir = 'doc/api'
+ rdoc.title = "Rails Framework Documentation"
+ rdoc.options << '--line-numbers --inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('vendor/railties/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/railties/MIT-LICENSE')
+ rdoc.rdoc_files.include('vendor/activerecord/README')
+ rdoc.rdoc_files.include('vendor/activerecord/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/activerecord/lib/active_record/**/*.rb')
+ rdoc.rdoc_files.exclude('vendor/activerecord/lib/active_record/vendor/*')
+ rdoc.rdoc_files.include('vendor/actionpack/README')
+ rdoc.rdoc_files.include('vendor/actionpack/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/actionpack/lib/action_controller/**/*.rb')
+ rdoc.rdoc_files.include('vendor/actionpack/lib/action_view/**/*.rb')
+ rdoc.rdoc_files.include('vendor/actionmailer/README')
+ rdoc.rdoc_files.include('vendor/actionmailer/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/actionmailer/lib/action_mailer/base.rb')
+}
+
+desc "Report code statistics (KLOCs, etc) from the application"
+task :stats do
+ CodeStatistics.new(
+ ["Controllers", "app/controllers"],
+ ["Helpers", "app/helpers"],
+ ["Models", "app/models"],
+ ["Units", "test/unit"],
+ ["Functionals", "test/functional"]
+ ).to_s
+end
+
+desc "Recreate the test databases from the development structure"
+task :clone_development_structure_to_test => [ :db_structure_dump, :purge_test_database ] do
+ if ActiveRecord::Base.configurations["test"]["adapter"] == "mysql"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0')
+ IO.readlines("db/development_structure.sql").join.split("\n\n").each do |table|
+ ActiveRecord::Base.connection.execute(table)
+ end
+ elsif ActiveRecord::Base.configurations["test"]["adapter"] == "postgresql"
+ `psql -U #{ActiveRecord::Base.configurations["test"]["username"]} -f db/development_structure.sql #{ActiveRecord::Base.configurations["test"]["database"]}`
+ end
+end
+
+desc "Dump the database structure to a SQL file"
+task :db_structure_dump do
+ if ActiveRecord::Base.configurations["test"]["adapter"] == "mysql"
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["test"])
+ File.open("db/development_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump }
+ elsif ActiveRecord::Base.configurations["test"]["adapter"] == "postgresql"
+ `pg_dump -U #{ActiveRecord::Base.configurations["test"]["username"]} -s -f db/development_structure.sql #{ActiveRecord::Base.configurations["test"]["database"]}`
+ end
+end
+
+desc "Drop the test database and bring it back again"
+task :purge_test_database do
+ if ActiveRecord::Base.configurations["test"]["adapter"] == "mysql"
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["development"])
+ ActiveRecord::Base.connection.recreate_database(ActiveRecord::Base.configurations["test"]["database"])
+ elsif ActiveRecord::Base.configurations["test"]["adapter"] == "postgresql"
+ `dropdb -U #{ActiveRecord::Base.configurations["test"]["username"]} #{ActiveRecord::Base.configurations["test"]["database"]}`
+ `createdb -U #{ActiveRecord::Base.configurations["test"]["username"]} #{ActiveRecord::Base.configurations["test"]["database"]}`
+ end
+end
diff --git a/railties/generators/new_controller.rb b/railties/generators/new_controller.rb
new file mode 100755
index 0000000000..3060c06382
--- /dev/null
+++ b/railties/generators/new_controller.rb
@@ -0,0 +1,43 @@
+#!/usr/local/bin/ruby
+require File.dirname(__FILE__) + '/../config/environment'
+require 'generator'
+
+unless ARGV.empty?
+ rails_root = File.dirname(__FILE__) + '/..'
+ name = ARGV.shift
+ actions = ARGV
+ Generator::Controller.new(rails_root, name, actions).generate
+else
+ puts <<-END_HELP
+
+NAME
+ new_controller - create controller and view stub files
+
+SYNOPSIS
+ new_controller ControllerName action [action ...]
+
+DESCRIPTION
+ The new_controller generator takes the name of the new controller as the
+ first argument and a variable number of view names as subsequent arguments.
+ The controller name should be supplied without a "Controller" suffix. The
+ generator will add that itself.
+
+ From the passed arguments, new_controller generates a controller file in
+ app/controllers with a render action for each of the view names passed.
+ It then creates a controller test suite in test/functional with one failing
+ test case. Finally, it creates an HTML stub for each of the view names in
+ app/views under a directory with the same name as the controller.
+
+EXAMPLE
+ new_controller Blog list display new edit
+
+ This will generate a BlogController class in
+ app/controllers/blog_controller.rb, a BlogHelper class in
+ app/helpers/blog_helper.rb and a BlogControllerTest in
+ test/functional/blog_controller_test.rb. It will also create list.rhtml,
+ display.rhtml, new.rhtml, and edit.rhtml in app/views/blog.
+
+ The BlogController class will have the following methods: list, display, new, edit.
+ Each will default to render the associated template file.
+END_HELP
+end
diff --git a/railties/generators/new_crud.rb b/railties/generators/new_crud.rb
new file mode 100755
index 0000000000..4eaa1cb1f3
--- /dev/null
+++ b/railties/generators/new_crud.rb
@@ -0,0 +1,34 @@
+#!/usr/local/bin/ruby
+require File.dirname(__FILE__) + '/../config/environment'
+require 'generator'
+
+unless ARGV.empty?
+ rails_root = File.dirname(__FILE__) + '/..'
+ name = ARGV.shift
+ actions = ARGV
+ Generator::Model.new(rails_root, name).generate
+ Generator::Controller.new(rails_root, name, actions, :scaffold => true).generate
+else
+ puts <<-END_HELP
+
+NAME
+ new_crud - create a model and a controller scaffold
+
+SYNOPSIS
+ new_crud ModelName [action ...]
+
+DESCRIPTION
+ The new_crud generator takes the name of the new model as the
+ first argument and an optional list of controller actions as the
+ subsequent arguments. All actions may be omitted since the controller
+ will have scaffolding automatically set up for this model.
+
+EXAMPLE
+ new_crud Account
+
+ This will generate an Account model and controller with scaffolding.
+ Now create the accounts table in your database and browse to
+ http://localhost/account/ -- voila, you're on Rails!
+
+END_HELP
+end
diff --git a/railties/generators/new_mailer.rb b/railties/generators/new_mailer.rb
new file mode 100644
index 0000000000..05d0c9ae82
--- /dev/null
+++ b/railties/generators/new_mailer.rb
@@ -0,0 +1,43 @@
+#!/usr/local/bin/ruby
+require File.dirname(__FILE__) + '/../config/environment'
+require 'generator'
+
+unless ARGV.empty?
+ rails_root = File.dirname(__FILE__) + '/..'
+ name = ARGV.shift
+ actions = ARGV
+ Generator::Mailer.new(rails_root, name, actions).generate
+else
+ puts <<-END_HELP
+
+NAME
+ new_mailer - create mailer and view stub files
+
+SYNOPSIS
+ new_mailer MailerName action [action ...]
+
+DESCRIPTION
+ The new_mailer generator takes the name of the new mailer class as the
+ first argument and a variable number of mail action names as subsequent
+ arguments.
+
+ From the passed arguments, new_mailer generates a class file in
+ app/models with a mail action for each of the mail action names passed.
+ It then creates a mail test suite in test/unit with one stub test case
+ and one stub fixture per mail action. Finally, it creates a template stub
+ for each of the mail action names in app/views under a directory with the
+ same name as the class.
+
+EXAMPLE
+ new_mailer Notifications signup forgot_password invoice
+
+ This will generate a Notifications class in
+ app/models/notifications.rb, a NotificationsTest in
+ test/unit/notifications_test.rb, and signup, forgot_password, and invoice
+ in test/fixture/notification. It will also create signup.rhtml,
+ forgot_password.rhtml, and invoice.rhtml in app/views/notifications.
+
+ The Notifications class will have the following methods: signup,
+ forgot_password, and invoice.
+END_HELP
+end
diff --git a/railties/generators/new_model.rb b/railties/generators/new_model.rb
new file mode 100755
index 0000000000..f6fbf5f002
--- /dev/null
+++ b/railties/generators/new_model.rb
@@ -0,0 +1,31 @@
+#!/usr/local/bin/ruby
+require File.dirname(__FILE__) + '/../config/environment'
+require 'generator'
+
+if ARGV.size == 1
+ rails_root = File.dirname(__FILE__) + '/..'
+ name = ARGV.shift
+ Generator::Model.new(rails_root, name).generate
+else
+ puts <<-HELP
+
+NAME
+ new_model - create model stub files
+
+SYNOPSIS
+ new_model ModelName
+
+DESCRIPTION
+ The new_model generator takes a model name (in CamelCase) and generates
+ a new, empty model in app/models, a test suite in test/unit with one
+ failing test case, and a fixtures directory in test/fixtures.
+
+EXAMPLE
+ new_model Account
+
+ This will generate an Account class in app/models/account.rb, an
+ AccountTest in test/unit/account_test.rb, and the directory
+ test/fixtures/account.
+
+HELP
+end
diff --git a/railties/generators/templates/controller.erb b/railties/generators/templates/controller.erb
new file mode 100644
index 0000000000..600f5d2c59
--- /dev/null
+++ b/railties/generators/templates/controller.erb
@@ -0,0 +1,19 @@
+class <%= class_name %>Controller < AbstractApplicationController
+ helper :<%= file_name %>
+<% if options[:scaffold] -%>
+ model :<%= file_name %>
+ scaffold :<%= options[:scaffold] %>
+
+ <%- for action in actions -%>
+ #def <%= action %>
+ #end
+
+ <%- end -%>
+<% else -%>
+ <%- for action in actions -%>
+ def <%= action %>
+ end
+
+ <%- end -%>
+<% end -%>
+end
diff --git a/railties/generators/templates/controller_test.erb b/railties/generators/templates/controller_test.erb
new file mode 100644
index 0000000000..5577379c62
--- /dev/null
+++ b/railties/generators/templates/controller_test.erb
@@ -0,0 +1,17 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require '<%= file_name %>_controller'
+
+# Re-raise errors caught by the controller.
+class <%= class_name %>Controller; def rescue_action(e) raise e end; end
+
+class <%= class_name %>ControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = <%= class_name %>Controller.new
+ @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests
+ def test_truth
+ assert true
+ end
+end
diff --git a/railties/generators/templates/controller_view.rhtml b/railties/generators/templates/controller_view.rhtml
new file mode 100644
index 0000000000..d8a310df50
--- /dev/null
+++ b/railties/generators/templates/controller_view.rhtml
@@ -0,0 +1,10 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <title><%= class_name %>#<%= action %></title>
+</head>
+<body>
+<h1><%= class_name %>#<%= action %></h1>
+<p>Find me in app/views/<%= file_name %>/<%= action %>.rhtml</p>
+</body>
+</html>
diff --git a/railties/generators/templates/helper.erb b/railties/generators/templates/helper.erb
new file mode 100644
index 0000000000..3fe2ecdc74
--- /dev/null
+++ b/railties/generators/templates/helper.erb
@@ -0,0 +1,2 @@
+module <%= class_name %>Helper
+end
diff --git a/railties/generators/templates/mailer.erb b/railties/generators/templates/mailer.erb
new file mode 100644
index 0000000000..5afc254923
--- /dev/null
+++ b/railties/generators/templates/mailer.erb
@@ -0,0 +1,15 @@
+require 'action_mailer'
+
+class <%= class_name %> < ActionMailer::Base
+
+<% for action in actions -%>
+ def <%= action %>(sent_on = Time.now)
+ @recipients = ''
+ @from = ''
+ @subject = ''
+ @body = {}
+ @sent_on = sent_on
+ end
+
+<% end -%>
+end
diff --git a/railties/generators/templates/mailer_action.rhtml b/railties/generators/templates/mailer_action.rhtml
new file mode 100644
index 0000000000..b481906829
--- /dev/null
+++ b/railties/generators/templates/mailer_action.rhtml
@@ -0,0 +1,3 @@
+<%= class_name %>#<%= action %>
+
+Find me in app/views/<%= file_name %>/<%= action %>.rhtml
diff --git a/railties/generators/templates/mailer_fixture.rhtml b/railties/generators/templates/mailer_fixture.rhtml
new file mode 100644
index 0000000000..f315d430ed
--- /dev/null
+++ b/railties/generators/templates/mailer_fixture.rhtml
@@ -0,0 +1,4 @@
+<%= class_name %>#<%= action %>
+
+Find me in test/fixtures/<%= file_name %>/<%= action %>.
+I'm tested against the view in app/views/<%= file_name %>/<%= action %>.
diff --git a/railties/generators/templates/mailer_test.erb b/railties/generators/templates/mailer_test.erb
new file mode 100644
index 0000000000..f17d614195
--- /dev/null
+++ b/railties/generators/templates/mailer_test.erb
@@ -0,0 +1,37 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require '<%= file_name %>'
+
+class <%= class_name %>Test < Test::Unit::TestCase
+ FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
+
+ def setup
+ ActionMailer::Base.delivery_method = :test
+ ActionMailer::Base.perform_deliveries = true
+ ActionMailer::Base.deliveries = []
+
+ @expected = TMail::Mail.new
+ @expected.to = 'test@localhost'
+ @expected.from = 'test@localhost'
+ @expected.subject = '<%= class_name %> test mail'
+ end
+
+<% for action in actions -%>
+ def test_<%= action %>
+ @expected.body = read_fixture('<%= action %>')
+ @expected.date = Time.now
+
+ created = nil
+ assert_nothing_raised { created = <%= class_name %>.create_<%= action %>(@expected.date) }
+ assert_not_nil created
+ assert_equal @expected.encoded, created.encoded
+
+ assert_nothing_raised { <%= class_name %>.deliver_<%= action %>(@expected.date) }
+ assert_equal @expected.encoded, ActionMailer::Base.deliveries.first.encoded
+ end
+
+<% end -%>
+ private
+ def read_fixture(action)
+ IO.readlines("#{FIXTURES_PATH}/<%= file_name %>/#{action}")
+ end
+end
diff --git a/railties/generators/templates/model.erb b/railties/generators/templates/model.erb
new file mode 100644
index 0000000000..8d4c89e912
--- /dev/null
+++ b/railties/generators/templates/model.erb
@@ -0,0 +1,2 @@
+class <%= class_name %> < ActiveRecord::Base
+end
diff --git a/railties/generators/templates/model_test.erb b/railties/generators/templates/model_test.erb
new file mode 100644
index 0000000000..a3ad2b72fb
--- /dev/null
+++ b/railties/generators/templates/model_test.erb
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require '<%= file_name %>'
+
+class <%= class_name %>Test < Test::Unit::TestCase
+ fixtures :<%= table_name %>
+
+ # Replace this with your real tests
+ def test_truth
+ assert true
+ end
+end \ No newline at end of file
diff --git a/railties/helpers/abstract_application.rb b/railties/helpers/abstract_application.rb
new file mode 100644
index 0000000000..fa26cd0399
--- /dev/null
+++ b/railties/helpers/abstract_application.rb
@@ -0,0 +1,5 @@
+# The filters added to this controller will be run for all controllers in the application.
+# Likewise will all the methods added be available for all controllers.
+class AbstractApplicationController < ActionController::Base
+ helper :application
+end \ No newline at end of file
diff --git a/railties/helpers/application_helper.rb b/railties/helpers/application_helper.rb
new file mode 100644
index 0000000000..0392b53b46
--- /dev/null
+++ b/railties/helpers/application_helper.rb
@@ -0,0 +1,3 @@
+# The methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+end
diff --git a/railties/helpers/test_helper.rb b/railties/helpers/test_helper.rb
new file mode 100644
index 0000000000..d348f26517
--- /dev/null
+++ b/railties/helpers/test_helper.rb
@@ -0,0 +1,16 @@
+ENV["RAILS_ENV"] ||= "test"
+require File.dirname(__FILE__) + "/../config/environment"
+
+require 'test/unit'
+require 'active_record/fixtures'
+require 'action_controller/test_process'
+
+# Make rubygems available for testing if possible
+begin require('rubygems'); rescue LoadError; end
+begin require('dev-utils/debug'); rescue LoadError; end
+
+def create_fixtures(*table_names)
+ Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures", table_names)
+end
+
+Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" \ No newline at end of file
diff --git a/railties/html/404.html b/railties/html/404.html
new file mode 100644
index 0000000000..edbc89bf99
--- /dev/null
+++ b/railties/html/404.html
@@ -0,0 +1,6 @@
+<html>
+<body>
+ <h1>File not found</h1>
+ <p>Change this error message for pages not found in public/404.html</p>
+</body>
+</html> \ No newline at end of file
diff --git a/railties/html/500.html b/railties/html/500.html
new file mode 100644
index 0000000000..ee0c919c4a
--- /dev/null
+++ b/railties/html/500.html
@@ -0,0 +1,6 @@
+<html>
+<body>
+ <h1>Application error (Apache)</h1>
+ <p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p>
+</body>
+</html> \ No newline at end of file
diff --git a/railties/html/index.html b/railties/html/index.html
new file mode 100644
index 0000000000..4949c64a5a
--- /dev/null
+++ b/railties/html/index.html
@@ -0,0 +1 @@
+<html><head><META HTTP-EQUIV="Refresh" CONTENT="0;URL=_doc/index.html"></head></html> \ No newline at end of file
diff --git a/railties/lib/code_statistics.rb b/railties/lib/code_statistics.rb
new file mode 100644
index 0000000000..53e7feb1c0
--- /dev/null
+++ b/railties/lib/code_statistics.rb
@@ -0,0 +1,71 @@
+class CodeStatistics
+ def initialize(*pairs)
+ @pairs = pairs
+ @statistics = calculate_statistics
+ @total = calculate_total if pairs.length > 1
+ end
+
+ def to_s
+ print_header
+ @statistics.each{ |k, v| print_line(k, v) }
+ print_splitter
+
+ if @total
+ print_line("Total", @total)
+ print_splitter
+ end
+ end
+
+ private
+ def calculate_statistics
+ @pairs.inject({}) { |stats, pair| stats[pair.first] = calculate_directory_statistics(pair.last); stats }
+ end
+
+ def calculate_directory_statistics(directory, pattern = /.*rb/)
+ stats = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 }
+
+ Dir.foreach(directory) do |file_name|
+ next unless file_name =~ pattern
+
+ f = File.open(directory + "/" + file_name)
+
+ while line = f.gets
+ stats["lines"] += 1
+ stats["classes"] += 1 if line =~ /class [A-Z]/
+ stats["methods"] += 1 if line =~ /def [a-z]/
+ stats["codelines"] += 1 unless line =~ /^\s*$/ || line =~ /^\s*#/
+ end
+ end
+
+ stats
+ end
+
+ def calculate_total
+ total = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 }
+ @statistics.each_value { |pair| pair.each { |k, v| total[k] += v } }
+ total
+ end
+
+ def print_header
+ print_splitter
+ puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |"
+ print_splitter
+ end
+
+ def print_splitter
+ puts "+----------------------+-------+-------+---------+---------+-----+-------+"
+ end
+
+ def print_line(name, statistics)
+ m_over_c = (statistics["methods"] / statistics["classes"]) rescue m_over_c = 0
+ loc_over_m = (statistics["codelines"] / statistics["methods"]) - 2 rescue loc_over_m = 0
+
+ puts "| #{name.ljust(20)} " +
+ "| #{statistics["lines"].to_s.rjust(5)} " +
+ "| #{statistics["codelines"].to_s.rjust(5)} " +
+ "| #{statistics["classes"].to_s.rjust(7)} " +
+ "| #{statistics["methods"].to_s.rjust(7)} " +
+ "| #{m_over_c.to_s.rjust(3)} " +
+ "| #{loc_over_m.to_s.rjust(5)} |"
+ end
+end \ No newline at end of file
diff --git a/railties/lib/dispatcher.rb b/railties/lib/dispatcher.rb
new file mode 100644
index 0000000000..aa7ae98edd
--- /dev/null
+++ b/railties/lib/dispatcher.rb
@@ -0,0 +1,55 @@
+#--
+# Copyright (c) 2004 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+class Dispatcher
+ DEFAULT_SESSION_OPTIONS = { "database_manager" => CGI::Session::PStore, "prefix" => "ruby_sess.", "session_path" => "/" }
+
+ def self.dispatch(cgi = CGI.new, session_options = DEFAULT_SESSION_OPTIONS, error_page = nil)
+ begin
+ request = ActionController::CgiRequest.new(cgi, session_options)
+ response = ActionController::CgiResponse.new(cgi)
+
+ controller_name = request.parameters["controller"].gsub(/[^_a-zA-Z0-9]/, "").untaint
+
+ if module_name = request.parameters["module"]
+ Module.new do
+ ActionController::Base.require_or_load "#{module_name}/#{Inflector.underscore(controller_name)}_controller"
+ Object.const_get("#{Inflector.camelize(controller_name)}Controller").process(request, response).out
+ end
+ else
+ ActionController::Base.require_or_load "#{Inflector.underscore(controller_name)}_controller"
+ Object.const_get("#{Inflector.camelize(controller_name)}Controller").process(request, response).out
+ end
+ rescue Exception => e
+ begin
+ ActionController::Base.logger.info "\n\nException throw during dispatch: #{e.message}\n#{e.backtrace.join("\n")}"
+ rescue Exception
+ # Couldn't log error
+ end
+
+ if error_page then cgi.out{ IO.readlines(error_page) } else raise e end
+ ensure
+ ActiveRecord::Base.reset_associations_loaded
+ end
+ end
+end
diff --git a/railties/lib/generator.rb b/railties/lib/generator.rb
new file mode 100644
index 0000000000..28b41c60f0
--- /dev/null
+++ b/railties/lib/generator.rb
@@ -0,0 +1,112 @@
+require 'fileutils'
+require 'active_record/support/inflector'
+
+module Generator
+ class GeneratorError < StandardError; end
+
+ class Base
+ @@template_root = File.dirname(__FILE__) + '/../generators/templates'
+ cattr_accessor :template_root
+
+ attr_reader :rails_root, :class_name, :file_name, :table_name,
+ :actions, :options
+
+ def initialize(rails_root, object_name, actions = [], options = {})
+ @rails_root = rails_root
+ @class_name = Inflector.camelize(object_name)
+ @file_name = Inflector.underscore(@class_name)
+ @table_name = Inflector.pluralize(@file_name)
+ @actions = actions
+ @options = options
+
+ # Use local templates if rails_root/generators directory exists.
+ local_template_root = File.join(@rails_root, 'generators')
+ if File.directory?(local_template_root)
+ self.class.template_root = local_template_root
+ end
+ end
+
+ protected
+
+ # Generate a file in a fresh Rails app from an ERB template.
+ # Takes a template path relative to +template_root+, a
+ # destination path relative to +rails_root+, evaluates the template,
+ # and writes the result to the destination.
+ def generate_file(template_file_path, rails_file_path, eval_binding = nil)
+ # Determine full paths for source and destination files.
+ template_path = File.join(template_root, template_file_path)
+ rails_path = File.join(rails_root, rails_file_path)
+
+ # Create destination directories.
+ FileUtils.mkdir_p(File.dirname(rails_path))
+
+ # Render template and write result.
+ eval_binding ||= binding
+ contents = ERB.new(File.read(template_path), nil, '-').result(eval_binding)
+ File.open(rails_path, 'w') { |file| file.write(contents) }
+ end
+ end
+
+ # Generate controller, helper, functional test, and views.
+ class Controller < Base
+ def generate
+ options[:scaffold] = file_name if options[:scaffold]
+
+ # Controller class.
+ generate_file "controller.erb", "app/controllers/#{file_name}_controller.rb"
+
+ # Helper class.
+ generate_file "helper.erb", "app/helpers/#{file_name}_helper.rb"
+
+ # Function test.
+ generate_file "controller_test.erb", "test/functional/#{file_name}_controller_test.rb"
+
+ # View template for each action.
+ @actions.each do |action|
+ generate_file "controller_view.rhtml",
+ "app/views/#{file_name}/#{action}.rhtml",
+ binding
+ end
+ end
+ end
+
+ # Generate model, unit test, and fixtures.
+ class Model < Base
+ def generate
+
+ # Model class.
+ generate_file "model.erb", "app/models/#{file_name}.rb"
+
+ # Model unit test.
+ generate_file "model_test.erb", "test/unit/#{file_name}_test.rb"
+
+ # Test fixtures directory.
+ FileUtils.mkdir_p("test/fixtures/#{table_name}")
+ end
+ end
+
+ # Generate mailer, helper, functional test, and views.
+ class Mailer < Base
+ def generate
+
+ # Mailer class.
+ generate_file "mailer.erb", "app/models/#{file_name}.rb"
+
+ # Mailer unit test.
+ generate_file "mailer_test.erb", "test/unit/#{file_name}_test.rb"
+
+ # Test fixtures directory.
+ FileUtils.mkdir_p("test/fixtures/#{table_name}")
+
+ # View template and fixture for each action.
+ @actions.each do |action|
+ generate_file "mailer_action.rhtml",
+ "app/views/#{file_name}/#{action}.rhtml",
+ binding
+ generate_file "mailer_fixture.rhtml",
+ "test/fixtures/#{table_name}/#{action}",
+ binding
+ end
+ end
+ end
+end
diff --git a/railties/lib/webrick_server.rb b/railties/lib/webrick_server.rb
new file mode 100644
index 0000000000..66c78fbd5f
--- /dev/null
+++ b/railties/lib/webrick_server.rb
@@ -0,0 +1,159 @@
+# Donated by Florian Gross
+
+require 'webrick'
+require 'cgi'
+require 'stringio'
+
+begin
+ require 'dev-utils/debug'
+ require 'irb/completion'
+
+ module DevUtils::Debug
+ alias_method :breakpoint_without_io, :breakpoint unless method_defined?(:breakpoint_without_io)
+
+ def breakpoint(name = nil, context = nil, &block)
+ $new_stdin, $new_stdout = $stdin, $stdout
+ $stdin, $stdout = $old_stdin, $old_stdout
+ breakpoint_without_io(name, context, &block)
+ $stdin, $stdout = $new_stdin, $new_stdout
+ end
+ end
+rescue LoadError
+ # dev utils not available
+end
+
+include WEBrick
+
+class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet
+ REQUEST_MUTEX = Mutex.new
+
+ def self.dispatch(options = {})
+ Socket.do_not_reverse_lookup = true # patch for OS X
+
+ server = WEBrick::HTTPServer.new(:Port => options[:port].to_i, :ServerType => options[:server_type], :BindAddress => options[:ip])
+ server.mount('/', DispatchServlet, options)
+
+ trap("INT") { server.shutdown }
+ server.start
+ end
+
+ def initialize(server, options)
+ @server_options = options
+ @file_handler = WEBrick::HTTPServlet::FileHandler.new(server, options[:server_root], {:FancyIndexing => true })
+ super
+ end
+
+ def do_GET(req, res)
+ begin
+ REQUEST_MUTEX.lock
+
+ unless handle_index(req, res)
+ unless handle_dispatch(req, res)
+ unless handle_file(req, res)
+ unless handle_mapped(req, res)
+ raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
+ end
+ end
+ end
+ end
+ ensure
+ REQUEST_MUTEX.unlock
+ end
+ end
+
+ alias :do_POST :do_GET
+
+ def handle_index(req, res)
+ if req.request_uri.path == "/"
+ if @server_options[:index_controller]
+ res.set_redirect WEBrick::HTTPStatus::MovedPermanently, "/#{@server_options[:index_controller]}/"
+ else
+ res.set_redirect WEBrick::HTTPStatus::MovedPermanently, "/_doc/index.html"
+ end
+
+ return true
+ else
+ return false
+ end
+ end
+
+ def handle_file(req, res)
+ begin
+ @file_handler.send(:do_GET, req, res)
+ return true
+ rescue HTTPStatus::PartialContent, HTTPStatus::NotModified => err
+ res.set_error(err)
+ return true
+ rescue => err
+ p err
+ return false
+ end
+ end
+
+ def handle_mapped(req, res)
+ parsed_ok, controller, action, id = DispatchServlet.parse_uri(req.request_uri.path)
+ if parsed_ok
+ query = "controller=#{controller}&action=#{action}&id=#{id}"
+ query << "&#{req.request_uri.query}" if req.request_uri.query
+ origin = req.request_uri.path + "?" + query
+ req.request_uri.path = "/dispatch.rb"
+ req.request_uri.query = query
+ handle_dispatch(req, res, origin)
+ else
+ return false
+ end
+ end
+
+ def handle_dispatch(req, res, origin = nil)
+ return false unless /^\/dispatch\.(?:cgi|rb|fcgi)$/.match(req.request_uri.path)
+
+ env = req.meta_vars.clone
+ env["QUERY_STRING"] = req.request_uri.query
+ env["REQUEST_URI"] = origin if origin
+
+ data = nil
+ $old_stdin, $old_stdout = $stdin, $stdout
+ $stdin, $stdout = StringIO.new(req.body || ""), StringIO.new
+
+ begin
+ require 'cgi'
+ CGI.send(:define_method, :env_table) { env }
+
+ load File.join(@server_options[:server_root], "dispatch.rb")
+
+ $stdout.rewind
+ data = $stdout.read
+ ensure
+ $stdin, $stdout = $old_stdin, $old_stdout
+ end
+
+ raw_header, body = *data.split(/^[\xd\xa]+/on, 2)
+ header = WEBrick::HTTPUtils::parse_header(raw_header)
+ if /^(\d+)/ =~ header['status'][0]
+ res.status = $1.to_i
+ header.delete('status')
+ end
+ header.each { |key, val| res[key] = val.join(", ") }
+
+ res.body = body
+ return true
+ rescue => err
+ p err, err.backtrace
+ return false
+ end
+
+ def self.parse_uri(path)
+ component = /([-_a-zA-Z0-9]+)/
+
+ case path.sub(%r{^/(?:fcgi|mruby|cgi)/}, "/")
+ when %r{^/#{component}/?$} then
+ [true, $1, "index", nil]
+ when %r{^/#{component}/#{component}/?$} then
+ [true, $1, $2, nil]
+ when %r{^/#{component}/#{component}/#{component}/?$} then
+ [true, $1, $2, $3]
+ else
+ [false, nil, nil, nil]
+ end
+ end
+end
diff --git a/railties/test/webrick_dispatcher_test.rb b/railties/test/webrick_dispatcher_test.rb
new file mode 100644
index 0000000000..2c6b51ae62
--- /dev/null
+++ b/railties/test/webrick_dispatcher_test.rb
@@ -0,0 +1,30 @@
+#!/bin/env ruby
+
+$:.unshift(File.dirname(__FILE__) + "/../lib")
+
+require 'test/unit'
+require 'webrick_server'
+
+class ParseUriTest < Test::Unit::TestCase
+
+ def test_parse_uri_old_behavior
+ assert_equal [true, 'forum', 'index', '1'], DispatchServlet.parse_uri('/forum/index/1')
+ assert_equal [true, 'forum', 'index', nil], DispatchServlet.parse_uri('/forum/index')
+ assert_equal [true, 'forum', 'index', nil], DispatchServlet.parse_uri('/forum/')
+ end
+
+ def test_parse_uri_new_behavior
+ assert_equal [true, 'forum', 'index', '1'], DispatchServlet.parse_uri('/forum/index/1/')
+ assert_equal [true, 'forum', 'index', nil], DispatchServlet.parse_uri('/forum/index/')
+ assert_equal [true, 'forum', 'index', nil], DispatchServlet.parse_uri('/forum')
+ end
+
+ def test_parse_uri_failures
+ assert_equal [false, nil, nil, nil], DispatchServlet.parse_uri('/')
+ assert_equal [false, nil, nil, nil], DispatchServlet.parse_uri('a')
+ assert_equal [false, nil, nil, nil], DispatchServlet.parse_uri('/forum//')
+ assert_equal [false, nil, nil, nil], DispatchServlet.parse_uri('/+forum/')
+ assert_equal [false, nil, nil, nil], DispatchServlet.parse_uri('forum/')
+ end
+
+end