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 +4 -4
- data/README.md +144 -0
- data/lib/needle_in_a_haystack/concerns/taggable.rb +1 -0
- data/lib/needle_in_a_haystack/factories/haystack_factory.rb +1 -7
- data/lib/needle_in_a_haystack/models/haystack_ontology.rb +13 -24
- data/lib/needle_in_a_haystack/models/haystack_tag.rb +5 -3
- data/lib/needle_in_a_haystack/version.rb +1 -1
- metadata +8 -9
- data/lib/tasks/location_create.rake +0 -7
- data/spec/strategies/find_by_tags_strategy_spec.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e9481dac23d82e6d43e9c796dc66ab30c2082ded78a11591e31946fc6d2dc9fa
|
4
|
+
data.tar.gz: a30f6c7cce478f82b5f22f340a2112a0dc28c8acd7d72ea140010cf78280263e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
```
|
@@ -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
|
-
|
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(".")
|
9
|
+
path.include?(".") ? find_tag_in_hierarchy(tags, path.split(".")) : find_tag_in_hierarchy(tags, [path])
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
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(
|
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)
|
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
|
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
|
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
|
+
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:
|
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
|
-
|
118
|
-
homepage: https://www.fouriq.nl
|
117
|
+
homepage: https://github.com/Fransver/needle_in_a_haystack
|
119
118
|
licenses:
|
120
|
-
-
|
119
|
+
- MIT
|
121
120
|
metadata:
|
122
|
-
homepage_uri: https://
|
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:
|
140
|
+
summary: haystack ontology implementation for FourIQ projects.
|
142
141
|
test_files: []
|
@@ -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
|