yodel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,15 @@
1
+ class HTMLField < TextField
2
+ def search_terms_set(record)
3
+ to_text(record.get(name)).gsub(/\W+/, ' ').split
4
+ end
5
+
6
+ def to_text(html)
7
+ Hpricot(html.to_s).search('//text()').collect(&:to_s).collect(&:strip).join(' ').strip
8
+ end
9
+
10
+ def default_input_type
11
+ :html
12
+ end
13
+ end
14
+
15
+ Field::TYPES['html'] = HTMLField
@@ -0,0 +1,11 @@
1
+ class ImageField < AttachmentField
2
+ def default_input_type
3
+ :image
4
+ end
5
+
6
+ def typecast(value, record)
7
+ Image.new(value, record, self)
8
+ end
9
+ end
10
+
11
+ Field::TYPES['image'] = ImageField
@@ -0,0 +1,25 @@
1
+ class IntegerField < Field
2
+ def numeric?
3
+ true
4
+ end
5
+
6
+ def json_action(action, value, record)
7
+ case action
8
+ when 'set'
9
+ record.set_raw(name, value.to_i)
10
+ when 'increment'
11
+ record.increment!(name, value.to_i)
12
+ end
13
+ record.changed!(name)
14
+ end
15
+
16
+ def untypecast(value, record)
17
+ value.to_i
18
+ end
19
+
20
+ def from_json(value, record)
21
+ value.to_i
22
+ end
23
+ end
24
+
25
+ Field::TYPES['integer'] = IntegerField
@@ -0,0 +1,21 @@
1
+ class PasswordField < StringField
2
+ undef search_terms_set
3
+ def default_input_type
4
+ :password
5
+ end
6
+
7
+ def validate(record, errors)
8
+ PasswordConfirmationValidation.validate(nil, self, name, nil, record, errors)
9
+ super
10
+ end
11
+
12
+ def from_json(value, record)
13
+ if value.blank?
14
+ throw :ignore_value
15
+ else
16
+ value.to_s
17
+ end
18
+ end
19
+ end
20
+
21
+ Field::TYPES['password'] = PasswordField
@@ -0,0 +1,27 @@
1
+ class SelfField < Field
2
+ def strip_nil?
3
+ true
4
+ end
5
+
6
+ def default_input_type
7
+ nil
8
+ end
9
+
10
+ def validate(record, errors)
11
+ # noop
12
+ end
13
+
14
+ def typecast(value, record)
15
+ record
16
+ end
17
+
18
+ def untypecast(value, record)
19
+ nil
20
+ end
21
+
22
+ def from_json(value, record)
23
+ nil
24
+ end
25
+ end
26
+
27
+ Field::TYPES['self'] = SelfField
@@ -0,0 +1,15 @@
1
+ class StringField < Field
2
+ def search_terms_set(record)
3
+ record.get(name).to_s.gsub(/\W+/, ' ').split
4
+ end
5
+
6
+ def untypecast(value, record)
7
+ value.nil? ? nil : value.to_s
8
+ end
9
+
10
+ def from_json(value, record)
11
+ value.to_s
12
+ end
13
+ end
14
+
15
+ Field::TYPES['string'] = StringField
@@ -0,0 +1,7 @@
1
+ class TagsField < ArrayField
2
+ def from_json(value, record)
3
+ value.to_s.split(',').map(&:strip).reject(&:blank?).uniq
4
+ end
5
+ end
6
+
7
+ Field::TYPES['tags'] = TagsField
@@ -0,0 +1,7 @@
1
+ class TextField < StringField
2
+ def default_input_type
3
+ :textarea
4
+ end
5
+ end
6
+
7
+ Field::TYPES['text'] = TextField
@@ -0,0 +1,36 @@
1
+ class TimeField < Field
2
+ def default_input_type
3
+ :datetime
4
+ end
5
+
6
+ def before_create(record)
7
+ return unless name == 'created_at' || name == 'updated_at'
8
+ record.set(name, Time.now.utc)
9
+ end
10
+
11
+ def before_update(record)
12
+ return unless name == 'updated_at'
13
+ record.set(name, Time.now.utc)
14
+ end
15
+
16
+ def typecast(value, record)
17
+ value.nil? ? nil : Time.at(value)
18
+ end
19
+
20
+ def untypecast(value, record)
21
+ value.blank? ? nil : Time.at(value.to_i).utc
22
+ end
23
+
24
+ def from_json(value, record)
25
+ return nil unless value.present? && (value.is_a?(String) || value.is_a?(Hash))
26
+ if value.is_a?(Hash)
27
+ return nil unless ['year', 'month', 'day', 'hour', 'min'].all? {|field| value.key?(field) && !value[field].blank?}
28
+ sec = value['sec'] || 0
29
+ Time.new(value['year'], value['month'], value['day'], value['hour'], value['min'], sec).utc
30
+ else
31
+ Time.parse(value).utc
32
+ end
33
+ end
34
+ end
35
+
36
+ Field::TYPES['time'] = TimeField
@@ -0,0 +1,471 @@
1
+ class Function
2
+ attr_accessor :instructions, :source
3
+
4
+ def initialize(param)
5
+ if param.is_a?(String)
6
+ @source = param
7
+ @instructions = compile(param)
8
+ else
9
+ @instructions = param
10
+ end
11
+ end
12
+
13
+ def inspect(instruction=nil)
14
+ instruction ||= self.instructions
15
+
16
+ name = instruction.shift
17
+ parameters = instruction.collect do |parameter|
18
+ if parameter.is_a?(Array)
19
+ inspect(parameter)
20
+ else
21
+ parameter
22
+ end
23
+ end
24
+
25
+ "#{name}(#{parameters.join(', ')})"
26
+ end
27
+
28
+
29
+ # ----------------------------------------
30
+ # Compilation
31
+ # ----------------------------------------
32
+ CALL_TOKEN = '.'
33
+ START_PARAMS_TOKEN = '('
34
+ END_PARAMS_TOKEN = ')'
35
+ START_HASH_TOKEN = '{'
36
+ END_HASH_TOKEN = '}'
37
+ PARAM_DELIM_TOKEN = ','
38
+ HASH_DELIM_TOKEN = ':'
39
+ ENTRY_FLAG = '!'
40
+ DOUBLE_QUOTE_TOKEN = '"'
41
+ SINGLE_QUOTE_TOKEN = "'"
42
+
43
+ def compile(source)
44
+ tokens = source.scan(/[\w\-\+]+|\.|\(|\)|\{|\}|:|,|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/)
45
+ parse(tokens)
46
+ end
47
+
48
+ def parse(tokens)
49
+ params = false
50
+ chain = false
51
+ hash = false
52
+ instructions = []
53
+
54
+ until tokens.empty?
55
+ token = tokens.shift
56
+ case token[0]
57
+ when CALL_TOKEN
58
+ chain = true
59
+ when START_PARAMS_TOKEN
60
+ params = true
61
+ instructions += parse(tokens)
62
+ when END_PARAMS_TOKEN
63
+ tokens.unshift(END_PARAMS_TOKEN) unless params
64
+ break
65
+ when ENTRY_FLAG
66
+ hash = true
67
+ instructions << ['entry'] + parse(tokens)
68
+ when START_HASH_TOKEN
69
+ tokens.unshift(ENTRY_FLAG)
70
+ instructions << ['hash'] + parse(tokens)
71
+ when END_HASH_TOKEN
72
+ tokens.unshift(END_HASH_TOKEN) if hash
73
+ break
74
+ when HASH_DELIM_TOKEN
75
+ instructions += parse(tokens)
76
+ when PARAM_DELIM_TOKEN
77
+ if params || hash
78
+ tokens.unshift(ENTRY_FLAG) if hash
79
+ instructions += parse(tokens)
80
+ else
81
+ tokens.unshift(PARAM_DELIM_TOKEN)
82
+ break
83
+ end
84
+ when DOUBLE_QUOTE_TOKEN, SINGLE_QUOTE_TOKEN
85
+ instructions << ['string', token[1...-1]]
86
+ else
87
+ if tokens.first == START_PARAMS_TOKEN
88
+ instructions << [token] + parse(tokens)
89
+ elsif token.to_i.to_s == token
90
+ instructions << ['int', token]
91
+ else
92
+ instructions << ['field', token]
93
+ end
94
+ end
95
+ end
96
+
97
+ if chain
98
+ [['chain'] + instructions]
99
+ else
100
+ instructions
101
+ end
102
+ end
103
+
104
+ # ----------------------------------------
105
+ # Execution
106
+ # ----------------------------------------
107
+ def execute(context, instruction=nil, parent_context=nil)
108
+ instruction ||= self.instructions.first
109
+ name, *params = instruction
110
+ parent_context ||= context
111
+
112
+ case name
113
+ when 'chain'
114
+ chain(context, parent_context, params)
115
+ when 'field'
116
+ get_field(context, parent_context, params.first)
117
+ when 'find'
118
+ find_record(context, parent_context, params[0], params[1])
119
+ when 'changed'
120
+ changed(context, parent_context, params.first)
121
+ when 'previous_value'
122
+ previous_value(context, parent_context, params.first)
123
+ when 'collect'
124
+ collect(context, parent_context, params.first)
125
+ when 'majority'
126
+ majority(context, parent_context, params.first)
127
+ when 'count'
128
+ count(context, parent_context, params.first)
129
+ when 'invert'
130
+ invert(context)
131
+ when 'unique'
132
+ unique(context, parent_context, params.first)
133
+ when 'average'
134
+ average(context, parent_context, params)
135
+ when 'as_a_percentage_of'
136
+ as_a_percentage_of(context, parent_context, params.first)
137
+ when 'present'
138
+ present(context)
139
+ when 'blank'
140
+ blank(context)
141
+ when 'sum'
142
+ sum(context, parent_context, params)
143
+ when 'subtract'
144
+ subtract(context, parent_context, params)
145
+ when 'multiply'
146
+ multiply(context, parent_context, params)
147
+ when 'round'
148
+ round(context)
149
+ when 'if'
150
+ binary_if(context, parent_context, params[0], params[1], params[2])
151
+ when 'greater_than'
152
+ greater_than(context, parent_context, params.first)
153
+ when 'greater_than_or_equal_to'
154
+ greater_than_or_equal_to(context, parent_context, params.first)
155
+ when 'less_than'
156
+ less_than(context, parent_context, params.first)
157
+ when 'less_than_or_equal_to'
158
+ less_than_or_equal_to(context, parent_context, params.first)
159
+ when 'not_equal'
160
+ not_equal(context, parent_context, params.first)
161
+ when 'and'
162
+ binary_and(context, parent_context, params[0], params[1])
163
+ when 'or'
164
+ binary_or(context, parent_context, params[0], params[1])
165
+ when 'include'
166
+ set_include(context, parent_context, params.first)
167
+ when 'strip'
168
+ strip(context)
169
+ when 'format'
170
+ format(context, parent_context, params.first)
171
+ when 'set'
172
+ set_field(context, parent_context, params[0], params[1])
173
+ when 'update'
174
+ update_field(context, parent_context, params[0], params[1])
175
+ when 'min'
176
+ min(context, parent_context, params[0], params[1])
177
+ when 'max'
178
+ max(context, parent_context, params[0], params[1])
179
+ when 'increment'
180
+ increment(context, parent_context, params[0], params[1])
181
+ when 'complement'
182
+ complement(context, parent_context, params[0], params[1])
183
+ when 'each'
184
+ each(context, parent_context, params.first)
185
+ when 'deliver'
186
+ deliver_email(context, parent_context, params[0], params[1])
187
+ when 'call_api'
188
+ call_api(context, parent_context, params[0], params[1])
189
+
190
+ # literals
191
+ when 'string'
192
+ params.first
193
+ when 'int'
194
+ params.first.to_i
195
+ when 'hash'
196
+ Hash[params.collect {|entry| execute(context, entry, parent_context)}]
197
+ when 'entry'
198
+ [execute(context, params.first, parent_context), execute(context, params.last, parent_context)]
199
+ end
200
+ end
201
+
202
+ protected
203
+ def chain(context, parent_context, methods)
204
+ #parent_context = context
205
+ methods.each do |method|
206
+ context = execute(context, method, parent_context)
207
+ end
208
+ context
209
+ end
210
+
211
+ def get_field(context, parent_context, name)
212
+ case name
213
+ when 'self'
214
+ context
215
+ when 'root'
216
+ parent_context
217
+ # TODO: these should be caught as booleans instead of being treated as field names
218
+ when 'true'
219
+ true
220
+ when 'false'
221
+ false
222
+ else
223
+ context.get(name)
224
+ end
225
+ rescue
226
+ nil
227
+ end
228
+
229
+ def find_record(context, parent_context, model_name, key)
230
+ raise "Parent context of find_record must respond to site" unless parent_context.respond_to?(:site)
231
+ model_name = execute(context, model_name, parent_context)
232
+ key = execute(context, key, parent_context)
233
+ model = parent_context.site.model_by_plural_name(model_name)
234
+
235
+ if key == 'id'
236
+ value = BSON::ObjectId.from_string(context)
237
+ else
238
+ value = context
239
+ end
240
+
241
+ model.where(key => value).first
242
+ end
243
+
244
+ def previous_value(context, parent_context, name)
245
+ context.field_was(execute(context, name, parent_context))
246
+ end
247
+
248
+ # TODO: change format from changed('name') to name.changed
249
+ def changed(context, parent_context, name)
250
+ context.changed?(execute(context, name, parent_context))
251
+ end
252
+
253
+ def set_field(context, parent_context, field, value)
254
+ field = execute(parent_context, field, parent_context)
255
+ value = execute(parent_context, value, parent_context)
256
+ context.set(field, value)
257
+ end
258
+
259
+ def update_field(context, parent_context, field, value)
260
+ set_field(context, parent_context, field, value)
261
+ context.save
262
+ end
263
+
264
+ def increment(context, parent_context, field, value)
265
+ field = execute(context, field, parent_context)
266
+ value = execute(context, value, parent_context)
267
+ context.increment!(field, value)
268
+ end
269
+
270
+ def collect(context, parent_context, field)
271
+ raise "Context to collect must respond to collect" unless context.respond_to?(:collect)
272
+ context.collect {|item| execute(item, field, parent_context)}
273
+ end
274
+
275
+ def each(context, parent_context, statement)
276
+ raise "Context to each must respond to each" unless context.respond_to?(:each)
277
+ context.each {|item| execute(item, statement, parent_context)}
278
+ end
279
+
280
+ def majority(context, parent_context, field)
281
+ unless context.respond_to?(:size) && context.respond_to?(:count)
282
+ raise "Majority context must be enumerable"
283
+ end
284
+
285
+ valid = context.count {|item| execute(item, field, parent_context)}
286
+ valid >= (context.size - valid)
287
+ end
288
+
289
+ def count(context, parent_context, field)
290
+ unless context.respond_to?(:size) && context.respond_to?(:count)
291
+ raise "Count context must be enumerable"
292
+ end
293
+
294
+ if field.nil?
295
+ context.size
296
+ else
297
+ context.count {|item| execute(item, field, parent_context)}
298
+ end
299
+ end
300
+
301
+ def invert(context)
302
+ !context
303
+ end
304
+
305
+ def unique(context, parent_context, field)
306
+ collect(context, parent_context, field).uniq
307
+ end
308
+
309
+ def average(context, parent_context, params)
310
+ if context.respond_to?(:collect) && context.respond_to?(:size) && params.size == 1
311
+ count = context.size
312
+ else
313
+ count = params.size
314
+ end
315
+
316
+ return 0.0 if count == 0
317
+ sum(context, parent_context, params).to_f / count.to_f
318
+ end
319
+
320
+ def as_a_percentage_of(context, parent_context, count)
321
+ count = execute(context, count, parent_context)
322
+ context = context.to_f
323
+ count = count.to_f
324
+
325
+ if context == 0 || count == 0
326
+ return 0
327
+ else
328
+ (context / count) * 100
329
+ end
330
+ end
331
+
332
+ def present(context)
333
+ context.present?
334
+ end
335
+
336
+ def blank(context)
337
+ context.blank?
338
+ end
339
+
340
+ def sum(context, parent_context, params)
341
+ if context.respond_to?(:collect) && params.size == 1
342
+ collect(context, parent_context, params.first).compact.inject(&:+)
343
+ else
344
+ params.collect {|method| execute(context, method, parent_context)}.compact.inject(&:+)
345
+ end
346
+ end
347
+
348
+ def subtract(context, parent_context, params)
349
+ if context.respond_to?(:collect) && params.size == 1
350
+ collect(context, parent_context, params.first).compact.inject(&:-)
351
+ else
352
+ params.collect {|method| execute(context, method, parent_context)}.compact.inject(&:-)
353
+ end
354
+ end
355
+
356
+ def multiply(context, parent_context, params)
357
+ if context.respond_to?(:collect) && params.size == 1
358
+ collect(context, parent_context, params.first).compact.inject(&:*)
359
+ else
360
+ params.collect {|method| execute(context, method, parent_context)}.compact.inject(&:*)
361
+ end
362
+ end
363
+
364
+ def complement(context, parent_context, set1, set2)
365
+ set1 = execute(context, set1, parent_context)
366
+ set2 = execute(context, set2, parent_context)
367
+ raise "Sets must be iterable" unless set1.respond_to?(:to_a) && set2.respond_to?(:to_a)
368
+ set2.to_a - set1.to_a
369
+ end
370
+
371
+ def set_include(context, parent_context, item)
372
+ item = execute(context, item, parent_context)
373
+ raise "Context must respond to include?" unless context.respond_to?(:include?)
374
+ context.include?(item)
375
+ end
376
+
377
+ def round(context)
378
+ context.to_f.round
379
+ end
380
+
381
+ def binary_if(context, parent_context, condition, true_exp, false_exp)
382
+ if execute(context, condition, parent_context)
383
+ execute(context, true_exp, parent_context) if true_exp
384
+ else
385
+ execute(context, false_exp, parent_context) if false_exp
386
+ end
387
+ end
388
+
389
+ def greater_than(context, parent_context, value)
390
+ value = execute(parent_context, value, parent_context)
391
+ context > value
392
+ end
393
+
394
+ def less_than(context, parent_context, value)
395
+ value = execute(parent_context, value, parent_context)
396
+ context < value
397
+ end
398
+
399
+ def greater_than_or_equal_to(context, parent_context, value)
400
+ value = execute(parent_context, value, parent_context)
401
+ context >= value
402
+ end
403
+
404
+ def less_than_or_equal_to(context, parent_context, value)
405
+ value = execute(parent_context, value, parent_context)
406
+ context <= value
407
+ end
408
+
409
+ def not_equal(context, parent_context, value)
410
+ value = execute(parent_context, value, parent_context)
411
+ context != value
412
+ end
413
+
414
+ def binary_and(context, parent_context, operand1, operand2)
415
+ operand1 = execute(context, operand1, parent_context)
416
+
417
+ if operand2
418
+ operand2 = execute(context, operand2, parent_context)
419
+ operand1 && operand2
420
+ else
421
+ context && operand1
422
+ end
423
+ end
424
+
425
+ def binary_or(context, parent_context, operand1, operand2)
426
+ operand1 = execute(context, operand1, parent_context)
427
+
428
+ if operand2
429
+ operand2 = execute(context, operand2, parent_context)
430
+ operand1 || operand2
431
+ else
432
+ context || operand1
433
+ end
434
+ end
435
+
436
+ def strip(context)
437
+ context.to_s.strip
438
+ end
439
+
440
+ def deliver_email(context, parent_context, name, hash)
441
+ raise "Context of deliver_email must respond to site" unless context.respond_to?(:site)
442
+ email = context.site.emails[execute(context, name, parent_context)]
443
+ email.deliver(execute(context, hash, parent_context))
444
+ end
445
+
446
+ def call_api(context, parent_context, name, hash)
447
+ raise "Context of call_api must respond to site" unless context.respond_to?(:site)
448
+ api = context.site.api_calls[execute(context, name, parent_context)]
449
+ api.call(execute(context, hash, parent_context))
450
+ end
451
+
452
+ def format(context, parent_context, str)
453
+ str = execute(context, str, parent_context)
454
+ str.gsub(/{{\s*([\w\.]+)\s*}}/) do |field|
455
+ fn = Function.new($1)
456
+ fn.execute(context, nil, parent_context)
457
+ end
458
+ end
459
+
460
+ # TODO: add call styles to min and max:
461
+ # items.min(index)
462
+ # items.collect(index).min
463
+ # min(one, two)
464
+ def min(context, parent_context, one, two)
465
+ [execute(context, one, parent_context), execute(context, two, parent_context)].min
466
+ end
467
+
468
+ def max(context, parent_context, one, two)
469
+ [execute(context, one, parent_context), execute(context, two, parent_context)].max
470
+ end
471
+ end