waz-storage 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. data/.gitignore +9 -9
  2. data/CHANGELOG.rdoc +72 -72
  3. data/Gemfile +4 -4
  4. data/Gemfile.lock +46 -40
  5. data/LICENSE +18 -18
  6. data/README.rdoc +310 -310
  7. data/lib/waz-blobs.rb +4 -4
  8. data/lib/waz-queues.rb +6 -6
  9. data/lib/waz-storage.rb +39 -39
  10. data/lib/waz-tables.rb +4 -4
  11. data/lib/waz/blobs/blob_object.rb +122 -122
  12. data/lib/waz/blobs/container.rb +172 -161
  13. data/lib/waz/blobs/exceptions.rb +10 -10
  14. data/lib/waz/blobs/service.rb +181 -156
  15. data/lib/waz/queues/exceptions.rb +28 -28
  16. data/lib/waz/queues/message.rb +64 -64
  17. data/lib/waz/queues/queue.rb +164 -164
  18. data/lib/waz/queues/service.rb +105 -105
  19. data/lib/waz/storage/base.rb +70 -70
  20. data/lib/waz/storage/exceptions.rb +33 -33
  21. data/lib/waz/storage/validation_rules.rb +25 -25
  22. data/lib/waz/tables/edm_type_helper.rb +44 -44
  23. data/lib/waz/tables/exceptions.rb +44 -44
  24. data/lib/waz/tables/service.rb +178 -178
  25. data/lib/waz/tables/table.rb +74 -74
  26. data/lib/waz/tables/table_array.rb +10 -10
  27. data/rakefile +8 -21
  28. data/{tests → spec}/configuration.rb +22 -22
  29. data/{tests/waz/blobs/blob_object_test.rb → spec/waz/blobs/blob_object_spec.rb} +80 -80
  30. data/{tests/waz/blobs/container_test.rb → spec/waz/blobs/container_spec.rb} +175 -162
  31. data/{tests/waz/blobs/service_test.rb → spec/waz/blobs/service_spec.rb} +336 -282
  32. data/{tests/waz/queues/message_test.rb → spec/waz/queues/message_spec.rb} +32 -32
  33. data/{tests/waz/queues/queue_test.rb → spec/waz/queues/queue_spec.rb} +205 -205
  34. data/{tests/waz/queues/service_test.rb → spec/waz/queues/service_spec.rb} +298 -298
  35. data/{tests → spec}/waz/storage/base_tests.rb +81 -81
  36. data/{tests/waz/storage/shared_key_core_service_test.rb → spec/waz/storage/shared_key_core_service_spec.rb} +141 -141
  37. data/{tests/waz/tables/service_test.rb → spec/waz/tables/service_spec.rb} +613 -613
  38. data/{tests/waz/tables/table_test.rb → spec/waz/tables/table_spec.rb} +97 -97
  39. data/waz-storage.gemspec +29 -27
  40. metadata +47 -26
