LittleWeasel 3.0.4 → 4.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 (152) 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/Gemfile +3 -1
  9. data/LittleWeasel.gemspec +31 -18
  10. data/README.md +408 -42
  11. data/Rakefile +296 -3
  12. data/lib/LittleWeasel.rb +5 -184
  13. data/lib/LittleWeasel/block_results.rb +81 -0
  14. data/lib/LittleWeasel/configure.rb +98 -0
  15. data/lib/LittleWeasel/dictionary.rb +125 -0
  16. data/lib/LittleWeasel/dictionary_key.rb +48 -0
  17. data/lib/LittleWeasel/dictionary_manager.rb +85 -0
  18. data/lib/LittleWeasel/errors/dictionary_file_already_loaded_error.rb +9 -0
  19. data/lib/LittleWeasel/errors/dictionary_file_empty_error.rb +8 -0
  20. data/lib/LittleWeasel/errors/dictionary_file_not_found_error.rb +8 -0
  21. data/lib/LittleWeasel/errors/dictionary_file_too_large_error.rb +16 -0
  22. data/lib/LittleWeasel/errors/language_required_error.rb +8 -0
  23. data/lib/LittleWeasel/errors/must_override_error.rb +8 -0
  24. data/lib/LittleWeasel/filters/en_us/currency_filter.rb +19 -0
  25. data/lib/LittleWeasel/filters/en_us/numeric_filter.rb +19 -0
  26. data/lib/LittleWeasel/filters/en_us/single_character_word_filter.rb +21 -0
  27. data/lib/LittleWeasel/filters/word_filter.rb +59 -0
  28. data/lib/LittleWeasel/filters/word_filter_managable.rb +80 -0
  29. data/lib/LittleWeasel/filters/word_filter_validatable.rb +31 -0
  30. data/lib/LittleWeasel/filters/word_filterable.rb +19 -0
  31. data/lib/LittleWeasel/filters/word_filters_validatable.rb +29 -0
  32. data/lib/LittleWeasel/metadata/dictionary_metadata.rb +145 -0
  33. data/lib/LittleWeasel/metadata/invalid_words_metadata.rb +134 -0
  34. data/lib/LittleWeasel/metadata/invalid_words_service_results.rb +45 -0
  35. data/lib/LittleWeasel/metadata/metadata_observable_validatable.rb +22 -0
  36. data/lib/LittleWeasel/metadata/metadata_observerable.rb +90 -0
  37. data/lib/LittleWeasel/metadata/metadatable.rb +136 -0
  38. data/lib/LittleWeasel/modules/class_name_to_symbol.rb +26 -0
  39. data/lib/LittleWeasel/modules/configurable.rb +26 -0
  40. data/lib/LittleWeasel/modules/deep_dup.rb +11 -0
  41. data/lib/LittleWeasel/modules/dictionary_cache_keys.rb +34 -0
  42. data/lib/LittleWeasel/modules/dictionary_cache_servicable.rb +26 -0
  43. data/lib/LittleWeasel/modules/dictionary_cache_validatable.rb +20 -0
  44. data/lib/LittleWeasel/modules/dictionary_creator_servicable.rb +27 -0
  45. data/lib/LittleWeasel/modules/dictionary_file_loader.rb +67 -0
  46. data/lib/LittleWeasel/modules/dictionary_key_validatable.rb +19 -0
  47. data/lib/LittleWeasel/modules/dictionary_keyable.rb +24 -0
  48. data/lib/LittleWeasel/modules/dictionary_loader_servicable.rb +27 -0
  49. data/lib/LittleWeasel/modules/dictionary_metadata_servicable.rb +29 -0
  50. data/lib/LittleWeasel/modules/dictionary_metadata_validatable.rb +17 -0
  51. data/lib/LittleWeasel/modules/dictionary_sourceable.rb +26 -0
  52. data/lib/LittleWeasel/modules/dictionary_validatable.rb +30 -0
  53. data/lib/LittleWeasel/modules/language.rb +23 -0
  54. data/lib/LittleWeasel/modules/language_validatable.rb +16 -0
  55. data/lib/LittleWeasel/modules/locale.rb +40 -0
  56. data/lib/LittleWeasel/modules/order_validatable.rb +18 -0
  57. data/lib/LittleWeasel/modules/orderable.rb +17 -0
  58. data/lib/LittleWeasel/modules/region.rb +23 -0
  59. data/lib/LittleWeasel/modules/region_validatable.rb +16 -0
  60. data/lib/LittleWeasel/modules/tag_validatable.rb +16 -0
  61. data/lib/LittleWeasel/modules/taggable.rb +31 -0
  62. data/lib/LittleWeasel/modules/word_results_validatable.rb +28 -0
  63. data/lib/LittleWeasel/preprocessors/en_us/capitalize_preprocessor.rb +22 -0
  64. data/lib/LittleWeasel/preprocessors/preprocessed_word.rb +28 -0
  65. data/lib/LittleWeasel/preprocessors/preprocessed_word_validatable.rb +55 -0
  66. data/lib/LittleWeasel/preprocessors/preprocessed_words.rb +55 -0
  67. data/lib/LittleWeasel/preprocessors/preprocessed_words_validatable.rb +27 -0
  68. data/lib/LittleWeasel/preprocessors/word_preprocessable.rb +19 -0
  69. data/lib/LittleWeasel/preprocessors/word_preprocessor.rb +122 -0
  70. data/lib/LittleWeasel/preprocessors/word_preprocessor_managable.rb +114 -0
  71. data/lib/LittleWeasel/preprocessors/word_preprocessor_validatable.rb +40 -0
  72. data/lib/LittleWeasel/preprocessors/word_preprocessors_validatable.rb +24 -0
  73. data/lib/LittleWeasel/services/dictionary_cache_service.rb +262 -0
  74. data/lib/LittleWeasel/services/dictionary_creator_service.rb +94 -0
  75. data/lib/LittleWeasel/services/dictionary_file_loader_service.rb +37 -0
  76. data/lib/LittleWeasel/services/dictionary_killer_service.rb +35 -0
  77. data/lib/LittleWeasel/services/dictionary_loader_service.rb +59 -0
  78. data/lib/LittleWeasel/services/dictionary_metadata_service.rb +114 -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/spec/factories/dictionary.rb +43 -0
  83. data/spec/factories/dictionary_cache_service.rb +95 -0
  84. data/spec/factories/dictionary_creator_service.rb +16 -0
  85. data/spec/factories/dictionary_file_loader_service.rb +13 -0
  86. data/spec/factories/dictionary_hash.rb +39 -0
  87. data/spec/factories/dictionary_key.rb +14 -0
  88. data/spec/factories/dictionary_killer_service.rb +14 -0
  89. data/spec/factories/dictionary_loader_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 +116 -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 +44 -0
  119. data/spec/lib/LittleWeasel/modules/language_spec.rb +52 -0
  120. data/spec/lib/LittleWeasel/modules/locale_spec.rb +140 -0
  121. data/spec/lib/LittleWeasel/modules/region_spec.rb +52 -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 +216 -0
  128. data/spec/lib/LittleWeasel/preprocessors/word_preprocessor_spec.rb +175 -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_loader_service_spec.rb +50 -0
  135. data/spec/lib/LittleWeasel/services/dictionary_metadata_service_spec.rb +279 -0
  136. data/spec/lib/LittleWeasel/word_results_spec.rb +275 -0
  137. data/spec/lib/LittleWeasel/workflow/workflow_spec.rb +20 -0
  138. data/spec/spec_helper.rb +117 -6
  139. data/spec/support/factory_bot.rb +15 -0
  140. data/spec/support/file_helpers.rb +32 -0
  141. data/spec/support/files/empty-dictionary.txt +0 -0
  142. data/{lib/dictionary → spec/support/files/en-US-big.txt} +262156 -31488
  143. data/spec/support/files/en-US-tagged.txt +26 -0
  144. data/spec/support/files/en-US.txt +26 -0
  145. data/spec/support/files/en.txt +26 -0
  146. data/spec/support/files/es-ES.txt +27 -0
  147. data/spec/support/files/es.txt +27 -0
  148. data/spec/support/general_helpers.rb +68 -0
  149. data/spec/support/shared_contexts.rb +108 -0
  150. data/spec/support/shared_examples.rb +105 -0
  151. metadata +408 -65
  152. data/spec/checker/checker_spec.rb +0 -286
