animoto 0.1.1.beta1 → 1.0.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 (49) hide show
  1. data/README.md +8 -4
  2. data/lib/animoto.rb +1 -1
  3. data/lib/animoto/assets/base.rb +1 -1
  4. data/lib/animoto/assets/footage.rb +1 -1
  5. data/lib/animoto/assets/image.rb +1 -1
  6. data/lib/animoto/assets/song.rb +1 -1
  7. data/lib/animoto/assets/title_card.rb +1 -1
  8. data/lib/animoto/client.rb +21 -27
  9. data/lib/animoto/http_engines/base.rb +15 -7
  10. data/lib/animoto/http_engines/curl_adapter.rb +3 -4
  11. data/lib/animoto/http_engines/net_http_adapter.rb +3 -5
  12. data/lib/animoto/http_engines/patron_adapter.rb +2 -4
  13. data/lib/animoto/http_engines/rest_client_adapter.rb +1 -3
  14. data/lib/animoto/http_engines/typhoeus_adapter.rb +1 -3
  15. data/lib/animoto/manifests/base.rb +1 -1
  16. data/lib/animoto/manifests/directing.rb +1 -1
  17. data/lib/animoto/manifests/directing_and_rendering.rb +55 -28
  18. data/lib/animoto/manifests/rendering.rb +7 -6
  19. data/lib/animoto/resources/base.rb +8 -8
  20. data/lib/animoto/resources/jobs/base.rb +2 -2
  21. data/lib/animoto/resources/jobs/directing.rb +7 -2
  22. data/lib/animoto/resources/jobs/directing_and_rendering.rb +34 -8
  23. data/lib/animoto/resources/jobs/rendering.rb +17 -3
  24. data/lib/animoto/resources/storyboard.rb +20 -4
  25. data/lib/animoto/resources/video.rb +33 -11
  26. data/lib/animoto/response_parsers/base.rb +13 -8
  27. data/lib/animoto/response_parsers/json_adapter.rb +2 -4
  28. data/lib/animoto/response_parsers/yajl_adapter.rb +2 -4
  29. data/lib/animoto/support/content_type.rb +1 -0
  30. data/lib/animoto/support/coverable.rb +1 -1
  31. data/lib/animoto/support/dynamic_class_loader.rb +82 -141
  32. data/lib/animoto/support/errors.rb +4 -0
  33. data/lib/animoto/support/hash.rb +25 -0
  34. data/lib/animoto/support/standard_envelope.rb +70 -19
  35. data/lib/animoto/support/string.rb +31 -0
  36. data/lib/animoto/support/visual.rb +1 -1
  37. data/spec/animoto/client_spec.rb +1 -25
  38. data/spec/animoto/http_engines/base_spec.rb +1 -1
  39. data/spec/animoto/resources/base_spec.rb +8 -7
  40. data/spec/spec_helper.rb +1 -0
  41. metadata +12 -21
  42. data/lib/animoto/callbacks/base.rb +0 -45
  43. data/lib/animoto/callbacks/directing.rb +0 -7
  44. data/lib/animoto/callbacks/directing_and_rendering.rb +0 -7
  45. data/lib/animoto/callbacks/rendering.rb +0 -7
  46. data/spec/animoto/callbacks/base_spec.rb +0 -76
  47. data/spec/animoto/callbacks/directing_and_rendering_spec.rb +0 -5
  48. data/spec/animoto/callbacks/directing_spec.rb +0 -5
  49. data/spec/animoto/callbacks/rendering_spec.rb +0 -5
@@ -19,7 +19,7 @@ module Animoto
19
19
 
20
20
  # Returns a representation of this visual as a Hash.
21
21
  #
22
- # @return [Hash<String,Object>] this visual as a Hash
22
+ # @return [Hash{String=>Object}] this visual as a Hash
23
23
  def to_hash
24
24
  hash = super rescue {}