@@ -1,70 +1,70 @@
1
- module WAZ
2
- module Storage
3
- # This class is used to handle a general connection with Windows Azure Storage Account and it
4
- # should be used at least once on the application bootstrap or configuration file.
5
- #
6
- # The usage is pretty simple as it's depicted on the following sample
7
- # WAZ::Storage::establish_connection!(:account_name => "my_account_name",
8
- # :access_key => "your_base64_key",
9
- # :use_ssl => false)
10
- #
11
- class Base
12
- class << self
13
- # Sets the basic configuration parameters to use the API on the current context
14
- # required parameters are :account_name, :access_key.
15
- #
16
- # All other parameters are optional.
17
- def establish_connection!(options = {})
18
- raise InvalidOption, :account_name unless options.keys.include? :account_name
19
- raise InvalidOption, :access_key if !options.keys.include? :use_sas_auth_only unless options.keys.include? :access_key
20
- raise InvalidOption, :use_sas_auth_only if !options.keys.include? :access_key unless options.keys.include? :use_sas_auth_only
21
- raise InvalidOption, :sharedaccesssignature if !options.keys.include? :access_key unless options.keys.include? :sharedaccesssignature and options.keys.include? :use_sas_auth_only
22
- options[:use_ssl] = false unless options.keys.include? :use_ssl
23
- (@connections ||= []) << options
24
- end
25
-
26
- # Block Syntax
27
- #
28
- # Pushes the named repository onto the context-stack,
29
- # yields a new session, and pops the context-stack.
30
- #
31
- # This helps you set contextual operations like in the following sample
32
- #
33
- # Base.establish_connection(options) do
34
- # # do some operations on the given context
35
- # end
36
- #
37
- # The context is restored to the previous one (what you configured on establish_connection!)
38
- # or nil.
39
- #
40
- # Non-Block Syntax
41
- #
42
- # Behaves exactly as establish_connection!
43
- def establish_connection(options = {}) # :yields: current_context
44
- establish_connection!(options)
45
- if (block_given?)
46
- begin
47
- return yield
48
- ensure
49
- @connections.pop() if connected?
50
- end
51
- end
52
- end
53
-
54
- # Returns the default connection (last set) parameters. It will raise an exception WAZ::Storage::NotConnected
55
- # when there's no connection information registered.
56
- def default_connection
57
- raise NotConnected unless connected?
58
- return @connections.last
59
- end
60
-
61
- # Returns a value indicating whether the current connection information has been set or not.
62
- def connected?
63
- return false if (@connections.nil?)
64
- return false if (@connections.empty?)
65
- return true
66
- end
67
- end
68
- end
69
- end
70
- end
1
+ module WAZ
2
+ module Storage
3
+ # This class is used to handle a general connection with Windows Azure Storage Account and it
4
+ # should be used at least once on the application bootstrap or configuration file.
5
+ #
6
+ # The usage is pretty simple as it's depicted on the following sample
7
+ # WAZ::Storage::establish_connection!(:account_name => "my_account_name",
8
+ # :access_key => "your_base64_key",
9
+ # :use_ssl => false)
10
+ #
11
+ class Base
12
+ class << self
13
+ # Sets the basic configuration parameters to use the API on the current context
14
+ # required parameters are :account_name, :access_key.
15
+ #
16
+ # All other parameters are optional.
17
+ def establish_connection!(options = {})
18
+ raise InvalidOption, :account_name unless options.keys.include? :account_name
19
+ raise InvalidOption, :access_key if !options.keys.include? :use_sas_auth_only unless options.keys.include? :access_key
20
+ raise InvalidOption, :use_sas_auth_only if !options.keys.include? :access_key unless options.keys.include? :use_sas_auth_only
21
+ raise InvalidOption, :sharedaccesssignature if !options.keys.include? :access_key unless options.keys.include? :sharedaccesssignature and options.keys.include? :use_sas_auth_only
22
+ options[:use_ssl] = false unless options.keys.include? :use_ssl
23
+ (@connections ||= []) << options
24
+ end
25
+
26
+ # Block Syntax
27
+ #
28
+ # Pushes the named repository onto the context-stack,
29
+ # yields a new session, and pops the context-stack.
30
+ #
31
+ # This helps you set contextual operations like in the following sample
32
+ #
33
+ # Base.establish_connection(options) do
34
+ # # do some operations on the given context
35
+ # end
36
+ #
37
+ # The context is restored to the previous one (what you configured on establish_connection!)
38
+ # or nil.
39
+ #
40
+ # Non-Block Syntax
41
+ #
42
+ # Behaves exactly as establish_connection!
43
+ def establish_connection(options = {}) # :yields: current_context
44
+ establish_connection!(options)
45
+ if (block_given?)
46
+ begin
47
+ return yield
48
+ ensure
49
+ @connections.pop() if connected?
50
+ end
51
+ end
52
+ end
53
+
54
+ # Returns the default connection (last set) parameters. It will raise an exception WAZ::Storage::NotConnected
55
+ # when there's no connection information registered.
56
+ def default_connection
57
+ raise NotConnected unless connected?
58
+ return @connections.last
59
+ end
60
+
61
+ # Returns a value indicating whether the current connection information has been set or not.
62
+ def connected?
63
+ return false if (@connections.nil?)
64
+ return false if (@connections.empty?)
65
+ return true
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,33 +1,33 @@
1
- module WAZ
2
- module Storage
3
- # This class is the base exception from where all the exceptions raised from this API
4
- # inherit from. If you want to handle an exception that your code may throw and you don't
5
- # know which specific type you should handle, handle this type of exception.
6
- class StorageException < StandardError
7
- end
8
-
9
- # This exception raises whenever a required parameter for initializing any class isn't provided. From
10
- # WAZ::Storage::Base up to WAZ::Queues::Queue all of them use this exception.
11
- class InvalidOption < StorageException
12
- def initialize(missing_option)
13
- super("You did not provide one of the required parameters. Please provide the #{missing_option}.")
14
- end
15
- end
16
-
17
- # This exception is raised when the user tries to perform an operation on any Storage API class
18
- # without previously specificying the connection options.
19
- class NotConnected < StorageException
20
- def initialize
21
- super("You should establish connection before using the services, the connection configuration is required.")
22
- end
23
- end
24
-
25
- # This exception if raised when the value given for an argument doesn't fall into the permitted values. For example
26
- # if values on the blocklisttype aren't [all|uncommitted|committed]
27
- class InvalidParameterValue < StorageException
28
- def initialize(args = {})
29
- super("The value supplied for the parameter #{args[:name]} is invalid. Permitted values are #{args[:values].join(',')}")
30
- end
31
- end
32
- end
33
- end
1
+ module WAZ
2
+ module Storage
3
+ # This class is the base exception from where all the exceptions raised from this API
4
+ # inherit from. If you want to handle an exception that your code may throw and you don't
5
+ # know which specific type you should handle, handle this type of exception.
6
+ class StorageException < StandardError
7
+ end
8
+
9
+ # This exception raises whenever a required parameter for initializing any class isn't provided. From
10
+ # WAZ::Storage::Base up to WAZ::Queues::Queue all of them use this exception.
11
+ class InvalidOption < StorageException
12
+ def initialize(missing_option)
13
+ super("You did not provide one of the required parameters. Please provide the #{missing_option}.")
14
+ end
15
+ end
16
+
17
+ # This exception is raised when the user tries to perform an operation on any Storage API class
18
+ # without previously specificying the connection options.
19
+ class NotConnected < StorageException
20
+ def initialize
21
+ super("You should establish connection before using the services, the connection configuration is required.")
22
+ end
23
+ end
24
+
25
+ # This exception if raised when the value given for an argument doesn't fall into the permitted values. For example
26
+ # if values on the blocklisttype aren't [all|uncommitted|committed]
27
+ class InvalidParameterValue < StorageException
28
+ def initialize(args = {})
29
+ super("The value supplied for the parameter #{args[:name]} is invalid. Permitted values are #{args[:values].join(',')}")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,26 +1,26 @@
1
- module WAZ
2
- module Storage
3
- class ValidationRules
4
- class << self
5
- # Validates that the Container/Queue name given matches with the requirements of Windows Azure.
6
- #
7
- # -Container/Queue names must start with a letter or number, and can contain only letters, numbers, and the dash (-) character.
8
- # -Every dash (-) character must be immediately preceded and followed by a letter or number.
9
- # -All letters in a container name must be lowercase.
10
- # -Container/Queue names must be from 3 through 63 characters long.
11
- def valid_name?(name)
12
- name =~ /^[a-z0-9][a-z0-9\-]{1,}[^-]$/ && name.length < 64
13
- end
14
-
15
- # Validates that the Table name given matches with the requirements of Windows Azure.
16
- #
17
- # -Table names must start with at least one lower / upper character.
18
- # -Table names can have character or any digit starting from the second position.
19
- # -Table names must be from 3 through 63 characters long.
20
- def valid_table_name?(name)
21
- name =~ /^([a-z]|[A-Z]){1}([a-z]|[A-Z]|\d){2,62}$/
22
- end
23
- end
24
- end
25
- end
1
+ module WAZ
2
+ module Storage
3
+ class ValidationRules
4
+ class << self
5
+ # Validates that the Container/Queue name given matches with the requirements of Windows Azure.
6
+ #
7
+ # -Container/Queue names must start with a letter or number, and can contain only letters, numbers, and the dash (-) character.
8
+ # -Every dash (-) character must be immediately preceded and followed by a letter or number.
9
+ # -All letters in a container name must be lowercase.
10
+ # -Container/Queue names must be from 3 through 63 characters long.
11
+ def valid_name?(name)
12
+ name =~ /^[a-z0-9][a-z0-9\-]{1,}[^-]$/ && name.length < 64
13
+ end
14
+
15
+ # Validates that the Table name given matches with the requirements of Windows Azure.
16
+ #
17
+ # -Table names must start with at least one lower / upper character.
18
+ # -Table names can have character or any digit starting from the second position.
19
+ # -Table names must be from 3 through 63 characters long.
20
+ def valid_table_name?(name)
21
+ name =~ /^([a-z]|[A-Z]){1}([a-z]|[A-Z]|\d){2,62}$/
22
+ end
23
+ end
24
+ end
25
+ end
26
26
  end
