needle_in_a_haystack 1.0.7 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 718b7304ea4ef3e86857b3952c4b006e322277d789dcc76a65c8dc17e27b0bd9
4
- data.tar.gz: 224dd77d2955d35c4c385bd628e705d131e11700e60de3f4ee785ddb1ca74dd6
3
+ metadata.gz: e9481dac23d82e6d43e9c796dc66ab30c2082ded78a11591e31946fc6d2dc9fa
4
+ data.tar.gz: a30f6c7cce478f82b5f22f340a2112a0dc28c8acd7d72ea140010cf78280263e
5
5
  SHA512:
6
- metadata.gz: d3aa136ace9cebc46612328471e932816ef65e4831ea264ef32e62c9423aaa3c8230c01adbf0ac7cc44c258bd80e3907eb48763c938d0afe3a7a899359188b7f
7
- data.tar.gz: 4dcfb4bd59a773d466f050fe00b0d308b309268a59238a2e4b8b9b6a60ae81f0c14934d9d7c54a39ba08ed09d01c13d000e073b952cc6e6951b51caf6981d955
6
+ metadata.gz: 3a25092104aa31124158fd1010e65bc8cf0fd483ddbfb2627913ca92de36281814f6826c98dcf1746268c9e043bfbbbde09ff9c9ee1d2a4d75a0a6eca6e3be93
7
+ data.tar.gz: 51c3a084b9d380f9ce5beaecbb671b64e2480a7bac2b09173268d98097328e0ba835acd42e3f95056a6d377408454526f308be7e4bd1d54bc3ee9558da051829
data/README.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Needle in a Haystack
2
2
 
