yodel 0.0.1

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 (200) hide show
  1. data/.document +5 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +63 -0
  5. data/LICENSE +1 -0
  6. data/README.rdoc +20 -0
  7. data/Rakefile +1 -0
  8. data/bin/yodel +4 -0
  9. data/lib/yodel.rb +22 -0
  10. data/lib/yodel/application/application.rb +44 -0
  11. data/lib/yodel/application/extension.rb +59 -0
  12. data/lib/yodel/application/request_handler.rb +48 -0
  13. data/lib/yodel/application/yodel.rb +25 -0
  14. data/lib/yodel/command/command.rb +94 -0
  15. data/lib/yodel/command/deploy.rb +67 -0
  16. data/lib/yodel/command/dns_server.rb +16 -0
  17. data/lib/yodel/command/installer.rb +229 -0
  18. data/lib/yodel/config/config.rb +30 -0
  19. data/lib/yodel/config/environment.rb +16 -0
  20. data/lib/yodel/config/yodel.rb +21 -0
  21. data/lib/yodel/exceptions/destroyed_record.rb +2 -0
  22. data/lib/yodel/exceptions/domain_not_found.rb +16 -0
  23. data/lib/yodel/exceptions/duplicate_layout.rb +2 -0
  24. data/lib/yodel/exceptions/exceptions.rb +3 -0
  25. data/lib/yodel/exceptions/inconsistent_lock_state.rb +2 -0
  26. data/lib/yodel/exceptions/invalid_field.rb +2 -0
  27. data/lib/yodel/exceptions/invalid_index.rb +2 -0
  28. data/lib/yodel/exceptions/invalid_mixin.rb +2 -0
  29. data/lib/yodel/exceptions/invalid_model_field.rb +2 -0
  30. data/lib/yodel/exceptions/layout_not_found.rb +2 -0
  31. data/lib/yodel/exceptions/mass_assignment.rb +2 -0
  32. data/lib/yodel/exceptions/missing_migration.rb +2 -0
  33. data/lib/yodel/exceptions/missing_root_directory.rb +15 -0
  34. data/lib/yodel/exceptions/unable_to_acquire_lock.rb +2 -0
  35. data/lib/yodel/exceptions/unauthorised.rb +2 -0
  36. data/lib/yodel/exceptions/unknown_field.rb +2 -0
  37. data/lib/yodel/middleware/development_server.rb +180 -0
  38. data/lib/yodel/middleware/error_pages.rb +72 -0
  39. data/lib/yodel/middleware/public_assets.rb +78 -0
  40. data/lib/yodel/middleware/request.rb +16 -0
  41. data/lib/yodel/middleware/site_detector.rb +22 -0
  42. data/lib/yodel/mime_types/default_mime_set.rb +28 -0
  43. data/lib/yodel/mime_types/mime_type.rb +68 -0
  44. data/lib/yodel/mime_types/mime_type_set.rb +41 -0
  45. data/lib/yodel/mime_types/mime_types.rb +6 -0
  46. data/lib/yodel/mime_types/yodel.rb +15 -0
  47. data/lib/yodel/models/api/api.rb +1 -0
  48. data/lib/yodel/models/api/api_call.rb +87 -0
  49. data/lib/yodel/models/core/associations/association.rb +37 -0
  50. data/lib/yodel/models/core/associations/associations.rb +22 -0
  51. data/lib/yodel/models/core/associations/counts/many_association.rb +18 -0
  52. data/lib/yodel/models/core/associations/counts/one_association.rb +22 -0
  53. data/lib/yodel/models/core/associations/embedded/embedded_association.rb +47 -0
  54. data/lib/yodel/models/core/associations/embedded/embedded_record_array.rb +12 -0
  55. data/lib/yodel/models/core/associations/embedded/many_embedded_association.rb +62 -0
  56. data/lib/yodel/models/core/associations/embedded/one_embedded_association.rb +49 -0
  57. data/lib/yodel/models/core/associations/query/many_query_association.rb +10 -0
  58. data/lib/yodel/models/core/associations/query/one_query_association.rb +10 -0
  59. data/lib/yodel/models/core/associations/query/query_association.rb +64 -0
  60. data/lib/yodel/models/core/associations/record_association.rb +38 -0
  61. data/lib/yodel/models/core/associations/store/many_store_association.rb +32 -0
  62. data/lib/yodel/models/core/associations/store/one_store_association.rb +14 -0
  63. data/lib/yodel/models/core/associations/store/store_association.rb +51 -0
  64. data/lib/yodel/models/core/attachments/attachment.rb +73 -0
  65. data/lib/yodel/models/core/attachments/image.rb +38 -0
  66. data/lib/yodel/models/core/core.rb +15 -0
  67. data/lib/yodel/models/core/fields/alias_field.rb +32 -0
  68. data/lib/yodel/models/core/fields/array_field.rb +64 -0
  69. data/lib/yodel/models/core/fields/attachment_field.rb +42 -0
  70. data/lib/yodel/models/core/fields/boolean_field.rb +28 -0
  71. data/lib/yodel/models/core/fields/change_sensitive_array.rb +96 -0
  72. data/lib/yodel/models/core/fields/change_sensitive_hash.rb +53 -0
  73. data/lib/yodel/models/core/fields/color_field.rb +4 -0
  74. data/lib/yodel/models/core/fields/date_field.rb +35 -0
  75. data/lib/yodel/models/core/fields/decimal_field.rb +19 -0
  76. data/lib/yodel/models/core/fields/email_field.rb +10 -0
  77. data/lib/yodel/models/core/fields/enum_field.rb +33 -0
  78. data/lib/yodel/models/core/fields/field.rb +154 -0
  79. data/lib/yodel/models/core/fields/fields.rb +29 -0
  80. data/lib/yodel/models/core/fields/fields_field.rb +31 -0
  81. data/lib/yodel/models/core/fields/filter_mixin.rb +9 -0
  82. data/lib/yodel/models/core/fields/filtered_string_field.rb +5 -0
  83. data/lib/yodel/models/core/fields/filtered_text_field.rb +5 -0
  84. data/lib/yodel/models/core/fields/function_field.rb +28 -0
  85. data/lib/yodel/models/core/fields/hash_field.rb +54 -0
  86. data/lib/yodel/models/core/fields/html_field.rb +15 -0
  87. data/lib/yodel/models/core/fields/image_field.rb +11 -0
  88. data/lib/yodel/models/core/fields/integer_field.rb +25 -0
  89. data/lib/yodel/models/core/fields/password_field.rb +21 -0
  90. data/lib/yodel/models/core/fields/self_field.rb +27 -0
  91. data/lib/yodel/models/core/fields/string_field.rb +15 -0
  92. data/lib/yodel/models/core/fields/tags_field.rb +7 -0
  93. data/lib/yodel/models/core/fields/text_field.rb +7 -0
  94. data/lib/yodel/models/core/fields/time_field.rb +36 -0
  95. data/lib/yodel/models/core/functions/function.rb +471 -0
  96. data/lib/yodel/models/core/functions/functions.rb +2 -0
  97. data/lib/yodel/models/core/functions/trigger.rb +14 -0
  98. data/lib/yodel/models/core/log/log.rb +33 -0
  99. data/lib/yodel/models/core/log/log_entry.rb +12 -0
  100. data/lib/yodel/models/core/model/abstract_model.rb +59 -0
  101. data/lib/yodel/models/core/model/model.rb +460 -0
  102. data/lib/yodel/models/core/model/mongo_model.rb +25 -0
  103. data/lib/yodel/models/core/model/site_model.rb +17 -0
  104. data/lib/yodel/models/core/mongo/mongo.rb +3 -0
  105. data/lib/yodel/models/core/mongo/primary_key_factory.rb +12 -0
  106. data/lib/yodel/models/core/mongo/query.rb +68 -0
  107. data/lib/yodel/models/core/mongo/record_index.rb +89 -0
  108. data/lib/yodel/models/core/record/abstract_record.rb +411 -0
  109. data/lib/yodel/models/core/record/embedded_record.rb +47 -0
  110. data/lib/yodel/models/core/record/mongo_record.rb +83 -0
  111. data/lib/yodel/models/core/record/record.rb +386 -0
  112. data/lib/yodel/models/core/record/section.rb +21 -0
  113. data/lib/yodel/models/core/record/site_record.rb +31 -0
  114. data/lib/yodel/models/core/site/migration.rb +52 -0
  115. data/lib/yodel/models/core/site/remote.rb +61 -0
  116. data/lib/yodel/models/core/site/site.rb +202 -0
  117. data/lib/yodel/models/core/validations/email_address_validation.rb +24 -0
  118. data/lib/yodel/models/core/validations/embedded_records_validation.rb +31 -0
  119. data/lib/yodel/models/core/validations/errors.rb +51 -0
  120. data/lib/yodel/models/core/validations/excluded_from_validation.rb +10 -0
  121. data/lib/yodel/models/core/validations/excludes_combinations_validation.rb +18 -0
  122. data/lib/yodel/models/core/validations/format_validation.rb +10 -0
  123. data/lib/yodel/models/core/validations/included_in_validation.rb +10 -0
  124. data/lib/yodel/models/core/validations/includes_combinations_validation.rb +14 -0
  125. data/lib/yodel/models/core/validations/length_validation.rb +28 -0
  126. data/lib/yodel/models/core/validations/password_confirmation_validation.rb +11 -0
  127. data/lib/yodel/models/core/validations/required_validation.rb +9 -0
  128. data/lib/yodel/models/core/validations/unique_validation.rb +9 -0
  129. data/lib/yodel/models/core/validations/validation.rb +39 -0
  130. data/lib/yodel/models/core/validations/validations.rb +15 -0
  131. data/lib/yodel/models/email/email.rb +79 -0
  132. data/lib/yodel/models/migrations/01_record_model.rb +29 -0
  133. data/lib/yodel/models/migrations/02_page_model.rb +45 -0
  134. data/lib/yodel/models/migrations/03_layout_model.rb +38 -0
  135. data/lib/yodel/models/migrations/04_group_model.rb +61 -0
  136. data/lib/yodel/models/migrations/05_user_model.rb +24 -0
  137. data/lib/yodel/models/migrations/06_snippet_model.rb +13 -0
  138. data/lib/yodel/models/migrations/07_search_page_model.rb +32 -0
  139. data/lib/yodel/models/migrations/08_default_site_options.rb +21 -0
  140. data/lib/yodel/models/migrations/09_security_page_models.rb +36 -0
  141. data/lib/yodel/models/migrations/10_record_proxy_page_model.rb +17 -0
  142. data/lib/yodel/models/migrations/11_email_model.rb +28 -0
  143. data/lib/yodel/models/migrations/12_api_call_model.rb +23 -0
  144. data/lib/yodel/models/migrations/13_redirect_page_model.rb +13 -0
  145. data/lib/yodel/models/migrations/14_menu_model.rb +20 -0
  146. data/lib/yodel/models/models.rb +8 -0
  147. data/lib/yodel/models/pages/form_builder.rb +379 -0
  148. data/lib/yodel/models/pages/html_decorator.rb +132 -0
  149. data/lib/yodel/models/pages/layout.rb +120 -0
  150. data/lib/yodel/models/pages/menu.rb +32 -0
  151. data/lib/yodel/models/pages/page.rb +378 -0
  152. data/lib/yodel/models/pages/pages.rb +7 -0
  153. data/lib/yodel/models/pages/record_proxy_page.rb +188 -0
  154. data/lib/yodel/models/pages/redirect_page.rb +11 -0
  155. data/lib/yodel/models/search/search.rb +1 -0
  156. data/lib/yodel/models/search/search_page.rb +58 -0
  157. data/lib/yodel/models/security/facebook_login_page.rb +55 -0
  158. data/lib/yodel/models/security/group.rb +10 -0
  159. data/lib/yodel/models/security/guests_group.rb +5 -0
  160. data/lib/yodel/models/security/login_page.rb +20 -0
  161. data/lib/yodel/models/security/logout_page.rb +13 -0
  162. data/lib/yodel/models/security/noone_group.rb +5 -0
  163. data/lib/yodel/models/security/owner_group.rb +8 -0
  164. data/lib/yodel/models/security/password.rb +5 -0
  165. data/lib/yodel/models/security/password_reset_page.rb +47 -0
  166. data/lib/yodel/models/security/security.rb +10 -0
  167. data/lib/yodel/models/security/user.rb +33 -0
  168. data/lib/yodel/public/core/css/core.css +257 -0
  169. data/lib/yodel/public/core/css/reset.css +48 -0
  170. data/lib/yodel/public/core/images/cross.png +0 -0
  171. data/lib/yodel/public/core/images/spinner.gif +0 -0
  172. data/lib/yodel/public/core/images/tick.png +0 -0
  173. data/lib/yodel/public/core/images/yodel.png +0 -0
  174. data/lib/yodel/public/core/js/jquery.min.js +18 -0
  175. data/lib/yodel/public/core/js/json2.js +480 -0
  176. data/lib/yodel/public/core/js/yodel_jquery.js +238 -0
  177. data/lib/yodel/request/authentication.rb +76 -0
  178. data/lib/yodel/request/flash.rb +28 -0
  179. data/lib/yodel/request/request.rb +4 -0
  180. data/lib/yodel/requires.rb +47 -0
  181. data/lib/yodel/task_queue/queue_daemon.rb +33 -0
  182. data/lib/yodel/task_queue/queue_worker.rb +32 -0
  183. data/lib/yodel/task_queue/stats_thread.rb +27 -0
  184. data/lib/yodel/task_queue/task.rb +62 -0
  185. data/lib/yodel/task_queue/task_queue.rb +40 -0
  186. data/lib/yodel/types/date.rb +5 -0
  187. data/lib/yodel/types/object_id.rb +11 -0
  188. data/lib/yodel/types/time.rb +5 -0
  189. data/lib/yodel/version.rb +3 -0
  190. data/system/Library/LaunchDaemons/com.yodelcms.dns.plist +26 -0
  191. data/system/Library/LaunchDaemons/com.yodelcms.server.plist +26 -0
  192. data/system/etc/resolver/yodel +2 -0
  193. data/system/usr/local/bin/yodel_command_runner +2 -0
  194. data/system/usr/local/etc/yodel/development_settings.rb +28 -0
  195. data/system/usr/local/etc/yodel/production_settings.rb +27 -0
  196. data/system/var/log/yodel.log +0 -0
  197. data/test/helper.rb +18 -0
  198. data/test/test_yodel.rb +4 -0
  199. data/yodel.gemspec +47 -0
  200. metadata +501 -0