@@ -1,45 +1,45 @@
1
- module WAZ
2
- module Tables
3
- class EdmTypeHelper
4
- class << self
5
- def parse_from(item)
6
- return nil if !item.attributes['m:null'].nil? and item.attributes['m:null'] == 'true'
7
- case item.attributes['m:type']
8
- when 'Edm.Int16', 'Edm.Int32', 'Edm.Int64'
9
- item.text.to_i
10
- when 'Edm.Single', 'Edm.Double'
11
- item.text.to_f
12
- when 'Edm.Boolean'
13
- item.text == 'true'
14
- when 'Edm.DateTime'
15
- Time.parse(item.text)
16
- when 'Edm.Binary'
17
- StringIO.new(Base64.decode64(item.text))
18
- else
19
- item.text
20
- end
21
- end
22
-
23
- def parse_to(item)
24
- case item.class.name
25
- when 'String'
26
- [item, 'Edm.String']
27
- when 'Fixnum'
28
- [item, 'Edm.Int32']
29
- when 'Float'
30
- [item, 'Edm.Double']
31
- when 'TrueClass', 'FalseClass'
32
- [item, 'Edm.Boolean']
33
- when 'Time'
34
- [item.iso8601, 'Edm.DateTime']
35
- when 'File', 'StringIO'
36
- item.pos = 0
37
- [Base64.encode64(item.read), 'Edm.Binary']
38
- else
39
- [item, 'Edm.String']
40
- end
41
- end
42
- end
43
- end
44
- end
1
+ module WAZ
2
+ module Tables
3
+ class EdmTypeHelper
4
+ class << self
5
+ def parse_from(item)
6
+ return nil if !item.attributes['m:null'].nil? and item.attributes['m:null'] == 'true'
7
+ case item.attributes['m:type']
8
+ when 'Edm.Int16', 'Edm.Int32', 'Edm.Int64'
9
+ item.text.to_i
10
+ when 'Edm.Single', 'Edm.Double'
11
+ item.text.to_f
12
+ when 'Edm.Boolean'
13
+ item.text == 'true'
14
+ when 'Edm.DateTime'
15
+ Time.parse(item.text)
16
+ when 'Edm.Binary'
17
+ StringIO.new(Base64.decode64(item.text))
18
+ else
19
+ item.text
20
+ end
21
+ end
22
+
23
+ def parse_to(item)
24
+ case item.class.name
25
+ when 'String'
26
+ [item, 'Edm.String']
27
+ when 'Fixnum'
28
+ [item, 'Edm.Int32']
29
+ when 'Float'
30
+ [item, 'Edm.Double']
31
+ when 'TrueClass', 'FalseClass'
32
+ [item, 'Edm.Boolean']
33
+ when 'Time'
34
+ [item.iso8601, 'Edm.DateTime']
35
+ when 'File', 'StringIO'
36
+ item.pos = 0
37
+ [Base64.encode64(item.read), 'Edm.Binary']
38
+ else
39
+ [item, 'Edm.String']
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
45
  end
