locabulary 0.3.1 → 0.5.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.
@@ -0,0 +1,78 @@
1
+ require 'set'
2
+ require 'date'
3
+ require 'locabulary/items'
4
+ require 'locabulary/exceptions'
5
+
6
+ module Locabulary
7
+ module Commands
8
+ # Responsible for transforming the flat data for the given :predicate_name
9
+ # into a hierarchy.
10
+ class ActiveHierarchicalRootsCommand
11
+ def self.cache
12
+ @cache ||= {}
13
+ end
14
+ private_class_method :cache
15
+
16
+ # @api private
17
+ def self.reset_cache!
18
+ @cache = {}
19
+ end
20
+
21
+ # @api private
22
+ # @since 0.5.0
23
+ #
24
+ # @param options [Hash]
25
+ # @option options [String] :predicate_name
26
+ # @option options [Date] :as_of (Date.today)
27
+ #
28
+ # @note A concession about the as_of; This is not a live Utility. The data has a
29
+ # low churn rate. And while the date is important, I'm not as concerned
30
+ # about the local controlled vocabulary exposing a date that has expired.
31
+ # When we next deploy the server changes, the deactivated will go away.
32
+ def self.call(options = {})
33
+ predicate_name = options.fetch(:predicate_name)
34
+ cache[predicate_name] ||= new(options).call
35
+ end
36
+
37
+ def initialize(options = {})
38
+ @predicate_name = options.fetch(:predicate_name)
39
+ @as_of = options.fetch(:as_of) { Date.today }
40
+ @builder = Item.builder_for(predicate_name: predicate_name)
41
+ @utility_service = options.fetch(:utility_service) { default_utility_service }
42
+ end
43
+
44
+ def call
45
+ items = []
46
+ hierarchy_graph_keys = {}
47
+ top_level_slugs = Set.new
48
+ utility_service.with_active_extraction_for(predicate_name, as_of) do |data|
49
+ item = builder.call(data.merge('predicate_name' => predicate_name))
50
+ items << item
51
+ top_level_slugs << item.root_slug
52
+ hierarchy_graph_keys[item.term_label] = item
53
+ end
54
+ associate_parents_and_childrens_for(hierarchy_graph_keys, items)
55
+ top_level_slugs.map { |slug| hierarchy_graph_keys.fetch(slug) }
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :predicate_name, :as_of, :builder, :utility_service
61
+
62
+ def default_utility_service
63
+ require 'locabulary/utility'
64
+ Utility
65
+ end
66
+
67
+ def associate_parents_and_childrens_for(hierarchy_graph_keys, items)
68
+ items.each do |item|
69
+ begin
70
+ hierarchy_graph_keys.fetch(item.parent_term_label).add_child(item) unless item.parent_slugs.empty?
71
+ rescue KeyError => error
72
+ raise Exceptions::MissingHierarchicalParentError.new(predicate_name, error)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,58 @@
1
+ require 'date'
2
+ require 'json'
3
+ require 'locabulary/items'
4
+ require 'locabulary/utility'
5
+
6
+ module Locabulary
7
+ module Commands
8
+ # Responsible for extracting a non-hierarchical sorted array of Locabulary::Item for the given predicate_name
9
+ #
10
+ # @see Locabulary::Item
11
+ class ActiveItemsForCommand
12
+ def self.cache
13
+ @cache ||= {}
14
+ end
15
+ private_class_method :cache
16
+
17
+ # @api private
18
+ # @since 0.5.0
19
+ def self.reset_cache!
20
+ @cache = {}
21
+ end
22
+
23
+ # @api private
24
+ # @since 0.5.0
25
+ #
26
+ # @param options [Hash]
27
+ # @option options [String] :predicate_name
28
+ # @option options [Date] :as_of (Date.today)
29
+ #
30
+ # @note A concession about the as_of; This is not a live Utility. The data has a
31
+ # low churn rate. And while the date is important, I'm not as concerned
32
+ # about the local controlled vocabulary exposing a date that has expired.
33
+ # When we next deploy the server changes, the deactivated will go away.
34
+ def self.call(options = {})
35
+ predicate_name = options.fetch(:predicate_name)
36
+ cache[predicate_name] ||= new(options).call
37
+ end
38
+
39
+ def initialize(options = {})
40
+ @predicate_name = options.fetch(:predicate_name)
41
+ @as_of = options.fetch(:as_of) { Date.today }
42
+ @builder = Item.builder_for(predicate_name: predicate_name)
43
+ end
44
+
45
+ def call
46
+ collector = []
47
+ Utility.with_active_extraction_for(predicate_name, as_of) do |data|
48
+ collector << builder.call(data.merge('predicate_name' => predicate_name))
49
+ end
50
+ collector.sort
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :predicate_name, :as_of, :builder
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,80 @@
1
+ require 'set'
2
+ require 'locabulary'
3
+ require 'locabulary/exceptions'
4
+ require 'locabulary/item'
5
+ require 'locabulary/facet_wrapper_for_item'
6
+
7
+ module Locabulary
8
+ module Commands
9
+ # Responsible for building a hierarchical tree from faceted items, and ordering the nodes as per the presentation sequence for the
10
+ # associated predicate_name.
11
+ class BuildOrderedHierarchicalTreeCommand
12
+ # @api private
13
+ # @since 0.5.0
14
+ #
15
+ # @param options [Hash]
16
+ # @option predicate_name [String]
17
+ # @option faceted_items [Array<#hits, #value>]
18
+ # @option faceted_item_hierarchy_delimiter [String]
19
+ #
20
+ # @return [Array<FacetWrapperForItem>]
21
+ def self.call(options = {})
22
+ new(options).call
23
+ end
24
+
25
+ def initialize(options = {})
26
+ @predicate_name = options.fetch(:predicate_name)
27
+ @faceted_items = options.fetch(:faceted_items)
28
+ @faceted_item_hierarchy_delimiter = options.fetch(:faceted_item_hierarchy_delimiter)
29
+ @builder = Item.builder_for(predicate_name: predicate_name)
30
+ end
31
+
32
+ def call
33
+ items = []
34
+ hierarchy_graph_keys = {}
35
+ top_level_slugs = Set.new
36
+ faceted_items.each do |faceted_item|
37
+ item = build_item(faceted_item)
38
+ items << item
39
+ top_level_slugs << item.root_slug
40
+ hierarchy_graph_keys[item.term_label] = item
41
+ end
42
+ associate_parents_and_childrens_for(hierarchy_graph_keys, items)
43
+ top_level_slugs.map { |slug| hierarchy_graph_keys.fetch(slug) }.sort
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :builder, :predicate_name, :faceted_items, :faceted_item_hierarchy_delimiter
49
+
50
+ def associate_parents_and_childrens_for(hierarchy_graph_keys, items)
51
+ items.each do |item|
52
+ begin
53
+ hierarchy_graph_keys.fetch(item.parent_term_label).add_child(item) unless item.parent_slugs.empty?
54
+ rescue KeyError => error
55
+ raise Exceptions::MissingHierarchicalParentError.new(predicate_name, error)
56
+ end
57
+ end
58
+ end
59
+
60
+ def build_item(faceted_node)
61
+ term_label = faceted_node.value
62
+ locabulary_item = find_locabulary_item(predicate_name: predicate_name, term_label: term_label)
63
+ if locabulary_item
64
+ FacetWrapperForItem.build_for_faceted_node_and_locabulary_item(faceted_node: faceted_node, locabulary_item: locabulary_item)
65
+ else
66
+ FacetWrapperForItem.build_for_faceted_node(faceted_node: faceted_node, predicate_name: predicate_name, term_label: term_label)
67
+ end
68
+ end
69
+
70
+ def find_locabulary_item(*args)
71
+ Locabulary.item_for(*args)
72
+ rescue Exceptions::ItemNotFoundError, Exceptions::MissingPredicateNameError
73
+ # Either the predicate name didn't exist or the item didn't exist for the given predicate name
74
+ # Given that we are building from an alternate source, this is an acceptable error. It is possible
75
+ # that we want to consider a developer notification but not abort the whole process.
76
+ nil
77
+ end
78
+ end
79
+ end
80
+ end
@@ -4,6 +4,17 @@ module Locabulary
4
4
  class RuntimeError < ::RuntimeError