@@ -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,262 @@
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
+ require_relative '../modules/dictionary_validatable'
8
+
9
+ module LittleWeasel
10
+ module Services
11
+ # This class provides methods and attributes that can be used to manage the
12
+ # dictionary cache. The "dictionary cache" is a simple Hash that provides
13
+ # access to informaiton related to dictionaries through a dictionary "key".
14
+ # A dictionary "key" is a unique key comprised of a locale and
15
+ # optional "tag" (see Modules::Taggable and DictionaryKey for more
16
+ # information). The dictionary cache also provides a way for dictionary
17
+ # objects to share dictionary information, in particular, the dictionary
18
+ # file and dictionary metadata.
19
+ class DictionaryCacheService
20
+ include Modules::DictionaryKeyable
21
+ include Modules::DictionaryCacheValidatable
22
+ include Modules::DictionaryCacheKeys
23
+ include Modules::DictionarySourceable
24
+ include Modules::DictionaryValidatable
25
+
26
+ attr_reader :dictionary_cache
27
+
28
+ # This class produces the following (example) Hash that represents the
29
+ # dictionary cache structure:
30
+ #
31
+ # @example This is an example:
32
+ #
33
+ # {
34
+ # 'dictionary_cache' =>
35
+ # {
36
+ # 'dictionary_references' =>
37
+ # {
38
+ # 'en' =>
39
+ # {
40
+ # 'dictionary_id' => 19ec7845
41
+ # },
42
+ # 'en-US' =>
43
+ # {
44
+ # 'dictionary_id' => 0987a3f2
45
+ # },
46
+ # 'en-US-temp' =>
47
+ # {
48
+ # 'dictionary_id' => 9273eac6
49
+ # }
50
+ # },
51
+ # 'dictionaries' =>
52
+ # {
53
+ # 19ec7845 =>
54
+ # {
55
+ # 'source' => '/en.txt',
56
+ # 'dictionary_object' => {}
57
+ # },
58
+ # 0987a3f2 =>
59
+ # {
60
+ # 'source' => '/en-US.txt',
61
+ # 'dictionary_object' => {}
62
+ # },
63
+ # 9273eac6 =>
64
+ # {
65
+ # 'source' => '*736ed423',
66
+ # 'dictionary_object' => {}
67
+ # }
68
+ # }
69
+ # }
70
+ # }
71
+ def initialize(dictionary_key:, dictionary_cache:)
72
+ validate_dictionary_key dictionary_key: dictionary_key
73
+ self.dictionary_key = dictionary_key
74
+
75
+ validate_dictionary_cache dictionary_cache: dictionary_cache
76
+ self.dictionary_cache = dictionary_cache
77
+
78
+ self.class.init(dictionary_cache: dictionary_cache) unless dictionary_cache[DICTIONARY_CACHE]
79
+ end
80
+
81
+ class << self
82
+ # This method resets dictionary_cache to its initialized state.
83
+ # This class method is different from the #init instance method
84
+ # in that ALL dictionary references and ALL dictionaries are
85
+ # initialized.
86
+ def init(dictionary_cache:)
87
+ Modules::DictionaryCacheKeys.initialize_dictionary_cache dictionary_cache: dictionary_cache
88
+ end
89
+
90
+ # Returns true if the dictionary cache is initialized; that
91
+ # is, if the given dictionary_cache is in the same state the
92
+ # dictionary cache would be in after .init were called.
93
+ def init?(dictionary_cache:)
94
+ initialized_dictionary_cache = init(dictionary_cache: {})
95
+ dictionary_cache.eql?(initialized_dictionary_cache)
96
+ end
97
+
98
+ # Returns the number of dictionaries currently in the cache.
99
+ def count(dictionary_cache:)
100
+ dictionary_cache.dig(self::DICTIONARY_CACHE, self::DICTIONARIES)&.keys&.count || 0
101
+ end
102
+ end
103
+
104
+ # This method resets the dictionary cache for the given key. This method
105
+ # is different from the .init class method in that ONLY the dictionary
106
+ # reference and dictionary specific to the given key is initialized.
107
+ def init
108
+ # TODO: Do not delete the dictionary if it is being pointed to by
109
+ # another dictionary reference.
110
+ dictionary_cache_hash = dictionary_cache[DICTIONARY_CACHE]
111
+ dictionary_cache_hash[DICTIONARIES]&.delete(dictionary_id)
112
+ dictionary_cache_hash[DICTIONARY_REFERENCES]&.delete(key)
113
+ self
114
+ end
115
+
116
+ # Returns true if the dictionary reference exists for the given key; false
117
+ # otherwise. This method is only concerned with the dictionary reference
118
+ # and has nothing to do with whether or not the associated dictionary
119
+ # is actually loaded into the dictionary cache.
120
+ def dictionary_reference?
121
+ dictionary_reference&.present? || false
122
+ end
123
+
124
+ # Returns true if a dictionaries Hash key exists for the given dictionary_id
125
+ # in the dictionary cache. This method is only concerned with the existance of
126
+ # the key and has nothing to do with whether or not file/memory sources are
127
+ # present or the presence of a dictionary object.
128
+ def dictionary?
129
+ dictionary_cache[DICTIONARY_CACHE][DICTIONARIES].key? dictionary_id
130
+ end
131
+
132
+ # Adds a dictionary source. A "dictionary source" specifies the source from which
133
+ # the dictionary ultimately obtains its words.
134
+ #
135
+ # @param source [String] the dictionary source. This can be a file path
136
+ # or a memory source indicator to signify that the dictionary was created
137
+ # dynamically from memory.
138
+ def add_dictionary_source(source:)
139
+ validate_dictionary_source_does_not_exist dictionary_cache_service: self
140
+
141
+ dictionary_id = dictionary_id_for_dictionary_source(source: source)
142
+ self.dictionary_reference = dictionary_id
143
+ # Only set the dictionary source if it doesn't already exist because settings
144
+ # the dictionary source wipes out the #dictionary_object; dictionary objects
145
+ # can have more than one dictionary reference pointing to them, and we don't
146
+ # want to blow away the #dictionary_object, metadata, or any other data
147
+ # associated with it if it already exists.
148
+ self.dictionary_source = source unless dictionary?
149
+ self
150
+ end
151
+
152
+ # Returns the dictionary id if there is a dictionary id in the dictionary
153
+ # cache associated with the given key; nil otherwise.
154
+ def dictionary_id
155
+ dictionary_cache.dig(DICTIONARY_CACHE, DICTIONARY_REFERENCES, key, DICTIONARY_ID)
156
+ end
157
+
158
+ # Returns the dictionary id if there is a dictionary id in the dictionary
159
+ # cache associated with the given key. This method raises an error if the
160
+ # dictionary id cannot be found.
161
+ def dictionary_id!
162
+ return dictionary_id if dictionary_id?
163
+
164
+ raise ArgumentError, "A dictionary id could not be found for key '#{key}'."
165
+ end
166
+
167
+ def dictionary_source!
168
+ raise ArgumentError, "A dictionary source could not be found for key '#{key}'." unless dictionary_reference?
169
+
170
+ dictionary_cache[DICTIONARY_CACHE][DICTIONARIES][dictionary_id!][SOURCE]
171
+ end
172
+ alias dictionary_file! dictionary_source!
173
+
174
+ def dictionary_source
175
+ dictionary_cache.dig(DICTIONARY_CACHE, DICTIONARIES, dictionary_id, SOURCE)
176
+ end
177
+ alias dictionary_file dictionary_source
178
+
179
+ # This method returns true if the dictionary associated with the
180
+ # given dictionary key is loaded/cached. If this is the case,
181
+ # a dictionary object is available in the dictionary cache.
182
+ def dictionary_object?
183
+ dictionary_object.present?
184
+ end
185
+ alias dictionary_exists? dictionary_object?
186
+
187
+ # Returns the dictionary object from the dictionary cache for the given
188
+ # key. This method raises an error if the dictionary is not in the cache;
189
+ # that is, if the dictionary was not previously loaded from disk or memory.
190
+ def dictionary_object!
191
+ unless dictionary_object?
192
+ raise ArgumentError,
193
+ "The dictionary object associated with argument key '#{key}' is not in the cache."
194
+ end
195
+
196
+ dictionary_object
197
+ end
198
+
199
+ def dictionary_object
200
+ dictionary_cache.dig(DICTIONARY_CACHE, DICTIONARIES, dictionary_id, DICTIONARY_OBJECT)
201
+ end
202
+
203
+ def dictionary_object=(object)
204
+ raise ArgumentError, 'Argument object is not a Dictionary object' unless object.is_a? Dictionary
205
+
206
+ unless dictionary_reference?
207
+ raise ArgumentError,
208
+ "The dictionary reference associated with key '#{key}' could not be found."
209
+ end
210
+ return if object.equal? dictionary_object
211
+
212
+ if dictionary_exists?
213
+ raise ArgumentError,
214
+ "The dictionary is already loaded/cached for key '#{key}'; use #unload or #kill first."
215
+ end
216
+
217
+ dictionary_cache[DICTIONARY_CACHE][DICTIONARIES][dictionary_id!][DICTIONARY_OBJECT] = object
218
+ end
219
+
220
+ private
221
+
222
+ attr_writer :dictionary_cache
223
+
224
+ def dictionary_reference
225
+ dictionary_cache.dig(DICTIONARY_CACHE, DICTIONARY_REFERENCES, key)
226
+ end
227
+
228
+ def dictionary_reference=(dictionary_id)
229
+ dictionary_cache[DICTIONARY_CACHE][DICTIONARY_REFERENCES][key] = {
230
+ DICTIONARY_ID => dictionary_id
231
+ }
232
+ end
233
+
234
+ # Returns the dictionary_id for the source if it exists in dictionaries;
235
+ # otherwise, returns the new dictionary id that should be used.
236
+ def dictionary_id_for_dictionary_source(source:)
237
+ dictionary_source?(source: source) || SecureRandom.uuid[0..7]
238
+ end
239
+
240
+ # Returns the dictionary_id associated with source if source exists;
241
+ # nil otherwise.
242
+ def dictionary_source?(source:)
243
+ dictionaries = dictionary_cache.dig(DICTIONARY_CACHE, DICTIONARIES)
244
+ dictionaries&.each_pair do |dictionary_id, dictionary_hash|
245
+ return dictionary_id if source == dictionary_hash[SOURCE]
246
+ end
247
+ nil
248
+ end
249
+
250
+ def dictionary_source=(source)
251
+ dictionary_cache[DICTIONARY_CACHE][DICTIONARIES][dictionary_id!] = {
252
+ SOURCE => source,
253
+ DICTIONARY_OBJECT => {}
254
+ }
255
+ end
256
+
257
+ def dictionary_id?
258
+ dictionary_id.present?
259
+ end
260
+ end
261
+ end
262
+ 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(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(source: memory_source)
91
+ end
92
+ end
93
+ end
94
+ end