storage_room 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/History.txt +6 -0
  2. data/README.rdoc +10 -4
  3. data/Rakefile +3 -1
  4. data/TODO +7 -2
  5. data/VERSION +1 -1
  6. data/examples/authentication.rb +5 -3
  7. data/examples/create_entry.rb +8 -5
  8. data/examples/destroy_entry.rb +2 -2
  9. data/examples/get_collections.rb +2 -2
  10. data/examples/import_csv.rb +4 -4
  11. data/examples/search_entries.rb +2 -2
  12. data/examples/update_entry.rb +2 -2
  13. data/lib/console.rb +12 -0
  14. data/lib/storage_room.rb +73 -34
  15. data/lib/storage_room/accessors.rb +190 -0
  16. data/lib/storage_room/array.rb +3 -23
  17. data/lib/storage_room/embedded.rb +1 -1
  18. data/lib/storage_room/embeddeds/field.rb +28 -0
  19. data/lib/storage_room/embeddeds/fields/association_field.rb +17 -0
  20. data/lib/storage_room/embeddeds/fields/associations/many_association_field.rb +9 -0
  21. data/lib/storage_room/embeddeds/fields/associations/one_association_field.rb +9 -0
  22. data/lib/storage_room/embeddeds/fields/atomic/boolean_field.rb +6 -0
  23. data/lib/storage_room/embeddeds/fields/atomic/date_field.rb +6 -0
  24. data/lib/storage_room/embeddeds/fields/atomic/float_field.rb +6 -0
  25. data/lib/storage_room/embeddeds/fields/atomic/integer_field.rb +6 -0
  26. data/lib/storage_room/embeddeds/fields/atomic/string_field.rb +6 -0
  27. data/lib/storage_room/embeddeds/fields/atomic/time_field.rb +6 -0
  28. data/lib/storage_room/embeddeds/fields/atomic_field.rb +15 -0
  29. data/lib/storage_room/embeddeds/fields/compound/attachment_field.rb +6 -0
  30. data/lib/storage_room/embeddeds/fields/compound/file_field.rb +6 -0
  31. data/lib/storage_room/embeddeds/fields/compound/image_field.rb +6 -0
  32. data/lib/storage_room/embeddeds/fields/compound/location_field.rb +6 -0
  33. data/lib/storage_room/embeddeds/fields/compound_field.rb +10 -0
  34. data/lib/storage_room/embeddeds/file.rb +1 -1
  35. data/lib/storage_room/embeddeds/image.rb +6 -0
  36. data/lib/storage_room/embeddeds/location.rb +2 -1
  37. data/lib/storage_room/extensions/const_defined.rb +12 -0
  38. data/lib/storage_room/identity_map.rb +117 -0
  39. data/lib/storage_room/model.rb +19 -23
  40. data/lib/storage_room/models/collection.rb +76 -11
  41. data/lib/storage_room/models/entry.rb +6 -32
  42. data/lib/storage_room/plugins.rb +22 -0
  43. data/lib/storage_room/proxy.rb +49 -0
  44. data/lib/storage_room/{base.rb → resource.rb} +13 -36
  45. data/spec/fixtures/collection.json +0 -16
  46. data/spec/spec_helper.rb +1 -0
  47. data/spec/storage_room/accessors_spec.rb +107 -0
  48. data/spec/storage_room/array_spec.rb +13 -9
  49. data/spec/storage_room/embedded_spec.rb +5 -1
  50. data/spec/storage_room/embeddeds/field_spec.rb +25 -0
  51. data/spec/storage_room/embeddeds/fields/association_field_spec.rb +29 -0
  52. data/spec/storage_room/embeddeds/fields/associations/many_association_field_spec.rb +21 -0
  53. data/spec/storage_room/embeddeds/fields/associations/one_association_field_spec.rb +19 -0
  54. data/spec/storage_room/embeddeds/fields/atomic/boolean_field_spec.rb +5 -0
  55. data/spec/storage_room/embeddeds/fields/atomic/date_field_spec.rb +5 -0
  56. data/spec/storage_room/embeddeds/fields/atomic/float_field_spec.rb +5 -0
  57. data/spec/storage_room/embeddeds/fields/atomic/integer_field_spec.rb +5 -0
  58. data/spec/storage_room/embeddeds/fields/atomic/string_field_spec.rb +5 -0
  59. data/spec/storage_room/embeddeds/fields/atomic/time_field_spec.rb +5 -0
  60. data/spec/storage_room/embeddeds/fields/atomic_field_spec.rb +27 -0
  61. data/spec/storage_room/embeddeds/fields/compound/attachment_field_spec.rb +5 -0
  62. data/spec/storage_room/embeddeds/fields/compound/file_field_spec.rb +5 -0
  63. data/spec/storage_room/embeddeds/fields/compound/image_field_spec.rb +5 -0
  64. data/spec/storage_room/embeddeds/fields/compound/location_field_spec.rb +5 -0
  65. data/spec/storage_room/embeddeds/fields/compound_field_spec.rb +17 -0
  66. data/spec/storage_room/embeddeds/location_spec.rb +13 -1
  67. data/spec/storage_room/identity_map_spec.rb +53 -0
  68. data/spec/storage_room/model_spec.rb +70 -50
  69. data/spec/storage_room/models/collection_spec.rb +57 -14
  70. data/spec/storage_room/models/entry_spec.rb +16 -20
  71. data/spec/storage_room/proxy_spec.rb +58 -0
  72. data/spec/storage_room/resource_spec.rb +98 -0
  73. data/spec/storage_room_spec.rb +45 -9
  74. data/storage_room.gemspec +48 -9
  75. data/tasks/storage_room.rake +3 -0
  76. metadata +49 -10
  77. data/lib/storage_room/attributes.rb +0 -46
  78. data/lib/storage_room/field.rb +0 -8
  79. data/spec/storage_room/attributes_spec.rb +0 -82
  80. data/spec/storage_room/base_spec.rb +0 -118
  81. data/spec/storage_room/field_spec.rb +0 -5
