tr8n_core 4.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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +69 -0
  4. data/Rakefile +9 -0
  5. data/config/config.yml +34 -0
  6. data/config/tokens/data.yml +45 -0
  7. data/config/tokens/decorations.yml +37 -0
  8. data/lib/tr8n/application.rb +320 -0
  9. data/lib/tr8n/base.rb +123 -0
  10. data/lib/tr8n/cache.rb +144 -0
  11. data/lib/tr8n/cache_adapters/cdb.rb +74 -0
  12. data/lib/tr8n/cache_adapters/file.rb +70 -0
  13. data/lib/tr8n/cache_adapters/memcache.rb +91 -0
  14. data/lib/tr8n/cache_adapters/redis.rb +94 -0
  15. data/lib/tr8n/component.rb +68 -0
  16. data/lib/tr8n/config.rb +291 -0
  17. data/lib/tr8n/decorators/base.rb +35 -0
  18. data/lib/tr8n/decorators/default.rb +30 -0
  19. data/lib/tr8n/decorators/html.rb +63 -0
  20. data/lib/tr8n/exception.rb +26 -0
  21. data/lib/tr8n/language.rb +250 -0
  22. data/lib/tr8n/language_case.rb +116 -0
  23. data/lib/tr8n/language_case_rule.rb +85 -0
  24. data/lib/tr8n/language_context.rb +115 -0
  25. data/lib/tr8n/language_context_rule.rb +62 -0
  26. data/lib/tr8n/logger.rb +83 -0
  27. data/lib/tr8n/rules_engine/evaluator.rb +156 -0
  28. data/lib/tr8n/rules_engine/parser.rb +83 -0
  29. data/lib/tr8n/source.rb +95 -0
  30. data/lib/tr8n/tokens/data.rb +410 -0
  31. data/lib/tr8n/tokens/data_tokenizer.rb +82 -0
  32. data/lib/tr8n/tokens/decoration_tokenizer.rb +200 -0
  33. data/lib/tr8n/tokens/hidden.rb +48 -0
  34. data/lib/tr8n/tokens/method.rb +52 -0
  35. data/lib/tr8n/tokens/transform.rb +191 -0
  36. data/lib/tr8n/translation.rb +104 -0
  37. data/lib/tr8n/translation_key.rb +205 -0
  38. data/lib/tr8n/translator.rb +62 -0
  39. data/lib/tr8n/utils.rb +124 -0
  40. data/lib/tr8n_core/ext/array.rb +74 -0
  41. data/lib/tr8n_core/ext/date.rb +63 -0
  42. data/lib/tr8n_core/ext/fixnum.rb +39 -0
  43. data/lib/tr8n_core/ext/hash.rb +126 -0
  44. data/lib/tr8n_core/ext/string.rb +44 -0
  45. data/lib/tr8n_core/ext/time.rb +71 -0
  46. data/lib/tr8n_core/generators/cache/base.rb +85 -0
  47. data/lib/tr8n_core/generators/cache/cdb.rb +27 -0
  48. data/lib/tr8n_core/generators/cache/file.rb +69 -0
  49. data/lib/tr8n_core/modules/logger.rb +43 -0
  50. data/lib/tr8n_core/version.rb +27 -0
  51. data/lib/tr8n_core.rb +68 -0
  52. data/spec/application_spec.rb +228 -0
  53. data/spec/base_spec.rb +19 -0
  54. data/spec/config_spec.rb +16 -0
  55. data/spec/decorator_spec.rb +10 -0
  56. data/spec/decorators/base_spec.rb +14 -0
  57. data/spec/decorators/default_spec.rb +12 -0
  58. data/spec/decorators/html_spec.rb +50 -0
  59. data/spec/fixtures/application.json +112 -0
  60. data/spec/fixtures/languages/en-US.json +1424 -0
  61. data/spec/fixtures/languages/es.json +291 -0
  62. data/spec/fixtures/languages/ru.json +550 -0
  63. data/spec/fixtures/translations/ru/basic.json +56 -0
  64. data/spec/fixtures/translations/ru/counters.json +43 -0
  65. data/spec/fixtures/translations/ru/genders.json +171 -0
  66. data/spec/fixtures/translations/ru/last_names.txt +200 -0
  67. data/spec/fixtures/translations/ru/names.json +1 -0
  68. data/spec/fixtures/translations/ru/names.txt +458 -0
  69. data/spec/helper.rb +84 -0
  70. data/spec/language_case_rule_spec.rb +57 -0
  71. data/spec/language_case_spec.rb +58 -0
  72. data/spec/language_context_rule_spec.rb +73 -0
  73. data/spec/language_context_spec.rb +331 -0
  74. data/spec/language_spec.rb +16 -0
  75. data/spec/rules_engine/evaluator_spec.rb +148 -0
  76. data/spec/rules_engine/parser_spec.rb +29 -0
  77. data/spec/tokens/data_spec.rb +160 -0
  78. data/spec/tokens/data_tokenizer_spec.rb +29 -0
  79. data/spec/tokens/decoration_tokenizer_spec.rb +81 -0
  80. data/spec/tokens/hidden_spec.rb +24 -0
  81. data/spec/tokens/method_spec.rb +84 -0
  82. data/spec/tokens/transform_spec.rb +50 -0
  83. data/spec/translation_key_spec.rb +96 -0
  84. data/spec/translation_spec.rb +24 -0
  85. data/spec/utils_spec.rb +64 -0
  86. metadata +176 -0