5
5
  end
6
6
 
7
+ # There was a problem finding an item
8
+ class ItemNotFoundError < RuntimeError
9
+ def initialize(predicate_name, label)
10
+ super("Unable to find label=#{label.inspect} for predicate_name=#{predicate_name.inspect}")
11
+ end
12
+ end
13
+
14
+ # An error occurred in attempting to find a given predicate_name in the data store
15
+ class MissingPredicateNameError < RuntimeError
16
+ end
17
+
7
18
  # There is a problem with the hierarchy; A child is being defined without a defined parent.
8
19
  class MissingHierarchicalParentError < RuntimeError
9
20
  attr_reader :predicate_name, :error
@@ -13,13 +24,5 @@ module Locabulary
13
24
  super("Expected #{predicate_name.inspect} to have a welformed tree. Error: #{error}")
14
25
  end
15
26
  end
16
-
17
- # There is a problem with the hierarchy; Instead of a tree we have a multitude of trees
18
- class TooManyHierarchicalRootsError < RuntimeError
19
- attr_reader :predicate_name, :roots
20
- def initialize(predicate_name, roots)
21
- super("Expected fewer root slugs for #{predicate_name.inspect}. Roots encountered: #{roots.inspect}")
22
- end
23
- end
24
27
  end
25
28
  end