@@ -1,34 +1,99 @@
1
1
  module StorageRoom
2
2
  # A collection is used to define the structure of a data set.
3
- class Collection < Model
3
+ class Collection < Model
4
+ key :name
5
+ key :primary_field_identifier
6
+
7
+ many :fields
8
+
4
9
  class << self
5
10
  def index_path # :nodoc:
6
- '/collections'
11
+ "#{Resource.base_uri}/collections"
7
12
  end
8
13
 
9
14
  def show_path(collection_id) # :nodoc:
10
15
  "#{index_path}/#{collection_id}"
11
16
  end
12
-
13
- def entries_path(collection_id) # :nodoc:
14
- "#{show_path(collection_id)}/entries"
15
- end
16
-
17
+
17
18
  def json_name # :nodoc:
18
19
  'collection'
19
20
  end
20
21
  end
21
22
 
23
+ # The class name of the collection's entries, can be overridden with a mapping
24
+ def entry_class_name
25
+ ensure_loaded { StorageRoom.entry_class_for_name(name)}
26
+ end
27
+
28
+ # The class for the collection's entries
29
+ def entry_class
30
+ require_initialized_entry_class do
31
+ self.entry_class_name.constantize
32
+ end
33
+ end
34
+
22
35
  # Load all the entries of a collection
23
36
  def entries
24
- Array.load(self[:@entries_url])
37
+ require_initialized_entry_class do
38
+ Array.load(self[:@entries_url])
39
+ end
25
40
  end
26
41
 
27
- # The class of the collection's objects
28
- def entry_class
29
- StorageRoom::Entry.class_with_options(self[:name].classify, :collection_path => self.url)
42
+ # The field with a specific identifier
43
+ def field(identifier)
44
+ ensure_loaded do
45
+ fields.each do |f|
46
+ return f if f.identifier == identifier
47
+ end
48
+ end
49
+
50
+ nil
30
51
  end
52
+
53
+ protected
54
+ def initialize_from_response_data
55
+ super
56
+ require_initialized_entry_class
57
+ end
58
+
59
+ def initialize_entry_class
60
+ name = self.entry_class_name
61
+
62
+ klass = if Object.is_constant_defined?(name.to_sym)
63
+ name.constantize
64
+ else
65
+ klass = Class.new(Entry)
66
+ Object.const_set(name, klass)
67
+ klass
68
+ end
31
69
 
70
+ klass.collection = self
71
+
72
+ fields.each do |field|
73
+ field.add_to_entry_class(klass)
74
+ end
75
+
76
+ true
77
+ end
32
78
 
79
+ def deconstruct_entry_class
80
+ if Object.is_const_defined?(self.entry_class_name.to_sym)
81
+ Object.send(:remove_const, self.entry_class_name)
82
+ end
83
+
84
+ true
85
+ end
86
+
87
+ def recreate_entry_class
88
+ self.deconstruct_entry_class
89
+ self.initialize_entry_class
90
+ end
91
+
92
+ def require_initialized_entry_class
93
+ ensure_loaded do
94
+ self.initialize_entry_class
95
+ yield if block_given?
96
+ end
97
+ end
33
98
  end
34
99
  end
@@ -1,20 +1,10 @@
1
1
  module StorageRoom
2
2
  class Entry < Model
3
- class_inheritable_accessor :collection_path
4
-
5
- class << self
6
- def class_with_options(name, options = {})
7
- # TODO_SK: check options
8
-
9
- klass = StorageRoom.class_for_name(name)
10
-
11
- klass.collection_path = options[:collection_path]
3
+ class_inheritable_accessor :collection
12
4
 
