needle_in_a_haystack 1.0.7 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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