@@ -1,45 +1,45 @@
1
- module WAZ
2
- module Tables
3
- # This exception is raised while trying to create table that already exists.
4
- class TableAlreadyExists < WAZ::Storage::StorageException
5
- def initialize(name)
6
- super("The table #{name} already exists on your account.")
7
- end
8
- end
9
-
10
- # This exception is raised while trying to delete an unexisting table.
11
- class TableDoesNotExist < WAZ::Storage::StorageException
12
- def initialize(name)
13
- super("The specified table #{name} does not exist.")
14
- end
15
- end
16
-
17
- # This exception is raised when an invalid table name is provided.
18
- class InvalidTableName < WAZ::Storage::StorageException
19
- def initialize(name)
20
- super("The table name #{name} is invalid, it must start with at least one lower/upper characted, must be from 3 through 63 characters long and can have character or any digit starting from the second position")
21
- end
22
- end
23
-
24
- # This exception is raised when provided more than the 252 properties allowed by the Rest API.
25
- class TooManyProperties < WAZ::Storage::StorageException
26
- def initialize(total)
27
- super("The entity contains more properties than allowed (252). The entity has #{total} properties.")
28
- end
29
- end
30
-
31
- # This exception is raised when the specified entity already exists.
32
- class EntityAlreadyExists < WAZ::Storage::StorageException
33
- def initialize(row_key)
34
- super("The specified entity already exists. RowKey: #{row_key}")
35
- end
36
- end
37
-
38
- # This exception is raised while trying to delete an unexisting entity.
39
- class EntityDoesNotExist < WAZ::Storage::StorageException
40
- def initialize(key)
41
- super("The specified entity with #{key} does not exist.")
42
- end
43
- end
44
- end
1
+ module WAZ
2
+ module Tables
3
+ # This exception is raised while trying to create table that already exists.
4
+ class TableAlreadyExists < WAZ::Storage::StorageException
5
+ def initialize(name)
6
+ super("The table #{name} already exists on your account.")
7
+ end
8
+ end
9
+
10
+ # This exception is raised while trying to delete an unexisting table.
11
+ class TableDoesNotExist < WAZ::Storage::StorageException
12
+ def initialize(name)
13
+ super("The specified table #{name} does not exist.")
14
+ end
15
+ end
16
+
17
+ # This exception is raised when an invalid table name is provided.
18
+ class InvalidTableName < WAZ::Storage::StorageException
19
+ def initialize(name)
20
+ super("The table name #{name} is invalid, it must start with at least one lower/upper characted, must be from 3 through 63 characters long and can have character or any digit starting from the second position")
21
+ end
22
+ end
23
+
24
+ # This exception is raised when provided more than the 252 properties allowed by the Rest API.
25
+ class TooManyProperties < WAZ::Storage::StorageException
26
+ def initialize(total)
27
+ super("The entity contains more properties than allowed (252). The entity has #{total} properties.")
28
+ end
29
+ end
30
+
31
+ # This exception is raised when the specified entity already exists.
32
+ class EntityAlreadyExists < WAZ::Storage::StorageException
33
+ def initialize(row_key)
34
+ super("The specified entity already exists. RowKey: #{row_key}")
35
+ end
36
+ end
37
+
38
+ # This exception is raised while trying to delete an unexisting entity.
39
+ class EntityDoesNotExist < WAZ::Storage::StorageException
40
+ def initialize(key)
41
+ super("The specified entity with #{key} does not exist.")
42
+ end
43
+ end
44
+ end
45
45
  end