13
- klass
14
- end
15
-
16
- def index_path
17
- "#{collection_path}/entries"
5
+ class << self
6
+ def index_path # :nodoc:
7
+ "#{collection[:@url]}/entries"
18
8
  end
19
9
 
20
10
  def show_path(entry_id) # :nodoc:
@@ -35,26 +25,10 @@ module StorageRoom
35
25
  end
36
26
  end
37
27
 
38
- # Sets a entry with a hash from the API.
39
- def set_from_api(attributes)
40
- super(attributes)
41
-
42
- self.attributes.each do |k, v|
43
- if v.is_a?(Hash) && v[:@type].present?
44
- object = StorageRoom.class_for_name(v[:@type]).new
45
- object.set_from_api(v)
46
- self.attributes[k] = object
47
- end
48
- end
49
-
50
- self.attributes
51
- end
52
-
53
28
  # The collection of a entry
54
29
  def collection
55
- Collection.load(self[:@collection_url] || self.class.collection_path)
56
- end
57
-
30
+ self.class.collection
31
+ end
58
32
 
59
33
  end
60
34
  end
@@ -0,0 +1,22 @@
1
+ # encoding: UTF-8
2
+
3
+ module StorageRoom
4
+ module Plugins
5
+ include ActiveSupport::DescendantsTracker
6
+
7
+ def plugins
8
+ @plugins ||= []
9
+ end
10
+
11
+ def plugin(mod)
12
+ include mod
13
+ direct_descendants.each {|model| model.send(:include, mod) }
14
+ plugins << mod
15
+ end
16
+
17
+ def included(base = nil, &block)
18
+ direct_descendants << base if base
19
+ super
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,49 @@
1
+ module StorageRoom
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
+ METHODS_WITHOUT_RELOAD = [:reload, :response_data, :loaded?, :class, :set_from_response_data, :present?, :is_a?, :respond_to?, :as_json, :to_hash] # TODO: figure out what other methods might be needed?
8
+
9
+ instance_methods.each { |m| undef_method m unless m =~ /^(__|instance_eval|object_id)/ } # http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc
10
+
11
+ def initialize(object, parameters={})
12
+ @object = object
13
+ @parameters = parameters
14
+ end
15
+
16
+ def proxy?
17
+ true
18
+ end
19
+
20
+ def inspect
21
+ if @object.loaded?
22
+ @object.inspect
23
+ else
24
+ "#<#{@object.class} (proxied)>"
25
+ end
26
+ end
27
+
28
+ def to_s
29
+ inspect
30
+ end
31
+
32
+ def _object
33
+ @object
34
+ end
35
+
36
+ # Forward all method calls to the proxied object, reload if necessary
37
+ def method_missing(method_name, *args, &block)
38
+ if @object.loaded? || METHODS_WITHOUT_RELOAD.include?(method_name)
39
+ # no need to reload
40
+ else
41
+ StorageRoom.log("Reloading #{@object['url']} due to #{method_name}")
42
+ @object.reload(@object['url'], @parameters)
43
+ end
44
+
45
+ @object.send(method_name, *args, &block)
46
+ end
47
+ end
48
+
49
+ end
@@ -1,22 +1,17 @@
1
1
  module StorageRoom
2
2
  # The superclass of all the classes that load data from the API
3
- class Base
4
- include Attributes
3
+ class Resource
5
4
  include HTTParty
5
+ extend Plugins
6
+
7
+ include Accessors
8
+
9
+ plugin IdentityMap
6
10
 
7
11
  headers 'User-Agent' => 'StorageRoom Ruby Gem', 'Accept' => 'application/json', 'Content-Type' => 'application/json'
8
12
  format :json
9
13
 
10
-
11
- class << self
12
- # Load an object with the specified URL from the API
13
- def load(url, parameters = {})
14
- httparty = get(url, parameters)
15
-
16
- handle_critical_response_errors(httparty)
17
- create_from_api(httparty.parsed_response.first[1])
18
- end
19
-
14
+ class << self
20
15
  # Handle known server errors
21
16
  def handle_critical_response_errors(httparty) # :nodoc:
22
17
  case httparty.response.code
@@ -26,21 +21,6 @@ module StorageRoom
26
21
  end
27
22
  end
28
23
 
29
- # Creates a new object with a hash from the API with a @type attribute
30
- def create_from_api(hash) # :nodoc:
31
- type = hash['@type']
32
-
33
- object = case type
34
- when 'Array' then Array.new
35
- when 'Collection' then Collection.new
36
- else # entry
37
- StorageRoom.class_for_name(type.classify).new
38
- end
39
-
40
- object.set_from_api(hash)
41
- object
42
- end
43
-
44
24
  # Find out if a key is user-defined or meta-data (begins with @)
