LittleWeasel 3.0.3 → 5.0.0

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 (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