tagcrumbs-tagcrumbs 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (178) hide show
  1. data/History.txt +9 -0
  2. data/License.txt +22 -0
  3. data/Manifest.txt +177 -0
  4. data/PostInstall.txt +2 -0
  5. data/README.rdoc +45 -0
  6. data/Rakefile +35 -0
  7. data/TODO.txt +17 -0
  8. data/bin/tagcrumbs +5 -0
  9. data/examples/address_update.rb +15 -0
  10. data/examples/comment_new.rb +19 -0
  11. data/examples/favorite_new.rb +17 -0
  12. data/examples/filter_new.rb +15 -0
  13. data/examples/friendship_create.rb +19 -0
  14. data/examples/link_new.rb +16 -0
  15. data/examples/location_update.rb +16 -0
  16. data/examples/picture_update.rb +16 -0
  17. data/examples/placemark_new_simple.rb +24 -0
  18. data/examples/placemark_new_suggestions.rb +24 -0
  19. data/examples/profile_link_new.rb +14 -0
  20. data/examples/profile_update.rb +12 -0
  21. data/examples/request_authorization.rb +25 -0
  22. data/examples/settings_update.rb +12 -0
  23. data/examples/subscription_new.rb +15 -0
  24. data/examples/tagcrumbs_find.rb +25 -0
  25. data/examples/tagcrumbs_search.rb +7 -0
  26. data/examples/user.rb +28 -0
  27. data/examples/user_recommendation_new.rb +15 -0
  28. data/lib/tagcrumbs.rb +232 -0
  29. data/lib/tagcrumbs/builders/builder.rb +45 -0
  30. data/lib/tagcrumbs/builders/json_builder.rb +38 -0
  31. data/lib/tagcrumbs/builders/xml_builder.rb +30 -0
  32. data/lib/tagcrumbs/cli.rb +268 -0
  33. data/lib/tagcrumbs/config_store.rb +35 -0
  34. data/lib/tagcrumbs/exceptions.rb +13 -0
  35. data/lib/tagcrumbs/node.rb +43 -0
  36. data/lib/tagcrumbs/parsers/json_parser.rb +79 -0
  37. data/lib/tagcrumbs/parsers/parser.rb +24 -0
  38. data/lib/tagcrumbs/parsers/xml_parser.rb +54 -0
  39. data/lib/tagcrumbs/proxy.rb +32 -0
  40. data/lib/tagcrumbs/requestor.rb +140 -0
  41. data/lib/tagcrumbs/resources/array.rb +108 -0
  42. data/lib/tagcrumbs/resources/models/accessors.rb +252 -0
  43. data/lib/tagcrumbs/resources/models/activity.rb +19 -0
  44. data/lib/tagcrumbs/resources/models/address.rb +16 -0
  45. data/lib/tagcrumbs/resources/models/city.rb +13 -0
  46. data/lib/tagcrumbs/resources/models/comment.rb +12 -0
  47. data/lib/tagcrumbs/resources/models/country.rb +15 -0
  48. data/lib/tagcrumbs/resources/models/fanship.rb +12 -0
  49. data/lib/tagcrumbs/resources/models/favorite.rb +22 -0
  50. data/lib/tagcrumbs/resources/models/filter.rb +32 -0
  51. data/lib/tagcrumbs/resources/models/flag.rb +10 -0
  52. data/lib/tagcrumbs/resources/models/friendship.rb +12 -0
  53. data/lib/tagcrumbs/resources/models/geoname.rb +8 -0
  54. data/lib/tagcrumbs/resources/models/link.rb +13 -0
  55. data/lib/tagcrumbs/resources/models/location.rb +13 -0
  56. data/lib/tagcrumbs/resources/models/model.rb +148 -0
  57. data/lib/tagcrumbs/resources/models/picture.rb +28 -0
  58. data/lib/tagcrumbs/resources/models/place.rb +17 -0
  59. data/lib/tagcrumbs/resources/models/placemark.rb +30 -0
  60. data/lib/tagcrumbs/resources/models/profile.rb +16 -0
  61. data/lib/tagcrumbs/resources/models/profile_link.rb +12 -0
  62. data/lib/tagcrumbs/resources/models/root.rb +19 -0
  63. data/lib/tagcrumbs/resources/models/settings.rb +14 -0
  64. data/lib/tagcrumbs/resources/models/state.rb +14 -0
  65. data/lib/tagcrumbs/resources/models/subscription.rb +13 -0
  66. data/lib/tagcrumbs/resources/models/suggestions.rb +11 -0
  67. data/lib/tagcrumbs/resources/models/tag.rb +6 -0
  68. data/lib/tagcrumbs/resources/models/tagcrumb.rb +58 -0
  69. data/lib/tagcrumbs/resources/models/user.rb +101 -0
  70. data/lib/tagcrumbs/resources/models/user_recommendation.rb +18 -0
  71. data/lib/tagcrumbs/resources/resource.rb +76 -0
  72. data/lib/tagcrumbs/validations.rb +301 -0
  73. data/script/console +10 -0
  74. data/script/destroy +14 -0
  75. data/script/generate +14 -0
  76. data/spec/fixtures/activity.json +210 -0
  77. data/spec/fixtures/activity.xml +66 -0
  78. data/spec/fixtures/address.json +183 -0
  79. data/spec/fixtures/address.xml +58 -0
  80. data/spec/fixtures/city.json +88 -0
  81. data/spec/fixtures/city.xml +34 -0
  82. data/spec/fixtures/comment.json +286 -0
  83. data/spec/fixtures/comment.xml +87 -0
  84. data/spec/fixtures/country.json +32 -0
  85. data/spec/fixtures/country.xml +13 -0
  86. data/spec/fixtures/fanship.json +180 -0
  87. data/spec/fixtures/fanship.xml +54 -0
  88. data/spec/fixtures/favorite.json +332 -0
  89. data/spec/fixtures/favorite.xml +109 -0
  90. data/spec/fixtures/filter.json +243 -0
  91. data/spec/fixtures/filter.xml +80 -0
  92. data/spec/fixtures/friendship.json +180 -0
  93. data/spec/fixtures/friendship.xml +54 -0
  94. data/spec/fixtures/geoname.json +18 -0
  95. data/spec/fixtures/geoname.xml +6 -0
  96. data/spec/fixtures/json_parser.json +25 -0
  97. data/spec/fixtures/link.json +288 -0
  98. data/spec/fixtures/link.xml +88 -0
  99. data/spec/fixtures/location.json +204 -0
  100. data/spec/fixtures/location.xml +71 -0
  101. data/spec/fixtures/picture.json +154 -0
  102. data/spec/fixtures/picture.png +0 -0
  103. data/spec/fixtures/picture.xml +49 -0
  104. data/spec/fixtures/placemark.json +357 -0
  105. data/spec/fixtures/placemark.xml +58 -0
  106. data/spec/fixtures/placemarks.json +887 -0
  107. data/spec/fixtures/placemarks.xml +348 -0
  108. data/spec/fixtures/profile.json +282 -0
  109. data/spec/fixtures/profile.xml +100 -0
  110. data/spec/fixtures/profile_link.json +156 -0
  111. data/spec/fixtures/profile_link.xml +49 -0
  112. data/spec/fixtures/root.json +46 -0
  113. data/spec/fixtures/root.xml +14 -0
  114. data/spec/fixtures/settings.json +147 -0
  115. data/spec/fixtures/settings.xml +45 -0
  116. data/spec/fixtures/state.json +62 -0
  117. data/spec/fixtures/state.xml +23 -0
  118. data/spec/fixtures/subscription.json +283 -0
  119. data/spec/fixtures/subscription.xml +86 -0
  120. data/spec/fixtures/suggestions.json +1877 -0
  121. data/spec/fixtures/suggestions.xml +724 -0
  122. data/spec/fixtures/tag.json +9 -0
  123. data/spec/fixtures/tag.xml +4 -0
  124. data/spec/fixtures/user.json +105 -0
  125. data/spec/fixtures/user.xml +31 -0
  126. data/spec/fixtures/user_recommendation.json +349 -0
  127. data/spec/fixtures/user_recommendation.xml +106 -0
  128. data/spec/fixtures/validation_errors.json +5 -0
  129. data/spec/fixtures/validation_errors.xml +5 -0
  130. data/spec/fixtures/xml_parser.xml +13 -0
  131. data/spec/spec.opts +1 -0
  132. data/spec/spec_helper.rb +75 -0
  133. data/spec/tagcrumbs/builders/builder_spec.rb +57 -0
  134. data/spec/tagcrumbs/builders/json_builder_spec.rb +47 -0
  135. data/spec/tagcrumbs/builders/xml_builder_spec.rb +34 -0
  136. data/spec/tagcrumbs/exceptions_spec.rb +16 -0
  137. data/spec/tagcrumbs/node_spec.rb +42 -0
  138. data/spec/tagcrumbs/parsers/json_parser_spec.rb +117 -0
  139. data/spec/tagcrumbs/parsers/parser_spec.rb +25 -0
  140. data/spec/tagcrumbs/parsers/xml_parser_spec.rb +117 -0
  141. data/spec/tagcrumbs/proxy_spec.rb +48 -0
  142. data/spec/tagcrumbs/requestor_spec.rb +62 -0
  143. data/spec/tagcrumbs/resources/array_spec.rb +62 -0
  144. data/spec/tagcrumbs/resources/models/accessors_spec.rb +123 -0
  145. data/spec/tagcrumbs/resources/models/activity_spec.rb +33 -0
  146. data/spec/tagcrumbs/resources/models/address_spec.rb +24 -0
  147. data/spec/tagcrumbs/resources/models/city_spec.rb +33 -0
  148. data/spec/tagcrumbs/resources/models/comment_spec.rb +23 -0
  149. data/spec/tagcrumbs/resources/models/country_spec.rb +26 -0
  150. data/spec/tagcrumbs/resources/models/fanship_spec.rb +28 -0
  151. data/spec/tagcrumbs/resources/models/favorite_spec.rb +28 -0
  152. data/spec/tagcrumbs/resources/models/filter_spec.rb +43 -0
  153. data/spec/tagcrumbs/resources/models/flag_spec.rb +5 -0
  154. data/spec/tagcrumbs/resources/models/friendship_spec.rb +28 -0
  155. data/spec/tagcrumbs/resources/models/geoname_spec.rb +18 -0
  156. data/spec/tagcrumbs/resources/models/link_spec.rb +24 -0
  157. data/spec/tagcrumbs/resources/models/location_spec.rb +28 -0
  158. data/spec/tagcrumbs/resources/models/model_spec.rb +27 -0
  159. data/spec/tagcrumbs/resources/models/picture_spec.rb +42 -0
  160. data/spec/tagcrumbs/resources/models/place_spec.rb +12 -0
  161. data/spec/tagcrumbs/resources/models/placemark_spec.rb +61 -0
  162. data/spec/tagcrumbs/resources/models/profile_link_spec.rb +29 -0
  163. data/spec/tagcrumbs/resources/models/profile_spec.rb +43 -0
  164. data/spec/tagcrumbs/resources/models/root_spec.rb +57 -0
  165. data/spec/tagcrumbs/resources/models/settings_spec.rb +32 -0
  166. data/spec/tagcrumbs/resources/models/state_spec.rb +30 -0
  167. data/spec/tagcrumbs/resources/models/subscription_spec.rb +28 -0
  168. data/spec/tagcrumbs/resources/models/suggestions_spec.rb +33 -0
  169. data/spec/tagcrumbs/resources/models/tag_spec.rb +16 -0
  170. data/spec/tagcrumbs/resources/models/tagcrumb_spec.rb +21 -0
  171. data/spec/tagcrumbs/resources/models/user_recommendation_spec.rb +35 -0
  172. data/spec/tagcrumbs/resources/models/user_spec.rb +128 -0
  173. data/spec/tagcrumbs/resources/resource_spec.rb +62 -0
  174. data/spec/tagcrumbs/validations_spec.rb +27 -0
  175. data/spec/tagcrumbs_spec.rb +103 -0
  176. data/tagcrumbs.gemspec +59 -0
  177. data/tasks/rspec.rake +21 -0
  178. metadata +325 -0
