A CloudKit::Resource represents a “resource” in the REST/HTTP sense of the word. It encapsulates a JSON document and its metadata such as its URI, ETag, Last-Modified date, remote user, and historical versions.
Methods
public class
protected class
public instance
- archived?
- current?
- delete
- deleted?
- parsed_json
- previous_version
- previous_versions
- save
- update
- versions
protected instance
Attributes
etag | [R] | |
json | [R] | |
last_modified | [R] | |
remote_user | [R] | |
uri | [R] |
Public class methods
Find all resources with the given properties. Expects a hash specifying the search parameters. Returns an array of Resources.
# File lib/cloudkit/store/resource.rb, line 144 144: def self.all(spec={}) 145: CloudKit.storage_adapter.query { |q| 146: spec.keys.each { |k| 147: q.add_condition(k.to_s, :eql, escape(spec[k])) 148: } 149: }.reverse.map { |hash| build_from_hash(hash) } 150: end
Create a new resource. Intializes and saves in one step.
Parameters
- uri - A CloudKit::URI for the resource or its parent collection.
- json - A string representing a valid JSON object.
- remote_user - Optional. The URI for the user creating the resource.
# File lib/cloudkit/store/resource.rb, line 130 130: def self.create(uri, json, remote_user=nil) 131: resource = new(uri, json, remote_user) 132: resource.save 133: resource 134: end
Find all current resources with the given properties. Expectes a hash specifying the search parameters. Returns an array of Resources.
# File lib/cloudkit/store/resource.rb, line 138 138: def self.current(spec={}) 139: all({:deleted => false, :archived => false}.merge(spec)) 140: end
Find the first matching resource or nil. Expects a hash specifying the search parameters.
# File lib/cloudkit/store/resource.rb, line 154 154: def self.first(spec) 155: all(spec)[0] 156: end
Initialize a new instance of a resource.
Parameters
- uri - A CloudKit::URI for the resource or its parent collection.
- json - A string representing a valid JSON object.
- remote_user - Optional. The URI for the user creating the resource.
- options - Optional. A hash of other internal properties to set, mostly for internal use.
# File lib/cloudkit/store/resource.rb, line 17 17: def initialize(uri, json, remote_user=nil, options={}) 18: load_from_options(options.merge( 19: :uri => uri, 20: :json => json, 21: :remote_user => remote_user)) 22: end
Protected class methods
# File lib/cloudkit/store/resource.rb, line 182 182: def self.build_from_hash(data) 183: new( 184: data['uri'], 185: data['json'], 186: data['remote_user'], 187: {}.filter_merge!( 188: :etag => data['etag'], 189: :last_modified => data['last_modified'], 190: :resource_reference => data['resource_reference'], 191: :collection_reference => data['collection_reference'], 192: :id => data[:pk], 193: :deleted => data['deleted'], 194: :archived => data['archived'])) 195: end
# File lib/cloudkit/store/resource.rb, line 216 216: def self.escape(value) 217: case value 218: when TrueClass 219: "true" 220: when FalseClass 221: "false" 222: when NilClass 223: "null" 224: when Fixnum, Bignum, Float 225: value.to_s 226: when Array, Hash 227: JSON.generate(value) # temporary bug fix prior to JSONQuery support 228: else 229: value 230: end 231: end
# File lib/cloudkit/store/resource.rb, line 237 237: def self.unescape(value) 238: case value 239: when "true" 240: true 241: when "false" 242: false 243: when "null" 244: nil 245: else 246: value 247: end 248: end
# File lib/cloudkit/store/resource.rb, line 201 201: def self.wrap_uri(uri) 202: case uri 203: when CloudKit::URI; uri 204: else CloudKit::URI.new(uri) 205: end 206: end
Public instance methods
Returns true if the resource is archived i.e. not the current version.
# File lib/cloudkit/store/resource.rb, line 110 110: def archived? 111: @archived 112: end
Returns true if the resource is not archived and not deleted.
# File lib/cloudkit/store/resource.rb, line 115 115: def current? 116: !@deleted && !@archived 117: end
Delete the given resource. This is a soft delete, archiving the previous resource and inserting a deleted resource placeholder at the old URI. Raises HistoricalIntegrityViolation for attempts to delete resources that are not current.
# File lib/cloudkit/store/resource.rb, line 66 66: def delete 67: raise HistoricalIntegrityViolation unless current? 68: transaction do 69: original_uri = @uri 70: record = CloudKit.storage_adapter[@id] 71: record['uri'] = "#{@uri.string}/versions/#{@etag}" 72: record['archived'] = escape(true) 73: @uri = wrap_uri(record['uri']) 74: @archived = unescape(record['archived']) 75: CloudKit.storage_adapter[@id] = record 76: self.class.new(original_uri, @json, @remote_user, {:deleted => true}).save 77: end 78: reload 79: end
Returns true if the resource has been deleted.
# File lib/cloudkit/store/resource.rb, line 105 105: def deleted? 106: @deleted 107: end
Returns and caches the parsed JSON representation for this resource.
# File lib/cloudkit/store/resource.rb, line 120 120: def parsed_json 121: @parsed_json ||= JSON.parse(@json) 122: end
Returns the most recent previous version of the given resource.
# File lib/cloudkit/store/resource.rb, line 100 100: def previous_version 101: @previous_version ||= previous_versions[0] 102: end
Returns all previous versions of a resource, reverse ordered by Last-Modified date.
# File lib/cloudkit/store/resource.rb, line 95 95: def previous_versions 96: @previous_versions ||= versions[1..-1] rescue [] 97: end
Save the resource. If this is a new resource with only a resource collection URI, its full URI will be generated. ETags and Last-Modified dates are also generated upon saving. No manual reloads are required.
# File lib/cloudkit/store/resource.rb, line 27 27: def save 28: @id ||= '%064d' % CloudKit.storage_adapter.generate_unique_id 29: @etag = UUID.generate unless @deleted 30: @last_modified = Time.now.httpdate 31: 32: CloudKit.storage_adapter[@id] = { 33: 'uri' => @uri.cannonical_uri_string, 34: 'etag' => escape(@etag), 35: 'last_modified' => @last_modified, 36: 'json' => @json, 37: 'deleted' => escape(@deleted), 38: 'archived' => escape(@archived), 39: 'remote_user' => escape(@remote_user), 40: 'collection_reference' => @collection_reference ||= @uri.collection_uri_fragment, 41: 'resource_reference' => @resource_reference ||= @uri.cannonical_uri_string 42: }.merge(escape_values(parsed_json)) 43: reload 44: end
Update the json and optionally the remote_user for the current resource. Automatically archives the previous version, generating new ETag and Last-Modified dates. Raises HistoricalIntegrityViolation for attempts to modify resources that are not current.
# File lib/cloudkit/store/resource.rb, line 50 50: def update(json, remote_user=nil) 51: raise HistoricalIntegrityViolation unless current? 52: transaction do 53: record = CloudKit.storage_adapter[@id] 54: record['uri'] = "#{@uri.string}/versions/#{@etag}" 55: record['archived'] = escape(true) 56: CloudKit.storage_adapter[@id] = record 57: self.class.create(@uri, json, remote_user || @remote_user) 58: end 59: reload 60: end
Returns all versions of the given resource, reverse ordered by Last-Modified date, including the current version of the resource.
# File lib/cloudkit/store/resource.rb, line 83 83: def versions 84: # TODO make this a collection proxy, only loading the first, then the 85: # rest as needed during iteration (possibly in chunks) 86: return nil if @archived 87: @versions ||= [self].concat(CloudKit.storage_adapter.query { |q| 88: q.add_condition('resource_reference', :eql, @resource_reference) 89: q.add_condition('archived', :eql, 'true') 90: }.reverse.map { |hash| self.class.build_from_hash(hash) }) 91: end
Protected instance methods
# File lib/cloudkit/store/resource.rb, line 212 212: def escape(value) 213: self.class.escape(value) 214: end
# File lib/cloudkit/store/resource.rb, line 254 254: def escape_values(hash) 255: hash.inject({}) { |memo, pair| memo.merge({pair[0] => escape(pair[1])}) } 256: end
# File lib/cloudkit/store/resource.rb, line 160 160: def load_from_options(opts) 161: options = symbolize_keys(opts) 162: 163: @uri = wrap_uri(options[:uri]) 164: @json = options[:json] 165: @last_modified = options[:last_modified] 166: @resource_reference = options[:resource_reference] 167: @collection_reference = options[:collection_reference] 168: @id = options[:id] || options[:pk] || nil 169: @etag = unescape(options[:etag]) 170: @remote_user = unescape(options[:remote_user]) 171: @archived = unescape(options[:archived]) || false 172: @deleted = unescape(options[:deleted]) || false 173: end
# File lib/cloudkit/store/resource.rb, line 175 175: def reload 176: result = CloudKit.storage_adapter.query { |q| 177: q.add_condition('uri', :eql, @resource_reference) 178: } 179: load_from_options(result[0]) 180: end
# File lib/cloudkit/store/resource.rb, line 208 208: def scope(key) 209: "#{@id}:#{key}" 210: end
# File lib/cloudkit/store/resource.rb, line 250 250: def symbolize_keys(hash) 251: hash.inject({}) { |memo, pair| memo.merge({pair[0].to_sym => pair[1]}) } 252: end
# File lib/cloudkit/store/resource.rb, line 258 258: def transaction 259: open('.lock', 'w+') do |f| 260: f.flock(File::LOCK_EX) 261: begin 262: yield 263: ensure 264: f.flock(File::LOCK_UN) 265: end 266: end 267: end
# File lib/cloudkit/store/resource.rb, line 233 233: def unescape(value) 234: self.class.unescape(value) 235: end
# File lib/cloudkit/store/resource.rb, line 197 197: def wrap_uri(uri) 198: self.class.wrap_uri(uri) 199: end