@@ -1,178 +1,178 @@
1
- module WAZ
2
- module Tables
3
- # This is internally used by the waz-tables part of the gem and it exposes the Windows Azure Blob API REST methods
4
- # implementation. You can use this class to perform an specific operation that isn't provided by the current API.
5
- class Service
6
- include WAZ::Storage::SharedKeyCoreService
7
-
8
- DATASERVICES_NAMESPACE = "http://schemas.microsoft.com/ado/2007/08/dataservices"
9
- DATASERVICES_METADATA_NAMESPACE = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
10
-
11
- # Creates a table on the current Windows Azure Storage account.
12
- def create_table(table_name)
13
- raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
14
-
15
- payload = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" \
16
- "<entry xmlns:d=\"#{DATASERVICES_NAMESPACE}\" xmlns:m=\"#{DATASERVICES_METADATA_NAMESPACE}\" xmlns=\"http://www.w3.org/2005/Atom\">" \
17
- "<title /><updated>#{Time.now.utc.iso8601}</updated><author><name/></author><id/>" \
18
- "<content type=\"application/xml\"><m:properties><d:TableName>#{table_name}</d:TableName></m:properties></content></entry>"
19
-
20
- begin
21
- execute :post, 'Tables', {}, default_headers, payload
22
- return {:name => table_name, :url => "#{self.base_url}/Tables('#{table_name}"}
23
- rescue RestClient::RequestFailed
24
- raise WAZ::Tables::TableAlreadyExists, table_name if $!.http_code == 409
25
- end
26
- end
27
-
28
- # Delete a table on the current Windows Azure Storage account.
29
- def delete_table(table_name)
30
- raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
31
- begin
32
- execute :delete, "Tables('#{table_name}')", {}, default_headers
33
- rescue RestClient::ResourceNotFound
34
- raise WAZ::Tables::TableDoesNotExist, table_name if $!.http_code == 404
35
- end
36
- end
37
-
38
- # Lists all existing tables on the current storage account.
39
- # remove Content-Type if it's not working
40
- def list_tables(next_table_name = nil)
41
- query = { 'NextTableName' => next_table_name } unless next_table_name.nil?
42
- content = execute :get, "Tables", query ||= {}, default_headers
43
-
44
- doc = REXML::Document.new(content)
45
- tables = REXML::XPath.each(doc, '/feed/entry').map do |item|
46
- { :name => REXML::XPath.first(item.elements['content'], "m:properties/d:TableName", {"m" => DATASERVICES_METADATA_NAMESPACE, "d" => DATASERVICES_NAMESPACE}).text,
47
- :url => REXML::XPath.first(item, "id").text }
48
- end
49
-
50
- return tables, content.headers[:x_ms_continuation_nexttablename]
51
- end
52
-
53
- # Retrieves an existing table on the current storage account.
54
- # remove Content-Type if it's not working
55
- def get_table(table_name)
56
- raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
57
-
58
- begin
59
- content = execute :get, "Tables('#{table_name}')", {}, default_headers
60
- doc = REXML::Document.new(content)
61
- item = REXML::XPath.first(doc, "entry")
62
- return { :name => REXML::XPath.first(item.elements['content'], "m:properties/d:TableName", {"m" => DATASERVICES_METADATA_NAMESPACE, "d" => DATASERVICES_NAMESPACE}).text,
63
- :url => REXML::XPath.first(item, "id").text }
64
- rescue RestClient::ResourceNotFound
65
- raise WAZ::Tables::TableDoesNotExist, table_name if $!.http_code == 404
66
- end
67
- end
68
-
69
- # Insert a new entity on the provided table for the current storage account
70
- # TODO: catch all api errors as described on Table Service Error Codes on MSDN (http://msdn.microsoft.com/en-us/library/dd179438.aspx)
71
- def insert_entity(table_name, entity)
72
- raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
73
- raise WAZ::Tables::TooManyProperties, entity.length if entity.length > 252
74
-
75
- begin
76
- response = execute(:post, table_name, {}, default_headers, generate_payload(table_name, entity))
77
- return parse_response(response)
78
- rescue RestClient::RequestFailed
79
- raise WAZ::Tables::EntityAlreadyExists, entity[:row_key] if $!.http_code == 409 and $!.response.body.include?('EntityAlreadyExists')
80
- end
81
- end
82
-
83
- # Update an existing entity on the current storage account.
84
- # TODO: handle specific errors
85
- def update_entity(table_name, entity)
86
- raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
87
- response = execute(:put, "#{table_name}(PartitionKey='#{entity[:partition_key]}',RowKey='#{entity[:row_key]}')", {}, default_headers.merge({'If-Match' => '*'}) , generate_payload(table_name, entity))
88
- return parse_response(response)
89
- end
90
-
91
- # Merge an existing entity on the current storage account.
92
- # The Merge Entity operation updates an existing entity by updating the entity's properties.
93
- # This operation does not replace the existing entity, as the Update Entity operation does
94
- # TODO: handle specific errors
95
- def merge_entity(table_name, entity)
96
- raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
97
- response = execute(:merge, "#{table_name}(PartitionKey='#{entity[:partition_key]}',RowKey='#{entity[:row_key]}')", {}, default_headers.merge({'If-Match' => '*'}), generate_payload(table_name, entity))
98
- return parse_response(response)
99
- end
100
-
101
- # Delete an existing entity in a table.
102
- def delete_entity(table_name, partition_key, row_key)
103
- raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
104
-
105
- begin
106
- execute :delete, "#{table_name}(PartitionKey='#{partition_key}',RowKey='#{row_key}')", {}, default_headers.merge({'If-Match' => '*'})
107
- rescue RestClient::ResourceNotFound
108
- raise WAZ::Tables::TableDoesNotExist, table_name if $!.http_code == 404 and $!.response.body.include?('TableNotFound')
109
- raise WAZ::Tables::EntityDoesNotExist, "(PartitionKey='#{partition_key}',RowKey='#{row_key}')" if $!.http_code == 404
110
- end
111
- end
112
-
113
- # Retrieves an existing entity on the current storage account.
114
- # TODO: handle specific errors
115
- def get_entity(table_name, partition_key, row_key)
116
- raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
117
- response = execute(:get, "#{table_name}(PartitionKey='#{partition_key}',RowKey='#{row_key}')", {}, default_headers)
118
- return parse_response(response)
119
- end
120
-
121
- # Retrieves a set of entities on the current storage account for a given query.
122
- # When the :top => n is passed it returns only the first n rows that match with the query
123
- # Optional parameters:
124
- # * :headers a hash containing the request headers
125
- # * :expression the filter query that will be executed against the table (see http://msdn.microsoft.com/en-us/library/dd179421.aspx for more information),
126
- # * :top limits the amount of fields for this query.
127
- # * :continuation_token the hash obtained when you perform a query that has more than 1000 records or exceeds the allowed timeout (see http://msdn.microsoft.com/en-us/library/dd135718.aspx)
128
- def query(table_name, options = {})
129
- raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
130
- query = {'$filter' => (options[:expression] or '') }
131
- query.merge!({ '$top' => options[:top] }) unless options[:top].nil?
132
- query.merge!(options[:continuation_token]) unless options[:continuation_token].nil?
133
- response = execute :get, "#{table_name}()", query, default_headers
134
- continuation_token = {'NextPartitionKey' => response.headers[:x_ms_continuation_nextpartitionkey], 'NextRowKey' => response.headers[:x_ms_continuation_nextrowkey]}
135
- parse_response(response, continuation_token)
136
- end
137
-
138
- private
139
- def generate_payload(table_name, entity)
140
- payload = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" \
141
- "<entry xmlns:d=\"#{DATASERVICES_NAMESPACE}\" xmlns:m=\"#{DATASERVICES_METADATA_NAMESPACE}\" xmlns=\"http://www.w3.org/2005/Atom\">" \
142
- "<id>#{generate_request_uri "#{table_name}"}(PartitionKey='#{REXML::Text.new(entity[:partition_key], false, nil, false).to_s}',RowKey='#{REXML::Text.new(entity[:row_key], false, nil, false).to_s}')</id>" \
143
- "<title /><updated>#{Time.now.utc.iso8601}</updated><author><name /></author><link rel=\"edit\" title=\"#{table_name}\" href=\"#{table_name}(PartitionKey='#{REXML::Text.new(entity[:partition_key], false, nil, false).to_s}',RowKey='#{REXML::Text.new(entity[:row_key], false, nil, false).to_s}')\" />" \
144
- "<content type=\"application/xml\"><m:properties>"
145
-
146
- entity.sort_by { |k| k.to_s }.each do |k,v|
147
- value, type = EdmTypeHelper.parse_to(v)[0].to_s, EdmTypeHelper.parse_to(v)[1].to_s
148
- payload << (!v.nil? ? "<d:#{k.to_s} m:type=\"#{k.edm_type || type}\">#{REXML::Text.new(value, false, nil, false).to_s}</d:#{k.to_s}>" : "<d:#{k.to_s} m:type=\"#{k.edm_type || type}\" m:null=\"true\" />") unless k.eql?(:partition_key) or k.eql?(:row_key)
149
- end
150
-
151
- payload << "<d:PartitionKey>#{REXML::Text.new(entity[:partition_key], false, nil, false).to_s}</d:PartitionKey>" \
152
- "<d:RowKey>#{REXML::Text.new(entity[:row_key], false, nil, false).to_s}</d:RowKey>" \
153
- "</m:properties></content></entry>"
154
- return payload
155
- end
156
-
157
- def parse_response(response, continuation_token = nil)
158
- doc = REXML::Document.new(response)
159
- entities = REXML::XPath.each(doc, '//entry').map do |entry|
160
- fields = REXML::XPath.each(entry.elements['content'], 'm:properties/*', {"m" => DATASERVICES_METADATA_NAMESPACE}).map do |f|
161
- { f.name.gsub(/PartitionKey/i, 'partition_key').gsub(/RowKey/i, 'row_key').to_sym => EdmTypeHelper.parse_from(f) }
162
- end
163
- Hash[*fields.collect {|h| h.to_a}.flatten]
164
- end
165
- entities = WAZ::Tables::TableArray.new(entities)
166
- entities.continuation_token = continuation_token
167
- return (REXML::XPath.first(doc, '/feed')) ? entities : entities.first
168
- end
169
-
170
- def default_headers
171
- { 'Date' => Time.new.httpdate,
172
- 'Content-Type' => 'application/atom+xml',
173
- 'DataServiceVersion' => '1.0;NetFx',
174
- 'MaxDataServiceVersion' => '1.0;NetFx' }
175
- end
176
- end
177
- end
178
- end
1
+ module WAZ
2
+ module Tables
3
+ # This is internally used by the waz-tables part of the gem and it exposes the Windows Azure Blob API REST methods
4
+ # implementation. You can use this class to perform an specific operation that isn't provided by the current API.
5
+ class Service
6
+ include WAZ::Storage::SharedKeyCoreService
7
+
8
+ DATASERVICES_NAMESPACE = "http://schemas.microsoft.com/ado/2007/08/dataservices"
9
+ DATASERVICES_METADATA_NAMESPACE = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
10
+
11
+ # Creates a table on the current Windows Azure Storage account.
12
+ def create_table(table_name)
13
+ raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
14
+
15
+ payload = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" \
16
+ "<entry xmlns:d=\"#{DATASERVICES_NAMESPACE}\" xmlns:m=\"#{DATASERVICES_METADATA_NAMESPACE}\" xmlns=\"http://www.w3.org/2005/Atom\">" \
17
+ "<title /><updated>#{Time.now.utc.iso8601}</updated><author><name/></author><id/>" \
18
+ "<content type=\"application/xml\"><m:properties><d:TableName>#{table_name}</d:TableName></m:properties></content></entry>"
19
+
20
+ begin
21
+ execute :post, 'Tables', {}, default_headers, payload
22
+ return {:name => table_name, :url => "#{self.base_url}/Tables('#{table_name}"}
23
+ rescue RestClient::RequestFailed
24
+ raise WAZ::Tables::TableAlreadyExists, table_name if $!.http_code == 409
25
+ end
26
+ end
27
+
28
+ # Delete a table on the current Windows Azure Storage account.
29
+ def delete_table(table_name)
30
+ raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
31
+ begin
32
+ execute :delete, "Tables('#{table_name}')", {}, default_headers
33
+ rescue RestClient::ResourceNotFound
34
+ raise WAZ::Tables::TableDoesNotExist, table_name if $!.http_code == 404
35
+ end
36
+ end
37
+
38
+ # Lists all existing tables on the current storage account.
39
+ # remove Content-Type if it's not working
40
+ def list_tables(next_table_name = nil)
41
+ query = { 'NextTableName' => next_table_name } unless next_table_name.nil?
42
+ content = execute :get, "Tables", query ||= {}, default_headers
43
+
44
+ doc = REXML::Document.new(content)
45
+ tables = REXML::XPath.each(doc, '/feed/entry').map do |item|
46
+ { :name => REXML::XPath.first(item.elements['content'], "m:properties/d:TableName", {"m" => DATASERVICES_METADATA_NAMESPACE, "d" => DATASERVICES_NAMESPACE}).text,
47
+ :url => REXML::XPath.first(item, "id").text }
48
+ end
49
+
50
+ return tables, content.headers[:x_ms_continuation_nexttablename]
51
+ end
52
+
53
+ # Retrieves an existing table on the current storage account.
54
+ # remove Content-Type if it's not working
55
+ def get_table(table_name)
56
+ raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
57
+
58
+ begin
59
+ content = execute :get, "Tables('#{table_name}')", {}, default_headers
60
+ doc = REXML::Document.new(content)
61
+ item = REXML::XPath.first(doc, "entry")
62
+ return { :name => REXML::XPath.first(item.elements['content'], "m:properties/d:TableName", {"m" => DATASERVICES_METADATA_NAMESPACE, "d" => DATASERVICES_NAMESPACE}).text,
63
+ :url => REXML::XPath.first(item, "id").text }
64
+ rescue RestClient::ResourceNotFound
65
+ raise WAZ::Tables::TableDoesNotExist, table_name if $!.http_code == 404
66
+ end
67
+ end
68
+
69
+ # Insert a new entity on the provided table for the current storage account
70
+ # TODO: catch all api errors as described on Table Service Error Codes on MSDN (http://msdn.microsoft.com/en-us/library/dd179438.aspx)
71
+ def insert_entity(table_name, entity)
72
+ raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
73
+ raise WAZ::Tables::TooManyProperties, entity.length if entity.length > 252
74
+
75
+ begin
76
+ response = execute(:post, table_name, {}, default_headers, generate_payload(table_name, entity))
77
+ return parse_response(response)
78
+ rescue RestClient::RequestFailed
79
+ raise WAZ::Tables::EntityAlreadyExists, entity[:row_key] if $!.http_code == 409 and $!.response.body.include?('EntityAlreadyExists')
80
+ end
81
+ end
82
+
83
+ # Update an existing entity on the current storage account.
84
+ # TODO: handle specific errors
85
+ def update_entity(table_name, entity)
86
+ raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
87
+ response = execute(:put, "#{table_name}(PartitionKey='#{entity[:partition_key]}',RowKey='#{entity[:row_key]}')", {}, default_headers.merge({'If-Match' => '*'}) , generate_payload(table_name, entity))
88
+ return parse_response(response)
89
+ end
90
+
91
+ # Merge an existing entity on the current storage account.
92
+ # The Merge Entity operation updates an existing entity by updating the entity's properties.
93
+ # This operation does not replace the existing entity, as the Update Entity operation does
94
+ # TODO: handle specific errors
95
+ def merge_entity(table_name, entity)
96
+ raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
97
+ response = execute(:merge, "#{table_name}(PartitionKey='#{entity[:partition_key]}',RowKey='#{entity[:row_key]}')", {}, default_headers.merge({'If-Match' => '*'}), generate_payload(table_name, entity))
98
+ return parse_response(response)
99
+ end
100
+
101
+ # Delete an existing entity in a table.
102
+ def delete_entity(table_name, partition_key, row_key)
103
+ raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
104
+
105
+ begin
106
+ execute :delete, "#{table_name}(PartitionKey='#{partition_key}',RowKey='#{row_key}')", {}, default_headers.merge({'If-Match' => '*'})
107
+ rescue RestClient::ResourceNotFound
108
+ raise WAZ::Tables::TableDoesNotExist, table_name if $!.http_code == 404 and $!.response.body.include?('TableNotFound')
109
+ raise WAZ::Tables::EntityDoesNotExist, "(PartitionKey='#{partition_key}',RowKey='#{row_key}')" if $!.http_code == 404
110
+ end
111
+ end
112
+
113
+ # Retrieves an existing entity on the current storage account.
114
+ # TODO: handle specific errors
115
+ def get_entity(table_name, partition_key, row_key)
116
+ raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
117
+ response = execute(:get, "#{table_name}(PartitionKey='#{partition_key}',RowKey='#{row_key}')", {}, default_headers)
118
+ return parse_response(response)
119
+ end
120
+
121
+ # Retrieves a set of entities on the current storage account for a given query.
122
+ # When the :top => n is passed it returns only the first n rows that match with the query
123
+ # Optional parameters:
124
+ # * :headers a hash containing the request headers
125
+ # * :expression the filter query that will be executed against the table (see http://msdn.microsoft.com/en-us/library/dd179421.aspx for more information),
126
+ # * :top limits the amount of fields for this query.
127
+ # * :continuation_token the hash obtained when you perform a query that has more than 1000 records or exceeds the allowed timeout (see http://msdn.microsoft.com/en-us/library/dd135718.aspx)
128
+ def query(table_name, options = {})
129
+ raise WAZ::Tables::InvalidTableName, table_name unless WAZ::Storage::ValidationRules.valid_table_name?(table_name)
130
+ query = {'$filter' => (options[:expression] or '') }
131
+ query.merge!({ '$top' => options[:top] }) unless options[:top].nil?
132
+ query.merge!(options[:continuation_token]) unless options[:continuation_token].nil?
133
+ response = execute :get, "#{table_name}()", query, default_headers
134
+ continuation_token = {'NextPartitionKey' => response.headers[:x_ms_continuation_nextpartitionkey], 'NextRowKey' => response.headers[:x_ms_continuation_nextrowkey]}
135
+ parse_response(response, continuation_token)
136
+ end
137
+
138
+ private
139
+ def generate_payload(table_name, entity)
140
+ payload = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" \
141
+ "<entry xmlns:d=\"#{DATASERVICES_NAMESPACE}\" xmlns:m=\"#{DATASERVICES_METADATA_NAMESPACE}\" xmlns=\"http://www.w3.org/2005/Atom\">" \
142
+ "<id>#{generate_request_uri "#{table_name}"}(PartitionKey='#{REXML::Text.new(entity[:partition_key], false, nil, false).to_s}',RowKey='#{REXML::Text.new(entity[:row_key], false, nil, false).to_s}')</id>" \
143
+ "<title /><updated>#{Time.now.utc.iso8601}</updated><author><name /></author><link rel=\"edit\" title=\"#{table_name}\" href=\"#{table_name}(PartitionKey='#{REXML::Text.new(entity[:partition_key], false, nil, false).to_s}',RowKey='#{REXML::Text.new(entity[:row_key], false, nil, false).to_s}')\" />" \
144
+ "<content type=\"application/xml\"><m:properties>"
145
+
146
+ entity.sort_by { |k| k.to_s }.each do |k,v|
147
+ value, type = EdmTypeHelper.parse_to(v)[0].to_s, EdmTypeHelper.parse_to(v)[1].to_s
148
+ payload << (!v.nil? ? "<d:#{k.to_s} m:type=\"#{k.edm_type || type}\">#{REXML::Text.new(value, false, nil, false).to_s}</d:#{k.to_s}>" : "<d:#{k.to_s} m:type=\"#{k.edm_type || type}\" m:null=\"true\" />") unless k.eql?(:partition_key) or k.eql?(:row_key)
149
+ end
150
+
151
+ payload << "<d:PartitionKey>#{REXML::Text.new(entity[:partition_key], false, nil, false).to_s}</d:PartitionKey>" \
152
+ "<d:RowKey>#{REXML::Text.new(entity[:row_key], false, nil, false).to_s}</d:RowKey>" \
153
+ "</m:properties></content></entry>"
154
+ return payload
155
+ end
156
+
157
+ def parse_response(response, continuation_token = nil)
158
+ doc = REXML::Document.new(response)
159
+ entities = REXML::XPath.each(doc, '//entry').map do |entry|
160
+ fields = REXML::XPath.each(entry.elements['content'], 'm:properties/*', {"m" => DATASERVICES_METADATA_NAMESPACE}).map do |f|
161
+ { f.name.gsub(/PartitionKey/i, 'partition_key').gsub(/RowKey/i, 'row_key').to_sym => EdmTypeHelper.parse_from(f) }
162
+ end
163
+ Hash[*fields.collect {|h| h.to_a}.flatten]
164
+ end
165
+ entities = WAZ::Tables::TableArray.new(entities)
166
+ entities.continuation_token = continuation_token
167
+ return (REXML::XPath.first(doc, '/feed')) ? entities : entities.first
168
+ end
169
+
170
+ def default_headers
171
+ { 'Date' => Time.new.httpdate,
172
+ 'Content-Type' => 'application/atom+xml',
173
+ 'DataServiceVersion' => '1.0;NetFx',
174
+ 'MaxDataServiceVersion' => '1.0;NetFx' }
175
+ end
176
+ end
177
+ end
178
+ end