45
25
  def meta_data?(key)
46
26
  key[0...1] == '@'
@@ -50,18 +30,15 @@ module StorageRoom
50
30
  # Reload an object from the API. Optionally pass an URL.
51
31
  def reload(url = nil, parameters = {})
52
32
  httparty = self.class.get(url || self[:@url], parameters)
53
- set_from_api(httparty.parsed_response.first[1])
33
+ set_from_response_data(httparty.parsed_response.first[1])
54
34
  true
55
35
  end
56
-
57
- # Returns the remote URL of the object
58
- def url
59
- self[:@url]
60
- end
61
36
 
62
- def version
63
- self[:@version]
37
+ # Has the Resource been loaded from the API?
38
+ def loaded?
39
+ self['@url'] ? true : false
64
40
  end
65
-
41
+
42
+
66
43
  end
67
44
  end
@@ -15,22 +15,6 @@
15
15
  "hint": "",
16
16
  "choices": [],
17
17
  "identifier": "title"
18
- },
19
- {
20
- "name": "PDF",
21
- "required": false,
22
- "input_type": "file",
23
- "@type": "FileField",
24
- "hint": "Upload a PDF file",
25
- "identifier": "pdf"
26
- },
27
- {
28
- "name": "location",
29
- "required": false,
30
- "input_type": "location",
31
- "@type": "LocationField",
32
- "hint": "",
33
- "identifier": "location"
34
18
  }],
35
19
  "@type": "Collection",
36
20
  "@url": "http://api.storageroomapp.com/accounts/4c8fd48542507175aa00002f/collections/4ddaf68b4d085d374a000003"
data/spec/spec_helper.rb CHANGED
@@ -10,6 +10,7 @@ RSpec.configure do |config|
10
10
 
11
11
  config.before(:each) do
12
12
  StorageRoom.authenticate('USER_ID', 'APPLICATION_API_KEY')
13
+ StorageRoom::IdentityMap.clear
13
14
  end
14
15
  end
15
16
 
@@ -0,0 +1,107 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ module StorageRoom
4
+ class TestAccessors
5
+ include Accessors
6
+
7
+ key :test
8
+ key :test2
9
+ end
10
+ end
11
+
12
+ describe StorageRoom::TestAccessors do
13
+
14
+ context "Instance" do
15
+ before(:each) do
16
+ @test = StorageRoom::TestAccessors.new(:test => 1)
17
+ end
18
+
19
+ describe "#initialize" do
20
+ it "should set attributes" do
21
+ @test.test.should == 1
22
+ @test['test'].should be_nil
23
+ @test.test2.should be_nil
24
+ @test['test2'].should be_nil
25
+ end
26
+ end
27
+
28
+ describe "#set_from_response_data" do
29
+ before(:each) do
30
+ @test.set_from_response_data(:test2 => 3)
31
+ end
32
+
33
+ it "should reset attributes" do
34
+ @test.test.should be_nil
35
+ end
36
+
37
+ it "should set new attributes" do
38
+ @test['test2'].should == 3
39
+ @test.test2.should == 3
40
+ end
41
+ end
42
+
43
+ describe "#inspect" do
44
+ it "should output string" do
45
+ @test.inspect.should be_present
46
+ end
47
+ end
48
+
49
+ describe "#[]" do
50
+ it "should get attribute" do
51
+ @test.set_from_response_data(:test2 => 3)
52
+
53
+ @test[:test].should be_nil
54
+ @test[:test2].should == 3
55
+ end
56
+ end
57
+
58
+ describe "#attributes" do
59
+ it "should return existing attributes" do
60
+ @test.attributes[:test].should == 1
61
+ @test.attributes['test'].should == 1
62
+ @test.attributes['test2'].should be_nil
63
+ end
64
+ end
65
+
66
+ describe "#attributes=" do
67
+ before(:each) do
68
+ @test.attributes = {:test => 0}
69
+ end
70
+
71
+ it "should set attributes" do
72
+ @test.attributes[:test].should == 0
73
+ end
74
+ end
75
+
76
+ describe "#as_json" do
77
+ it "should return attributes as hash" do
78
+ hash = @test.as_json
79
+ hash.should be_an_instance_of(Hash)
80
+ hash['test'].should == 1
81
+ end
82
+ end
83
+
84
+ describe "#reset!" do
85
+ it "should reset" do
86
+ @test.response_data = {'test' => 1}
87
+ @test.reset!
88
+ @test.attributes.should == {}
89
+ @test.response_data.should == {}
90
+ end
91
+ end
92
+
93
+ describe "#loaded?" do
94
+ it "should return true" do
95
+ @test.response_data = {'test' => 1}
96
+ @test.should be_loaded
97
+ end
98
+
99
+ it "should return false" do
100
+ @test.reset!
101
+ @test.should_not be_loaded
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ end