- Needle in a Haystack
The HaystackTag class represents tags in a hierarchical structure. Each tag can have a parent tag and multiple child tags. This model is used to create and manage a tree structure of tags.
name: The name of the tag (required and unique).description: A description of the tag (required).parent_tag: An optional reference to the parent tag.
name: Must be present and unique.description: Must be present.prevent_circular_reference: Prevents circular references in the tag hierarchy.
belongs_to :parent_tag: Refers to the parent tag.has_many :children: Refers to the child tags.has_many :haystack_taggings: Refers to the taggings that use this tag.has_many :taggables: Refers to the objects tagged with this tag.
ancestors: Returns an array of all ancestor tags.full_path: Returns the full path of the tag in the tree structure.self.find_by_path(path): Finds a tag based on a path.descendants: Returns an array of all descendant tags.siblings: Returns an array of all sibling tags.root?: Checks if the tag is a root tag.leaf?: Checks if the tag is a leaf tag.depth: Returns the depth of the tag in the tree structure.
# Creating a new tag
root_tag = HaystackTag.create(name: "root", description: "Root tag")
# Creating a child tag
child_tag = HaystackTag.create(name: "child", description: "Child tag", parent_tag: root_tag)
# Getting the full path of a tag
puts child_tag.full_path # Output: "root > child"
# Getting all ancestor tags
ancestors = child_tag.ancestorsThe HaystackTagging class represents the relationship between tags and taggable objects (polymorphic). This model is used to tag objects with HaystackTag tags.
belongs_to :haystack_tag: Refers to theHaystackTag.belongs_to :taggable: Refers to the taggable object (polymorphic).
# Creating a new tagging
tag = HaystackTag.find_by(name: "child")
device = Device.find(1) # Example of a taggable object
tagging = HaystackTagging.create(haystack_tag: tag, taggable: device)The HaystackOntology class is responsible for managing the ontology of tags. It provides methods to load, find, and create tags based on a YAML configuration file.
self.tags: Loads the tags from theconfig/haystack_ontology.ymlfile and caches them.self.find_tag(path): Finds a tag based on a given path. It supports both flat and hierarchical paths.self.find_tag_in_hierarchy(current_hash, target_key, path = []): Recursively searches for a tag in a nested hash structure.self.create_tags: Uses theHaystackFactoryto create tags from the loaded ontology.self.find_or_create_tag(name): Finds or creates a tag based on the name using theHaystackFactory.self.import_full_ontology: Imports the full ontology by creating all tags.
The HaystackFactory class is responsible for creating and managing tags and taggings. It uses a strategy pattern to allow different tag creation strategies.
initialize(tag_strategy = DefaultTagStrategy.new): Initializes the factory with a given tag strategy.create_tag(name, description): Creates a tag using the current strategy.create_tagging(tag, taggable): Creates a tagging for a taggable object.find_or_create_tag(name, attributes = {}): Finds or creates a tag with the given attributes.create_tags(tag_hash, parent_tag = nil): Recursively creates tags from a nested hash structure.
- Loading Tags:
HaystackOntologyloads the tags from the YAML file using theself.tagsmethod. - Finding Tags:
HaystackOntologycan find tags based on a path using theself.find_tagandself.find_tag_in_hierarchymethods. - Creating Tags:
HaystackOntologyuses theself.create_tagsmethod to create tags. This method initializes aHaystackFactorywith anOntologyTagStrategyand calls the factory'screate_tagsmethod. - Finding or Creating Tags:
HaystackOntologyuses theself.find_or_create_tagmethod to find or create a tag. This method initializes aHaystackFactorywith anOntologyTagStrategyand calls the factory'sfind_or_create_tagmethod. - Importing Full Ontology:
HaystackOntologyuses theself.import_full_ontologymethod to import the full ontology by calling theself.create_tagsmethod.
# Load and create all tags from the ontology
HaystackOntology.import_full_ontology
# Find a specific tag by path
tag = HaystackOntology.find_tag("root.child")
# Find or create a tag by name
tag = HaystackOntology.find_or_create_tag("child")The query strategies are used to bind several data objects together based on their tags. This is achieved using the Strategy design pattern, which allows different query strategies to be implemented and executed dynamically.
The QueryContext class is responsible for executing a given strategy. It takes a strategy as an argument and calls the execute method on that strategy.
# Define a strategy
strategy = FindByTagsStrategy.new(Model, tags)
# Create a context with the strategy
context = QueryContext.new(strategy)
# Execute the strategy
result = context.executeThe QueryStrategy class is an abstract base class for all query strategies. It defines an execute method that must be implemented by subclasses.
class CustomStrategy < QueryStrategy def execute # Custom query logic end end
strategy = CustomStrategy.new
context = QueryContext.new(strategy)
result = context.executeThe FindByTagsStrategy class is a concrete implementation of QueryStrategy. It finds records that are associated with any of the given tags.
tags = [tag1, tag2]
strategy = FindByTagsStrategy.new(Model, tags)
context = QueryContext.new(strategy)
result = context.executeThe FindPointsWithTagStrategy class is a concrete implementation of QueryStrategy. It finds records that are associated with a specific tag.
The HaystackTag model includes comprehensive validations and associations to ensure data integrity and support hierarchical relationships.
RSpec.describe HaystackTag, type: :model do
describe "validations and associations" do
subject { build(:haystack_tag) }
# Validations
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:description) }
# Associations
it { is_expected.to belong_to(:parent_tag).class_name("HaystackTag").optional }
it { is_expected.to have_many(:children).class_name("HaystackTag").with_foreign_key("parent_tag_id").dependent(:destroy).inverse_of(:parent_tag) }
it { is_expected.to have_many(:haystack_taggings).dependent(:destroy) }
it { is_expected.to have_many(:taggables).through(:haystack_taggings).source(:taggable) }
endHaystackTag ensures unique tag names within the same parent but allows identical names across different parents.
context "when creating duplicate names" do
it "validates uniqueness within the same parent" do
parent = create(:haystack_tag)
create(:haystack_tag, name: "test", parent_tag: parent)
duplicate = build(:haystack_tag, name: "test", parent_tag: parent)
expect(duplicate).not_to be_valid
expect(duplicate.errors[:name]).to include("Must be unique in same category")
end
it "allows the same name under different parents" do
parent1 = create(:haystack_tag)
parent2 = create(:haystack_tag)
create(:haystack_tag, name: "test", parent_tag: parent1)
tag2 = build(:haystack_tag, name: "test", parent_tag: parent2)
expect(tag2).to be_valid
end
endThe HaystackTag model supports hierarchical operations such as identifying roots, leaves, depth, and ancestor relationships.
describe "hierarchy functionality" do
let(:root) { create(:haystack_tag, name: "root") }
let(:child) { create(:haystack_tag, name: "child", parent_tag: root) }
let(:grandchild) { create(:haystack_tag, name: "grandchild", parent_tag: child) }
context "basic hierarchy methods" do
it "identifies root and leaf nodes correctly" do
expect(root.root?).to be true
expect(child.root?).to be false
expect(grandchild.leaf?).to be true
expect(child.leaf?).to be false
end
it "calculates depth accurately" do
expect(root.depth).to eq(0)
expect(child.depth).to eq(1)
expect(grandchild.depth).to eq(2)
end
it "returns correct ancestors" do
expect(grandchild.ancestors).to eq([child, root])
expect(child.ancestors).to eq([root])
expect(root.ancestors).to be_empty
end
end
endHaystackTag provides methods for finding tags based on hierarchical paths.
context "path operations" do
it "retrieves tags by valid paths" do
expect(HaystackTag.find_by_path("root")).to eq(root)
expect(HaystackTag.find_by_path("root.child")).to eq(child)
expect(HaystackTag.find_by_path("root.child.grandchild")).to eq(grandchild)
end
it "handles invalid paths gracefully" do
expect(HaystackTag.find_by_path("invalid")).to be_nil
expect(HaystackTag.find_by_path("root.invalid")).to be_nil
end
endThe HaystackTag model supports efficient retrieval of descendants and siblings.
context "sibling and descendant operations" do
let(:sibling) { create(:haystack_tag, name: "sibling", parent_tag: root) }
it "returns all descendants correctly" do
expect(root.descendants).to contain_exactly(child, grandchild, sibling)
expect(child.descendants).to contain_exactly(grandchild)
expect(grandchild.descendants).to be_empty
end
end