swivel 0.0.146 → 0.0.149

Sign up to get free protection for your applications and to get access to all the features.
Files changed (183) hide show
  1. data/README +4 -1
  2. data/Rakefile +1 -1
  3. data/lib/swivel.rb +1 -1
  4. data/lib/swivel2/benchmarking.rb +1 -0
  5. data/lib/swivel2/config.rb +45 -0
  6. data/lib/swivel2/connection.rb +89 -0
  7. data/lib/swivel2/formats.rb +11 -0
  8. data/lib/swivel2/logging.rb +1 -0
  9. data/lib/swivel2/performance.rb +21 -0
  10. data/lib/swivel2/response.rb +5 -0
  11. data/lib/swivel2/swivelrc.default +5 -0
  12. data/vendor/activeresource-2.0.2-/CHANGELOG +223 -0
  13. data/vendor/activeresource-2.0.2-/README +165 -0
  14. data/vendor/activeresource-2.0.2-/Rakefile +133 -0
  15. data/vendor/activeresource-2.0.2-/lib/active_resource.rb +47 -0
  16. data/vendor/activeresource-2.0.2-/lib/active_resource/base.rb +872 -0
  17. data/vendor/activeresource-2.0.2-/lib/active_resource/connection.rb +172 -0
  18. data/vendor/activeresource-2.0.2-/lib/active_resource/custom_methods.rb +105 -0
  19. data/vendor/activeresource-2.0.2-/lib/active_resource/formats.rb +14 -0
  20. data/vendor/activeresource-2.0.2-/lib/active_resource/formats/json_format.rb +23 -0
  21. data/vendor/activeresource-2.0.2-/lib/active_resource/formats/xml_format.rb +34 -0
  22. data/vendor/activeresource-2.0.2-/lib/active_resource/http_mock.rb +147 -0
  23. data/vendor/activeresource-2.0.2-/lib/active_resource/validations.rb +288 -0
  24. data/vendor/activeresource-2.0.2-/lib/active_resource/version.rb +9 -0
  25. data/vendor/activeresource-2.0.2-/lib/activeresource.rb +1 -0
  26. data/vendor/activeresource-2.0.2-/test/abstract_unit.rb +10 -0
  27. data/vendor/activeresource-2.0.2-/test/authorization_test.rb +82 -0
  28. data/vendor/activeresource-2.0.2-/test/base/custom_methods_test.rb +96 -0
  29. data/vendor/activeresource-2.0.2-/test/base/equality_test.rb +43 -0
  30. data/vendor/activeresource-2.0.2-/test/base/load_test.rb +111 -0
  31. data/vendor/activeresource-2.0.2-/test/base_errors_test.rb +48 -0
  32. data/vendor/activeresource-2.0.2-/test/base_test.rb +454 -0
  33. data/vendor/activeresource-2.0.2-/test/connection_test.rb +170 -0
  34. data/vendor/activeresource-2.0.2-/test/fixtures/beast.rb +14 -0
  35. data/vendor/activeresource-2.0.2-/test/fixtures/person.rb +3 -0
  36. data/vendor/activeresource-2.0.2-/test/fixtures/street_address.rb +4 -0
  37. data/vendor/activeresource-2.0.2-/test/format_test.rb +42 -0
  38. data/vendor/activeresource-2.0.2-/test/setter_trap.rb +27 -0
  39. data/vendor/activesupport-2.0.2-/CHANGELOG +986 -0
  40. data/vendor/activesupport-2.0.2-/README +43 -0
  41. data/vendor/activesupport-2.0.2-/lib/active_support.rb +49 -0
  42. data/vendor/activesupport-2.0.2-/lib/active_support/basic_object.rb +5 -0
  43. data/vendor/activesupport-2.0.2-/lib/active_support/buffered_logger.rb +107 -0
  44. data/vendor/activesupport-2.0.2-/lib/active_support/clean_logger.rb +127 -0
  45. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext.rb +4 -0
  46. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/array.rb +13 -0
  47. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/array/access.rb +28 -0
  48. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/array/conversions.rb +94 -0
  49. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/array/extract_options.rb +19 -0
  50. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/array/grouping.rb +68 -0
  51. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/array/random_access.rb +12 -0
  52. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/bigdecimal.rb +2 -0
  53. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/bigdecimal/conversions.rb +6 -0
  54. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/blank.rb +50 -0
  55. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/cgi.rb +5 -0
  56. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb +14 -0
  57. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/class.rb +4 -0
  58. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/class/attribute_accessors.rb +48 -0
  59. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/class/delegating_attributes.rb +40 -0
  60. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/class/inheritable_attributes.rb +140 -0
  61. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/class/removal.rb +24 -0
  62. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/date.rb +10 -0
  63. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/date/behavior.rb +13 -0
  64. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/date/calculations.rb +188 -0
  65. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/date/conversions.rb +98 -0
  66. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/date_time.rb +10 -0
  67. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/date_time/calculations.rb +77 -0
  68. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/date_time/conversions.rb +74 -0
  69. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/duplicable.rb +37 -0
  70. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/enumerable.rb +63 -0
  71. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/exception.rb +33 -0
  72. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/file.rb +21 -0
  73. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/float.rb +5 -0
  74. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/float/rounding.rb +24 -0
  75. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/hash.rb +13 -0
  76. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/hash/conversions.rb +242 -0
  77. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/hash/diff.rb +19 -0
  78. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/hash/except.rb +24 -0
  79. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/hash/indifferent_access.rb +102 -0
  80. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/hash/keys.rb +54 -0
  81. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/hash/reverse_merge.rb +25 -0
  82. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/hash/slice.rb +28 -0
  83. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/integer.rb +7 -0
  84. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/integer/even_odd.rb +24 -0
  85. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/integer/inflections.rb +21 -0
  86. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/kernel.rb +5 -0
  87. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/kernel/agnostics.rb +11 -0
  88. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/kernel/daemonizing.rb +15 -0
  89. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/kernel/debugger.rb +13 -0
  90. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/kernel/reporting.rb +51 -0
  91. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/kernel/requires.rb +24 -0
  92. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/load_error.rb +38 -0
  93. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/logger.rb +16 -0
  94. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/module.rb +8 -0
  95. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/module/aliasing.rb +70 -0
  96. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/module/attr_accessor_with_default.rb +31 -0
  97. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/module/attr_internal.rb +31 -0
  98. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/module/attribute_accessors.rb +48 -0
  99. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/module/delegation.rb +62 -0
  100. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/module/inclusion.rb +11 -0
  101. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/module/introspection.rb +35 -0
  102. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/module/loading.rb +13 -0
  103. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/name_error.rb +17 -0
  104. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/numeric.rb +7 -0
  105. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/numeric/bytes.rb +44 -0
  106. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/numeric/time.rb +91 -0
  107. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/object.rb +4 -0
  108. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/object/conversions.rb +14 -0
  109. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/object/extending.rb +58 -0
  110. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/object/instance_variables.rb +22 -0
  111. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/object/misc.rb +59 -0
  112. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/pathname.rb +7 -0
  113. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/pathname/clean_within.rb +14 -0
  114. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/proc.rb +12 -0
  115. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/range.rb +11 -0
  116. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/range/blockless_step.rb +22 -0
  117. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/range/conversions.rb +23 -0
  118. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/range/include_range.rb +22 -0
  119. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/range/overlaps.rb +12 -0
  120. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/string.rb +23 -0
  121. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/string/access.rb +58 -0
  122. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/string/conversions.rb +28 -0
  123. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/string/inflections.rb +153 -0
  124. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/string/iterators.rb +17 -0
  125. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/string/starts_ends_with.rb +27 -0
  126. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/string/unicode.rb +42 -0
  127. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/string/xchar.rb +11 -0
  128. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/symbol.rb +14 -0
  129. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/test.rb +1 -0
  130. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/test/unit/assertions.rb +62 -0
  131. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/time.rb +19 -0
  132. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/time/behavior.rb +13 -0
  133. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/time/calculations.rb +224 -0
  134. data/vendor/activesupport-2.0.2-/lib/active_support/core_ext/time/conversions.rb +94 -0
  135. data/vendor/activesupport-2.0.2-/lib/active_support/dependencies.rb +540 -0
  136. data/vendor/activesupport-2.0.2-/lib/active_support/deprecation.rb +204 -0
  137. data/vendor/activesupport-2.0.2-/lib/active_support/duration.rb +96 -0
  138. data/vendor/activesupport-2.0.2-/lib/active_support/inflections.rb +53 -0
  139. data/vendor/activesupport-2.0.2-/lib/active_support/inflector.rb +282 -0
  140. data/vendor/activesupport-2.0.2-/lib/active_support/json.rb +31 -0
  141. data/vendor/activesupport-2.0.2-/lib/active_support/json/decoding.rb +60 -0
  142. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/date.rb +5 -0
  143. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/date_time.rb +5 -0
  144. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/enumerable.rb +12 -0
  145. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/false_class.rb +5 -0
  146. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/hash.rb +50 -0
  147. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/nil_class.rb +5 -0
  148. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/numeric.rb +5 -0
  149. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/object.rb +6 -0
  150. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/regexp.rb +5 -0
  151. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/string.rb +30 -0
  152. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/symbol.rb +5 -0
  153. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/time.rb +5 -0
  154. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoders/true_class.rb +5 -0
  155. data/vendor/activesupport-2.0.2-/lib/active_support/json/encoding.rb +38 -0
  156. data/vendor/activesupport-2.0.2-/lib/active_support/json/variable.rb +10 -0
  157. data/vendor/activesupport-2.0.2-/lib/active_support/multibyte.rb +9 -0
  158. data/vendor/activesupport-2.0.2-/lib/active_support/multibyte/chars.rb +141 -0
  159. data/vendor/activesupport-2.0.2-/lib/active_support/multibyte/generators/generate_tables.rb +149 -0
  160. data/vendor/activesupport-2.0.2-/lib/active_support/multibyte/handlers/passthru_handler.rb +9 -0
  161. data/vendor/activesupport-2.0.2-/lib/active_support/multibyte/handlers/utf8_handler.rb +564 -0
  162. data/vendor/activesupport-2.0.2-/lib/active_support/multibyte/handlers/utf8_handler_proc.rb +43 -0
  163. data/vendor/activesupport-2.0.2-/lib/active_support/option_merger.rb +25 -0
  164. data/vendor/activesupport-2.0.2-/lib/active_support/ordered_options.rb +49 -0
  165. data/vendor/activesupport-2.0.2-/lib/active_support/test_case.rb +5 -0
  166. data/vendor/activesupport-2.0.2-/lib/active_support/testing.rb +1 -0
  167. data/vendor/activesupport-2.0.2-/lib/active_support/testing/default.rb +12 -0
  168. data/vendor/activesupport-2.0.2-/lib/active_support/values/time_zone.rb +181 -0
  169. data/vendor/activesupport-2.0.2-/lib/active_support/values/unicode_tables.dat +0 -0
  170. data/vendor/activesupport-2.0.2-/lib/active_support/vendor.rb +14 -0
  171. data/vendor/activesupport-2.0.2-/lib/active_support/vendor/builder-2.1.2/blankslate.rb +113 -0
  172. data/vendor/activesupport-2.0.2-/lib/active_support/vendor/builder-2.1.2/builder.rb +13 -0
  173. data/vendor/activesupport-2.0.2-/lib/active_support/vendor/builder-2.1.2/builder/blankslate.rb +20 -0
  174. data/vendor/activesupport-2.0.2-/lib/active_support/vendor/builder-2.1.2/builder/css.rb +250 -0
  175. data/vendor/activesupport-2.0.2-/lib/active_support/vendor/builder-2.1.2/builder/xchar.rb +115 -0
  176. data/vendor/activesupport-2.0.2-/lib/active_support/vendor/builder-2.1.2/builder/xmlbase.rb +139 -0
  177. data/vendor/activesupport-2.0.2-/lib/active_support/vendor/builder-2.1.2/builder/xmlevents.rb +63 -0
  178. data/vendor/activesupport-2.0.2-/lib/active_support/vendor/builder-2.1.2/builder/xmlmarkup.rb +328 -0
  179. data/vendor/activesupport-2.0.2-/lib/active_support/vendor/xml-simple-1.0.11/xmlsimple.rb +1021 -0
  180. data/vendor/activesupport-2.0.2-/lib/active_support/version.rb +9 -0
  181. data/vendor/activesupport-2.0.2-/lib/active_support/whiny_nil.rb +38 -0
  182. data/vendor/activesupport-2.0.2-/lib/activesupport.rb +1 -0
  183. metadata +222 -2