25
25
  hash['cover'] = cover? unless @cover.nil?
@@ -1,166 +1,107 @@
1
1
  module Animoto
2
2
  module Support
3
-
4
- # Sets the search path where classes to be dynamically loaded can be found.
5
- # This method is meant to be used in the following manner:
6
- # class Thing
7
- # extend Animoto::Support::DynamicClassLoader("some/search/path")
8
- # end
9
- # Behind the scenes, this method sets a temporary instance variable on the
10
- # DynamicClassLoader module which will be used (and then removed) the next time
11
- # the module is extended into a class. Therefore, don't call this method if you
12
- # don't intend to immediately extend the DynamicClassLoader module into the class
13
- # that the search path given applies to, else strange behaviors could emerge.
14
- #
15
- # @param [String] search_path the path where classes to be dynamically loaded are located
16
- # @return [Module] the DynamicClassLoader module
17
- def self.DynamicClassLoader search_path
18
- self::DynamicClassLoader.instance_variable_set(:@carryover_search_path, search_path)
19
- self::DynamicClassLoader
20
- end
21
-
22
- # This module is a helper for families of "plugins" which rely on external libraries.
23
- # It loads classes (and therefore their dependencies) on demand, obviating the need
24
- # to require every possible plugin dependency all the time or the need for the end-user
25
- # to explicitly require which certain plugin they want to use or know anything about the
26
- # specific directory structure where plugins are located.
27
- #
28
- # A class gets dynamically loaded in one of two ways: either a const_missing hook, or by
29
- # symbol-reference. Both methods interpolate the name of the file to load based on
30
- # their inputs.
31
- #
32
- # Referencing a class name under a module that is extended by the DynamicClassLoader will
33
- # search in the pre-set search path for a file matching the underscored version of the
34
- # class' name. For example, assuming the Thing module is extended with DynamicClassLoader
35
- # and its search path is "~/.things/plugins", referencing Thing::KlassName will look for
36
- # a file called "klass_name.rb" in "~/.things/plugins", returning the class object if it's
37
- # found or raising an exception if it isn't.
38
- #
39
- # Using a symbol reference is a little trickier. When a symbol is given to the [] method,
40
- # the name will be transformed (explained later) and a file in the search path with the
41
- # transformed name will be loaded, or an exception will be raised if the file isn't found.
42
- #
43
- # Name transformations are handled by three methods: symbol_reference_prefix,
44
- # symbol_reference_transform, and symbol_reference_prefix. As their names suggest,
45
- # symbol_reference_prefix and symbol_reference_suffix add the strings returned to the front
46
- # or end of the symbol name. symbol_reference_transform takes the string version of the symbol
47
- # and applies an arbitrary transform on it.
48
- #
49
- # After the file matching the transformed name is found, the class itself still has to
50
- # get loaded. While it's easy enough to take a class name and produce an associated file name,
51
- # the same is not true in reverse (for example, "HTTPEngine" => "http_engine" is straightforward,
52
- # but there's not enough information to turn "http_engine" into "HTTPEngine" without enforcing
53
- # class-naming restrictions). To get around this, any class that could get dynamically loaded
54
- # should add itself to its containing module's adapter map, using a key that matches the class'
55
- # file name.
56
3
  module DynamicClassLoader
57
-
58
- # When this module is extended into a class, it will set the search path where
59
- # dynamically loaded classes can be found if it was set using the DynamicClassLoader
60
- # method (see above). If not search path is set beforehand, it defaults to '.'.
4
+
5
+ # Retrieves the adapter class that the name corresponds to. The name in case- and
6
+ # punctuation-insensitive, meaning "SomeModule", "some_module", and "somemodule"
7
+ # are all treated as the same.
61
8
  #
