contentful 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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