contentful 0.1.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 (133) hide show
  1. data/.README.md.swp +0 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +10 -0
  5. data/ChangeLog.md +3 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +193 -0
  9. data/Rakefile +30 -0
  10. data/contentful.gemspec +31 -0
  11. data/do_request.sh +5 -0
  12. data/doc/Contentful.html +131 -0
  13. data/doc/Contentful/AccessDenied.html +158 -0
  14. data/doc/Contentful/Array.html +346 -0
  15. data/doc/Contentful/Asset.html +315 -0
  16. data/doc/Contentful/BadRequest.html +158 -0
  17. data/doc/Contentful/Client.html +1407 -0
  18. data/doc/Contentful/ContentType.html +183 -0
  19. data/doc/Contentful/DynamicEntry.html +333 -0
  20. data/doc/Contentful/Entry.html +198 -0
  21. data/doc/Contentful/Error.html +413 -0
  22. data/doc/Contentful/Field.html +161 -0
  23. data/doc/Contentful/File.html +160 -0
  24. data/doc/Contentful/Link.html +275 -0
  25. data/doc/Contentful/Locale.html +161 -0
  26. data/doc/Contentful/NotFound.html +158 -0
  27. data/doc/Contentful/Request.html +669 -0
  28. data/doc/Contentful/Resource.html +606 -0
  29. data/doc/Contentful/Resource/AssetFields.html +413 -0
  30. data/doc/Contentful/Resource/AssetFields/ClassMethods.html +174 -0
  31. data/doc/Contentful/Resource/ClassMethods.html +271 -0
  32. data/doc/Contentful/Resource/Fields.html +398 -0
  33. data/doc/Contentful/Resource/Fields/ClassMethods.html +187 -0
  34. data/doc/Contentful/Resource/SystemProperties.html +444 -0
  35. data/doc/Contentful/Resource/SystemProperties/ClassMethods.html +174 -0
  36. data/doc/Contentful/ResourceBuilder.html +1400 -0
  37. data/doc/Contentful/Response.html +546 -0
  38. data/doc/Contentful/ServerError.html +158 -0
  39. data/doc/Contentful/Space.html +183 -0
  40. data/doc/Contentful/Support.html +198 -0
  41. data/doc/Contentful/Unauthorized.html +158 -0
  42. data/doc/Contentful/UnparsableJson.html +158 -0
  43. data/doc/Contentful/UnparsableResource.html +158 -0
  44. data/doc/_index.html +410 -0
  45. data/doc/class_list.html +54 -0
  46. data/doc/css/common.css +1 -0
  47. data/doc/css/full_list.css +57 -0
  48. data/doc/css/style.css +338 -0
  49. data/doc/file.README.html +214 -0
  50. data/doc/file_list.html +56 -0
  51. data/doc/frames.html +26 -0
  52. data/doc/index.html +214 -0
  53. data/doc/js/app.js +219 -0
  54. data/doc/js/full_list.js +178 -0
  55. data/doc/js/jquery.js +4 -0
  56. data/doc/method_list.html +533 -0
  57. data/doc/top-level-namespace.html +112 -0
  58. data/examples/custom_classes.rb +43 -0
  59. data/examples/dynamic_entries.rb +126 -0
  60. data/examples/example_queries.rb +27 -0
  61. data/examples/raise_errors.rb +22 -0
  62. data/examples/raw_mode.rb +15 -0
  63. data/examples/resource_mapping.rb +33 -0
  64. data/lib/contentful.rb +3 -0
  65. data/lib/contentful/array.rb +47 -0
  66. data/lib/contentful/asset.rb +34 -0
  67. data/lib/contentful/client.rb +193 -0
  68. data/lib/contentful/content_type.rb +16 -0
  69. data/lib/contentful/dynamic_entry.rb +54 -0
  70. data/lib/contentful/entry.rb +12 -0
  71. data/lib/contentful/error.rb +52 -0
  72. data/lib/contentful/field.rb +16 -0
  73. data/lib/contentful/file.rb +13 -0
  74. data/lib/contentful/link.rb +20 -0
  75. data/lib/contentful/locale.rb +12 -0
  76. data/lib/contentful/location.rb +12 -0
  77. data/lib/contentful/request.rb +37 -0
  78. data/lib/contentful/resource.rb +146 -0
  79. data/lib/contentful/resource/asset_fields.rb +50 -0
  80. data/lib/contentful/resource/fields.rb +39 -0
  81. data/lib/contentful/resource/system_properties.rb +48 -0
  82. data/lib/contentful/resource_builder.rb +197 -0
  83. data/lib/contentful/response.rb +64 -0
  84. data/lib/contentful/space.rb +14 -0
  85. data/lib/contentful/support.rb +18 -0
  86. data/lib/contentful/version.rb +3 -0
  87. data/spec/array_spec.rb +69 -0
  88. data/spec/asset_spec.rb +62 -0
  89. data/spec/auto_includes_spec.rb +12 -0
  90. data/spec/client_class_spec.rb +59 -0
  91. data/spec/client_configuration_spec.rb +197 -0
  92. data/spec/coercions_spec.rb +10 -0
  93. data/spec/content_type_spec.rb +44 -0
  94. data/spec/dynamic_entry_spec.rb +34 -0
  95. data/spec/entry_spec.rb +53 -0
  96. data/spec/error_class_spec.rb +60 -0
  97. data/spec/error_requests_spec.rb +32 -0
  98. data/spec/field_spec.rb +36 -0
  99. data/spec/file_spec.rb +28 -0
  100. data/spec/fixtures/json_responses/content_type.json +83 -0
  101. data/spec/fixtures/json_responses/not_found.json +13 -0
  102. data/spec/fixtures/json_responses/nyancat.json +48 -0
  103. data/spec/fixtures/json_responses/unparsable.json +13 -0
  104. data/spec/fixtures/vcr_cassettes/array.yml +288 -0
  105. data/spec/fixtures/vcr_cassettes/array_page_1.yml +106 -0
  106. data/spec/fixtures/vcr_cassettes/array_page_2.yml +73 -0
  107. data/spec/fixtures/vcr_cassettes/asset.yml +96 -0
  108. data/spec/fixtures/vcr_cassettes/bad_request.yml +76 -0
  109. data/spec/fixtures/vcr_cassettes/content_type.yml +147 -0
  110. data/spec/fixtures/vcr_cassettes/entries.yml +561 -0
  111. data/spec/fixtures/vcr_cassettes/entry.yml +112 -0
  112. data/spec/fixtures/vcr_cassettes/entry_cache.yml +288 -0
  113. data/spec/fixtures/vcr_cassettes/field.yml +147 -0
  114. data/spec/fixtures/vcr_cassettes/locale.yml +81 -0
  115. data/spec/fixtures/vcr_cassettes/location.yml +305 -0
  116. data/spec/fixtures/vcr_cassettes/not_found.yml +71 -0
  117. data/spec/fixtures/vcr_cassettes/nyancat.yml +112 -0
  118. data/spec/fixtures/vcr_cassettes/nyancat_include.yml +112 -0
  119. data/spec/fixtures/vcr_cassettes/reloaded_entry.yml +112 -0
  120. data/spec/fixtures/vcr_cassettes/space.yml +81 -0
  121. data/spec/fixtures/vcr_cassettes/unauthorized.yml +64 -0
  122. data/spec/link_spec.rb +40 -0
  123. data/spec/locale_spec.rb +20 -0
  124. data/spec/location_spec.rb +30 -0
  125. data/spec/request_spec.rb +48 -0
  126. data/spec/resource_spec.rb +52 -0
  127. data/spec/response_spec.rb +50 -0
  128. data/spec/space_spec.rb +36 -0
  129. data/spec/spec_helper.rb +6 -0
  130. data/spec/support/client.rb +6 -0
  131. data/spec/support/json_responses.rb +11 -0
  132. data/spec/support/vcr.rb +16 -0
  133. metadata +374 -0