@@ -0,0 +1,51 @@
1
+ module StoreAssociation
2
+ include RecordAssociation
3
+
4
+ def associate(associated_record, store, record)
5
+ if store.is_a?(Array)
6
+ store << associated_record.id
7
+ else
8
+ record.set_raw(name, associated_record.id)
9
+ end
10
+ end
11
+
12
+ def validate(record, errors)
13
+ # noop
14
+ end
15
+
16
+ def unassociate(associated_record, store, record)
17
+ if store.is_a?(Array)
18
+ store.delete(associated_record.id)
19
+ else
20
+ record.set_raw(name, nil)
21
+ end
22
+ end
23
+
24
+ def record_options(record)
25
+ query = model(record).where()
26
+ query = query.sort(@options['order'].to_s) if @options['order']
27
+ query.all
28
+ end
29
+
30
+
31
+ private
32
+ def clear(store, record)
33
+ if store.is_a?(Array)
34
+ store.clear
35
+ else
36
+ record.set_raw(name, nil)
37
+ return nil
38
+ end
39
+ end
40
+
41
+ def all(store, record)
42
+ query = model(record).where(_id: store)
43
+ query = query.sort(@options['sort']) if @options['sort']
44
+ query.all
45
+ end
46
+
47
+ def associated(store, record)
48
+ return nil if store.nil?
49
+ model(record).first(_id: store)
50
+ end
51
+ end
@@ -0,0 +1,73 @@
1
+ class Attachment
2
+ attr_accessor :field, :record, :name, :mime
3
+
4
+ def initialize(value, record, field)
5
+ @field = field
6
+ @record = record
7
+ value ||= {}
8
+ @name = value['name']
9
+ @mime = value['mime']
10
+ end
11
+
12
+ def to_hash
13
+ {'name' => name, 'mime' => mime}
14
+ end
15
+
16
+ def url
17
+ @url ||= Pathname.new('/').join(Yodel::ATTACHMENTS_DIRECTORY_NAME, relative_path)
18
+ end
19
+
20
+ def relative_path
21
+ @relative_path ||= File.join(relative_directory_path, @name)
22
+ end
23
+
24
+ def relative_directory_path
25
+ @relative_directory_path ||= File.join(@field.name, @record.id.to_s)
26
+ end
27
+
28
+ def path
29
+ @path ||= File.join(@record.site.attachments_directory, relative_path)
30
+ end
31
+
32
+ def directory_path
33
+ @directory_path ||= File.join(@record.site.attachments_directory, relative_directory_path)
34
+ end
35
+
36
+ def exist?
37
+ return false if @name.nil?
38
+ File.exist?(path)
39
+ end
40
+
41
+ def remove_files
42
+ FileUtils.rm_r directory_path if exist?
43
+ end
44
+
45
+ def reset_memoised_values
46
+ @url = @relative_path = @relative_directory_path = @path = @directory_path = nil
47
+ end
48
+
49
+ def set_file(file)
50
+ # delete the old file and reset memoised paths
51
+ unless @record.new?
52
+ remove_files
53
+ reset_memoised_values
54
+ end
55
+
56
+ # reset the name and mime type of the attachment
57
+ @name = file[:filename]
58
+ @mime = file[:type]
59
+ temp = file[:tempfile]
60
+ temp_path = temp.path
61
+ temp.close
62
+
63
+ # for simplicity we move the uploaded file (from /tmp) rather than copying
64
+ FileUtils.mkpath directory_path
65
+ FileUtils.mv(temp_path, path)
66
+ FileUtils.chmod(0664, path) # (owner: rw, group: rw, other: r)
67
+ end
68
+
69
+ def length
70
+ return 0 unless self.exist?
71
+ return File.size(path)
72
+ end
73
+ end
@@ -0,0 +1,38 @@
1
+ class Image < Attachment
2
+ def set_file(file)
3
+ super(file)
4
+ crop_image
5
+ end
6
+
7
+ def crop_image
8
+ sizes = @field.options['sizes'].to_hash.merge('admin_thumb' => '100x100')
9
+ return unless exist?
10
+ sizes.each do |size_name, size|
11
+ image = MiniMagick::Image.open(path.to_s)
12
+ image.resize("#{size}^")
13
+ image.format('jpeg')
14
+ image.write(resized_image_path(size_name, false).to_s)
15
+ end
16
+ end
17
+
18
+ # TODO: shouldn't always be .jpg; have image extension as an option
19
+ def resized_image_path(size, crop_if_required=true)
20
+ return path if size.nil? || size == :original
21
+ sized_path = File.join(@record.site.attachments_directory, relative_directory_path, "#{size}.jpg")
22
+ crop_image unless File.exist?(sized_path) || !crop_if_required
23
+ sized_path
24
+ end
25
+
26
+ # TODO: relative path from is quite a complex method; we should optimise the whole path system here somehow
27
+ def relative_resized_image_path(name, crop_if_required=true)
28
+ Pathname.new(resized_image_path(name, crop_if_required)).relative_path_from(Pathname.new(@record.site.attachments_directory))
29
+ end
30
+
31
+ def url(size=:original, crop_if_required=true)
32
+ if size == :original
33
+ super()
34
+ else
35
+ Pathname.new('/').join(relative_resized_image_path(size, crop_if_required))
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ Dir.chdir(File.dirname(__FILE__)) do
2
+ require './attachments/attachment'
3
+ require './attachments/image'
4
+ require './fields/fields'
5
+ require './validations/validations'
6
+ require './associations/associations'
7
+ require './mongo/mongo'
8
+ require './record/record'
9
+ require './model/model'
10
+ require './functions/functions'
11
+ require './log/log'
12
+ require './site/migration'
13
+ require './site/remote'
14
+ require './site/site'
15
+ end
@@ -0,0 +1,32 @@
1
+ class AliasField < Field
2
+ # FIXME: assignments don't work; alias = val won't be saved
3
+ # FIXME: changes to the original value aren't observed;
4
+ # original = val_1, alias => val_1; original = val_2; alias => val_1
5
+ def strip_nil?
6
+ true
7
+ end
8
+
9
+ def default_input_type
10
+ nil
11
+ end
12
+
13
+ def validate(record, errors)
14
+ # noop
15
+ end
16
+
17
+ def typecast(value, record)
18
+ field_name = @options['of'].to_s
19
+ raise InvalidField, "Alias fields must have a from property" if field_name.blank?
20
+ record.get(field_name)
21
+ end
22
+
23
+ def untypecast(value, record)
24
+ nil
25
+ end
26
+
27
+ def from_json(value, record)
28
+ nil
29
+ end
30
+ end
31
+
32
+ Field::TYPES['alias'] = AliasField
@@ -0,0 +1,64 @@
1
+ class ArrayField < Field
2
+ # TODO: validate should defer to @element_type over each element
3
+ def initialize(name, options={})
4
+ @element_type = Field.from_options(name, 'type' => options['of'].to_s.singularize) if options['of']
5
+ super
6
+ end
7
+
8
+ def json_action(action, value, record)
9
+ array = record.get_raw(name)
10
+ value = process(value, record, :from_json)
11
+ value = [value] unless value.is_a?(Array)
12
+
13
+ case action
14
+ when 'set'
15
+ array = value
16
+ when 'add'
17
+ array += value
18
+ when 'add_unique'
19
+ array |= value
20
+ when 'remove'
21
+ array -= value
22
+ when 'clear'
23
+ array = []
24
+ end
25
+
26
+ record.set_raw(name, array)
27
+ record.changed!(name)
28
+ end
29
+
30
+ def typecast(value, record)
31
+ value = [] unless value.is_a?(Array)
32
+ ChangeSensitiveArray.new(record, name, process(value, record, :typecast))
33
+ end
34
+
35
+ def untypecast(value, record)
36
+ return [] if value.blank?
37
+ process(value.to_a, record, :untypecast)
38
+ end
39
+
40
+ def from_json(value, record)
41
+ return [] if value.blank?
42
+ if value.is_a?(String)
43
+ if value.include?(',')
44
+ value = value.split(',')
45
+ else
46
+ value = value.split(' ')
47
+ end
48
+ end
49
+ process(value.to_a, record, :from_json)
50
+ end
51
+
52
+
53
+ private
54
+ def process(values, record, method)
55
+ return values if @element_type.nil?
56
+ if values.is_a?(Array)
57
+ values.collect {|element| @element_type.send(method, element, record)}
58
+ else
59
+ @element_type.send(method, values, record)
60
+ end
61
+ end
62
+ end
63
+
64
+ Field::TYPES['array'] = ArrayField
@@ -0,0 +1,42 @@
1
+ class AttachmentField < Field
2
+ def default_input_type
3
+ :file
4
+ end
5
+
6
+ def untypecast(value, record)
7
+ # expecting an Attachment object, or a hash of the original data
8
+ value.to_hash
9
+ end
10
+
11
+ def typecast(value, record)
12
+ Attachment.new(value, record, self)
13
+ end
14
+
15
+ def json_action(action, value, record)
16
+ original = record.get(name)
17
+ case action
18
+ when 'set'
19
+ return if value.nil?
20
+ original.set_file(value)
21
+ record.set_raw(name, original.to_hash)
22
+ when 'clear'
23
+ original.remove_files
24
+ record.set_raw(name, nil)
25
+ end
26
+ record.changed!(name)
27
+ end
28
+
29
+ def from_json(value, record)
30
+ original = record.get(name)
31
+ original.set_file(value)
32
+ original
33
+ end
34
+
35
+ def after_destroy(record)
36
+ if record.get(name).is_a?(Attachment)
37
+ record.get(name).remove_files
38
+ end
39
+ end
40
+ end
41
+
42
+ Field::TYPES['attachment'] = AttachmentField
@@ -0,0 +1,28 @@
1
+ class BooleanField < Field
2
+ def default_input_type
3
+ :radio
4
+ end
5
+
6
+ def json_action(action, value, record)
7
+ case action
8
+ when 'set'
9
+ record.set_raw(name, !!value)
10
+ when 'toggle'
11
+ record.set_raw(name, !record.get(name))
12
+ end
13
+
14
+ record.changed!(name)
15
+ end
16
+
17
+ def from_json(value, record)
18
+ if value == 'true'
19
+ true
20
+ elsif value == 'false'
21
+ false
22
+ else
23
+ !!value
24
+ end
25
+ end
26
+ end
27
+
28
+ Field::TYPES['boolean'] = BooleanField
@@ -0,0 +1,96 @@
1
+ # Notify the record owning this value whenever the underlying array
2
+ # changes. Records rely on assignment to determine when a value has
3
+ # changed, so mutable objects need to notify the record when they are
4
+ # updated. This is not an exhaustive list of ways to mutate an array,
5
+ # just some common methods used in Yodel already.
6
+ class ChangeSensitiveArray
7
+ attr_reader :array
8
+ def initialize(record, field, array)
9
+ @record = record
10
+ @field = field
11
+ @array = array
12
+ end
13
+
14
+ def inspect
15
+ @array.inspect
16
+ end
17
+
18
+ def to_a
19
+ @array
20
+ end
21
+
22
+ def to_s
23
+ @array.collect(&:to_s).join(', ')
24
+ end
25
+
26
+ def push(value)
27
+ notify!
28
+ @array.push(value)
29
+ end
30
+
31
+ def clear
32
+ notify!
33
+ @array.clear
34
+ end
35
+
36
+ def pop
37
+ notify!
38
+ @array.pop
39
+ end
40
+
41
+ def <<(value)
42
+ notify!
43
+ @array << value
44
+ end
45
+
46
+ def delete(value)
47
+ notify!
48
+ @array.delete(value)
49
+ end
50
+
51
+ def []=(index, value)
52
+ notify!
53
+ @array[index] = value
54
+ end
55
+
56
+ def each(&block)
57
+ @array.each(&block)
58
+ end
59
+
60
+ def collect(&block)
61
+ @array.collect(&block)
62
+ end
63
+
64
+ def count(*item, &block)
65
+ @array.count(*item, &block)
66
+ end
67
+
68
+ def size
69
+ @array.size
70
+ end
71
+
72
+ def include?(item)
73
+ @array.include?(item)
74
+ end
75
+
76
+ def method_missing(name, *args, &block)
77
+ notify! if name.to_s.end_with?('!')
78
+ @array.send(name, *args, &block)
79
+ end
80
+
81
+ # Calling changed! on @record will call dup on this array before any mutating
82
+ # operation has been performed. We need to store the original unedited version
83
+ # in typecast (the 'was' value), then return this array since the mutating
84
+ # operation is being performed on it. Since dup returns self, the array being
85
+ # operated on will be stored in @record.changed, and be modified by the op.
86
+ def dup
87
+ copy = ChangeSensitiveArray.new(@record.dup, @field.dup, @array.dup)
88
+ @record.typecast[@field] = copy
89
+ self
90
+ end
91
+
92
+ private
93
+ def notify!
94
+ @record.try(:changed!, @field)
95
+ end
96
+ end