62
- # @param [Class] base the class this module will be extended into
63
- # @return [void]
64
- def self.extended base
65
- if instance_variable_defined?(:@carryover_search_path)
66
- base.instance_variable_set(:@dynamic_class_loader_search_path, @carryover_search_path)
67
- remove_instance_variable(:@carryover_search_path)
9
+ # Note that classes defined as an #adapter for this module will be autoloaded upon
10
+ # successful reference, so there's no need to ensure that the file defining the class
11
+ # is already loaded when calling this method.
12
+ #
13
+ # @param [String] name the name of the module to access
14
+ # @return [Class] the class
15
+ # @raise [NameError] if the class doesn't exist.
16
+ def [] name
17
+ if klass = adapter_map[normalize_const_name(name)]
18
+ const_get(klass)
68
19
  else
69
- base.instance_variable_set(:@dynamic_class_loader_search_path, '.')
20
+ raise NameError, "uninitialized constant #{self.name}::#{name}"
70
21
  end
71
22
  end
72
-
73
- # If a reference is made to a class under this one that hasn't been initialized yet,
74
- # this will attempt to require a file with that class' name (underscored). If one is
75
- # found, and the file defines the class requested, will return that class object.
76
- #
77
- # @param [Symbol] const the uninitialized class' name
78
- # @return [Class] the class found
79
- # @raise [NameError] if the file defining the class isn't found, or if the file required
80
- # doesn't define the class.
81
- def const_missing const
82
- load_missing_file_named underscore_class_name(const.to_s)
83
- const_defined?(const) ? const_get(const) : super
84
- end
85
23
 
86
- # Returns the adapter class for the given symbol. If the file defining the class
87
- # hasn't been loaded, will try to load it.
24
+ private
25
+
26
+ # Gets or sets the path where this module will look for files it autoloads.
88
27
  #
89
- # @param [Symbol] engine the symbolic name of the adapter
90
- # @return [Class] the class
91
- # @raise [NameError] if the class isn't found
92
- def [] engine
93
- load_missing_file_named "#{symbol_reference_prefix}#{symbol_reference_transform(engine.to_s)}#{symbol_reference_suffix}"
94
- adapter_map[engine]
28
+ # @param [String] path the path; if path is nil, returns the old path
29
+ # @return [String] the path
30
+ def dynamic_class_path path = nil
31
+ @dynamic_class_path = path if path
32
+ @dynamic_class_path
95
33
  end
96
34
 
97
- private
98
-
99
- # When a class is loaded by symbol reference, the return value of this method
100
- # will be prepended to the symbol to form the file name. Defaults to a blank
101
- # string.
35
+ # A map of normalized class names to the actual class names.
102
36
  #
103
- # @return [String] the prefix
104
- def symbol_reference_prefix
105
- ''
37
+ # @return [Hash{String=>Symbol}] the map
38
+ def adapter_map
39
+ @adapter_map ||= {}
106
40
  end
107
-
108
- # When a class is loaded by symbol reference, the return value of this method
109
- # will replace the symbol that was passed in. Defaults to return the symbol
110
- # unchanged.
111
- # Note, the word 'symbol' is used, but in reality it's a String and won't need
112
- # typecasting.
41
+
42
+ # Declare a class as an 'adapter' for this module. Given a name, for example
43
+ # "ChunkyBacon", it will expect the adapter class to be called ChunkyBaconAdapter
44
+ # and be defined in "chunky_bacon_adapter.rb" in this module's #dynamic_class_path.
113
45
  #
114
- # @param [String] name the symbol referencing the class to load
115
- # @return [String] the transformed symbol
116
- def symbol_reference_transform name
117
- name
118
- end
119
-
120
- # When a class is loaded by symbol reference, the return value of this method
121
- # will be appended to the symbol to form the file name. Defaults to '_adapter'.
46
+ # Once a class is declared as an adapter, will set this module to autoload the
47
+ # file on first reference, and sets a key in the #adapter_map mapping the normalized
48
+ # name of the adapter (in the case of ChunkyBaconAdapter, the normalized name is
49
+ # "chunkybacon").
122
50
  #