@@ -0,0 +1,58 @@
1
+ require 'forwardable'
2
+ require 'delegate'
3
+
4
+ module Locabulary
5
+ # A wrapper for a Locabulary::Items::Base that includes information from
6
+ # the SOLR Utility.
7
+ class FacetWrapperForItem < SimpleDelegator
8
+ # @api public
9
+ # @since 0.5.0
10
+ #
11
+ # In some cases, we may have a facet with a term_label that is not found in the existing data storage.
12
+ # Instead of throwing an exception, we can make a reasonable approximation for that item based on the given parameters.
13
+ #
14
+ # @see Locabulary::FacetedHierarchicalTreeSorter
15
+ #
16
+ # @param options [Hash]
17
+ # @option predicate_name [String]
18
+ # @option faceted_node [#qvalue, #value, #hits]
19
+ # @option term_label [String]
20
+ # @return Locabulary::FacetWrapperForItem
21
+ def self.build_for_faceted_node(options = {})
22
+ predicate_name = options.fetch(:predicate_name)
23
+ faceted_node = options.fetch(:faceted_node)
24
+ term_label = options.fetch(:term_label)
25
+ locabulary_item = Locabulary::Item.build(predicate_name: predicate_name, term_label: term_label, default_presentation_sequence: nil)
26
+ new(faceted_node: faceted_node, locabulary_item: locabulary_item)
27
+ end
28
+
29
+ # @api public
30
+ # @since 0.5.0
31
+ #
32
+ # In some cases, we have a facet and a locabulary item and this is the public method for building the wrapped object.
33
+ #
34
+ # @see Locabulary::FacetedHierarchicalTreeSorter
35
+ # @param options [Hash]
36
+ # @option predicate_name [String]
37
+ # @option faceted_node [#qvalue, #value, #hits]
38
+ # @option term_label [String]
39
+ # @return Locabulary::FacetWrapperForItem
40
+ def self.build_for_faceted_node_and_locabulary_item(options = {})
41
+ new(options)
42
+ end
43
+
44
+ # Don't access .new directly; Use the above builders
45
+ private_class_method :new
46
+
47
+ def initialize(options = {})
48
+ @__faceted_node__ = options.fetch(:faceted_node)
49
+ @__locabulary_item__ = options.fetch(:locabulary_item)
50
+ super(@__locabulary_item__)
51
+ end
52
+
53
+ attr_reader :__faceted_node__, :__locabulary_item__
54
+
55
+ extend Forwardable
56
+ def_delegators :__faceted_node__, :qvalue, :value, :hits
57
+ end
58
+ end
@@ -0,0 +1,40 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+ require 'locabulary/items'
3
+
4
+ module Locabulary
5
+ # A container of builder methods for items
6
+ module Item
7
+ # @api public
8
+ # @since 0.2.1
9
+ #
10
+ # A Factory method that is responsible for building the appropriate object given a :predicate_name and additional attributes.
11
+ #
12
+ # @param attributes [Hash]
13
+ # @option predicate_name [String]
14
+ # @return [Locabulary::Item]
15
+ # @see Locabulary::Items
16
+ def self.build(attributes = {})
17
+ predicate_name = attributes.fetch(:predicate_name) { attributes.fetch('predicate_name') }
18
+ builder_for(predicate_name: predicate_name).call(attributes)
19
+ end
20
+
21
+ # @api public
22
+ # @since 0.2.1
23
+ #
24
+ # Responsible for finding the appropriate Factory method for building a Locabulary::Item
25
+ #
26
+ # @param options [Hash]
27
+ # @option predicate_name [String] Used for lookup of the correct Locabulary::Item type
28
+ # @return [#call] A builder method (`.new` for the given constant)
29
+ def self.builder_for(options = {})
30
+ predicate_name = options.fetch(:predicate_name)
31
+ possible_class_name_for_predicate_name = predicate_name.singularize.classify
32
+ klass = begin
33
+ Items.const_get(possible_class_name_for_predicate_name)
34
+ rescue NameError
35
+ Items::Base
36
+ end
37
+ klass.method(:new)
38
+ end
39
+ end
40
+ end
@@ -1,32 +1,11 @@
1
1
  require 'locabulary/items/base'
2
- require 'hanami/utils/string'
2
+ require 'locabulary/items/administrative_unit'
3
3
  module Locabulary
4
- # A container for the various types of Locabulary Items
4
+ # @since 0.5.0
5
+ #
6
+ # A container module for data structures that conform to the Locabulary::Items::Base interface
7
+ #
8
+ # @see Locabulary::Item for additional interaction
5
9
  module Items
6
- module_function
7
-
8
- # @api public
9
- # @since 0.2.1
10
- def build(options = {})
11
- predicate_name = options.fetch(:predicate_name) { options.fetch('predicate_name') }
12
- builder_for(predicate_name: predicate_name).call(options)
13
- end
14
-
15
- # @api public
16
- # @since 0.2.1
17
- #
18
- # @param options [Hash]
19
- # @option predicate_name [String] Used for lookup of the correct Locabulary::Item type
20
- def builder_for(options = {})
21
- predicate_name = options.fetch(:predicate_name)
22
- possible_class_name_for_predicate_name = Hanami::Utils::String.new(predicate_name).singularize.classify
23
- klass = begin
24
- Items.const_get(possible_class_name_for_predicate_name)
25
- rescue NameError
26
- Items::Base
27
- end
28
- klass.method(:new)
29
- end
30
10
  end
31
11
  end
32
- require 'locabulary/items/administrative_unit'
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  require 'locabulary/exceptions'
2
3
  require 'locabulary/items/base'
3
4
 
@@ -27,45 +28,16 @@ module Locabulary
27
28
 
28
29
  public
29
30
 
30
- def initialize(*args)
31
- super
32
- @children = []
33
- end
34
-
35
- def children
36
- @children.sort
37
- end
38
-
39
- def add_child(*input)
40
- @children += input
41
- end
42
-
43
- HIERARCHY_SEPARATOR = '::'.freeze
44
- def slugs
45
- term_label.split(HIERARCHY_SEPARATOR)
46
- end
47
-
48
- def parent_slugs
49
- slugs[0..-2]
50
- end
51
-
52
- def parent_term_label
53
- parent_slugs.join(HIERARCHY_SEPARATOR)
54
- end
55
-
56
- def root_slug
57
- slugs[0]
58
- end
59
-
60
- def selectable?
61
- children.count == 0
62
- end
63
-
31
+ NON_DEPARTMENTAL_SLUG = "Non-Departmental".freeze
32
+ # NOTE: The whitespace characters are "thin spaces", U+200A
33
+ HUMAN_FRIENDLY_HIERARCHY_DELIMITER = ' — '.freeze
64
34
  def selectable_label
65
- slugs[1..-1].join(HIERARCHY_SEPARATOR)
35
+ if slugs[-1] == NON_DEPARTMENTAL_SLUG
36
+ slugs[-2..-1].join(HUMAN_FRIENDLY_HIERARCHY_DELIMITER)
37
+ else
38
+ slugs[-1]
39
+ end
66
40
  end
67
-
68
- alias selectable_id id
69
41
  end
70
42
  end
71
43
  end