@@ -0,0 +1,146 @@
1
+ require_relative 'resource/system_properties'
2
+ require 'date'
3
+
4
+ module Contentful
5
+ # Include this module to declare a class to be a contentful resource.
6
+ # This is done by the default in the existing resource classes
7
+ #
8
+ # You can define your own classes that behave like contentful resources:
9
+ # See examples/custom_classes.rb to see how.
10
+ #
11
+ # Take a look at examples/resource_mapping.rb on how to register them to be returned
12
+ # by the client by default
13
+ module Resource
14
+ COERCIONS = {
15
+ string: ->(v){ v.to_s },
16
+ integer: ->(v){ v.to_i },
17
+ float: ->(v){ v.to_f },
18
+ boolean: ->(v){ !!v },
19
+ date: ->(v){ DateTime.parse(v) },
20
+ }
21
+
22
+ attr_reader :properties, :request, :client
23
+
24
+ def initialize(object, request = nil, client = nil)
25
+ self.class.update_coercions!
26
+
27
+ @properties = extract_from_object object, :property, self.class.property_coercions.keys
28
+ @request = request
29
+ @client = client
30
+ end
31
+
32
+ def inspect(info = nil)
33
+ properties_info = properties.empty? ? "" : " @properties=#{properties.inspect}"
34
+ "#<#{self.class}:#{properties_info}#{info}>"
35
+ end
36
+
37
+ # Returns true for resources that behave like an array
38
+ def array?
39
+ false
40
+ end
41
+
42
+ # Resources that don't include SystemProperties return nil for #sys
43
+ def sys
44
+ nil
45
+ end
46
+
47
+ # Resources that don't include Fields or AssetFields return nil for #fields
48
+ def fields
49
+ nil
50
+ end
51
+
52
+ # Issues the request that was made to fetch this response again.
53
+ # Only works for top-level resources
54
+ def reload
55
+ if request
56
+ request.get
57
+ else
58
+ false
59
+ end
60
+ end
61
+
62
+
63
+ private
64
+
65
+ def extract_from_object(object, namespace, keys = nil)
66
+ if object
67
+ keys ||= object.keys
68
+ keys.each.with_object({}){ |name, res|
69
+ res[name.to_sym] = coerce_value_or_array(
70
+ object.is_a?(::Array) ? object : object[name.to_s],
71
+ self.class.public_send(:"#{namespace}_coercions")[name.to_sym],
72
+ )
73
+ }
74
+ else
75
+ {}
76
+ end
77
+ end
78
+
79
+ def coerce_value_or_array(value, what = nil)
80
+ if value.is_a? ::Array
81
+ value.map{ |v| coerce_or_create_class(v, what) }
82
+ else
83
+ coerce_or_create_class(value, what)
84
+ end
85
+ end
86
+
87
+ def coerce_or_create_class(value, what)
88
+ case what
89
+ when Symbol
90
+ COERCIONS[what] ? COERCIONS[what][value] : value
91
+ when Class
92
+ what.new(value, client)
93
+ else
94
+ value
95
+ end
96
+ end
97
+
98
+ # Register the resources properties on class level by using the #property method
99
+ module ClassMethods
100
+ def property_coercions
101
+ @property_coercions ||= {}
102
+ end
103
+
104
+ # Defines which properties of a resource your class expects
105
+ # Define them in :camelCase, they will be available as #snake_cased methods
106
+ #
107
+ # You can pass in a second "type" argument:
108
+ # - If it is a class, it will be initialized for the property
109
+ # - Symbols are looked up in the COERCION constant for a lambda that
110
+ # defines a type conversion to apply
111
+ #
112
+ # Note: This second argument is not meant for contentful sub-resources,
113
+ # but for structured objects (like locales in a space)
114
+ # Sub-resources are handled by the resource builder
115
+ def property(name, property_class = nil)
116
+ property_coercions[name.to_sym] = property_class
117
+ define_method Contentful::Support.snakify(name) do
118
+ properties[name.to_sym]
119
+ end
120
+ end
121
+
122
+ # Ensure inherited classes pick up coercions
123
+ def update_coercions!
124
+ return if @coercions_updated
125
+
126
+ if superclass.respond_to? :property_coercions
127
+ @property_coercions = superclass.property_coercions.dup.merge(@property_coercions || {})
128
+ end
129
+
130
+ if superclass.respond_to? :sys_coercions
131
+ @sys_coercions = superclass.sys_coercions.dup.merge(@sys_coercions || {})
132
+ end
133
+
134
+ if superclass.respond_to? :fields_coercions
135
+ @fields_coercions = superclass.fields_coercions.dup.merge(@fields_coercions || {})
136
+ end
137
+
138
+ @coercions_updated = true
139
+ end
140
+ end
141
+
142
+ def self.included(base)
143
+ base.extend(ClassMethods)
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,50 @@
1
+ require_relative '../file'
2
+
3
+ module Contentful
4
+ module Resource
5
+ # Special fields for Asset. Don't include together wit Contentful::Resource::Fields
6
+ #
7
+ # It depends on system properties being available
8
+ module AssetFields
9
+ FIELDS_COERCIONS = {
10
+ title: :string,
11
+ description: :string,
12
+ file: File,
13
+ }
14
+
15
+ def fields
16
+ @fields[locale]
17
+ end
18
+
19
+ def initialize(object, *)
20
+ super
21
+ @fields = {}
22
+ @fields[locale] = extract_from_object object["fields"], :fields
23
+ end
24
+
25
+ def inspect(info = nil)
26
+ if fields.empty?
27
+ super(info)
28
+ else
29
+ super("#{info} @fields=#{fields.inspect}")
30
+ end
31
+ end
32
+
33
+ module ClassMethods
34
+ def fields_coercions
35
+ FIELDS_COERCIONS
36
+ end
37
+ end
38
+
39
+ def self.included(base)
40
+ base.extend(ClassMethods)
41
+
42
+ base.fields_coercions.keys.each{ |name|
43
+ base.send :define_method, Contentful::Support.snakify(name) do
44
+ fields[name.to_sym]
45
+ end
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,39 @@
1
+ module Contentful
2
+ module Resource
3
+ # Include this module into your Resource class to enable it
4
+ # to deal with entry fields (but not asset fields)
5
+ #
6
+ # It depends on system properties being available
7
+ module Fields
8
+ # Returns all fields of the asset
9
+ def fields
10
+ @fields[locale]
11
+ end
12
+
13
+ def initialize(object, *)
14
+ super
15
+ @fields = {}
16
+ @fields[locale] = extract_from_object object["fields"], :fields
17
+ end
18
+
19
+ def inspect(info = nil)
20
+ if fields.empty?
21
+ super(info)
22
+ else
23
+ super("#{info} @fields=#{fields.inspect}")
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ # No coercions, since no content type available
29
+ def fields_coercions
30
+ {}
31
+ end
32
+ end
33
+
34
+ def self.included(base)
35
+ base.extend(ClassMethods)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,48 @@
1
+ module Contentful
2
+ module Resource
3
+ # Adds the feature to have system properties to a Resource.
4
+ module SystemProperties
5
+ SYS_COERCIONS = {
6
+ type: :string,
7
+ id: :string,
8
+ space: nil,
9
+ contentType: nil,
10
+ linkType: :string,
11
+ revision: :integer,
12
+ createdAt: :date,
13
+ updatedAt: :date,
14
+ locale: :string,
15
+ }
16
+ attr_reader :sys
17
+
18
+ def initialize(object, *)
19
+ super
20
+ @sys = extract_from_object object["sys"], :sys
21
+ end
22
+
23
+ def inspect(info = nil)
24
+ if sys.empty?
25
+ super(info)
26
+ else
27
+ super("#{info} @sys=#{sys.inspect}")
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+ def sys_coercions
33
+ SYS_COERCIONS
34
+ end
35
+ end
36
+
37
+ def self.included(base)
38
+ base.extend(ClassMethods)
39
+
40
+ base.sys_coercions.keys.each{ |name|
41
+ base.send :define_method, Contentful::Support.snakify(name) do
42
+ sys[name.to_sym]
43
+ end
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,197 @@
1
+ require_relative 'error'
2
+ require_relative 'resource'
3
+ require_relative 'space'
4
+ require_relative 'content_type'
5
+ require_relative 'entry'
6
+ require_relative 'dynamic_entry'
7
+ require_relative 'asset'
8
+ require_relative 'array'
9
+ require_relative 'link'
10
+
11
+ module Contentful
12
+ # Transforms a Contentful::Response into a Contentful::Resource or a Contentful::Error
13
+ # See example/resource_mapping.rb for avanced usage
14
+ class ResourceBuilder
15
+ DEFAULT_RESOURCE_MAPPING = {
16
+ 'Space' => Space,
17
+ 'ContentType' => ContentType,
18
+ 'Entry' => :try_dynamic_entry,
19
+ 'Asset' => Asset,
20
+ 'Array' => Array,
21
+ 'Link' => Link,
22
+ }
23
+
24
+ attr_reader :client, :response, :resource_mapping
25
+
26
+
27
+ def initialize(client, response, resource_mapping = {})
28
+ @response = response
29
+ @client = client
30
+ @included_resources = {}
31
+ @resource_mapping = default_resource_mapping.merge(resource_mapping)
32
+ end
33
+
34
+ # Starts the parsing process.
35
+ # Either returns an Error, or the parsed Resource
36
+ def run
37
+ case response.status
38
+ when :contentful_error
39
+ Error[response.raw.response.status].new(response)
40
+ when :unparsable_json
41
+ UnparsableJson.new(response)
42
+ when :not_contentful
43
+ Error.new(response)
44
+ else
45
+ begin
46
+ create_all_resources!
47
+ rescue UnparsableResource => error
48
+ error
49
+ end
50
+ end
51
+ end
52
+
53
+ # PARSING MECHANISM
54
+ # - raise error if response not valid
55
+ # - look for included objects and parse them to resources
56
+ # - parse main object to resource
57
+ # - replace links in included resources with included resources
58
+ # - replace links in main resource with included resources
59
+ # - return main resource
60
+ def create_all_resources!
61
+ create_included_resources! response.object["includes"]
62
+ res = create_resource(response.object)
63
+
64
+ unless @included_resources.empty?
65
+ replace_links_in_included_resources_with_included_resources
66
+ replace_links_with_included_resources(res)
67
+ end
68
+
69
+ res
70
+ end
71
+
72
+ # Creates a single resource from the
73
+ def create_resource(object)
74
+ res = detect_resource_class(object).new(object, response.request, client)
75
+ replace_children res, object
76
+ replace_children_array(res, :items) if res.array?
77
+
78
+ res
79
+ end
80
+
81
+ # When using Dynamic Entry Mode: Automatically converts Entry to DynamicEntry
82
+ def try_dynamic_entry(object)
83
+ get_dynamic_entry(object) || Entry
84
+ end
85
+
86
+ # Finds the proper DynamicEntry class for an entry
87
+ def get_dynamic_entry(object)
88
+ if id = object["sys"] &&
89
+ object["sys"]["contentType"] &&
90
+ object["sys"]["contentType"]["sys"] &&
91
+ object["sys"]["contentType"]["sys"]["id"]
92
+ client.dynamic_entry_cache[id.to_sym]
93
+ end
94
+ end
95
+
96
+ # Uses the resource mapping to find the proper Resource class to initialize
97
+ # for this Response object type
98
+ #
99
+ # The mapping value can be a
100
+ # - Class
101
+ # - Proc: Will be called, expected to return the proper Class
102
+ # - Symbol: Will be called as method of the ResourceBuilder itself
103
+ def detect_resource_class(object)
104
+ type = object["sys"] && object["sys"]["type"]
105
+ case res_class = resource_mapping[type]
106
+ when Symbol
107
+ public_send(res_class, object)
108
+ when Proc
109
+ res_class[object]
110
+ when nil
111
+ raise UnsparsableResource.new(response)
112
+ else
113
+ res_class
114
+ end
115
+ end
116
+
117
+ # The default mapping for #detect_resource_class
118
+ def default_resource_mapping
119
+ DEFAULT_RESOURCE_MAPPING
120
+ end
121
+
122
+
123
+ private
124
+
125
+ def detect_child_objects(object)
126
+ if object.is_a?(Hash)
127
+ object.select{ |k,v| v.is_a?(Hash) && v.has_key?("sys") }
128
+ else
129
+ {}
130
+ end
131
+ end
132
+
133
+ def replace_children(res, object)
134
+ object.each{ |name, potential_objects|
135
+ detect_child_objects(potential_objects).each{ |child_name, child_object|
136
+ res.public_send(name)[child_name.to_sym] = create_resource(child_object)
137
+ }
138
+ }
139
+ end
140
+
141
+ def replace_children_array(res, array_field)
142
+ items = res.public_send(array_field)
143
+ items.map!{ |resource_object| create_resource(resource_object) }
144
+ end
145
+
146
+ def create_included_resources!(included_objects)
147
+ if included_objects
148
+ included_objects.each{ |type, objects|
149
+ @included_resources[type] = Hash[
150
+ objects.map{ |object|
151
+ res = create_resource(object)
152
+ [res.id, res]
153
+ }
154
+ ]
155
+ }
156
+ end
157
+ end
158
+
159
+ def replace_links_with_included_resources(res)
160
+ [:properties, :sys, :fields].each{ |_what|
161
+ if what = res.public_send(_what)
162
+ what.each{ |name, child_res|
163
+ replace_link_or_check_recursively child_res, what, name, res
164
+ }
165
+ end
166
+ }
167
+ if res.array?
168
+ res.each.with_index{ |child_res, index|
169
+ replace_link_or_check_recursively child_res, res.items, index, child_res
170
+ }
171
+ end
172
+ end
173
+
174
+ def replace_link_or_check_recursively(res, what, name, resource_to_replace)
175
+ if res.is_a? Link
176
+ maybe_replace_link(res, what, name)
177
+ elsif res.is_a?(Resource) && res.sys
178
+ replace_links_with_included_resources(resource_to_replace)
179
+ end
180
+ end
181
+
182
+ def replace_links_in_included_resources_with_included_resources
183
+ @included_resources.each{ |_, for_type|
184
+ for_type.each{ |_, res|
185
+ replace_links_with_included_resources(res)
186
+ }
187
+ }
188
+ end
189
+
190
+ def maybe_replace_link(resource, parent, index)
191
+ if @included_resources[resource.link_type] &&
192
+ @included_resources[resource.link_type].has_key?(resource.id)
193
+ parent[index] = @included_resources[resource.link_type][resource.id]
194
+ end
195
+ end
196
+ end
197
+ end