@@ -0,0 +1,165 @@
1
+ = Active Resource
2
+
3
+ Active Resource (ARes) connects business objects and Representational State Transfer (REST)
4
+ web services. It implements object-relational mapping for REST webservices to provide transparent
5
+ proxying capabilities between a client (ActiveResource) and a RESTful service (which is provided by Simply RESTful routing
6
+ in ActionController::Resources).
7
+
8
+ == Philosophy
9
+
10
+ Active Resource attempts to provide a coherent wrapper object-relational mapping for REST
11
+ web services. It follows the same philosophy as Active Record, in that one of its prime aims
12
+ is to reduce the amount of code needed to map to these resources. This is made possible
13
+ by relying on a number of code- and protocol-based conventions that make it easy for Active Resource
14
+ to infer complex relations and structures. These conventions are outlined in detail in the documentation
15
+ for ActiveResource::Base.
16
+
17
+ == Overview
18
+
19
+ Model classes are mapped to remote REST resources by Active Resource much the same way Active Record maps model classes to database
20
+ tables. When a request is made to a remote resource, a REST XML request is generated, transmitted, and the result
21
+ received and serialized into a usable Ruby object.
22
+
23
+ === Configuration and Usage
24
+
25
+ Putting ActiveResource to use is very similar to ActiveRecord. It's as simple as creating a model class
26
+ that inherits from ActiveResource::Base and providing a <tt>site</tt> class variable to it:
27
+
28
+ class Person < ActiveResource::Base
29
+ self.site = "http://api.people.com:3000/"
30
+ end
31
+
32
+ Now the Person class is REST enabled and can invoke REST services very similarly to how ActiveRecord invokes
33
+ lifecycle methods that operate against a persistent store.
34
+
35
+ # Find a person with id = 1
36
+ ryan = Person.find(1)
37
+ Person.exists?(1) #=> true
38
+
39
+ As you can see, the methods are quite similar to Active Record's methods for dealing with database
40
+ records. But rather than dealing with
41
+
42
+ ==== Protocol
43
+
44
+ Active Resource is built on a standard XML format for requesting and submitting resources over HTTP. It mirrors the RESTful routing
45
+ built into ActionController but will also work with any other REST service that properly implements the protocol.
46
+ REST uses HTTP, but unlike "typical" web applications, it makes use of all the verbs available in the HTTP specification:
47
+
48
+ * GET requests are used for finding and retrieving resources.
49
+ * POST requests are used to create new resources.
50
+ * PUT requests are used to update existing resources.
51
+ * DELETE requests are used to delete resources.
52
+
53
+ For more information on how this protocol works with Active Resource, see the ActiveResource::Base documentation;
54
+ for more general information on REST web services, see the article here[http://en.wikipedia.org/wiki/Representational_State_Transfer].
55
+
56
+ ==== Find
57
+
58
+ GET Http requests expect the XML form of whatever resource/resources is/are being requested. So,
59
+ for a request for a single element - the XML of that item is expected in response:
60
+
61
+ # Expects a response of
62
+ #
63
+ # <person><id type="integer">1</id><attribute1>value1</attribute1><attribute2>..</attribute2></person>
64
+ #
65
+ # for GET http://api.people.com:3000/people/1.xml
66
+ #
67
+ ryan = Person.find(1)
68
+
69
+ The XML document that is received is used to build a new object of type Person, with each
70
+ XML element becoming an attribute on the object.
71
+
72
+ ryan.is_a? Person #=> true
73
+ ryan.attribute1 #=> 'value1'
74
+
75
+ Any complex element (one that contains other elements) becomes its own object:
76
+
77
+ # With this response:
78
+ #
79
+ # <person><id>1</id><attribute1>value1</attribute1><complex><attribute2>value2</attribute2></complex></person>
80
+ #
81
+ # for GET http://api.people.com:3000/people/1.xml
82
+ #
83
+ ryan = Person.find(1)
84
+ ryan.complex #=> <Person::Complex::xxxxx>
85
+ ryan.complex.attribute2 #=> 'value2'
86
+
87
+ Collections can also be requested in a similar fashion
88
+
89
+ # Expects a response of
90
+ #
91
+ # <people type="array">
92
+ # <person><id type="integer">1</id><first>Ryan</first></person>
93
+ # <person><id type="integer">2</id><first>Jim</first></person>
94
+ # </people>
95
+ #
96
+ # for GET http://api.people.com:3000/people.xml
97
+ #
98
+ people = Person.find(:all)
99
+ people.first #=> <Person::xxx 'first' => 'Ryan' ...>
100
+ people.last #=> <Person::xxx 'first' => 'Jim' ...>
101
+
102
+ ==== Create
103
+
104
+ Creating a new resource submits the xml form of the resource as the body of the request and expects
105
+ a 'Location' header in the response with the RESTful URL location of the newly created resource. The
106
+ id of the newly created resource is parsed out of the Location response header and automatically set
107
+ as the id of the ARes object.
108
+
109
+ # <person><first>Ryan</first></person>
110
+ #
111
+ # is submitted as the body on
112
+ #
113
+ # POST http://api.people.com:3000/people.xml
114
+ #
115
+ # when save is called on a new Person object. An empty response is
116
+ # is expected with a 'Location' header value:
117
+ #
118
+ # Response (201): Location: http://api.people.com:3000/people/2
119
+ #
120
+ ryan = Person.new(:first => 'Ryan')
121
+ ryan.new? #=> true
122
+ ryan.save #=> true
123
+ ryan.new? #=> false
124
+ ryan.id #=> 2
125
+
126
+ ==== Update
127
+
128
+ 'save' is also used to update an existing resource - and follows the same protocol as creating a resource
129
+ with the exception that no response headers are needed - just an empty response when the update on the
130
+ server side was successful.
131
+
132
+ # <person><first>Ryan</first></person>
133
+ #
134
+ # is submitted as the body on
135
+ #
136
+ # PUT http://api.people.com:3000/people/1.xml
137
+ #
138
+ # when save is called on an existing Person object. An empty response is
139
+ # is expected with code (204)
140
+ #
141
+ ryan = Person.find(1)
142
+ ryan.first #=> 'Ryan'
143
+ ryan.first = 'Rizzle'
144
+ ryan.save #=> true
145
+
146
+ ==== Delete
147
+
148
+ Destruction of a resource can be invoked as a class and instance method of the resource.
149
+
150
+ # A request is made to
151
+ #
152
+ # DELETE http://api.people.com:3000/people/1.xml
153
+ #
154
+ # for both of these forms. An empty response with
155
+ # is expected with response code (200)
156
+ #
157
+ ryan = Person.find(1)
158
+ ryan.destroy #=> true
159
+ ryan.exists? #=> false
160
+ Person.delete(2) #=> true
161
+ Person.exists?(2) #=> false
162
+
163
+
164
+ You can find more usage information in the ActiveResource::Base documentation.
165
+
@@ -0,0 +1,133 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/contrib/rubyforgepublisher'
8
+ require File.join(File.dirname(__FILE__), 'lib', 'active_resource', 'version')
9
+
10
+ PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
11
+ PKG_NAME = 'activeresource'
12
+ PKG_VERSION = ActiveResource::VERSION::STRING + PKG_BUILD
13
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
14
+
15
+ RELEASE_NAME = "REL #{PKG_VERSION}"
16
+
17
+ RUBY_FORGE_PROJECT = "activerecord"
18
+ RUBY_FORGE_USER = "webster132"
19
+
20
+ PKG_FILES = FileList[
21
+ "lib/**/*", "test/**/*", "[A-Z]*", "Rakefile"
22
+ ].exclude(/\bCVS\b|~$/)
23
+
24
+ desc "Default Task"
25
+ task :default => [ :test ]
26
+
27
+ # Run the unit tests
28
+
29
+ Rake::TestTask.new { |t|
30
+ t.libs << "test"
31
+ t.pattern = 'test/**/*_test.rb'
32
+ t.verbose = true
33
+ t.warning = true
34
+ }
35
+
36
+
37
+ # Generate the RDoc documentation
38
+
39
+ Rake::RDocTask.new { |rdoc|
40
+ rdoc.rdoc_dir = 'doc'
41
+ rdoc.title = "Active Resource -- Object-oriented REST services"
42
+ rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
43
+ rdoc.options << '--charset' << 'utf-8'
44
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
45
+ rdoc.rdoc_files.include('README', 'CHANGELOG')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ }
48
+
49
+
50
+ # Create compressed packages
51
+
52
+ dist_dirs = [ "lib", "test", "examples", "dev-utils" ]
53
+
54
+ spec = Gem::Specification.new do |s|
55
+ s.name = PKG_NAME
56
+ s.version = PKG_VERSION
57
+ s.summary = "Think Active Record for web resources."
58
+ s.description = %q{Wraps web resources in model classes that can be manipulated through XML over REST.}
59
+
60
+ s.files = [ "Rakefile", "README", "CHANGELOG" ]
61
+ dist_dirs.each do |dir|
62
+ s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
63
+ end
64
+
65
+ s.add_dependency('activesupport', '= 2.0.2' + PKG_BUILD)
66
+
67
+ s.require_path = 'lib'
68
+ s.autorequire = 'active_resource'
69
+
70
+ s.has_rdoc = true
71
+ s.extra_rdoc_files = %w( README )
72
+ s.rdoc_options.concat ['--main', 'README']
73
+
74
+ s.author = "David Heinemeier Hansson"
75
+ s.email = "david@loudthinking.com"
76
+ s.homepage = "http://www.rubyonrails.org"
77
+ s.rubyforge_project = "activeresource"
78
+ end
79
+
80
+ Rake::GemPackageTask.new(spec) do |p|
81
+ p.gem_spec = spec
82
+ p.need_tar = true
83
+ p.need_zip = true
84
+ end
85
+
86
+ task :lines do
87
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
88
+
89
+ for file_name in FileList["lib/active_resource/**/*.rb"]
90
+ next if file_name =~ /vendor/
91
+ f = File.open(file_name)
92
+
93
+ while line = f.gets
94
+ lines += 1
95
+ next if line =~ /^\s*$/
96
+ next if line =~ /^\s*#/
97
+ codelines += 1
98
+ end
99
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
100
+
101
+ total_lines += lines
102
+ total_codelines += codelines
103
+
104
+ lines, codelines = 0, 0
105
+ end
106
+
107
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
108
+ end
109
+
110
+
111
+ # Publishing ------------------------------------------------------
112
+
113
+ desc "Publish the beta gem"
114
+ task :pgem => [:package] do
115
+ Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
116
+ `ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'`
117
+ end
118
+
119
+ desc "Publish the API documentation"
120
+ task :pdoc => [:rdoc] do
121
+ Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/ar", "doc").upload
122
+ end
123
+
124
+ desc "Publish the release files to RubyForge."
125
+ task :release => [ :package ] do
126
+ `rubyforge login`
127
+
128
+ for ext in %w( gem tgz zip )
129
+ release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}"
130
+ puts release_command
131
+ system(release_command)
132
+ end
133
+ end
@@ -0,0 +1,47 @@
1
+ #--
2
+ # Copyright (c) 2006 David Heinemeier Hansson
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ $:.unshift(File.dirname(__FILE__)) unless
25
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
26
+
27
+ unless defined?(ActiveSupport)
28
+ begin
29
+ $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
30
+ require 'active_support'
31
+ rescue LoadError
32
+ require 'rubygems'
33
+ gem 'activesupport'
34
+ end
35
+ end
36
+
37
+ require 'active_resource/formats'
38
+ require 'active_resource/base'
39
+ require 'active_resource/validations'
40
+ require 'active_resource/custom_methods'
41
+
42
+ module ActiveResource
43
+ Base.class_eval do
44
+ include Validations
45
+ include CustomMethods
46
+ end
47
+ end
@@ -0,0 +1,872 @@
1
+ require 'active_resource/connection'
2
+ require 'cgi'
3
+ require 'set'
4
+
5
+ module ActiveResource
6
+ # ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application.
7
+ #
8
+ # For an outline of what Active Resource is capable of, see link:files/README.html.
9
+ #
10
+ # == Automated mapping
11
+ #
12
+ # Active Resource objects represent your RESTful resources as manipulatable Ruby objects. To map resources
13
+ # to Ruby objects, Active Resource only needs a class name that corresponds to the resource name (e.g., the class
14
+ # Person maps to the resources people, very similarly to Active Record) and a +site+ value, which holds the
15
+ # URI of the resources.
16
+ #
17
+ # class Person < ActiveResource::Base
18
+ # self.site = "http://api.people.com:3000/"
19
+ # end
20
+ #
21
+ # Now the Person class is mapped to RESTful resources located at <tt>http://api.people.com:3000/people/</tt>, and
22
+ # you can now use Active Resource's lifecycles methods to manipulate resources.
23
+ #
24
+ # == Lifecycle methods
25
+ #
26
+ # Active Resource exposes methods for creating, finding, updating, and deleting resources
27
+ # from REST web services.
28
+ #
29
+ # ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
30
+ # ryan.save #=> true
31
+ # ryan.id #=> 2
32
+ # Person.exists?(ryan.id) #=> true
33
+ # ryan.exists? #=> true
34
+ #
35
+ # ryan = Person.find(1)
36
+ # # => Resource holding our newly create Person object
37
+ #
38
+ # ryan.first = 'Rizzle'
39
+ # ryan.save #=> true
40
+ #
41
+ # ryan.destroy #=> true
42
+ #
43
+ # As you can see, these are very similar to Active Record's lifecycle methods for database records.
44
+ # You can read more about each of these methods in their respective documentation.
45
+ #
46
+ # === Custom REST methods
47
+ #
48
+ # Since simple CRUD/lifecycle methods can't accomplish every task, Active Resource also supports
49
+ # defining your own custom REST methods.
50
+ #
51
+ # Person.new(:name => 'Ryan).post(:register)
52
+ # # => { :id => 1, :name => 'Ryan', :position => 'Clerk' }
53
+ #
54
+ # Person.find(1).put(:promote, :position => 'Manager')
55
+ # # => { :id => 1, :name => 'Ryan', :position => 'Manager' }
56
+ #
57
+ # For more information on creating and using custom REST methods, see the
58
+ # ActiveResource::CustomMethods documentation.
59
+ #
60
+ # == Validations
61
+ #
62
+ # You can validate resources client side by overriding validation methods in the base class.
63
+ #
64
+ # class Person < ActiveResource::Base
65
+ # self.site = "http://api.people.com:3000/"
66
+ # protected
67
+ # def validate
68
+ # errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
69
+ # end
70
+ # end
71
+ #
72
+ # See the ActiveResource::Validations documentation for more information.
73
+ #
74
+ # == Authentication
75
+ #
76
+ # Many REST APIs will require authentication, usually in the form of basic
77
+ # HTTP authentication. Authentication can be specified by putting the credentials
78
+ # in the +site+ variable of the Active Resource class you need to authenticate.
79
+ #
80
+ # class Person < ActiveResource::Base
81
+ # self.site = "http://ryan:password@api.people.com:3000/"
82
+ # end
83
+ #
84
+ # For obvious security reasons, it is probably best if such services are available
85
+ # over HTTPS.
86
+ #
87
+ # == Errors & Validation
88
+ #
89
+ # Error handling and validation is handled in much the same manner as you're used to seeing in
90
+ # Active Record. Both the response code in the Http response and the body of the response are used to
91
+ # indicate that an error occurred.
92
+ #
93
+ # === Resource errors
94
+ #
95
+ # When a get is requested for a resource that does not exist, the HTTP +404+ (Resource Not Found)
96
+ # response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
97
+ # exception.
98
+ #
99
+ # # GET http://api.people.com:3000/people/999.xml
100
+ # ryan = Person.find(999) # => Raises ActiveResource::ResourceNotFound
101
+ # # => Response = 404
102
+ #
103
+ # +404+ is just one of the HTTP error response codes that ActiveResource will handle with its own exception. The
104
+ # following HTTP response codes will also result in these exceptions:
105
+ #
106
+ # 200 - 399:: Valid response, no exception
107
+ # 404:: ActiveResource::ResourceNotFound
108
+ # 409:: ActiveResource::ResourceConflict
109
+ # 422:: ActiveResource::ResourceInvalid (rescued by save as validation errors)
110
+ # 401 - 499:: ActiveResource::ClientError
111
+ # 500 - 599:: ActiveResource::ServerError
112
+ #
113
+ # These custom exceptions allow you to deal with resource errors more naturally and with more precision
114
+ # rather than returning a general HTTP error. For example:
115
+ #
116
+ # begin
117
+ # ryan = Person.find(my_id)
118
+ # rescue ActiveResource::ResourceNotFound
119
+ # redirect_to :action => 'not_found'
120
+ # rescue ActiveResource::ResourceConflict, ActiveResource::ResourceInvalid
121
+ # redirect_to :action => 'new'
122
+ # end
123
+ #
124
+ # === Validation errors
125
+ #
126
+ # Active Resource supports validations on resources and will return errors if any these validations fail
127
+ # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by
128
+ # a response code of +422+ and an XML representation of the validation errors. The save operation will
129
+ # then fail (with a +false+ return value) and the validation errors can be accessed on the resource in question.
130
+ #
131
+ # ryan = Person.find(1)
132
+ # ryan.first #=> ''
133
+ # ryan.save #=> false
134
+ #
135
+ # # When
136
+ # # PUT http://api.people.com:3000/people/1.xml
137
+ # # is requested with invalid values, the response is:
138
+ # #
139
+ # # Response (422):
140
+ # # <errors type="array"><error>First cannot be empty</error></errors>
141
+ # #
142
+ #
143
+ # ryan.errors.invalid?(:first) #=> true
144
+ # ryan.errors.full_messages #=> ['First cannot be empty']
145
+ #
146
+ # Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
147
+ #
148
+ class Base
149
+ # The logger for diagnosing and tracing Active Resource calls.
150
+ cattr_accessor :logger
151
+
152
+ class << self
153
+ # Gets the URI of the REST resources to map for this class. The site variable is required
154
+ # ActiveResource's mapping to work.
155
+ def site
156
+ if defined?(@site)
157
+ @site
158
+ elsif superclass != Object && superclass.site
159
+ superclass.site.dup.freeze
160
+ end
161
+ end
162
+
163
+ # Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
164
+ # The site variable is required ActiveResource's mapping to work.
165
+ def site=(site)
166
+ @connection = nil
167
+ @site = site.nil? ? nil : create_site_uri_from(site)
168
+ end
169
+
170
+ # Sets the format that attributes are sent and received in from a mime type reference. Example:
171
+ #
172
+ # Person.format = :json
173
+ # Person.find(1) # => GET /people/1.json
174
+ #
175
+ # Person.format = ActiveResource::Formats::XmlFormat
176
+ # Person.find(1) # => GET /people/1.xml
177
+ #
178
+ # Default format is :xml.
179
+ def format=(mime_type_reference_or_format)
180
+ format = mime_type_reference_or_format.is_a?(Symbol) ?
181
+ ActiveResource::Formats[mime_type_reference_or_format] : mime_type_reference_or_format
182
+
183
+ write_inheritable_attribute("format", format)
184
+ connection.format = format
185
+ end
186
+
187
+ # Returns the current format, default is ActiveResource::Formats::XmlFormat
188
+ def format # :nodoc:
189
+ read_inheritable_attribute("format") || ActiveResource::Formats[:xml]
190
+ end
191
+
192
+ # An instance of ActiveResource::Connection that is the base connection to the remote service.
193
+ # The +refresh+ parameter toggles whether or not the connection is refreshed at every request
194
+ # or not (defaults to +false+).
195
+ def connection(refresh = false)
196
+ if defined?(@connection) || superclass == Object
197
+ @connection = Connection.new(site, format) if refresh || @connection.nil?
198
+ @connection
199
+ else
200
+ superclass.connection
201
+ end
202
+ end
203
+
204
+ def headers
205
+ @headers ||= {}
206
+ end
207
+
208
+ # Do not include any modules in the default element name. This makes it easier to seclude ARes objects
209
+ # in a separate namespace without having to set element_name repeatedly.
210
+ attr_accessor_with_default(:element_name) { to_s.split("::").last.underscore } #:nodoc:
211
+
212
+ attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
213
+ attr_accessor_with_default(:primary_key, 'id') #:nodoc:
214
+
215
+ # Gets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>)
216
+ # This method is regenerated at runtime based on what the prefix is set to.
217
+ def prefix(options={})
218
+ default = site.path
219
+ default << '/' unless default[-1..-1] == '/'
220
+ # generate the actual method based on the current site path
221
+ self.prefix = default
222
+ prefix(options)
223
+ end
224
+
225
+ # An attribute reader for the source string for the resource path prefix. This
226
+ # method is regenerated at runtime based on what the prefix is set to.
227
+ def prefix_source
228
+ prefix # generate #prefix and #prefix_source methods first
229
+ prefix_source
230
+ end
231
+
232
+ # Sets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>).
233
+ # Default value is <tt>site.path</tt>.
234
+ def prefix=(value = '/')
235
+ # Replace :placeholders with '#{embedded options[:lookups]}'
236
+ prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
237
+
238
+ # Redefine the new methods.
239
+ code = <<-end_code
240
+ def prefix_source() "#{value}" end
241
+ def prefix(options={}) "#{prefix_call}" end
242
+ end_code
243
+ silence_warnings { instance_eval code, __FILE__, __LINE__ }
244
+ rescue
245
+ logger.error "Couldn't set prefix: #{$!}\n #{code}"
246
+ raise
247
+ end
248
+
249
+ alias_method :set_prefix, :prefix= #:nodoc:
250
+
251
+ alias_method :set_element_name, :element_name= #:nodoc:
252
+ alias_method :set_collection_name, :collection_name= #:nodoc:
253
+
254
+ # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails
255
+ # will split from the prefix options.
256
+ #
257
+ # ==== Options
258
+ # +prefix_options+:: A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
259
+ # would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
260
+ # +query_options+:: A hash to add items to the query string for the request.
261
+ #
262
+ # ==== Examples
263
+ # Post.element_path(1)
264
+ # # => /posts/1.xml
265
+ #
266
+ # Comment.element_path(1, :post_id => 5)
267
+ # # => /posts/5/comments/1.xml
268
+ #
269
+ # Comment.element_path(1, :post_id => 5, :active => 1)
270
+ # # => /posts/5/comments/1.xml?active=1
271
+ #
272
+ # Comment.element_path(1, {:post_id => 5}, {:active => 1})
273
+ # # => /posts/5/comments/1.xml?active=1
274
+ #
275
+ def element_path(id, prefix_options = {}, query_options = nil)
276
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
277
+ "#{prefix(prefix_options)}#{collection_name}/#{id}.#{format.extension}#{query_string(query_options)}"
278
+ end
279
+
280
+ # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
281
+ # will split from the +prefix_options+.
282
+ #
283
+ # ==== Options
284
+ # +prefix_options+:: A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
285
+ # would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
286
+ # +query_options+:: A hash to add items to the query string for the request.
287
+ #
288
+ # ==== Examples
289
+ # Post.collection_path
290
+ # # => /posts.xml
291
+ #
292
+ # Comment.collection_path(:post_id => 5)
293
+ # # => /posts/5/comments.xml
294
+ #
295
+ # Comment.collection_path(:post_id => 5, :active => 1)
296
+ # # => /posts/5/comments.xml?active=1
297
+ #
298
+ # Comment.collection_path({:post_id => 5}, {:active => 1})
299
+ # # => /posts/5/comments.xml?active=1
300
+ #
301
+ def collection_path(prefix_options = {}, query_options = nil)
302
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
303
+ "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
304
+ end
305
+
306
+ alias_method :set_primary_key, :primary_key= #:nodoc:
307
+
308
+ # Create a new resource instance and request to the remote service
309
+ # that it be saved, making it equivalent to the following simultaneous calls:
310
+ #
311
+ # ryan = Person.new(:first => 'ryan')
312
+ # ryan.save
313
+ #
314
+ # The newly created resource is returned. If a failure has occurred an
315
+ # exception will be raised (see save). If the resource is invalid and
316
+ # has not been saved then valid? will return <tt>false</tt>,
317
+ # while new? will still return <tt>true</tt>.
318
+ #
319
+ # ==== Examples
320
+ # Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true)
321
+ # my_person = Person.find(:first)
322
+ # my_person.email
323
+ # # => myname@nospam.com
324
+ #
325
+ # dhh = Person.create(:name => 'David', :email => 'dhh@nospam.com', :enabled => true)
326
+ # dhh.valid?
327
+ # # => true
328
+ # dhh.new?
329
+ # # => false
330
+ #
331
+ # # We'll assume that there's a validation that requires the name attribute
332
+ # that_guy = Person.create(:name => '', :email => 'thatguy@nospam.com', :enabled => true)
333
+ # that_guy.valid?
334
+ # # => false
335
+ # that_guy.new?
336
+ # # => true
337
+ #
338
+ def create(attributes = {})
339
+ returning(self.new(attributes)) { |res| res.save }
340
+ end
341
+
342
+ # Core method for finding resources. Used similarly to Active Record's find method.
343
+ #
344
+ # ==== Arguments
345
+ # The first argument is considered to be the scope of the query. That is, how many
346
+ # resources are returned from the request. It can be one of the following.
347
+ #
348
+ # +:one+:: Returns a single resource.
349
+ # +:first+:: Returns the first resource found.
350
+ # +:all+:: Returns every resource that matches the request.
351
+ #
352
+ # ==== Options
353
+ # +from+:: Sets the path or custom method that resources will be fetched from.
354
+ # +params+:: Sets query and prefix (nested URL) parameters.
355
+ #
356
+ # ==== Examples
357
+ # Person.find(1)
358
+ # # => GET /people/1.xml
359
+ #
360
+ # Person.find(:all)
361
+ # # => GET /people.xml
362
+ #
363
+ # Person.find(:all, :params => { :title => "CEO" })
364
+ # # => GET /people.xml?title=CEO
365
+ #
366
+ # Person.find(:first, :from => :managers)
367
+ # # => GET /people/managers.xml
368
+ #
369
+ # Person.find(:all, :from => "/companies/1/people.xml")
370
+ # # => GET /companies/1/people.xml
371
+ #
372
+ # Person.find(:one, :from => :leader)
373
+ # # => GET /people/leader.xml
374
+ #
375
+ # Person.find(:one, :from => "/companies/1/manager.xml")
376
+ # # => GET /companies/1/manager.xml
377
+ #
378
+ # StreetAddress.find(1, :params => { :person_id => 1 })
379
+ # # => GET /people/1/street_addresses/1.xml
380
+ def find(*arguments)
381
+ scope = arguments.slice!(0)
382
+ options = arguments.slice!(0) || {}
383
+
384
+ case scope
385
+ when :all then find_every(options)
386
+ when :first then find_every(options).first
387
+ when :one then find_one(options)
388
+ else find_single(scope, options)
389
+ end
390
+ end
391
+
392
+ # Deletes the resources with the ID in the +id+ parameter.
393
+ #
394
+ # ==== Options
395
+ # All options specify prefix and query parameters.
396
+ #
397
+ # ==== Examples
398
+ # Event.delete(2)
399
+ # # => DELETE /events/2
400
+ #
401
+ # Event.create(:name => 'Free Concert', :location => 'Community Center')
402
+ # my_event = Event.find(:first)
403
+ # # => Events (id: 7)
404
+ # Event.delete(my_event.id)
405
+ # # => DELETE /events/7
406
+ #
407
+ # # Let's assume a request to events/5/cancel.xml
408
+ # Event.delete(params[:id])
409
+ # # => DELETE /events/5
410
+ #
411
+ def delete(id, options = {})
412
+ connection.delete(element_path(id, options))
413
+ end
414
+
415
+ # Asserts the existence of a resource, returning <tt>true</tt> if the resource is found.
416
+ #
417
+ # ==== Examples
418
+ # Note.create(:title => 'Hello, world.', :body => 'Nothing more for now...')
419
+ # Note.exists?(1)
420
+ # # => true
421
+ #
422
+ # Note.exists(1349)
423
+ # # => false
424
+ def exists?(id, options = {})
425
+ id && !find_single(id, options).nil?
426
+ rescue ActiveResource::ResourceNotFound
427
+ false
428
+ end
429
+
430
+ private
431
+ # Find every resource
432
+ def find_every(options)
433
+ case from = options[:from]
434
+ when Symbol
435
+ instantiate_collection(get(from, options[:params]))
436
+ when String
437
+ path = "#{from}#{query_string(options[:params])}"
438
+ instantiate_collection(connection.get(path, headers) || [])
439
+ else
440
+ prefix_options, query_options = split_options(options[:params])
441
+ path = collection_path(prefix_options, query_options)
442
+ instantiate_collection( (connection.get(path, headers) || []), prefix_options )
443
+ end
444
+ end
445
+
446
+ # Find a single resource from a one-off URL
447
+ def find_one(options)
448
+ case from = options[:from]
449
+ when Symbol
450
+ instantiate_record(get(from, options[:params]))
451
+ when String
452
+ path = "#{from}#{query_string(options[:params])}"
453
+ instantiate_record(connection.get(path, headers))
454
+ end
455
+ end
456
+
457
+ # Find a single resource from the default URL
458
+ def find_single(scope, options)
459
+ prefix_options, query_options = split_options(options[:params])
460
+ path = element_path(scope, prefix_options, query_options)
461
+ instantiate_record(connection.get(path, headers), prefix_options)
462
+ end
463
+
464
+ def instantiate_collection(collection, prefix_options = {})
465
+ collection.collect! { |record| instantiate_record(record, prefix_options) }
466
+ end
467
+
468
+ def instantiate_record(record, prefix_options = {})
469
+ returning new(record) do |resource|
470
+ resource.prefix_options = prefix_options
471
+ end
472
+ end
473
+
474
+
475
+ # Accepts a URI and creates the site URI from that.
476
+ def create_site_uri_from(site)
477
+ site.is_a?(URI) ? site.dup : URI.parse(site)
478
+ end
479
+
480
+ # contains a set of the current prefix parameters.
481
+ def prefix_parameters
482
+ @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
483
+ end
484
+
485
+ # Builds the query string for the request.
486
+ def query_string(options)
487
+ "?#{options.to_query}" unless options.nil? || options.empty?
488
+ end
489
+
490
+ # split an option hash into two hashes, one containing the prefix options,
491
+ # and the other containing the leftovers.
492
+ def split_options(options = {})
493
+ prefix_options, query_options = {}, {}
494
+
495
+ (options || {}).each do |key, value|
496
+ next if key.blank?
497
+ (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
498
+ end
499
+
500
+ [ prefix_options, query_options ]
501
+ end
502
+ end
503
+
504
+ attr_accessor :attributes #:nodoc:
505
+ attr_accessor :prefix_options #:nodoc:
506
+
507
+ # Constructor method for new resources; the optional +attributes+ parameter takes a +Hash+
508
+ # of attributes for the new resource.
509
+ #
510
+ # ==== Examples
511
+ # my_course = Course.new
512
+ # my_course.name = "Western Civilization"
513
+ # my_course.lecturer = "Don Trotter"
514
+ # my_course.save
515
+ #
516
+ # my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling")
517
+ # my_other_course.save
518
+ def initialize(attributes = {})
519
+ @attributes = {}
520
+ @prefix_options = {}
521
+ load(attributes)
522
+ end
523
+
524
+ # A method to determine if the resource a new object (i.e., it has not been POSTed to the remote service yet).
525
+ #
526
+ # ==== Examples
527
+ # not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
528
+ # not_new.new?
529
+ # # => false
530
+ #
531
+ # is_new = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM')
532
+ # is_new.new?
533
+ # # => true
534
+ #
535
+ # is_new.save
536
+ # is_new.new?
537
+ # # => false
538
+ #
539
+ def new?
540
+ id.nil?
541
+ end
542
+
543
+ # Get the +id+ attribute of the resource.
544
+ def id
545
+ attributes[self.class.primary_key]
546
+ end
547
+
548
+ # Set the +id+ attribute of the resource.
549
+ def id=(id)
550
+ attributes[self.class.primary_key] = id
551
+ end
552
+
553
+ # Allows ActiveResource objects to be used as parameters in ActionPack URL generation.
554
+ def to_param
555
+ id && id.to_s
556
+ end
557
+
558
+ # Test for equality. Resource are equal if and only if +other+ is the same object or
559
+ # is an instance of the same class, is not +new?+, and has the same +id+.
560
+ #
561
+ # ==== Examples
562
+ # ryan = Person.create(:name => 'Ryan')
563
+ # jamie = Person.create(:name => 'Jamie')
564
+ #
565
+ # ryan == jamie
566
+ # # => false (Different name attribute and id)
567
+ #
568
+ # ryan_again = Person.new(:name => 'Ryan')
569
+ # ryan == ryan_again
570
+ # # => false (ryan_again is new?)
571
+ #
572
+ # ryans_clone = Person.create(:name => 'Ryan')
573
+ # ryan == ryans_clone
574
+ # # => false (Different id attributes)
575
+ #
576
+ # ryans_twin = Person.find(ryan.id)
577
+ # ryan == ryans_twin
578
+ # # => true
579
+ #
580
+ def ==(other)
581
+ other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
582
+ end
583
+
584
+ # Tests for equality (delegates to ==).
585
+ def eql?(other)
586
+ self == other
587
+ end
588
+
589
+ # Delegates to id in order to allow two resources of the same type and id to work with something like:
590
+ # [Person.find(1), Person.find(2)] & [Person.find(1), Person.find(4)] # => [Person.find(1)]
591
+ def hash
592
+ id.hash
593
+ end
594
+
595
+ # Duplicate the current resource without saving it.
596
+ #
597
+ # ==== Examples
598
+ # my_invoice = Invoice.create(:customer => 'That Company')
599
+ # next_invoice = my_invoice.dup
600
+ # next_invoice.new?
601
+ # # => true
602
+ #
603
+ # next_invoice.save
604
+ # next_invoice == my_invoice
605
+ # # => false (different id attributes)
606
+ #
607
+ # my_invoice.customer
608
+ # # => That Company
609
+ # next_invoice.customer
610
+ # # => That Company
611
+ def dup
612
+ returning self.class.new do |resource|
613
+ resource.attributes = @attributes
614
+ resource.prefix_options = @prefix_options
615
+ end
616
+ end
617
+
618
+ # A method to save (+POST+) or update (+PUT+) a resource. It delegates to +create+ if a new object,
619
+ # +update+ if it is existing. If the response to the save includes a body, it will be assumed that this body
620
+ # is XML for the final object as it looked after the save (which would include attributes like +created_at+
621
+ # that weren't part of the original submit).
622
+ #
623
+ # ==== Examples
624
+ # my_company = Company.new(:name => 'RoleModel Software', :owner => 'Ken Auer', :size => 2)
625
+ # my_company.new?
626
+ # # => true
627
+ # my_company.save
628
+ # # => POST /companies/ (create)
629
+ #
630
+ # my_company.new?
631
+ # # => false
632
+ # my_company.size = 10
633
+ # my_company.save
634
+ # # => PUT /companies/1 (update)
635
+ def save
636
+ new? ? create : update
637
+ end
638
+
639
+ # Deletes the resource from the remote service.
640
+ #
641
+ # ==== Examples
642
+ # my_id = 3
643
+ # my_person = Person.find(my_id)
644
+ # my_person.destroy
645
+ # Person.find(my_id)
646
+ # # => 404 (Resource Not Found)
647
+ #
648
+ # new_person = Person.create(:name => 'James')
649
+ # new_id = new_person.id
650
+ # # => 7
651
+ # new_person.destroy
652
+ # Person.find(new_id)
653
+ # # => 404 (Resource Not Found)
654
+ def destroy
655
+ connection.delete(element_path, self.class.headers)
656
+ end
657
+
658
+ # Evaluates to <tt>true</tt> if this resource is not +new?+ and is
659
+ # found on the remote service. Using this method, you can check for
660
+ # resources that may have been deleted between the object's instantiation
661
+ # and actions on it.
662
+ #
663
+ # ==== Examples
664
+ # Person.create(:name => 'Theodore Roosevelt')
665
+ # that_guy = Person.find(:first)
666
+ # that_guy.exists?
667
+ # # => true
668
+ #
669
+ # that_lady = Person.new(:name => 'Paul Bean')
670
+ # that_lady.exists?
671
+ # # => false
672
+ #
673
+ # guys_id = that_guy.id
674
+ # Person.delete(guys_id)
675
+ # that_guy.exists?
676
+ # # => false
677
+ def exists?
678
+ !new? && self.class.exists?(id, :params => prefix_options)
679
+ end
680
+
681
+ # A method to convert the the resource to an XML string.
682
+ #
683
+ # ==== Options
684
+ # The +options+ parameter is handed off to the +to_xml+ method on each
685
+ # attribute, so it has the same options as the +to_xml+ methods in
686
+ # ActiveSupport.
687
+ #
688
+ # indent:: Set the indent level for the XML output (default is +2+).
689
+ # dasherize:: Boolean option to determine whether or not element names should
690
+ # replace underscores with dashes (default is +false+).
691
+ # skip_instruct:: Toggle skipping the +instruct!+ call on the XML builder
692
+ # that generates the XML declaration (default is +false+).
693
+ #
694
+ # ==== Examples
695
+ # my_group = SubsidiaryGroup.find(:first)
696
+ # my_group.to_xml
697
+ # # => <?xml version="1.0" encoding="UTF-8"?>
698
+ # # <subsidiary_group> [...] </subsidiary_group>
699
+ #
700
+ # my_group.to_xml(:dasherize => true)
701
+ # # => <?xml version="1.0" encoding="UTF-8"?>
702
+ # # <subsidiary-group> [...] </subsidiary-group>
703
+ #
704
+ # my_group.to_xml(:skip_instruct => true)
705
+ # # => <subsidiary_group> [...] </subsidiary_group>
706
+ def to_xml(options={})
707
+ attributes.to_xml({:root => self.class.element_name}.merge(options))
708
+ end
709
+
710
+ # A method to reload the attributes of this object from the remote web service.
711
+ #
712
+ # ==== Examples
713
+ # my_branch = Branch.find(:first)
714
+ # my_branch.name
715
+ # # => Wislon Raod
716
+ #
717
+ # # Another client fixes the typo...
718
+ #
719
+ # my_branch.name
720
+ # # => Wislon Raod
721
+ # my_branch.reload
722
+ # my_branch.name
723
+ # # => Wilson Road
724
+ def reload
725
+ self.load(self.class.find(id, :params => @prefix_options).attributes)
726
+ end
727
+
728
+ # A method to manually load attributes from a hash. Recursively loads collections of
729
+ # resources. This method is called in initialize and create when a +Hash+ of attributes
730
+ # is provided.
731
+ #
732
+ # ==== Examples
733
+ # my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'}
734
+ #
735
+ # the_supplier = Supplier.find(:first)
736
+ # the_supplier.name
737
+ # # => 'J&M Textiles'
738
+ # the_supplier.load(my_attrs)
739
+ # the_supplier.name('J&J Textiles')
740
+ #
741
+ # # These two calls are the same as Supplier.new(my_attrs)
742
+ # my_supplier = Supplier.new
743
+ # my_supplier.load(my_attrs)
744
+ #
745
+ # # These three calls are the same as Supplier.create(my_attrs)
746
+ # your_supplier = Supplier.new
747
+ # your_supplier.load(my_attrs)
748
+ # your_supplier.save
749
+ def load(attributes)
750
+ raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
751
+ @prefix_options, attributes = split_options(attributes)
752
+ attributes.each do |key, value|
753
+ @attributes[key.to_s] =
754
+ case value
755
+ when Array
756
+ resource = find_or_create_resource_for_collection(key)
757
+ value.map { |attrs| resource.new(attrs) }
758
+ when Hash
759
+ resource = find_or_create_resource_for(key)
760
+ resource.new(value)
761
+ else
762
+ value.dup rescue value
763
+ end
764
+ end
765
+ self
766
+ end
767
+
768
+ # For checking respond_to? without searching the attributes (which is faster).
769
+ alias_method :respond_to_without_attributes?, :respond_to?
770
+
771
+ # A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a +Person+ object with a
772
+ # +name+ attribute can answer +true+ to +my_person.respond_to?("name")+, +my_person.respond_to?("name=")+, and
773
+ # +my_person.respond_to?("name?")+.
774
+ def respond_to?(method, include_priv = false)
775
+ method_name = method.to_s
776
+ if attributes.nil?
777
+ return super
778
+ elsif attributes.has_key?(method_name)
779
+ return true
780
+ elsif ['?','='].include?(method_name.last) && attributes.has_key?(method_name.first(-1))
781
+ return true
782
+ end
783
+ # super must be called at the end of the method, because the inherited respond_to?
784
+ # would return true for generated readers, even if the attribute wasn't present
785
+ super
786
+ end
787
+
788
+
789
+ protected
790
+ def connection(refresh = false)
791
+ self.class.connection(refresh)
792
+ end
793
+
794
+ # Update the resource on the remote service.
795
+ def update
796
+ returning connection.put(element_path(prefix_options), to_xml, self.class.headers) do |response|
797
+ load_attributes_from_response(response)
798
+ end
799
+ end
800
+
801
+ # Create (i.e., save to the remote service) the new resource.
802
+ def create
803
+ returning connection.post(collection_path, to_xml, self.class.headers) do |response|
804
+ self.id = id_from_response(response)
805
+ load_attributes_from_response(response)
806
+ end
807
+ end
808
+
809
+ def load_attributes_from_response(response)
810
+ if response['Content-Length'] != "0" && response.body.strip.size > 0
811
+ load(self.class.format.decode(response.body))
812
+ end
813
+ end
814
+
815
+ # Takes a response from a typical create post and pulls the ID out
816
+ def id_from_response(response)
817
+ response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1]
818
+ end
819
+
820
+ def element_path(options = nil)
821
+ self.class.element_path(id, options || prefix_options)
822
+ end
823
+
824
+ def collection_path(options = nil)
825
+ self.class.collection_path(options || prefix_options)
826
+ end
827
+
828
+ private
829
+ # Tries to find a resource for a given collection name; if it fails, then the resource is created
830
+ def find_or_create_resource_for_collection(name)
831
+ find_or_create_resource_for(name.to_s.singularize)
832
+ end
833
+
834
+ # Tries to find a resource for a given name; if it fails, then the resource is created
835
+ def find_or_create_resource_for(name)
836
+ resource_name = name.to_s.camelize
837
+
838
+ # FIXME: Make it generic enough to support any depth of module nesting
839
+ if (ancestors = self.class.name.split("::")).size > 1
840
+ begin
841
+ ancestors.first.constantize.const_get(resource_name)
842
+ rescue NameError
843
+ self.class.const_get(resource_name)
844
+ end
845
+ else
846
+ self.class.const_get(resource_name)
847
+ end
848
+ rescue NameError
849
+ resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
850
+ resource.prefix = self.class.prefix
851
+ resource.site = self.class.site
852
+ resource
853
+ end
854
+
855
+ def split_options(options = {})
856
+ self.class.send!(:split_options, options)
857
+ end
858
+
859
+ def method_missing(method_symbol, *arguments) #:nodoc:
860
+ method_name = method_symbol.to_s
861
+
862
+ case method_name.last
863
+ when "="
864
+ attributes[method_name.first(-1)] = arguments.first
865
+ when "?"
866
+ attributes[method_name.first(-1)]
867
+ else
868
+ attributes.has_key?(method_name) ? attributes[method_name] : super
869
+ end
870
+ end
871
+ end
872
+ end