3
+ <img src="storage/logo.jpg" alt="Logo" width="40%">
4
+
5
+ # Table of Contents
6
+
7
+ - [Needle in a Haystack](#needle-in-a-haystack)
8
+ - [Models](#models)
9
+ - [HaystackTag](#haystacktag)
10
+ - [Attributes](#attributes)
11
+ - [Validations](#validations)
12
+ - [Associations](#associations)
13
+ - [Methods](#methods)
14
+ - [Example Usage](#example-usage)
15
+ - [HaystackTagging](#haystacktagging)
16
+ - [Associations](#associations-1)
17
+ - [Example Usage](#example-usage-1)
18
+ - [Ontology and Factory](#ontology-and-factory)
19
+ - [HaystackOntology](#haystackontology)
20
+ - [Key Methods](#key-methods)
21
+ - [Example Usage](#example-usage-2)
22
+ - [HaystackFactory](#haystackfactory)
23
+ - [Key Methods](#key-methods-1)
24
+ - [How They Work Together](#how-they-work-together)
25
+ - [Example Usage](#example-usage-3)
26
+ - [Query Strategies](#query-strategies)
27
+ - [QueryContext](#querycontext)
28
+ - [Example Usage](#example-usage-4)
29
+ - [QueryStrategy](#querystrategy)
30
+ - [FindByTagsStrategy](#findbytagsstrategy)
31
+ - [FindPointsWithTagStrategy](#findpointswithtagstrategy)
32
+ - [HaystackTag Validations and Associations](#haystacktag-validations-and-associations)
33
+ - [Duplicate Name Validation](#duplicate-name-validation)
34
+ - [Hierarchy Functionality](#hierarchy-functionality)
35
+ - [Path Operations](#path-operations)
36
+ - [Descendant and Sibling Operations](#descendant-and-sibling-operations)
37
+
3
38
  ## Models
4
39
 
5
40
  ### HaystackTag
@@ -169,3 +204,112 @@ The FindPointsWithTagStrategy class is a concrete implementation of QueryStrateg
169
204
 
170
205
 
171
206
 
207
+ ## HaystackTag Validations and Associations
208
+ The HaystackTag model includes comprehensive validations and associations to ensure data integrity and support hierarchical relationships.
209
+
210
+ ``` ruby
211
+ RSpec.describe HaystackTag, type: :model do
212
+ describe "validations and associations" do
213
+ subject { build(:haystack_tag) }
214
+
215
+ # Validations
216
+ it { is_expected.to validate_presence_of(:name) }
217
+ it { is_expected.to validate_presence_of(:description) }
218
+
219
+ # Associations
220
+ it { is_expected.to belong_to(:parent_tag).class_name("HaystackTag").optional }
221
+ it { is_expected.to have_many(:children).class_name("HaystackTag").with_foreign_key("parent_tag_id").dependent(:destroy).inverse_of(:parent_tag) }
222
+ it { is_expected.to have_many(:haystack_taggings).dependent(:destroy) }
223
+ it { is_expected.to have_many(:taggables).through(:haystack_taggings).source(:taggable) }
224
+ end
225
+ ```
226
+
227
+ # Duplicate Name Validation
228
+ HaystackTag ensures unique tag names within the same parent but allows identical names across different parents.
229
+
230
+ ``` ruby
231
+ context "when creating duplicate names" do
232
+ it "validates uniqueness within the same parent" do
233
+ parent = create(:haystack_tag)
234
+ create(:haystack_tag, name: "test", parent_tag: parent)
235
+ duplicate = build(:haystack_tag, name: "test", parent_tag: parent)
236
+
237
+ expect(duplicate).not_to be_valid
238
+ expect(duplicate.errors[:name]).to include("Must be unique in same category")
239
+ end
240
+
241
+ it "allows the same name under different parents" do
242
+ parent1 = create(:haystack_tag)
243
+ parent2 = create(:haystack_tag)
244
+ create(:haystack_tag, name: "test", parent_tag: parent1)
245
+ tag2 = build(:haystack_tag, name: "test", parent_tag: parent2)
246
+
247
+ expect(tag2).to be_valid
248
+ end
249
+ end
250
+ ```
251
+
252
+ # Hierarchy Functionality
253
+ The HaystackTag model supports hierarchical operations such as identifying roots, leaves, depth, and ancestor relationships.
254
+
255
+ ``` ruby
256
+ describe "hierarchy functionality" do
257
+ let(:root) { create(:haystack_tag, name: "root") }
258
+ let(:child) { create(:haystack_tag, name: "child", parent_tag: root) }
259
+ let(:grandchild) { create(:haystack_tag, name: "grandchild", parent_tag: child) }
260
+
261
+ context "basic hierarchy methods" do
262
+ it "identifies root and leaf nodes correctly" do
263
+ expect(root.root?).to be true
264
+ expect(child.root?).to be false
265
+ expect(grandchild.leaf?).to be true
266
+ expect(child.leaf?).to be false
267
+ end
268
+
269
+ it "calculates depth accurately" do
270
+ expect(root.depth).to eq(0)
271
+ expect(child.depth).to eq(1)
272
+ expect(grandchild.depth).to eq(2)
273
+ end
274
+
275
+ it "returns correct ancestors" do
276
+ expect(grandchild.ancestors).to eq([child, root])
277
+ expect(child.ancestors).to eq([root])
278
+ expect(root.ancestors).to be_empty
279
+ end
280
+ end
281
+ end
282
+ ```
283
+
284
+ # Path Operations
285
+ HaystackTag provides methods for finding tags based on hierarchical paths.
286
+
287
+ ``` ruby
288
+ context "path operations" do
289
+ it "retrieves tags by valid paths" do
290
+ expect(HaystackTag.find_by_path("root")).to eq(root)
291
+ expect(HaystackTag.find_by_path("root.child")).to eq(child)
292
+ expect(HaystackTag.find_by_path("root.child.grandchild")).to eq(grandchild)
293
+ end
294
+
295
+ it "handles invalid paths gracefully" do
296
+ expect(HaystackTag.find_by_path("invalid")).to be_nil
297
+ expect(HaystackTag.find_by_path("root.invalid")).to be_nil
298
+ end
299
+ end
300
+ ```
301
+
302
+ # Descendant and Sibling Operations
303
+ The HaystackTag model supports efficient retrieval of descendants and siblings.
304
+
305
+ ``` ruby
306
+ context "sibling and descendant operations" do
307
+ let(:sibling) { create(:haystack_tag, name: "sibling", parent_tag: root) }
308
+
309
+ it "returns all descendants correctly" do
310
+ expect(root.descendants).to contain_exactly(child, grandchild, sibling)
311
+ expect(child.descendants).to contain_exactly(grandchild)
312
+ expect(grandchild.descendants).to be_empty
313
+ end
314
+ end
315
+ ```
@@ -1,3 +1,4 @@
1
+ # This model represents an entity that can be tagged with HaystackTags through HaystackTaggings.
1
2
  class Taggable < ApplicationRecord
2
3
  has_many :haystack_taggings, as: :taggable, dependent: :destroy
3
4
  has_many :haystack_tags, through: :haystack_taggings
@@ -16,11 +16,7 @@ class HaystackFactory < BaseFactory
16
16
 
17
17
  def find_or_create_tag(name, attributes = {})
18
18
  tag = HaystackTag.find_or_create_by(name: name)
19
- if tag.persisted?
20
- @tag_strategy.update_tag(tag, attributes)
21
- else
22
- tag.update(attributes)
23
- end
19
+ tag.persisted? ? @tag_strategy.update_tag(tag, attributes) : tag.update(attributes)
24
20
  tag
25
21
  end
26
22
 
@@ -30,9 +26,7 @@ class HaystackFactory < BaseFactory
30
26
 
31
27
  tag = find_or_create_tag(name, description: data["description"], haystack_marker: data["marker"])
32
28
  tag.update(parent_tag_id: parent_tag&.id)
33
-
34
29
  Rails.logger.info("Created tag: #{tag.name}, Parent: #{parent_tag&.name}, Parent ID: #{parent_tag&.id}")
35
-
36
30
  create_tags(data["children"], tag) if data["children"]
37
31
  end
38
32
  end
@@ -6,35 +6,24 @@ class HaystackOntology < ApplicationRecord
6
6
  def self.find_tag(path)
7
7
  return nil if path.nil?
8
8
 
9
- path.include?(".") ? find_tag_in_hierarchy(tags, path.split(".").last) : find_tag_in_hierarchy(tags, path)
9
+ path.include?(".") ? find_tag_in_hierarchy(tags, path.split(".")) : find_tag_in_hierarchy(tags, [path])
10
10
  end
11
11
 
12
- def self.find_tag_in_hierarchy(current_hash, target_key, path = [])
13
- return nil unless current_hash.is_a?(Hash)
12
+ # This method recursively searches for a tag in a nested hash structure.
13
+ # - `current_hash`: The current level of the hash being searched.
14
+ # - `keys`: An array of keys representing the path to the desired tag.
15
+ # - `path`: An array to keep track of the current path (used for constructing the full path of the tag).
16
+ def self.find_tag_in_hierarchy(current_hash, keys, path = [])
17
+ return nil unless current_hash.is_a?(Hash) && keys.any?
14
18
 
15
- result = current_hash[target_key]
16
- return result.is_a?(Hash) ? result.merge("path" => path.push(target_key).join(".")) : result if result
19
+ if current_hash[keys.first].is_a?(Hash)
20
+ path.push(keys.shift)
21
+ return current_hash[path.last].merge("path" => path.join(".")) if keys.empty?
17
22
 
18
- children = current_hash["children"]
19
- if children.is_a?(Hash)
20
- children.each do |key, value|
21
- new_path = path + [key]
22
- return value.merge("path" => new_path.join(".")) if key == target_key
23
-
24
- result = find_tag_in_hierarchy(value, target_key, new_path)
25
- return result if result
26
- end
23
+ find_tag_in_hierarchy(current_hash[path.last], keys, path)
24
+ else
25
+ current_hash[keys.shift]
27
26
  end
28
-
29
- current_hash.each do |key, value|
30
- next if %w[description children].include?(key) || !value.is_a?(Hash)
31
-
32
- new_path = path + [key]
33
- result = find_tag_in_hierarchy(value, target_key, new_path)
34
- return result if result
35
- end
36
-
37
- nil
38
27
  end
39
28
 
40
29
  def self.create_tags
@@ -1,6 +1,8 @@
1
1
  require "yaml"
2
-
3
2
  class HaystackTag < BaseTag
3
+ # PATH_ONTOLOGY = "/Users/frans/Documents/fouriq/fouriq_haystack/config/haystack_ontology.yml"
4
+ PATH_ONTOLOGY = "/Users/frans/Documents/fouriq/fouriq_haystack/config/haystack_ontology.yml"
5
+
4
6
  belongs_to :parent_tag, class_name: "HaystackTag", optional: true
5
7
  has_many :children, class_name: "HaystackTag", foreign_key: "parent_tag_id", dependent: :destroy, inverse_of: :parent_tag
6
8
  has_many :haystack_taggings, dependent: :destroy
@@ -11,13 +13,13 @@ class HaystackTag < BaseTag
11
13
  validate :prevent_circular_reference
12
14
  validate :ensure_identity
13
15
 
14
- CATEGORIES = YAML.load_file("/Users/frans/Documents/fouriq/fouriq_haystack/lib/haystack_ontology.yml")
16
+ CATEGORIES = YAML.load_file(PATH_ONTOLOGY)
15
17
 
16
18
  def ancestors
17
19
  ancestors = []
18
20
  current = self
19
21
  while current.parent_tag
20
- break if ancestors.include?(current.parent_tag) # Voorkom oneindige lus
22
+ break if ancestors.include?(current.parent_tag)
21
23
 
22
24
  ancestors << current.parent_tag
23
25
  current = current.parent_tag
@@ -1 +1 @@
1
- VERSION = "1.0.7".freeze
1
+ VERSION = "1.1.0".freeze
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: needle_in_a_haystack
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frans Verberne
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-11 00:00:00.000000000 Z
11
+ date: 2024-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dotenv-rails
@@ -80,7 +80,8 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
- description: haystack ontology implementation for FourIQ projects.
83
+ description: A Ruby gem for implementing the Haystack ontology, providing a structured
84
+ way to manage and interact with building and equipment data.
84
85
  email:
85
86
  - frans.verberne@fouriq.nl
86
87
  executables: []
@@ -112,14 +113,12 @@ files:
112
113
  - lib/needle_in_a_haystack/strategies/query_strategy.rb
113
114
  - lib/needle_in_a_haystack/strategies/tag_strategy.rb
114
115
  - lib/needle_in_a_haystack/version.rb
115
- - lib/tasks/location_create.rake
116
116
  - spec/models/haystack_tag_spec.rb
117
- - spec/strategies/find_by_tags_strategy_spec.rb
118
- homepage: https://www.fouriq.nl
117
+ homepage: https://github.com/Fransver/needle_in_a_haystack
119
118
  licenses:
120
- - Nonstandard
119
+ - MIT
121
120
  metadata:
122
- homepage_uri: https://www.fouriq.nl
121
+ homepage_uri: https://github.com/Fransver/needle_in_a_haystack
123
122
  post_install_message:
124
123
  rdoc_options: []
125
124
  require_paths:
@@ -138,5 +137,5 @@ requirements: []
138
137
  rubygems_version: 3.5.18
139
138
  signing_key:
140
139
  specification_version: 4
141
- summary: Models and dependencies shared across our projects.
140
+ summary: haystack ontology implementation for FourIQ projects.
142
141
  test_files: []
@@ -1,7 +0,0 @@
1
- namespace :locations do
2
- desc "Interactive location creator"
3
- task create: :environment do
4
- load "app/services/location_creator.rb"
5
- LocationCreator.start
6
- end
7
- end
@@ -1,38 +0,0 @@
1
- require "rails_helper"
2
-
3
- RSpec.describe FindByTagsStrategy, type: :strategy do
4
- let(:bedroom) { HaystackTag.create(name: "bedRoom", description: "A Bedroom") }
5
- let(:lora) { HaystackTag.create(name: "loraMeter", description: "A Lora Meter") }
6
- let(:indoor_temp) { HaystackTag.create(name: "tempIndoor", description: "A Indoor temperature sensor") }
7
- let(:device) { Device.create(name: "Device1") }
8
- let(:point) { Point.create }
9
-
10
- before do
11
- device.haystack_tags << [bedroom, lora]
12
- folder = device.folders.create(name: "Folder1")
13
- folder.points << point
14
- point.haystack_tags << [indoor_temp, bedroom]
15
- end
16
-
17
- describe "#execute" do
18
- context "when finding devices by tags" do
19
- it "returns devices with the specified tags" do
20
- strategy = described_class.new(Device, [bedroom, lora])
21
- context = QueryContext.new(strategy)
22
- result = context.execute
23
-
24
- expect(result).to include(device)
25
- end
26
- end
27
-
28
- context "when finding points by tags" do
29
- it "returns points with the specified tags" do
30
- strategy = described_class.new(Point, [indoor_temp])
31
- context = QueryContext.new(strategy)
32
- result = context.execute
33
-
34
- expect(result).to include(point)
35
- end
36
- end
37
- end
38
- end