LittleWeasel 3.0.3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.reek.yml +17 -0
  4. data/.rspec +4 -2
  5. data/.rubocop.yml +187 -0
  6. data/.ruby-version +1 -1
  7. data/.yardopts +2 -0
  8. data/CHANGELOG.md +22 -1
  9. data/Gemfile +3 -1
  10. data/Jenkinsfile +20 -0
  11. data/LittleWeasel.gemspec +31 -18
  12. data/README.md +408 -42
  13. data/Rakefile +296 -3
  14. data/lib/LittleWeasel/block_results.rb +81 -0
  15. data/lib/LittleWeasel/configure.rb +98 -0
  16. data/lib/LittleWeasel/dictionary.rb +125 -0
  17. data/lib/LittleWeasel/dictionary_key.rb +48 -0
  18. data/lib/LittleWeasel/dictionary_manager.rb +91 -0
  19. data/lib/LittleWeasel/errors/dictionary_file_already_loaded_error.rb +9 -0
  20. data/lib/LittleWeasel/errors/dictionary_file_empty_error.rb +8 -0
  21. data/lib/LittleWeasel/errors/dictionary_file_not_found_error.rb +8 -0
  22. data/lib/LittleWeasel/errors/dictionary_file_too_large_error.rb +16 -0
  23. data/lib/LittleWeasel/errors/language_required_error.rb +8 -0
  24. data/lib/LittleWeasel/errors/must_override_error.rb +8 -0
  25. data/lib/LittleWeasel/filters/en_us/currency_filter.rb +19 -0
  26. data/lib/LittleWeasel/filters/en_us/numeric_filter.rb +19 -0
  27. data/lib/LittleWeasel/filters/en_us/single_character_word_filter.rb +21 -0
  28. data/lib/LittleWeasel/filters/word_filter.rb +59 -0
  29. data/lib/LittleWeasel/filters/word_filter_managable.rb +80 -0
  30. data/lib/LittleWeasel/filters/word_filter_validatable.rb +31 -0
  31. data/lib/LittleWeasel/filters/word_filterable.rb +19 -0
  32. data/lib/LittleWeasel/filters/word_filters_validatable.rb +29 -0
  33. data/lib/LittleWeasel/metadata/dictionary_metadata.rb +145 -0
  34. data/lib/LittleWeasel/metadata/invalid_words_metadata.rb +134 -0
  35. data/lib/LittleWeasel/metadata/invalid_words_service_results.rb +45 -0
  36. data/lib/LittleWeasel/metadata/metadata_observable_validatable.rb +22 -0
  37. data/lib/LittleWeasel/metadata/metadata_observerable.rb +90 -0
  38. data/lib/LittleWeasel/metadata/metadatable.rb +134 -0
  39. data/lib/LittleWeasel/modules/class_name_to_symbol.rb +26 -0
  40. data/lib/LittleWeasel/modules/configurable.rb +26 -0
  41. data/lib/LittleWeasel/modules/deep_dup.rb +11 -0
  42. data/lib/LittleWeasel/modules/dictionary_cache_keys.rb +34 -0
  43. data/lib/LittleWeasel/modules/dictionary_cache_servicable.rb +26 -0
  44. data/lib/LittleWeasel/modules/dictionary_cache_validatable.rb +18 -0
  45. data/lib/LittleWeasel/modules/dictionary_creator_servicable.rb +27 -0
  46. data/lib/LittleWeasel/modules/dictionary_file_loader.rb +67 -0
  47. data/lib/LittleWeasel/modules/dictionary_key_validatable.rb +17 -0
  48. data/lib/LittleWeasel/modules/dictionary_keyable.rb +24 -0
  49. data/lib/LittleWeasel/modules/dictionary_metadata_servicable.rb +29 -0
  50. data/lib/LittleWeasel/modules/dictionary_metadata_validatable.rb +15 -0
  51. data/lib/LittleWeasel/modules/dictionary_source_validatable.rb +15 -0
  52. data/lib/LittleWeasel/modules/dictionary_sourceable.rb +86 -0
  53. data/lib/LittleWeasel/modules/dictionary_validatable.rb +18 -0
  54. data/lib/LittleWeasel/modules/language.rb +24 -0
  55. data/lib/LittleWeasel/modules/language_validatable.rb +14 -0
  56. data/lib/LittleWeasel/modules/locale.rb +23 -0
  57. data/lib/LittleWeasel/modules/order_validatable.rb +16 -0
  58. data/lib/LittleWeasel/modules/orderable.rb +17 -0
  59. data/lib/LittleWeasel/modules/region.rb +24 -0
  60. data/lib/LittleWeasel/modules/region_validatable.rb +14 -0
  61. data/lib/LittleWeasel/modules/tag_validatable.rb +14 -0
  62. data/lib/LittleWeasel/modules/taggable.rb +31 -0
  63. data/lib/LittleWeasel/modules/word_results_validatable.rb +28 -0
  64. data/lib/LittleWeasel/preprocessors/en_us/capitalize_preprocessor.rb +22 -0
  65. data/lib/LittleWeasel/preprocessors/preprocessed_word.rb +29 -0
  66. data/lib/LittleWeasel/preprocessors/preprocessed_word_validatable.rb +56 -0
  67. data/lib/LittleWeasel/preprocessors/preprocessed_words.rb +59 -0
  68. data/lib/LittleWeasel/preprocessors/preprocessed_words_validatable.rb +28 -0
  69. data/lib/LittleWeasel/preprocessors/word_preprocessable.rb +19 -0
  70. data/lib/LittleWeasel/preprocessors/word_preprocessor.rb +123 -0
  71. data/lib/LittleWeasel/preprocessors/word_preprocessor_managable.rb +114 -0
  72. data/lib/LittleWeasel/preprocessors/word_preprocessor_validatable.rb +40 -0
  73. data/lib/LittleWeasel/preprocessors/word_preprocessors_validatable.rb +24 -0
  74. data/lib/LittleWeasel/services/dictionary_cache_service.rb +211 -0
  75. data/lib/LittleWeasel/services/dictionary_creator_service.rb +94 -0
  76. data/lib/LittleWeasel/services/dictionary_file_loader_service.rb +37 -0
  77. data/lib/LittleWeasel/services/dictionary_killer_service.rb +35 -0
  78. data/lib/LittleWeasel/services/dictionary_metadata_service.rb +116 -0
  79. data/lib/LittleWeasel/services/invalid_words_service.rb +59 -0
  80. data/lib/LittleWeasel/version.rb +3 -1
  81. data/lib/LittleWeasel/word_results.rb +146 -0
  82. data/lib/LittleWeasel.rb +5 -184
  83. data/spec/factories/dictionary.rb +43 -0
  84. data/spec/factories/dictionary_cache_service.rb +95 -0
  85. data/spec/factories/dictionary_creator_service.rb +16 -0
  86. data/spec/factories/dictionary_file_loader_service.rb +13 -0
  87. data/spec/factories/dictionary_hash.rb +39 -0
  88. data/spec/factories/dictionary_key.rb +14 -0
  89. data/spec/factories/dictionary_killer_service.rb +14 -0
  90. data/spec/factories/dictionary_manager.rb +10 -0
  91. data/spec/factories/dictionary_metadata.rb +16 -0
  92. data/spec/factories/dictionary_metadata_service.rb +16 -0
  93. data/spec/factories/numeric_filter.rb +12 -0
  94. data/spec/factories/preprocessed_word.rb +16 -0
  95. data/spec/factories/preprocessed_words.rb +41 -0
  96. data/spec/factories/single_character_word_filter.rb +12 -0
  97. data/spec/factories/word_results.rb +16 -0
  98. data/spec/lib/LittleWeasel/block_results_spec.rb +248 -0
  99. data/spec/lib/LittleWeasel/configure_spec.rb +74 -0
  100. data/spec/lib/LittleWeasel/dictionary_key_spec.rb +118 -0
  101. data/spec/lib/LittleWeasel/dictionary_manager_spec.rb +166 -0
  102. data/spec/lib/LittleWeasel/dictionary_spec.rb +289 -0
  103. data/spec/lib/LittleWeasel/filters/en_us/currency_filter_spec.rb +80 -0
  104. data/spec/lib/LittleWeasel/filters/en_us/numeric_filter_spec.rb +66 -0
  105. data/spec/lib/LittleWeasel/filters/en_us/single_character_word_filter_spec.rb +58 -0
  106. data/spec/lib/LittleWeasel/filters/word_filter_managable_spec.rb +180 -0
  107. data/spec/lib/LittleWeasel/filters/word_filter_spec.rb +151 -0
  108. data/spec/lib/LittleWeasel/filters/word_filter_validatable_spec.rb +94 -0
  109. data/spec/lib/LittleWeasel/filters/word_filters_validatable_spec.rb +48 -0
  110. data/spec/lib/LittleWeasel/integraton_tests/dictionary_integration_spec.rb +201 -0
  111. data/spec/lib/LittleWeasel/metadata/dictionary_creator_servicable_spec.rb +54 -0
  112. data/spec/lib/LittleWeasel/metadata/dictionary_metadata_spec.rb +209 -0
  113. data/spec/lib/LittleWeasel/metadata/invalid_words_metadata_spec.rb +155 -0
  114. data/spec/lib/LittleWeasel/metadata/metadata_observerable_spec.rb +31 -0
  115. data/spec/lib/LittleWeasel/metadata/metadatable_spec.rb +35 -0
  116. data/spec/lib/LittleWeasel/modules/class_name_to_symbol_spec.rb +21 -0
  117. data/spec/lib/LittleWeasel/modules/dictionary_file_loader_spec.rb +125 -0
  118. data/spec/lib/LittleWeasel/modules/dictionary_sourceable_spec.rb +81 -0
  119. data/spec/lib/LittleWeasel/modules/language_spec.rb +112 -0
  120. data/spec/lib/LittleWeasel/modules/locale_spec.rb +95 -0
  121. data/spec/lib/LittleWeasel/modules/region_spec.rb +112 -0
  122. data/spec/lib/LittleWeasel/preprocessors/en_us/capitalize_preprocessor_spec.rb +34 -0
  123. data/spec/lib/LittleWeasel/preprocessors/preprocessed_word_spec.rb +105 -0
  124. data/spec/lib/LittleWeasel/preprocessors/preprocessed_word_validatable_spec.rb +143 -0
  125. data/spec/lib/LittleWeasel/preprocessors/preprocessed_words_spec.rb +77 -0
  126. data/spec/lib/LittleWeasel/preprocessors/preprocessed_words_validatable_spec.rb +58 -0
  127. data/spec/lib/LittleWeasel/preprocessors/word_preprocessor_managable_spec.rb +242 -0
  128. data/spec/lib/LittleWeasel/preprocessors/word_preprocessor_spec.rb +218 -0
  129. data/spec/lib/LittleWeasel/preprocessors/word_preprocessor_validatable_spec.rb +109 -0
  130. data/spec/lib/LittleWeasel/preprocessors/word_preprocessors_validatable_spec.rb +49 -0
  131. data/spec/lib/LittleWeasel/services/dictionary_cache_service_spec.rb +444 -0
  132. data/spec/lib/LittleWeasel/services/dictionary_creator_service_spec.rb +119 -0
  133. data/spec/lib/LittleWeasel/services/dictionary_file_loader_service_spec.rb +71 -0
  134. data/spec/lib/LittleWeasel/services/dictionary_metadata_service_spec.rb +279 -0
  135. data/spec/lib/LittleWeasel/word_results_spec.rb +275 -0
  136. data/spec/lib/LittleWeasel/workflow/workflow_spec.rb +20 -0
  137. data/spec/spec_helper.rb +117 -6
  138. data/spec/support/factory_bot.rb +15 -0
  139. data/spec/support/file_helpers.rb +46 -0
  140. data/spec/support/files/empty-dictionary.txt +0 -0
  141. data/{lib/dictionary → spec/support/files/en-US-big.txt} +262156 -31488
  142. data/spec/support/files/en-US-tagged.txt +26 -0
  143. data/spec/support/files/en-US.txt +26 -0
  144. data/spec/support/files/en.txt +26 -0
  145. data/spec/support/files/es-ES.txt +27 -0
  146. data/spec/support/files/es.txt +27 -0
  147. data/spec/support/general_helpers.rb +68 -0
  148. data/spec/support/shared_contexts.rb +107 -0
  149. data/spec/support/shared_examples.rb +105 -0
  150. metadata +378 -38
  151. data/spec/checker/checker_spec.rb +0 -286
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors/must_override_error'
4
+ require_relative '../modules/class_name_to_symbol'
5
+ require_relative '../modules/orderable'
6
+ require_relative 'preprocessed_word'
7
+
8
+ module LittleWeasel
9
+ module Preprocessors
10
+ # This is a base class that provides methods and functionality for
11
+ # word preprocessors. A "word preprocessor" is an object that manipulates a
12
+ # word before it is passed to any word filters and before it is compared
13
+ # against the dictionary for validity.
14
+ # :reek:MissingSafeMethod, ignored - safe methods for preprocessor_on!/off! make no sense in this case
15
+ class WordPreprocessor
16
+ include Modules::ClassNameToSymbol
17
+ include Modules::Orderable
18
+
19
+ attr_reader :preprocessor_on
20
+
21
+ # order:Integer, the order in which this preprocessor should
22
+ # be applied.
23
+ # preprocessor_on:Boolean, whether or not this preprocessor
24
+ # should be applied to any words.
25
+ def initialize(order:)
26
+ validate_order order: order
27
+ self.order = order
28
+ preprocessor_on!
29
+ end
30
+
31
+ class << self
32
+ # Should return true if word matches the preprocess criteria;
33
+ # false, otherwise. If this preprocessor has no preprocess criteria,
34
+ # simply return true. This class method is unlike the instance method in
35
+ # that it does not consider whether or not this preprocessor is "on"
36
+ # or "off"; it simply returns true or false based on whether or not the
37
+ # word matches the preprocess criteria.
38
+ def preprocess?(_word)
39
+ true
40
+ end
41
+
42
+ # This method should UNconditionally apply preprocessing to word ONLY if
43
+ # word meets the criteria for preprocessing (.preprocess?).
44
+ #
45
+ # This method should return the following Array:
46
+ #
47
+ # [<preprocessed?>, <preprocessed word | nil>]
48
+ #
49
+ # Where:
50
+ #
51
+ # <preprocessed?> == whether or not the word was preprocessed
52
+ # based on whether or not the word meets the preprocessing
53
+ # criteria (.preprocess?).
54
+ #
55
+ # <preprocessed word | nil> == the preprocessed word (if word
56
+ # met the preprocessing criteria (.preprocessed?)) or nil if
57
+ # word was NOT preprocessed (word did NOT meet the preprocessing
58
+ # criteria).
59
+ def preprocess(_word)
60
+ raise Errors::MustOverrideError
61
+ end
62
+ end
63
+
64
+ def preprocessor_on=(value)
65
+ raise ArgumentError, "Argument value is not true or false: #{value}" \
66
+ unless [true, false].include? value
67
+
68
+ @preprocessor_on = value
69
+ end
70
+
71
+ # Returns true if word meets the criteria for preprocessing. false
72
+ # is returned if word does not meet the criteria for preprocessing, or,
73
+ # if the preprocessor is "off".
74
+ def preprocess?(word)
75
+ return false if preprocessor_off?
76
+
77
+ self.class.preprocess? word
78
+ end
79
+
80
+ # Applies preprocessing to word if this preprocessor is "on" AND if word
81
+ # meets the criteria for preprocessing; no preprocessing is applied to
82
+ # word otherwise.
83
+ #
84
+ # This method should return a Preprocessors::PreprocessedWord object.
85
+ def preprocess(word)
86
+ preprocessed, preprocessed_word = if preprocessor_on?
87
+ self.class.preprocess word
88
+ else
89
+ [false, nil]
90
+ end
91
+ preprocessed_word(original_word: word, preprocessed_word: preprocessed_word, preprocessed: preprocessed)
92
+ end
93
+
94
+ # Returns true if this preprocessor is "on"; false, otherwise. If this
95
+ # preprocessor is "on", preprocessing should be applied to a word if word
96
+ # meets the criteria for preprocessing.
97
+ def preprocessor_on?
98
+ preprocessor_on
99
+ end
100
+
101
+ def preprocessor_on!
102
+ @preprocessor_on = true
103
+ end
104
+
105
+ # Returns true if this preprocessor is "off". Preprocessing should not
106
+ # be applied to a word if this preprocessor is "off".
107
+ def preprocessor_off?
108
+ !preprocessor_on?
109
+ end
110
+
111
+ def preprocessor_off!
112
+ @preprocessor_on = false
113
+ end
114
+
115
+ private
116
+
117
+ def preprocessed_word(original_word:, preprocessed:, preprocessed_word:)
118
+ PreprocessedWord.new(original_word: original_word, preprocessed: preprocessed,
119
+ preprocessed_word: preprocessed_word, preprocessor: to_sym, preprocessor_order: order)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'word_preprocessable'
4
+ require_relative 'word_preprocessor_validatable'
5
+
6
+ module LittleWeasel
7
+ module Preprocessors
8
+ # This module provides methods and functionality to manage word
9
+ # preprocessors. A "word preprocessor" is an object that manipulates a word
10
+ # before it is passed to any word filters and before it is compared against
11
+ # the dictionary for validity.
12
+ #
13
+ # When creating your own word preprocessors, here are some things you
14
+ # need to consider:
15
+ #
16
+ # Multiple word preprocessors can be applied to a given word. Word
17
+ # processors will be applied to a word in
18
+ # Preprocessors::WordPreprocessor#order order (ascending). Even though this
19
+ # is the case, it doesn't mean you should seek to apply more than one word
20
+ # preprocessor at a time. However, if you do, write and order your word
21
+ # preprocessors in such a way that each preprocessor manipulates the word
22
+ # in a complimentary rather than contridictionary way. For example,
23
+ # applying one word preprocessor that convert a word to uppercase and a
24
+ # second that converts the word to lowercase, contradict each other.
25
+ #
26
+ # Another thing you need to consider, is whether or not metadata observers
27
+ # should be notified of the preprocessed word (now that it has been
28
+ # potentially manipulated) or if they should be notified of the original
29
+ # word; this is because, the original word may not be found as a valid word
30
+ # in the dictionary, while the preprocessed word might and vise versa.
31
+ module WordPreprocessorManagable
32
+ include WordPreprocessable
33
+ include WordPreprocessorsValidatable
34
+
35
+ # Override attr_reader word_preprocessor found in WordPreprocessable
36
+ # so that we don't raise nil errors when using word_preprocessors.
37
+ def word_preprocessors
38
+ @word_preprocessors ||= []
39
+ end
40
+
41
+ def clear_preprocessors
42
+ self.word_preprocessors = []
43
+ end
44
+
45
+ # Appends word preprocessors to the #word_preprocessors Array.
46
+ #
47
+ # If Argument word_preprocessor is nil, a block must be passed to populate
48
+ # the word_preprocessors with an Array of valid word preprocessor objects.
49
+ #
50
+ # This method is used for adding/appending word preprocessors to the
51
+ # word_preprocessors Array. To replace word preprocessors, use #replace_preprocessors;
52
+ # to perform any other manipulation of the word_preprocessors Array,
53
+ # use #word_preprocessors directly.
54
+ def add_preprocessors(word_preprocessors: nil)
55
+ return if word_preprocessors.is_a?(Array) && word_preprocessors.blank?
56
+
57
+ unless word_preprocessors.present? || block_given?
58
+ raise 'A block is required if argument word_preprocessors is nil'
59
+ end
60
+
61
+ word_preprocessors ||= []
62
+ yield word_preprocessors if block_given?
63
+
64
+ concat_and_sort_word_preprocessors! word_preprocessors
65
+ end
66
+ alias append_preprocessors add_preprocessors
67
+
68
+ def replace_preprocessors(word_preprocessors:)
69
+ clear_preprocessors
70
+ add_preprocessors word_preprocessors: word_preprocessors
71
+ end
72
+
73
+ def preprocessors_on=(on)
74
+ raise ArgumentError, "Argument on is not true or false: #{on.class}" unless [true, false].include?(on)
75
+
76
+ word_preprocessors.each { |word_preprocessor| word_preprocessor.preprocessor_on = on }
77
+ end
78
+
79
+ # Returns a Preprocessors::PreprocessedWords object.
80
+ def preprocess(word:)
81
+ preprocessed_words = preprocessed_words word: word
82
+ PreprocessedWords.new(original_word: word, preprocessed_words: preprocessed_words)
83
+ end
84
+
85
+ def preprocessed_words(word:)
86
+ word_preprocessors.map do |word_preprocessor|
87
+ word_preprocessor.preprocess(word).tap do |processed_word|
88
+ word = processed_word.preprocessed_word
89
+ end
90
+ end
91
+ end
92
+
93
+ # Returns the final (or last) preprocessed word in the Array of
94
+ # preprocessed words. The final preprocessed word is the word that has
95
+ # passed through all the word preprocessors.
96
+ def preprocessed_word(word:)
97
+ preprocessed_words = self.preprocessed_words word: word
98
+ preprocessed_words.max_by(&:preprocessor_order).preprocessed_word unless preprocessed_words.blank?
99
+ end
100
+
101
+ private
102
+
103
+ # This method concatinates preprocessors to #word_preprocessors,
104
+ # sorts #word_preprocessors by WordPreprocessor#order and
105
+ # returns the results.
106
+ def concat_and_sort_word_preprocessors!(preprocessors)
107
+ validate_word_preprocessors word_preprocessors: preprocessors
108
+
109
+ word_preprocessors.concat preprocessors
110
+ word_preprocessors.sort_by!(&:order)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LittleWeasel
4
+ module Preprocessors
5
+ # This module validates word preprocessor types.
6
+ # rubocop: disable Layout/LineLength
7
+ module WordPreprocessorValidatable
8
+ module_function
9
+
10
+ # :reek:ManualDispatch - ignored, this is duck-typing not 'simulated polymorphism'
11
+ # :reek:TooManyStatements - ignored, "too many statements" is easier to understand than arbitrarily breaking all this down into individual methods
12
+ def validate_word_preprocessor(word_preprocessor:)
13
+ # You can use your own word preprocessor types as long as they quack
14
+ # correctly; however, you are responsible for the behavior of these
15
+ # required methods/ attributes. It's probably better to follow the
16
+ # pattern of existing word preprocessor objects and inherit from
17
+ # Preprocessors::WordPreprocessor.
18
+
19
+ word_preprocessor_class = word_preprocessor.class
20
+
21
+ # class methods
22
+ raise validation_error_message(object: word_preprocessor_class, respond_to: '.preprocess') unless word_preprocessor_class.respond_to?(:preprocess)
23
+ raise validation_error_message(object: word_preprocessor_class, respond_to: '.preprocess?') unless word_preprocessor_class.respond_to?(:preprocess?)
24
+
25
+ # instance methods
26
+ raise validation_error_message(object: word_preprocessor_class, respond_to: '#preprocess') unless word_preprocessor.respond_to?(:preprocess)
27
+ raise validation_error_message(object: word_preprocessor_class, respond_to: '#preprocess?') unless word_preprocessor.respond_to?(:preprocess?)
28
+ raise validation_error_message(object: word_preprocessor_class, respond_to: '#preprocessor_off?') unless word_preprocessor.respond_to?(:preprocessor_off?)
29
+ raise validation_error_message(object: word_preprocessor_class, respond_to: '#preprocessor_on') unless word_preprocessor.respond_to?(:preprocessor_on)
30
+ raise validation_error_message(object: word_preprocessor_class, respond_to: '#preprocessor_on=') unless word_preprocessor.respond_to?(:preprocessor_on=)
31
+ raise validation_error_message(object: word_preprocessor_class, respond_to: '#preprocessor_on?') unless word_preprocessor.respond_to?(:preprocessor_on?)
32
+ end
33
+
34
+ def validation_error_message(object:, respond_to:)
35
+ "Argument word_preprocessor: does not respond to: #{object}#{respond_to}"
36
+ end
37
+ end
38
+ # rubocop: enable Layout/LineLength
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'word_preprocessor_validatable'
4
+
5
+ module LittleWeasel
6
+ module Preprocessors
7
+ # This module provides methods to validate an Array of word preprocessor
8
+ # objects.
9
+ module WordPreprocessorsValidatable
10
+ module_function
11
+
12
+ def validate_word_preprocessors(word_preprocessors:)
13
+ return if word_preprocessors.blank?
14
+
15
+ raise ArgumentError, "Argument word_preprocessors is not an Array: #{word_preprocessors.class}" \
16
+ unless word_preprocessors.is_a? Array
17
+
18
+ word_preprocessors.each do |word_preprocessor|
19
+ WordPreprocessorValidatable.validate_word_preprocessor word_preprocessor: word_preprocessor
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../modules/dictionary_cache_keys'
4
+ require_relative '../modules/dictionary_cache_validatable'
5
+ require_relative '../modules/dictionary_keyable'
6
+ require_relative '../modules/dictionary_sourceable'
7
+
8
+ module LittleWeasel
9
+ module Services
10
+ # This class provides methods and attributes that can be used to manage the
11
+ # dictionary cache. The "dictionary cache" is a simple Hash that provides
12
+ # access to informaiton related to dictionaries through a dictionary "key".
13
+ # A dictionary "key" is a unique key comprised of a locale and
14
+ # optional "tag" (see Modules::Taggable and DictionaryKey for more
15
+ # information). The dictionary cache also provides a way for dictionary
16
+ # objects to share dictionary information, in particular, the dictionary
17
+ # file and dictionary metadata.
18
+ class DictionaryCacheService
19
+ include Modules::DictionaryCacheKeys
20
+ include Modules::DictionaryCacheValidatable
21
+ include Modules::DictionaryKeyable
22
+ include Modules::DictionarySourceable
23
+
24
+ attr_reader :dictionary_cache
25
+
26
+ # This class produces the following (example) Hash that represents the
27
+ # dictionary cache structure:
28
+ #
29
+ # @example This is an example:
30
+ #
31
+ # {
32
+ # 'dictionary_cache' =>
33
+ # {
34
+ # 'dictionary_references' =>
35
+ # {
36
+ # 'en' =>
37
+ # {
38
+ # 'dictionary_id' => 19ec7845
39
+ # },
40
+ # 'en-US' =>
41
+ # {
42
+ # 'dictionary_id' => 0987a3f2
43
+ # },
44
+ # 'en-US-temp' =>
45
+ # {
46
+ # 'dictionary_id' => 9273eac6
47
+ # }
48
+ # },
49
+ # 'dictionaries' =>
50
+ # {
51
+ # 19ec7845 =>
52
+ # {
53
+ # 'source' => '/en.txt',
54
+ # 'dictionary_object' => {}
55
+ # },
56
+ # 0987a3f2 =>
57
+ # {
58
+ # 'source' => '/en-US.txt',
59
+ # 'dictionary_object' => {}
60
+ # },
61
+ # 9273eac6 =>
62
+ # {
63
+ # 'source' => '*736ed423',
64
+ # 'dictionary_object' => {}
65
+ # }
66
+ # }
67
+ # }
68
+ # }
69
+ def initialize(dictionary_key:, dictionary_cache:)
70
+ validate_dictionary_key dictionary_key: dictionary_key
71
+ self.dictionary_key = dictionary_key
72
+
73
+ validate_dictionary_cache dictionary_cache: dictionary_cache
74
+ self.dictionary_cache = dictionary_cache
75
+
76
+ self.class.init(dictionary_cache: dictionary_cache) unless dictionary_cache[DICTIONARY_CACHE]
77
+ end
78
+
79
+ class << self
80
+ # This method resets dictionary_cache to its initialized state.
81
+ # This class method is different from the #init instance method
82
+ # in that ALL dictionary references and ALL dictionaries are
83
+ # initialized.
84
+ def init(dictionary_cache:)
85
+ Modules::DictionaryCacheKeys.initialize_dictionary_cache dictionary_cache: dictionary_cache
86
+ end
87
+
88
+ # Returns true if the dictionary cache is initialized; that
89
+ # is, if the given dictionary_cache is in the same state the
90
+ # dictionary cache would be in after .init were called.
91
+ def init?(dictionary_cache:)
92
+ initialized_dictionary_cache = init(dictionary_cache: {})
93
+ dictionary_cache.eql?(initialized_dictionary_cache)
94
+ end
95
+
96
+ # Returns the number of dictionaries currently in the cache.
97
+ def count(dictionary_cache:)
98
+ dictionary_cache.dig(self::DICTIONARY_CACHE, self::DICTIONARIES)&.keys&.count || 0
99
+ end
100
+ end
101
+
102
+ # This method resets the dictionary cache for the given key. This method
103
+ # is different from the .init class method in that ONLY the dictionary
104
+ # reference and dictionary specific to the given key is initialized.
105
+ def init
106
+ # TODO: Do not delete the dictionary if it is being pointed to by
107
+ # another dictionary reference.
108
+ dictionary_cache_hash = dictionary_cache[DICTIONARY_CACHE]
109
+ dictionary_cache_hash[DICTIONARIES]&.delete(dictionary_id)
110
+ dictionary_cache_hash[DICTIONARY_REFERENCES]&.delete(key)
111
+ self
112
+ end
113
+
114
+ # Returns true if the dictionary reference exists for the given key; false
115
+ # otherwise. This method is only concerned with the dictionary reference
116
+ # and has nothing to do with whether or not the associated dictionary
117
+ # is actually loaded into the dictionary cache.
118
+ def dictionary_reference?
119
+ dictionary_reference&.present? || false
120
+ end
121
+
122
+ # Returns true if a dictionaries Hash key exists for the given dictionary_id
123
+ # in the dictionary cache. This method is only concerned with the existance of
124
+ # the key and has nothing to do with whether or not file/memory sources are
125
+ # present or the presence of a dictionary object.
126
+ def dictionary?
127
+ dictionary_cache[DICTIONARY_CACHE][DICTIONARIES].key? dictionary_id
128
+ end
129
+
130
+ # Returns the dictionary id if there is a dictionary id in the dictionary
131
+ # cache associated with the given key; nil otherwise.
132
+ def dictionary_id
133
+ dictionary_cache.dig(DICTIONARY_CACHE, DICTIONARY_REFERENCES, key, DICTIONARY_ID)
134
+ end
135
+
136
+ # Returns the dictionary id if there is a dictionary id in the dictionary
137
+ # cache associated with the given key. This method raises an error if the
138
+ # dictionary id cannot be found.
139
+ def dictionary_id!
140
+ return dictionary_id if dictionary_id?
141
+
142
+ raise ArgumentError, "A dictionary id could not be found for key '#{key}'."
143
+ end
144
+
145
+ # This method returns true if the dictionary associated with the
146
+ # given dictionary key is loaded/cached. If this is the case,
147
+ # a dictionary object is available in the dictionary cache.
148
+ def dictionary_object?
149
+ dictionary_object.present?
150
+ end
151
+ alias dictionary_exist? dictionary_object?
152
+
153
+ # <b>DEPRECATED:</b> Use <tt>dictionary_exist?</tt> instead.
154
+ def dictionary_exists?
155
+ warn "[DEPRECATION] 'dictionary_exists?' is deprecated. Please use 'dictionary_exist?' instead."
156
+ dictionary_object?
157
+ end
158
+
159
+ # Returns the dictionary object from the dictionary cache for the given
160
+ # key. This method raises an error if the dictionary is not in the cache;
161
+ # that is, if the dictionary was not previously loaded from disk or memory.
162
+ def dictionary_object!
163
+ unless dictionary_object?
164
+ raise ArgumentError,
165
+ "The dictionary object associated with argument key '#{key}' is not in the cache."
166
+ end
167
+
168
+ dictionary_object
169
+ end
170
+
171
+ def dictionary_object
172
+ dictionary_cache.dig(DICTIONARY_CACHE, DICTIONARIES, dictionary_id, DICTIONARY_OBJECT)
173
+ end
174
+
175
+ def dictionary_object=(object)
176
+ raise ArgumentError, 'Argument object is not a Dictionary object' unless object.is_a? Dictionary
177
+
178
+ unless dictionary_reference?
179
+ raise ArgumentError,
180
+ "The dictionary reference associated with key '#{key}' could not be found."
181
+ end
182
+ return if object.equal? dictionary_object
183
+
184
+ if dictionary_exist?
185
+ raise ArgumentError,
186
+ "The dictionary is already loaded/cached for key '#{key}'; use #unload or #kill first."
187
+ end
188
+
189
+ dictionary_cache[DICTIONARY_CACHE][DICTIONARIES][dictionary_id!][DICTIONARY_OBJECT] = object
190
+ end
191
+
192
+ private
193
+
194
+ attr_writer :dictionary_cache
195
+
196
+ def dictionary_reference
197
+ dictionary_cache.dig(DICTIONARY_CACHE, DICTIONARY_REFERENCES, key)
198
+ end
199
+
200
+ def set_dictionary_reference(dictionary_id:)
201
+ dictionary_cache[DICTIONARY_CACHE][DICTIONARY_REFERENCES][key] = {
202
+ DICTIONARY_ID => dictionary_id
203
+ }
204
+ end
205
+
206
+ def dictionary_id?
207
+ dictionary_id.present?
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../dictionary'
4
+ require_relative '../filters/word_filterable'
5
+ require_relative '../filters/word_filters_validatable'
6
+ require_relative '../metadata/dictionary_metadata'
7
+ require_relative '../modules/dictionary_cache_servicable'
8
+ require_relative '../modules/dictionary_creator_servicable'
9
+ require_relative '../modules/dictionary_keyable'
10
+ require_relative '../modules/dictionary_metadata_servicable'
11
+ require_relative '../modules/dictionary_sourceable'
12
+ require_relative '../preprocessors/word_preprocessor_managable'
13
+ require_relative 'dictionary_file_loader_service'
14
+
15
+ module LittleWeasel
16
+ module Services
17
+ # This class provides a service to load dictionaries from disk, create
18
+ # and return a Dictionary object.
19
+ class DictionaryCreatorService
20
+ include Filters::WordFilterable
21
+ include Filters::WordFiltersValidatable
22
+ include Modules::DictionaryCacheServicable
23
+ include Modules::DictionaryKeyable
24
+ include Modules::DictionaryMetadataServicable
25
+ include Modules::DictionarySourceable
26
+ include Preprocessors::WordPreprocessorManagable
27
+
28
+ def initialize(dictionary_key:, dictionary_cache:, dictionary_metadata:,
29
+ word_filters: nil, word_preprocessors: nil)
30
+ validate_dictionary_key dictionary_key: dictionary_key
31
+ self.dictionary_key = dictionary_key
32
+
33
+ validate_dictionary_cache dictionary_cache: dictionary_cache
34
+ self.dictionary_cache = dictionary_cache
35
+
36
+ validate_dictionary_metadata dictionary_metadata: dictionary_metadata
37
+ self.dictionary_metadata = dictionary_metadata
38
+
39
+ validate_word_filters word_filters: word_filters unless word_filters.blank?
40
+ self.word_filters = word_filters
41
+
42
+ validate_word_preprocessors word_preprocessors: word_preprocessors unless word_preprocessors.blank?
43
+ self.word_preprocessors = word_preprocessors
44
+ end
45
+
46
+ def from_file_source(file:)
47
+ add_dictionary_file_source file: file
48
+ dictionary_words = dictionary_file_loader_service.execute
49
+ create_dictionary dictionary_words: dictionary_words
50
+ end
51
+
52
+ def from_memory_source(dictionary_words:)
53
+ add_dictionary_memory_source
54
+ create_dictionary dictionary_words: dictionary_words
55
+ end
56
+
57
+ private
58
+
59
+ def dictionary_file_loader_service
60
+ Services::DictionaryFileLoaderService.new dictionary_key: dictionary_key, dictionary_cache: dictionary_cache
61
+ end
62
+
63
+ def create_dictionary(dictionary_words:)
64
+ Dictionary.new(dictionary_key: dictionary_key, dictionary_cache: dictionary_cache,
65
+ dictionary_metadata: dictionary_metadata, dictionary_words: dictionary_words, word_filters: word_filters,
66
+ word_preprocessors: word_preprocessors).tap do |dictionary|
67
+ dictionary_cache_service.dictionary_object = dictionary
68
+ end
69
+ end
70
+
71
+ # Adds a dictionary file source. A "file source" is a file path that
72
+ # indicates that the dictionary words associated with this dictionary are
73
+ # located on disk. This file path is used to locate and load the
74
+ # dictionary words into the dictionary cache for use.
75
+ #
76
+ # @param file [String] a file path pointing to the dictionary file to load and use.
77
+ #
78
+ # @return returns a reference to self.
79
+ def add_dictionary_file_source(file:)
80
+ dictionary_cache_service.add_dictionary_source(dictionary_source: file)
81
+ end
82
+
83
+ # Adds a dictionary memory source. A "memory source" indicates that the
84
+ # dictionary words associated with this dictionary were created
85
+ # dynamically and will be located in memory, as opposed to loaded from
86
+ # a file on disk.
87
+ #
88
+ # @return returns a reference to self.
89
+ def add_dictionary_memory_source
90
+ dictionary_cache_service.add_dictionary_source(dictionary_source: memory_source)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../modules/configurable'
4
+ require_relative '../modules/dictionary_cache_servicable'
5
+ require_relative '../modules/dictionary_file_loader'
6
+ require_relative '../modules/dictionary_keyable'
7
+
8
+ module LittleWeasel
9
+ module Services
10
+ # This class provides a service for loading dictionaries from disk and
11
+ # returning a Hash of dictionary words that can be used to instantiate
12
+ # a Dictionary object or otherwise.
13
+ class DictionaryFileLoaderService
14
+ include Modules::Configurable
15
+ include Modules::DictionaryCacheServicable
16
+ include Modules::DictionaryFileLoader
17
+ include Modules::DictionaryKeyable
18
+
19
+ def initialize(dictionary_key:, dictionary_cache:)
20
+ validate_dictionary_key dictionary_key: dictionary_key
21
+ self.dictionary_key = dictionary_key
22
+
23
+ validate_dictionary_cache dictionary_cache: dictionary_cache
24
+ self.dictionary_cache = dictionary_cache
25
+ end
26
+
27
+ def execute
28
+ if dictionary_cache_service.dictionary_exist?
29
+ raise ArgumentError,
30
+ "The dictionary associated with key '#{key}' already exists."
31
+ end
32
+
33
+ load dictionary_cache_service.dictionary_file!
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../modules/dictionary_cache_servicable'
4
+ require_relative '../modules/dictionary_metadata_servicable'
5
+ require_relative '../modules/dictionary_keyable'
6
+
7
+ module LittleWeasel
8
+ module Services
9
+ # This service removes a dictionary (Dictionary object) associated with
10
+ # the dictionary key from the dictionary cache along with the dictionary
11
+ # file reference and any metadata associated with the dictionary from the
12
+ # dictionary cache.
13
+ class DictionaryKillerService
14
+ include Modules::DictionaryCacheServicable
15
+ include Modules::DictionaryMetadataServicable
16
+ include Modules::DictionaryKeyable
17
+
18
+ def initialize(dictionary_key:, dictionary_cache:, dictionary_metadata:)
19
+ validate_dictionary_key dictionary_key: dictionary_key
20
+ self.dictionary_key = dictionary_key
21
+
22
+ validate_dictionary_cache dictionary_cache: dictionary_cache
23
+ self.dictionary_cache = dictionary_cache
24
+
25
+ validate_dictionary_metadata dictionary_metadata: dictionary_metadata
26
+ self.dictionary_metadata = dictionary_metadata
27
+ end
28
+
29
+ def execute
30
+ dictionary_cache_service.init
31
+ dictionary_metadata_service.class.init dictionary_metadata: dictionary_metadata
32
+ end
33
+ end
34
+ end
35
+ end