123
- # @return [String] the suffix
124
- def symbol_reference_suffix
125
- '_adapter'
126
- end
127
-
128
- # Requires the file in the search path with the given name (minus trailing '.rb').
51
+ # The class can be accessed by its normalized name through the #[] method, or simply
52
+ # referenced directly through a #const_missing hook. Using #const_missing will normalize
53
+ # the name of the constant and look it up in the #adapter_map, so capitalization (or even
54
+ # punctuation) need not be exact.
129
55
  #
130
- # @param [String] name the name of the file
56
+ # @example Setting up a module and a dynamically-loaded adapter
57
+ # module FunderfulWonderment
58
+ # extend Animoto::Support::DynamicClassLoader
59
+ # adapter 'ChunkyBacon'
60
+ # end
61
+ #
62
+ # Any reference to FunderfulWonderment::ChunkyBaconAdapter will autoload the class file
63
+ # and return the ChunkyBaconAdapter. As will FunderfulWonderment::Chunky_bacon_ADAPTER or
64
+ # any other name whose normalized name maps to "chunkybacon".
65
+ #
66
+ # @param [String] name the name of the adapter class, minus the word "Adapter" at the end.
131
67
  # @return [void]
132
- # @raise [NameError] if the file isn't found
133
- def load_missing_file_named name
134
- require "#{search_path}/#{name}.rb"
135
- rescue LoadError
136
- raise NameError, "Couldn't locate adapter named \"#{name}\""
68
+ # @see #[]
69
+ # @see #const_missing
70
+ def adapter name
71
+ klass_name = :"#{name}Adapter"
72
+ klass_file = klass_name.to_s.underscore
73
+ self.autoload klass_name, "#{dynamic_class_path}/#{klass_file}"
74
+ adapter_map[normalize_const_name(klass_file)] = klass_name
137
75
  end
138
-
139
- # Returns the path where dynamically loaded files can be found. Defaults to '.' or
140
- # whatever was declared as the search path when this module was extended. The path
141
- # should not end with a trailing slash.
76
+
77
+ # Normalizes the name of the missing constant and tries to look it up in the #adapter_map.
142
78
  #
143
- # @return [String] the path
144
- def search_path
145
- @dynamic_class_loader_search_path
79
+ # @param [Symbol] name the name of the constant, as a symbol
80
+ # @return [Class] the class the name corresponds to
81
+ # @raise [NameError] if the class can't be found
82
+ def const_missing name
83
+ if klass_name = adapter_map[normalize_const_name(name)]
84
+ klass = const_get(klass_name)
85
+ const_set name, klass
86
+ klass
87
+ end
88
+ rescue NameError
89
+ super
146
90
  end
147
91
 
148
- # Returns a Hash mapping the symbolic names for adapters to their classes.
92
+ # Normalizes a constant name by downcasing it, stripping off the trailing word
93
+ # 'adapter', and removing all non-letter characters. Note that the name should
94
+ # be a valid Ruby constant name.
149
95
  #
150
- # @return [Hash<Symbol,Class>] the map of adapters
151
- def adapter_map
152
- @adapter_map ||= {}
153
- end
154
-
155
- # Turns a camel-cased class name into an underscored version. Will only affect the base name
156
- # of the class, so, for example, Animoto::DirectingAndRenderingJob becomes 'directing_and_rendering_job'
96
+ # @example
97
+ # normalize_const_name(:ChunkyBaconAdapter) # => "chunkybacon"
157
98
  #
158
- # @param [Class,String] klass a class or name of a class
159
- # @return [String] the underscored version of the name
160
- def underscore_class_name klass
161
- klass_name = klass.is_a?(Class) ? klass.name.split('::').last : klass
162
- klass_name.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
99
+ # @param [String, Symbol] name the name of the class as a String or Symbol
100
+ # @return [String] the normalized name
101
+ def normalize_const_name name
102
+ name.to_s.downcase[/^([\w_]+?)(?:_?adapter)?$/,1].gsub(/[^a-z]/,'')
163
103
  end