@@ -0,0 +1,116 @@
1
+ #--
2
+ # Copyright (c) 2013 Michael Berkovich, tr8nhub.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ class Tr8n::LanguageCase < Tr8n::Base
25
+ belongs_to :language
26
+ attributes :id, :keyword, :latin_name, :native_name, :description, :application
27
+ has_many :rules
28
+
29
+ TR8N_HTML_TAGS_REGEX = /<\/?[^>]*>/
30
+
31
+ def initialize(attrs = {})
32
+ super
33
+ self.attributes[:rules] = []
34
+ if hash_value(attrs, :rules)
35
+ self.attributes[:rules] = hash_value(attrs, :rules).collect{ |rule| Tr8n::LanguageCaseRule.new(rule.merge(:language_case => self)) }
36
+ end
37
+ end
38
+
39
+ def find_matching_rule(value, object = nil)
40
+ rules.each do |rule|
41
+ return rule if rule.evaluate(value, object)
42
+ end
43
+ nil
44
+ end
45
+
46
+ #######################################################################################################
47
+ ## Evaluation Methods
48
+ #######################################################################################################
49
+
50
+ def apply(value, object = nil, options = {})
51
+ value = value.to_s
52
+ html_tokens = value.scan(TR8N_HTML_TAGS_REGEX).uniq
53
+ sanitized_value = value.gsub(TR8N_HTML_TAGS_REGEX, "")
54
+
55
+ if application.to_sym == :phrase
56
+ words = [sanitized_value]
57
+ else
58
+ words = sanitized_value.split(/[\s\/\\]/).uniq
59
+ end
60
+
61
+ # replace html tokens with temporary placeholders {$h1}
62
+ html_tokens.each_with_index do |html_token, index|
63
+ value = value.gsub(html_token, "{$h#{index}}")
64
+ end
65
+
66
+ # replace words with temporary placeholders {$w1}
67
+ words.each_with_index do |word, index|
68
+ value = value.gsub(word, "{$w#{index}}")
69
+ end
70
+
71
+ transformed_words = []
72
+ words.each do |word|
73
+ case_rule = find_matching_rule(word, object)
74
+ case_value = case_rule ? case_rule.apply(word) : word
75
+ transformed_words << decorate(word, case_value, case_rule, options)
76
+ end
77
+
78
+ # replace back the temporary placeholders with the html tokens
79
+ transformed_words.each_with_index do |word, index|
80
+ value = value.gsub("{$w#{index}}", word)
81
+ end
82
+
83
+ # replace back the temporary placeholders with the html tokens
84
+ html_tokens.each_with_index do |html_token, index|
85
+ value = value.gsub("{$h#{index}}", html_token)
86
+ end
87
+
88
+ value
89
+ end
90
+
91
+ #######################################################################################################
92
+ ## Decoration Methods - TODO: move to decorators
93
+ #######################################################################################################
94
+
95
+ def decorate(word, case_value, case_rule, options = {})
96
+ return case_value if options[:skip_decorations]
97
+ return case_value if language.default?
98
+ return case_value unless Tr8n.config.current_translator and Tr8n.config.current_translator.inline?
99
+
100
+ "<span class='tr8n_language_case' data-case_id='#{id}' data-rule_id='#{case_rule ? case_rule.id : ''}' data-case_key='#{word.gsub("'", "\'")}'>#{case_value}</span>"
101
+ end
102
+
103
+ #######################################################################################################
104
+ ## Cache Methods
105
+ #######################################################################################################
106
+
107
+ def to_cache_hash
108
+ hash = to_hash(:id, :keyword, :description, :latin_name, :native_name)
109
+ hash["rules"] = []
110
+ rules.each do |rule|
111
+ hash["rules"] << rule.to_cache_hash
112
+ end
113
+ hash
114
+ end
115
+
116
+ end
@@ -0,0 +1,85 @@
1
+ #--
2
+ # Copyright (c) 2013 Michael Berkovich, tr8nhub.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ class Tr8n::LanguageCaseRule < Tr8n::Base
25
+ belongs_to :language_case
26
+ attributes :id, :description, :examples, :conditions, :conditions_expression, :operations, :operations_expression
27
+
28
+ def conditions_expression
29
+ self.attributes[:conditions_expression] ||= Tr8n::RulesEngine::Parser.new(conditions).parse
30
+ end
31
+
32
+ def operations_expression
33
+ self.attributes[:operations_expression] ||= Tr8n::RulesEngine::Parser.new(operations).parse
34
+ end
35
+
36
+ def gender_variables(object)
37
+ return {} unless conditions.index('@gender')
38
+ return {"@gender" => "unknown"} unless object
39
+ context = language_case.language.context_by_keyword(:gender)
40
+ return {"@gender" => "unknown"} unless context
41
+ context.vars(object)
42
+ end
43
+
44
+ #######################################################################################################
45
+ ## Evaluation Methods
46
+ #######################################################################################################
47
+
48
+ def evaluate(value, object = nil)
49
+ return false if conditions.nil?
50
+
51
+ re = Tr8n::RulesEngine::Evaluator.new
52
+ re.evaluate(["let", "@value", value])
53
+
54
+ gender_variables(object).each do |key, value|
55
+ re.evaluate(["let", key, value])
56
+ end
57
+
58
+ re.evaluate(conditions_expression)
59
+ rescue Exception => ex
60
+ Tr8n.logger.error("Failed to evaluate language case #{conditions}: #{ex.message}")
61
+ value
62
+ end
63
+
64
+ def apply(value)
65
+ value = value.to_s
66
+ return value if operations.nil?
67
+
68
+ re = Tr8n::RulesEngine::Evaluator.new
69
+ re.evaluate(["let", "@value", value])
70
+
71
+ re.evaluate(operations_expression)
72
+ rescue Exception => ex
73
+ Tr8n.logger.error("Failed to apply language case rule [case: #{language_case.id}] [rule: #{id}] [conds: #{conditions_expression}] [opers: #{operations_expression}]: #{ex.message}")
74
+ value
75
+ end
76
+
77
+ #######################################################################################################
78
+ ## Cache Methods
79
+ #######################################################################################################
80
+
81
+ def to_cache_hash
82
+ to_hash(:id, :description, :examples, :conditions, :conditions_expression, :operations, :operations_expression)
83
+ end
84
+
85
+ end
@@ -0,0 +1,115 @@
1
+ #--
2
+ # Copyright (c) 2013 Michael Berkovich, tr8nhub.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+
25
+ class Tr8n::LanguageContext < Tr8n::Base
26
+ belongs_to :language
27
+ attributes :keyword, :description, :default_key, :token_expression, :variables, :token_mapping
28
+ has_many :keys, :rules
29
+
30
+ def initialize(attrs = {})
31
+ super
32
+
33
+ self.attributes[:rules] = {}
34
+ if hash_value(attrs, :rules)
35
+ hash_value(attrs, :rules).each do |key, rule|
36
+ self.attributes[:rules][key] = Tr8n::LanguageContextRule.new(rule.merge(:language_context => self))
37
+ end
38
+ end
39
+ end
40
+
41
+ def config
42
+ context_rules = Tr8n.config.context_rules
43
+ hash_value(context_rules, keyword) || {}
44
+ end
45
+
46
+ def token_expression
47
+ @token_expression ||= begin
48
+ exp = self.attributes[:token_expression]
49
+ exp = Regexp.new(exp[1..-2])
50
+ exp
51
+ end
52
+ end
53
+
54
+ def applies_to_token?(token)
55
+ token_expression.match(token) != nil
56
+ end
57
+
58
+ def fallback_rule
59
+ @fallback_rule ||= rules.values.detect{|rule| rule.fallback?}
60
+ end
61
+
62
+ # prepare variables for evaluation
63
+ def vars(obj)
64
+ vars = {}
65
+
66
+ variables.each do |key|
67
+ method = hash_value(config, "variables.#{key}")
68
+ unless method
69
+ vars[key] = obj
70
+ next
71
+ end
72
+
73
+ if method.is_a?(String)
74
+ if obj.is_a?(Hash)
75
+ object = hash_value(obj, 'object') || obj
76
+ if object.is_a?(Hash)
77
+ vars[key] = hash_value(object, method, :whole => true)
78
+ else
79
+ vars[key] = object.send(method)
80
+ end
81
+ else
82
+ vars[key] = obj.send(method)
83
+ end
84
+ elsif method.is_a?(Proc)
85
+ vars[key] = method.call(obj)
86
+ else
87
+ vars[key] = obj
88
+ end
89
+ end
90
+ vars
91
+ end
92
+
93
+ def find_matching_rule(obj)
94
+ token_vars = vars(obj)
95
+ rules.values.each do |rule|
96
+ next if rule.fallback?
97
+ return rule if rule.evaluate(token_vars)
98
+ end
99
+ fallback_rule
100
+ end
101
+
102
+ #######################################################################################################
103
+ ## Cache Methods
104
+ #######################################################################################################
105
+
106
+ def to_cache_hash
107
+ hash = to_hash(:keyword, :description, :keys, :default_key, :token_expression, :variables, :token_mapping)
108
+ hash[:rules] = {}
109
+ rules.each do |key, rule|
110
+ hash[:rules][key] = rule.to_cache_hash
111
+ end
112
+ hash
113
+ end
114
+
115
+ end
@@ -0,0 +1,62 @@
1
+ #--
2
+ # Copyright (c) 2013 Michael Berkovich, tr8nhub.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ class Tr8n::LanguageContextRule < Tr8n::Base
25
+ belongs_to :language_context
26
+ attributes :keyword, :description, :examples, :conditions, :conditions_expression
27
+
28
+ def fallback?
29
+ keyword.to_s.to_sym == :other
30
+ end
31
+
32
+ def conditions_expression
33
+ self.attributes[:conditions_expression] ||= Tr8n::RulesEngine::Parser.new(conditions).parse
34
+ end
35
+
36
+ #######################################################################################################
37
+ ## Evaluation Methods
38
+ #######################################################################################################
39
+
40
+ def evaluate(vars = {})
41
+ return true if fallback?
42
+
43
+ re = Tr8n::RulesEngine::Evaluator.new
44
+ vars.each do |key, value|
45
+ re.evaluate(["let", key, value])
46
+ end
47
+
48
+ re.evaluate(conditions_expression)
49
+ #rescue Exception => ex
50
+ # Tr8n.logger.error("Failed to evaluate settings context rule #{conditions_expression}: #{ex.message}")
51
+ # false
52
+ end
53
+
54
+ #######################################################################################################
55
+ ## Cache Methods
56
+ #######################################################################################################
57
+
58
+ def to_cache_hash
59
+ to_hash(:keyword, :description, :examples, :conditions, :conditions_expression)
60
+ end
61
+
62
+ end
@@ -0,0 +1,83 @@
1
+ #--
2
+ # Copyright (c) 2013 Michael Berkovich, tr8nhub.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'logger'
25
+
26
+ module Tr8n
27
+
28
+ def self.logger
29
+ @logger ||= begin
30
+ logfile_path = File.expand_path(Tr8n.config.log_path)
31
+ logfile_dir = logfile_path.split("/")[0..-2].join("/")
32
+ FileUtils.mkdir_p(logfile_dir) unless File.exist?(logfile_dir)
33
+ logfile = File.open(logfile_path, 'a')
34
+ logfile.sync = true
35
+ Tr8n::Logger.new(logfile)
36
+ end
37
+ end
38
+
39
+ class Logger < ::Logger
40
+
41
+ def format_message(severity, timestamp, progname, msg)
42
+ "[#{timestamp.strftime("%D %T")}]: #{" " * stack.size}#{msg}\n"
43
+ end
44
+
45
+ def add(severity, message = nil, progname = nil, &block)
46
+ return unless Tr8n.config.logger_enabled?
47
+ super
48
+ end
49
+
50
+ def stack
51
+ @stack ||= []
52
+ end
53
+
54
+ def trace_api_call(path, params)
55
+ debug("api: [/#{path}] #{params.inspect}")
56
+ stack.push(caller)
57
+ t0 = Time.now
58
+ if block_given?
59
+ ret = yield
60
+ end
61
+ t1 = Time.now
62
+ stack.pop
63
+ debug("call took #{t1 - t0} seconds")
64
+ ret
65
+ end
66
+
67
+ def trace(message)
68
+ debug(message)
69
+ stack.push(caller)
70
+ t0 = Time.now
71
+ if block_given?
72
+ ret = yield
73
+ end
74
+ t1 = Time.now
75
+ stack.pop
76
+ debug("execution took #{t1 - t0} seconds")
77
+ ret
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+
@@ -0,0 +1,156 @@
1
+ #--
2
+ # Copyright (c) 2013 Michael Berkovich, tr8nhub.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ module Tr8n
25
+ module RulesEngine
26
+
27
+ class Evaluator
28
+ attr_reader :env, :vars
29
+
30
+ def initialize
31
+ @vars = {}
32
+ @env = {
33
+ # McCarthy's Elementary S-functions and Predicates
34
+ 'label' => lambda { |l, r| @env[l] = @vars[l] = r },
35
+ 'quote' => lambda { |expr| expr },
36
+ 'car' => lambda { |list| list[1] },
37
+ 'cdr' => lambda { |list| list.drop(1) },
38
+ 'cons' => lambda { |e, cell| [e] + cell },
39
+ 'eq' => lambda { |l, r| l == r },
40
+ 'atom' => lambda { |expr| [Symbol, String, Fixnum, Float].include?(expr.class) },
41
+ 'cond' => lambda { |c, t, f| evaluate(c) ? evaluate(t) : evaluate(f) },
42
+
43
+ # Tr8n Extensions
44
+ '=' => lambda { |l, r| l == r }, # ['=', 1, 2]
45
+ '!=' => lambda { |l, r| l != r }, # ['!=', 1, 2]
46
+ '<' => lambda { |l, r| l < r }, # ['<', 1, 2]
47
+ '>' => lambda { |l, r| l > r }, # ['>', 1, 2]
48
+ '+' => lambda { |l, r| l + r }, # ['+', 1, 2]
49
+ '-' => lambda { |l, r| l - r }, # ['-', 1, 2]
50
+ '*' => lambda { |l, r| l * r }, # ['*', 1, 2]
51
+ '%' => lambda { |l, r| l % r }, # ['%', 14, 10]
52
+ 'mod' => lambda { |l, r| l % r }, # ['mod', '@n', 10]
53
+ '/' => lambda { |l, r| (l * 1.0) / r }, # ['/', 1, 2]
54
+ '!' => lambda { |expr| not expr }, # ['!', ['true']]
55
+ 'not' => lambda { |val| not val }, # ['not', ['true']]
56
+ '&&' => lambda { |*expr| expr.all?{|e| evaluate(e)} }, # ['&&', [], [], ...]
57
+ 'and' => lambda { |*expr| expr.all?{|e| evaluate(e)} }, # ['and', [], [], ...]
58
+ '||' => lambda { |*expr| expr.any?{|e| evaluate(e)} }, # ['||', [], [], ...]
59
+ 'or' => lambda { |*expr| expr.any?{|e| evaluate(e)} }, # ['or', [], [], ...]
60
+ 'if' => lambda { |c, t, f| evaluate(c) ? evaluate(t) : evaluate(f) }, # ['if', 'cond', 'true', 'false']
61
+ 'let' => lambda { |l, r| @env[l] = @vars[l] = r }, # ['let', 'n', 5]
62
+ 'true' => lambda { true }, # ['true']
63
+ 'false' => lambda { false }, # ['false']
64
+
65
+ 'date' => lambda { |date| Date.strptime(date, '%Y-%m-%d') }, # ['date', '2010-01-01']
66
+ 'today' => lambda { Time.now.to_date }, # ['today']
67
+ 'time' => lambda { |expr| Time.strptime(expr, '%Y-%m-%d %H:%M:%S') }, # ['time', '2010-01-01 10:10:05']
68
+ 'now' => lambda { Time.now }, # ['now']
69
+
70
+ 'append' => lambda { |l, r| r.to_s + l.to_s }, # ['append', 'world', 'hello ']
71
+ 'prepend' => lambda { |l, r| l.to_s + r.to_s }, # ['prepend', 'hello ', 'world']
72
+ 'match' => lambda { |search, subject| # ['match', /a/, 'abc']
73
+ search = regexp_from_string(search)
74
+ not search.match(subject).nil?
75
+ },
76
+ 'in' => lambda { |values, search| # ['in', '1,2,3,5..10,20..24', '@n']
77
+ search = search.to_s.strip
78
+ values.split(',').each do |e|
79
+ if e.index('..')
80
+ bounds = e.strip.split('..')
81
+ return true if (bounds.first.strip..bounds.last.strip).include?(search)
82
+ end
83
+ return true if e.strip == search
84
+ end
85
+ false
86
+ },
87
+ 'within' => lambda { |values, search| # ['within', '0..3', '@n']
88
+ bounds = values.split('..').map{|d| Integer(d)}
89
+ (bounds.first..bounds.last).include?(search)
90
+ },
91
+ 'replace' => lambda { |search, replace, subject| # ['replace', '/^a/', 'v', 'abc']
92
+ # handle regular expression
93
+ if /\/i$/.match(search)
94
+ replace = replace.gsub(/\$(\d+)/, '\\\\\1') # for compatibility with Perl notation
95
+ end
96
+ search = regexp_from_string(search)
97
+ subject.gsub(search, replace)
98
+ },
99
+ 'count' => lambda { |list| # ['count', '@genders']
100
+ (list.is_a?(String) ? vars[list] : list).count
101
+ },
102
+ 'all' => lambda { |list, value| # ['all', '@genders', 'male']
103
+ list = (list.is_a?(String) ? vars[list] : list)
104
+ list.is_a?(Array) ? list.all?{|e| e == value} : false
105
+ },
106
+ 'any' => lambda { |list, value| # ['any', '@genders', 'female']
107
+ list = (list.is_a?(String) ? vars[list] : list)
108
+ list.is_a?(Array) ? list.any?{|e| e == value} : false
109
+ },
110
+ }
111
+ end
112
+
113
+ def regexp_from_string(str)
114
+ return Regexp.new(/#{str}/) unless /^\//.match(str)
115
+
116
+ str = str.gsub(/^\//, '')
117
+
118
+ if /\/i$/.match(str)
119
+ str = str.gsub(/\/i$/, '')
120
+ return Regexp.new(/#{str}/i)
121
+ end
122
+
123
+ str = str.gsub(/\/$/, '')
124
+ Regexp.new(/#{str}/)
125
+ end
126
+
127
+ def reset!
128
+ @vars.each do |var|
129
+ @env.delete(var)
130
+ end
131
+ @vars = {}
132
+ end
133
+
134
+ def apply(fn, args)
135
+ raise "undefined symbols #{fn}" unless @env.keys.include?(fn)
136
+ @env[fn].call(*args)
137
+ end
138
+
139
+ def evaluate(expr)
140
+ if @env['atom'].call(expr)
141
+ return @env[expr] if @env[expr]
142
+ return expr
143
+ end
144
+
145
+ fn = expr[0]
146
+ args = expr.drop(1)
147
+
148
+ unless ['quote', 'car', 'cdr', 'cond', 'if', '&&', '||', 'and', 'or', 'true', 'false', 'let', 'count', 'all', 'any'].member?(fn)
149
+ args = args.map { |a| self.evaluate(a) }
150
+ end
151
+ apply(fn, args)
152
+ end
153
+ end
154
+
155
+ end
156
+ end