@@ -0,0 +1,79 @@
1
+ module Tagcrumbs
2
+ # A parser for JSON
3
+ class JsonParser < Parser
4
+ attr_accessor :_name # save the name of the json hash
5
+
6
+ def initialize(parser_or_document)
7
+ if parser_or_document.is_a?(String)
8
+ tmp = JSON.parse(parser_or_document)
9
+ self._name = tmp.keys.first
10
+ self.document = tmp[self._name]
11
+ elsif(parser_or_document.is_a?(Tagcrumbs::JsonParser))
12
+ self.document = parser_or_document.document
13
+ else
14
+ self.document = parser_or_document
15
+ end
16
+
17
+ self.format = :json
18
+ end
19
+
20
+ # Get a node with a specific name from the document
21
+ def get_node(name)
22
+ self.document[name.to_s]
23
+ end
24
+
25
+ # Is this a stub and therefore only a "link" in the webservice or is a resource loaded?
26
+ def node_loaded?
27
+ badgerfish_json_loaded?
28
+ end
29
+
30
+ # Get all child nodes with a specific name
31
+ def nodes_with_name(name)
32
+ self.document[name] || []
33
+ end
34
+
35
+ # Get all attributes from the badgerfish JSON that (all keys starting with '@')
36
+ def get_attributes
37
+ attributes_from_badgerfish_json
38
+ end
39
+
40
+ # Get one attribute from the JSON
41
+ def get_attribute(attribute)
42
+ self.document["@#{attribute}"]
43
+ end
44
+
45
+ # Get the value of an element ('$' key in badgerfish JSON)
46
+ def get_value
47
+ self.document['$']
48
+ end
49
+
50
+ # Get the name of the current element
51
+ def name
52
+ _name || self.document.keys.select{|k| k.first != '@'}.first
53
+ end
54
+
55
+ private
56
+
57
+ def attributes_from_badgerfish_json
58
+ hash = {}
59
+
60
+ self.document.each do |k, v|
61
+ if k.first == '@'
62
+ hash[k[1..-1]] = v
63
+ end
64
+ end
65
+
66
+ hash
67
+ end
68
+
69
+ # is there anything else besides badgerfish style attributes?
70
+ def badgerfish_json_loaded?
71
+ self.document.each do |k, v|
72
+ return true if k.first != '@'
73
+ end
74
+
75
+ false
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,24 @@
1
+ module Tagcrumbs
2
+ # Parser is an abstract class that provides common functions for all parsers and that helps to get a correct parser for
3
+ # a specific format
4
+ class Parser
5
+ attr_accessor :document, :format
6
+
7
+ # Create a parser for a specific format (xml or json)
8
+ # +document_or_parser+ can either be a Parser or a document, if it is a Parser it will be returned as it is,
9
+ # otherwise a parser will be created out of the document
10
+ def self.new_for_format(document_or_parser, format)
11
+ if document_or_parser.is_a?(Tagcrumbs::Parser) # allow to also pass a document right into the method
12
+ document_or_parser
13
+ else
14
+ case format
15
+ when :xml: XmlParser.new(document_or_parser)
16
+ when :json: JsonParser.new(document_or_parser)
17
+ else
18
+ raise UnsupportedFormat
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ module Tagcrumbs
2
+ class XmlParser < Parser
3
+
4
+ def initialize(parser_or_document)
5
+ if parser_or_document.is_a?(String)
6
+ xml = Hpricot.XML(parser_or_document)
7
+ self.document = xml.root ? xml.root : xml
8
+ elsif(parser_or_document.is_a?(Tagcrumbs::XmlParser))
9
+ self.document = parser_or_document.document
10
+ else
11
+ self.document = parser_or_document
12
+ end
13
+
14
+ self.format = :xml
15
+ end
16
+
17
+ # Get a node with a specific name from the document
18
+ def get_node(name)
19
+ self.document.children_of_type(name.to_s).first
20
+ end
21
+
22
+ # Does the document contain a resource or is it just a "link" in the webservice?
23
+ def node_loaded?
24
+ #self.document.innerText.present?
25
+ !self.document.empty?
26
+ end
27
+
28
+ # Get all children with name
29
+ def nodes_with_name(name)
30
+ self.document.children_of_type(name)
31
+ end
32
+
33
+ # Get all attributes
34
+ def get_attributes
35
+ self.document.attributes
36
+ end
37
+
38
+ # Get one specific attribute
39
+ def get_attribute(attribute)
40
+ self.document.get_attribute(attribute)
41
+ end
42
+
43
+ # Get the value of the tag
44
+ def get_value
45
+ self.document.innerHTML
46
+ end
47
+
48
+ # Get the name of the document
49
+ def name
50
+ self.document.name
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,32 @@
1
+ module Tagcrumbs
2
+ # The Proxy class is used in resource associations to delay loading of a resource for as long as possible.
3
+ # Some method calls are directly forwarded to the resource, for others the resource is loaded first from the webservice
4
+ # and afterwards the call is made.
5
+ # The Proxy tries to be as transparent as possible so that you basically never know that you have a proxy object instead of a real resource.
6
+ class Proxy
7
+ UNPROXIED_METHODS = [:reload, :resource_url, :properties, :loaded?, :class] # TODO: figure out what other methods might be needed?
8
+
9
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ } # http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc
10
+
11
+ def initialize(object)
12
+ @object = object
13
+ end
14
+
15
+
16
+ # Forward all method calls to the proxied object, reload if necessary
17
+ def method_missing(method_name, *args, &block)
18
+ #puts "** Proxying #{method_name}. Loaded: #{@object.loaded?}"
19
+
20
+ if @object.loaded? || UNPROXIED_METHODS.include?(method_name) #|| !@object.respond_to?(method_name) # having this doesn't allow first etc on arrays
21
+ @object.send(method_name, *args, &block)
22
+ else
23
+ #puts "** Reloading"
24
+ @object.reload if @object.resource_url.present?
25
+ @object.send(method_name, *args, &block)
26
+ end
27
+
28
+
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,140 @@
1
+ module Tagcrumbs
2
+ # The Requestor class is used to make the actual requests to the Tagcrumbs web service
3
+ # It uses the OAuth gem to make the requests.
4
+
5
+ class Requestor
6
+ attr_accessor :options, :response
7
+
8
+ # A Requestor can have custom options. If no custom options are specified the general Tagcrumbs.options are used.
9
+ # You can use this to have a Requestor with different consumer tokens for example.
10
+ def initialize(options = {})
11
+ options.reverse_merge!(Tagcrumbs.options)
12
+ self.options = options.with_indifferent_access
13
+ end
14
+
15
+ # ================
16
+ # = OAuth Tokens =
17
+ # ================
18
+
19
+ # Returns the OAuth consumer token
20
+ def consumer
21
+ @consumer ||= ::OAuth::Consumer.new(options[:consumer_token], options[:consumer_secret], options[:consumer_options])
22
+ end
23
+
24
+ # Requests a Request Token from the web service or builds it from the options
25
+ def request_token
26
+ return @request_token if defined?(@request_token)
27
+
28
+ if options[:request_token] && options[:request_secret]
29
+ @request_token = ::OAuth::RequestToken.new(consumer, options[:request_token], options[:request_secret])
30
+ else
31
+ @request_token = consumer.get_request_token
32
+ end
33
+ end
34
+
35
+ # Exchanges a Request Token against an Access Token that has to be saved and used for all further calls to the webservice.
36
+ def exchange_request_token!
37
+ @access_token = request_token.get_access_token
38
+ end
39
+
40
+ # Returns the configured access token. Calls should be made with the get, post, put and delete methods, direct access to this
41
+ # should usually not be needed.
42
+ def access_token
43
+ return @access_token if defined?(@access_token)
44
+
45
+ if options[:access_token] && options[:access_secret]
46
+ @access_token = ::OAuth::AccessToken.new(consumer, options[:access_token], options[:access_secret])
47
+ else
48
+ nil
49
+ end
50
+ end
51
+
52
+ # ========
53
+ # = HTTP =
54
+ # ========
55
+
56
+ # Create a custom request to the webservice
57
+ # If an access token is configured it will be used to make webservice calls (3-legged oauth)
58
+ # If a consumer is configured and it is a get request only it will make 2-legged oauth calls
59
+ def request(http_method, path, query_string = {}, body = '', headers = {})
60
+ url = path_with_query_string(path, query_string)
61
+ arguments = body.present? ? [body, default_headers.merge(headers)] : [default_headers.merge(headers)]
62
+
63
+ if access_token # 3-legged request with access token
64
+ response = self.access_token.request(http_method, url, *arguments)
65
+ elsif http_method == :get && consumer # 2-legged consumer only request
66
+ response = self.consumer.request(:get, url, nil, {}, *arguments)
67
+ else
68
+ raise InvalidRequest.new("Setup the Access Token first or only make get requests with a valid consumer.")
69
+ end
70
+
71
+ handle_response(response)
72
+ end
73
+
74
+ # Create a get request on path, optionally pass a hash of query parameters
75
+ # Can also be used with 2-legged oauth to request public resources
76
+ def get(path, query_string = {})
77
+ request(:get, path, query_string)
78
+ end
79
+
80
+ # Post body to path, access_token needed
81
+ def post(path, body='')
82
+ request(:post, path, {}, body)
83
+ end
84
+
85
+ # Put body to path, access_token needed
86
+ def put(path, body='')
87
+ request(:put, path, {}, body)
88
+ end
89
+
90
+ # Delete a resource, access_token needed
91
+ def delete(path)
92
+ request(:delete, path)
93
+ end
94
+
95
+ # Return the format of the response object
96
+ def response_format
97
+ raise 'Request a resource first' unless response
98
+
99
+ Tagcrumbs.supported_formats.each do |format, content_types|
100
+ return format if content_types.include?(response.content_type)
101
+ end
102
+
103
+ :xml
104
+ end
105
+
106
+ private
107
+ # Build a URL from a path and a hash of query string parameters
108
+ def path_with_query_string(path, arguments = {})
109
+ if arguments.present?
110
+ path + '?' + arguments.to_param
111
+ else
112
+ path
113
+ end
114
+ end
115
+
116
+ # Handle responses from the webservice, raise an error on unexpected HTTP response codes
117
+ def handle_response(response)
118
+ self.response = response
119
+ case response.code
120
+ when '200', '201', '422': # ok, created, invalid
121
+ response
122
+ else
123
+ response.error!
124
+ end
125
+ end
126
+
127
+ # Set the Content-Type and Accept HTTP Headers to the correct values depending on format
128
+ def default_headers(format = nil)
129
+ format ||= self.options[:format]
130
+
131
+ raise UnsupportedFormat unless Tagcrumbs.supported_formats[format]
132
+
133
+ {
134
+ "Content-Type" => Tagcrumbs.supported_formats[format].first,
135
+ "Accept" => Tagcrumbs.supported_formats[format].join(', ')
136
+ }
137
+ end
138
+
139
+ end
140
+ end
@@ -0,0 +1,108 @@
1
+ module Tagcrumbs
2
+ # Basically a regular Array that holds many Resources.
3
+ # Used to get collections from the webservice.
4
+ class Array < Resource
5
+ ATTRIBUTES = [:total_entries, :pages, :per_page, :page, :sort, :order, :next_page, :previous_page]
6
+
7
+ attr_accessor :array
8
+ attr_reader *ATTRIBUTES
9
+
10
+ # set instance variables and automatically convert them to real integers if they are an integer
11
+ ATTRIBUTES.each do |a|
12
+ define_method "#{a}=" do |value|
13
+ begin
14
+ typecasted_value = Integer(value)
15
+ rescue ArgumentError
16
+ typecasted_value = value
17
+ ensure
18
+ self.instance_variable_set("@#{a}", typecasted_value)
19
+ end
20
+ end
21
+ end
22
+
23
+ def initialize
24
+ reset!
25
+ end
26
+
27
+ # Reset the state of the Array
28
+ def reset!
29
+ super
30
+ @array = []
31
+
32
+ true
33
+ end
34
+
35
+ # Replace the Array with the next page of elements if there is one
36
+ def next_page!
37
+ reload(next_page) if next_page.present?
38
+ end
39
+
40
+ # Replace the Array with the privious page of elements if there is one
41
+ def previous_page!
42
+ reload(previous_page) if previous_page.present?
43
+ end
44
+
45
+ # Iterate over all pages
46
+ def each_page(args={})
47
+ begin
48
+ yield(self)
49
+ end while(args[:reverse] == true ? previous_page! : next_page!)
50
+ end
51
+
52
+ # Iterate over all items with pagination
53
+ def each_page_each_item(args={})
54
+ self.each_page(args) do |array|
55
+ array.each do |item|
56
+ yield item
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ # Set the properties and typecast some attributes automatically
63
+ def properties=(properties)
64
+ super
65
+
66
+ self.properties.each do |key, value|
67
+ send("#{key}=", value) if ATTRIBUTES.include?(key.to_sym)
68
+ end
69
+ end
70
+
71
+ # delegate unkown methods to the underlying array
72
+ def method_missing(name, *args, &block)
73
+ if @array.respond_to?(name)
74
+ @array.send(name, *args, &block)
75
+ else
76
+ super
77
+ end
78
+ end
79
+
80
+ def inspect
81
+ @array.inspect
82
+ end
83
+
84
+ # Load data into the object from a document
85
+ def initialize_from_document(parser, format = Tagcrumbs.options[:format])
86
+ parser = super
87
+
88
+ if self.loaded?
89
+ nodes = parser.nodes_with_name(parser.name.singularize)
90
+ return if nodes.blank?
91
+
92
+ nodes.each do |node|
93
+ node_parser = Parser.new_for_format(node, parser.format)
94
+ node_type = node_parser.get_attribute('type')
95
+
96
+ if node_type # a nested model
97
+ @array << Tagcrumbs.class_by_type(node_type).new_from_document(node, parser.format)
98
+ else # a nested node
99
+ @array << node_parser.get_value
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+
106
+
107
+ end
108
+ end
@@ -0,0 +1,252 @@
1
+ module Tagcrumbs
2
+ module Accessors
3
+ def self.included(base)
4
+ base.class_eval do
5
+ extend(ClassMethods)
6
+
7
+ # class variables that save the options for attributes and associations
8
+ class_inheritable_accessor :writeable_attributes_options
9
+ class_inheritable_accessor :readable_attributes_options
10
+ class_inheritable_accessor :has_one_associations_options
11
+ class_inheritable_accessor :has_many_associations_options
12
+ class_inheritable_accessor :can_be_options
13
+
14
+ self.can_be_options ||= []
15
+ self.writeable_attributes_options ||= {}
16
+ self.readable_attributes_options ||= {}
17
+ self.has_one_associations_options ||= {}
18
+ self.has_many_associations_options ||= {}
19
+ end
20
+ end
21
+
22
+ # The nodes contain the values that were read from the web service
23
+ def nodes
24
+ @nodes ||= {}
25
+ end
26
+
27
+ # nodes_updates contain the changes that were done to nodes
28
+ def nodes_updates
29
+ @nodes_updates ||= {}
30
+ end
31
+
32
+ # has_one_associations contain the related objects
33
+ def has_one_associations
34
+ @has_one_associations ||= {}
35
+ end
36
+
37
+ # has_one_associations_updates contain updates of the related objects
38
+ def has_one_associations_updates
39
+ @has_one_associations_updates ||= {}
40
+ end
41
+
42
+ # has_many_associations contains Arrays of further objects
43
+ def has_many_associations
44
+ @has_many_associations ||= {}
45
+ end
46
+
47
+ # Restore back to the original state, discard any changes that were done after the object
48
+ # was loaded
49
+ def restore!
50
+ @nodes_updates = {}
51
+ @has_one_associations_updates = {}
52
+
53
+ true
54
+ end
55
+
56
+ # Set attributes with a hash
57
+ def attributes=(args={})
58
+ args.each do |k, v|
59
+ if respond_to?("#{k}=")
60
+ self.send("#{k}=", v)
61
+ end
62
+ end
63
+ end
64
+
65
+ # Return all attributes dynamically, pass :modifiable_only => true to only return
66
+ # modifiable arguments
67
+ def attributes(args={})
68
+ hash = {}
69
+
70
+ self.class.attributes(args).each do |k|
71
+ hash[k] = send(k)
72
+ end
73
+
74
+ hash
75
+ end
76
+
77
+ # Nice output for console testing
78
+ def inspect
79
+ attributes_for_inspect = []
80
+ attributes.each{|k, v| attributes_for_inspect << "#{k}: #{attribute_for_inspect(v)}"}
81
+ "#<#{self.class} #{attributes_for_inspect.join(', ')}>"
82
+ end
83
+
84
+ def attribute_for_inspect(value)
85
+ if value.is_a?(String) && value.length > 50
86
+ "#{value[0..50]}...".inspect
87
+ elsif value.is_a?(Date) || value.is_a?(Time)
88
+ %("#{value.iso8601}")
89
+ else
90
+ value.inspect
91
+ end
92
+ end
93
+
94
+ # Returns the class name without modules
95
+ def class_name
96
+ self.class.to_s.split('::').last
97
+ end
98
+
99
+ # Returns the URL to which a post request must be sent to create a new resource
100
+ def create_url
101
+ raise CreateImpossible unless can_be?(:created)
102
+
103
+ # convention over configuration, can also be overwritten in subclasses
104
+ has_many_association_name = class_name.tableize
105
+
106
+ if Tagcrumbs.current_user.respond_to?(has_many_association_name) && Tagcrumbs.current_user.send(has_many_association_name)
107
+ Tagcrumbs.current_user.send("#{has_many_association_name}_url")
108
+ else
109
+ raise CreateURLMissing
110
+ end
111
+ end
112
+
113
+ # Can this object be :created, :updated or :destroyed?
114
+ def can_be?(method)
115
+ self.class.can_be_options.include?(method.to_sym)
116
+ end
117
+
118
+
119
+ module ClassMethods
120
+ # Configure if objects of this class can be :created, :updated or :destroyed
121
+ def can_be(*args)
122
+ self.can_be_options = args
123
+ end
124
+
125
+ # Returns an array of all the attributes of a class, pass :modifiable_only => true to only get modifiable attributes
126
+ def attributes(args = {})
127
+ if args[:modifiable_only]
128
+ writeable_attributes_options.keys + modifiable_has_one_associations_options.keys
129
+ else
130
+ writeable_attributes_options.keys + readable_attributes_options.keys + modifiable_has_one_associations_options.keys
131
+ end
132
+ end
133
+
134
+ # Configure the writeable attributes of the class that will be sent to the server on a post or put
135
+ def writeable_attributes(*attributes)
136
+ attributes.each do |attribute|
137
+ define_method attribute do
138
+ if nodes_updates[attribute]
139
+ nodes_updates[attribute].value
140
+ elsif nodes[attribute]
141
+ nodes[attribute].value
142
+ else
143
+ nil
144
+ end
145
+ end
146
+
147
+ define_method "#{attribute}=" do |value|
148
+ self.nodes_updates[attribute] = Node.new(attribute, value)
149
+ end
150
+
151
+ self.writeable_attributes_options[attribute] = nil
152
+ end
153
+ end
154
+
155
+ # Configure attributes that are only readable and that cannot be changed by the client
156
+ def readable_attributes(*attributes)
157
+ attributes.each do |attribute|
158
+ define_method attribute do
159
+ if nodes[attribute]
160
+ nodes[attribute].value
161
+ else
162
+ nil
163
+ end
164
+ end
165
+
166
+ self.readable_attributes_options[attribute] = nil
167
+ end
168
+ end
169
+
170
+ # Configure a has_one relationship
171
+ # Pass :modifiable => true if this relationship can be modified.
172
+ # When the resource is sent to the webservice the resource_url of the object will be sent to identify it
173
+ def has_one(name, options={})
174
+ options[:node_name] ||= name.to_s.underscore
175
+
176
+ self.has_one_associations_options[name] = options
177
+
178
+ define_method name do # define a reader that loads the association if not done already
179
+ if has_one_associations_updates[name]
180
+ has_one_associations_updates[name]
181
+ elsif !has_one_associations.has_key?(name)
182
+ nil
183
+ else
184
+ Proxy.new(has_one_associations[name]) # use a proxy to load objects as late as possible
185
+ end
186
+ end
187
+
188
+ define_method "#{name}=" do |value|
189
+ self.has_one_associations_updates[name] = value
190
+ end
191
+
192
+ # define a convenience url accessor method
193
+ define_method "#{name}_url" do
194
+ if has_one_associations_updates[name]
195
+ has_one_associations_updates[name].resource_url
196
+ elsif has_one_associations[name]
197
+ has_one_associations[name].resource_url
198
+ end
199
+ end
200
+
201
+ # define aliases for tagcrumb
202
+ if name == :tagcrumb
203
+ define_method :placemark do
204
+ self.tagcrumb
205
+ end
206
+
207
+ define_method :placemark= do |value|
208
+ self.tagcrumb = value
209
+ end
210
+ end
211
+ end
212
+
213
+ # Configure a has_many association
214
+ def has_many(name, options={})
215
+ options[:node_name] ||= name.to_s.underscore
216
+
217
+ self.has_many_associations_options[name] = options
218
+
219
+ define_method name do # define a reader that loads the association if not done already
220
+ if !has_many_associations.has_key?(name)
221
+ nil
222
+ else
223
+ Proxy.new(has_many_associations[name]) # use a proxy to load objects as late as possible
224
+ end
225
+ end
226
+
227
+ # define a convenience url accessor method
228
+ define_method "#{name}_url" do
229
+ has_many_associations[name].resource_url if has_many_associations[name]
230
+ end
231
+ end
232
+
233
+ # Return the names of all modifiable has one associations of the class
234
+ def modifiable_has_one_associations_options
235
+ hash = {}
236
+
237
+ self.has_one_associations_options.each do |k, v|
238
+ hash[k] = v if v[:modifiable]
239
+ end
240
+
241
+ hash
242
+ end
243
+
244
+ # Return has_one and has_many options
245
+ def association_options
246
+ has_one_associations_options.merge(has_many_associations_options)
247
+ end
248
+
249
+ end
250
+
251
+ end
252
+ end