104
+
164
105
  end
165
- end
166
- end
106
+ end
107
+ end
@@ -2,4 +2,8 @@ module Animoto
2
2
  # This is the base class that all Animoto-specific errors descend from.
3
3
  class Error < StandardError
4
4
  end
5
+
6
+ # Raised when an abstract method is called.
7
+ class AbstractMethodError < Animoto::Error
8
+ end
5
9
  end
@@ -0,0 +1,25 @@
1
+ module Animoto
2
+ module Support
3
+ module Hash
4
+
5
+ # Returns a new hash with only the listed keys from this hash.
6
+ #
7
+ # @param [Array<Object>] keys the keys to include
8
+ # @return [Hash{Object=>Object}] a new hash with only the listed keys
9
+ def only *keys
10
+ self.delete_if { |k,v| !keys.include?(k) }
11
+ end
12
+
13
+ # Returns a new hash with all keys from this hash except the listed ones.
14
+ #
15
+ # @param [Array<Object>] keys the keys to exclude
16
+ # @return [Hash{Object=>Object}] a new hash without the listed keys
17
+ def except *keys
18
+ self.delete_if { |k,v| keys.include?(v) }
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+
25
+ Hash.__send__ :include, Animoto::Support::Hash
@@ -4,6 +4,9 @@ module Animoto
4
4
 
5
5
  # When included into a class, also extends ClassMethods, inludes InstanceMethods, and
6
6
  # includes Support::ContentType
7
+ #
8
+ # @param [Module] base the module this is being included into
9
+ # @return [void]
7
10
  def self.included base
8
11
  base.class_eval {
9
12
  include Animoto::Support::StandardEnvelope::InstanceMethods
@@ -11,12 +14,31 @@ module Animoto
11
14
  include Animoto::Support::ContentType
12
15
  }
13
16
  end
17
+
18
+ # Scans the structure of a standard envelope for the 'payload key' and tries to find
19
+ # the matching class for that key. Returns nil if the class isn't found (instead of,
20
+ # say, raising a NameError).
21
+ #
22
+ # @param [Hash{String=>Object}] envelope a 'standard envelope' hash
23
+ # @return [Class, nil] the class, or nil if either the payload key or the class couldn't be found
24
+ def self.find_class_for envelope
25
+ if payload_key = ((envelope['response'] || {})['payload'] || {}).keys.first
26
+ klass_name = payload_key.camelize
27
+ if /(?:Job|Callback)$/ === klass_name
28
+ Animoto::Jobs::const_get(klass_name) if Animoto::Jobs::const_defined?(klass_name)
29
+ else
30
+ Animoto::Resources::const_get(klass_name) if Animoto::Resources::const_defined?(klass_name)
31
+ end
32
+ end
33
+ rescue NameError
34
+ nil
35
+ end
14
36
 
15
37
  module InstanceMethods
16
38
  # Calls the class-level unpack_standard_envelope method.
17
- # @return [Hash<Symbol,Object>]
39
+ # @return [Hash{Symbol=>Object}]
18
40
  # @see Animoto::Support::StandardEnvelope::ClassMethods#unpack_standard_envelope
19
- def unpack_standard_envelope body = {}
41
+ def unpack_standard_envelope body
20
42
  self.class.unpack_standard_envelope body
21
43
  end
22
44
 
@@ -30,18 +52,14 @@ module Animoto
30
52
  end
31
53
 
32
54
  module ClassMethods
33
- # @overload payload_key(key)
34
- # Sets the payload key for this class. When building an instance of this class from
35
- # a response body, the payload key determines which object in the response payload
36
- # holds the attributes for the instance.
55
+
56
+ # Get or set the payload key for this class. When building an instance of this
57
+ # class from a response body, the payload key determines which object in the
58
+ # response payload holds the attributes for the instance.
37
59
  #
38
- # @param [String] key the key to set
39
- # @return [String] the key
40
- #
41
- # @overload payload_key()
42
- # Returns the payload key for this class.
43
- #
44
- # @return [String] the key
60
+ # @param [String, nil] key the key to set
61
+ # @return [String] the payload key (the old key if no argument was given), or
62
+ # the inferred payload key if not set
45
63
  def payload_key key = nil
46
64
  @payload_key = key if key
47
65
  @payload_key || infer_content_type
@@ -49,17 +67,50 @@ module Animoto
49
67
 
50
68
  protected
51
69
 
70
+ # Extracts the base payload element from the envelope.
71
+ #
72
+ # @param [Hash{String=>Object}] body the response body
73
+ # @return [Hash{String=>Object}] the base payload at $.response.payload
74
+ def unpack_base_payload body
75
+ (body['response'] || {})['payload'] || {}
76
+ end
77
+
78
+ # Extracts the base status element from the envelope.
79
+ #
80
+ # @param [Hash{String=>Object}] body the response body
81
+ # @return [Hash{String=>Object}] the status element at $.response.status
82
+ def unpack_status body
83
+ (body['response'] || {})['status'] || {}
84
+ end
85
+
86
+ # Returns the payload, which is the part of the response classes will find the most
87
+ # interesting.
88
+ #
89
+ # @param [Hash{String=>Object}] body the response body
90
+ # @return [Hash{String=>Object}] the payload at $.response.payload[payload_key]
91
+ def unpack_payload body
92
+ unpack_base_payload(body)[payload_key] || {}
93
+ end
94
+
95
+ # Returns the links element of the payload, or an empty hash if it doesn't exist.
96
+ #
97
+ # @param [Hash{String=>Object}] body the response body
98
+ # @return [Hash{String=>Object}] the links at $.response.payload[payload_key].links
99
+ def unpack_links body
100
+ unpack_payload(body)['links'] || {}
101
+ end
102
+
52
103
  # Extracts common elements from the 'standard envelope' and returns them in a
53
104
  # easier-to-work-with hash.
54
105
  #
55
- # @param [Hash<String,Object>] body the body, structured in the 'standard envelope'
56
- # @return [Hash<Symbol,Object>] the nicer hash
57
- def unpack_standard_envelope body = {}
106
+ # @param [Hash{String=>Object}] body the body, structured in the 'standard envelope'
107
+ # @return [Hash{Symbol=>Object}] the nicer hash
108
+ def unpack_standard_envelope body
58
109
  {
59
- :url => body['response']['payload'][payload_key]['links']['self'],
60
- :errors => body['response']['status'] ? (body['response']['status']['errors'] || []) : []
110
+ :url => unpack_links(body)['self'],
111
+ :errors => unpack_status(body)['errors'] || []
61
112
  }
62
- end
113
+ end
63
114
  end
64
115
  end
65
116
  end
@@ -0,0 +1,31 @@
1
+ module Animoto
2
+ module Support
3
+ module String
4
+
5
+ # Transforms this string from underscore-form to camel case. Capitalizes
6
+ # the first letter.
7
+ #
8
+ # @example
9
+ # "this_is_a_underscored_string" # => "ThisIsAUnderscoredString"
10
+ #
11
+ # @return [String] the camel case form of this string
12
+ def camelize
13
+ self.gsub(/(?:^|_)(.)/) { $1.upcase }
14
+ end
15
+
16
+ # Transforms this string from camel case to underscore-form. Downcases the
17
+ # entire string.
18
+ #
19
+ # @example
20
+ # "ThisIsACamelCasedString" # => "this_is_a_camel_cased_string"
21
+ #
22
+ # @return [String] the underscored form of this string
23
+ def underscore
24
+ self.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+
31
+ String.__send__ :include, Animoto::Support::String