govuk_taxonomy_helpers 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4a9bcf516b96e7dcec97d7c83fc3915100922700
4
+ data.tar.gz: ded38f9b3e0d1602a22b4d3d1e498995a65048a9
5
+ SHA512:
6
+ metadata.gz: 2a7bbbb176477e4d4c7611c900f4df8830579cc9b2289b93cb18b3a39bf397c2cfca51e57b3ef3c274cc1140fb4548a83e5912a9e85a22f34312593e4c3b6bdd
7
+ data.tar.gz: 2730337dfffb046d85fb25c84c728285bdd111245e3758ff7e0188dd87d2545936cfab48e3e7c4a7f4af3363e897e84cd673e14f43ce72c80b3b785c34f52642
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,12 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+
4
+ # Method documentation is required to ensure the gem is fully documented
5
+ Style/DocumentationMethod:
6
+ Enabled: true
7
+
8
+ Style/VariableNumber:
9
+ Enabled: false
10
+
11
+ Style/SafeNavigation:
12
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.4.0
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.0.1
2
+
3
+ * Gem created
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in govuk_taxonomy_helpers.gemspec
4
+ gemspec
data/Jenkinsfile ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env groovy
2
+
3
+ REPOSITORY = 'govuk_taxonomy_helpers'
4
+
5
+ node {
6
+ def govuk = load '/var/lib/jenkins/groovy_scripts/govuk_jenkinslib.groovy'
7
+
8
+ try {
9
+ stage('Checkout') {
10
+ checkout scm
11
+ }
12
+
13
+ stage('Clean') {
14
+ govuk.cleanupGit()
15
+ govuk.mergeMasterBranch()
16
+ }
17
+
18
+ stage('Bundle') {
19
+ govuk.bundleGem()
20
+ }
21
+
22
+ stage('Linter') {
23
+ govuk.rubyLinter()
24
+ }
25
+
26
+ stage('Tests') {
27
+ govuk.runTests('spec')
28
+ }
29
+
30
+ if(env.BRANCH_NAME == "master") {
31
+ stage('Publish Gem') {
32
+ govuk.publishGem(REPOSITORY, env.BRANCH_NAME)
33
+ }
34
+ }
35
+
36
+ } catch (e) {
37
+ currentBuild.result = 'FAILED'
38
+ step([$class: 'Mailer',
39
+ notifyEveryUnstableBuild: true,
40
+ recipients: 'govuk-ci-notifications@digital.cabinet-office.gov.uk',
41
+ sendToIndividuals: true])
42
+ throw e
43
+ }
44
+ }
data/LICENCE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2017 Crown copyright (Government Digital Service)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # GovukTaxonomyHelpers
2
+
3
+ Parses the taxonomy of GOV.UK into a browseable tree structure.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'govuk_taxonomy_helpers'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install govuk_taxonomy_helpers
20
+
21
+ ## Usage
22
+
23
+ The API is provisional and is likely to change for versions < 1.0.0.
24
+
25
+ To access the taxonomy, first request the content from the [publishing api](https://github.com/alphagov/publishing-api), then parse it to get a `LinkedContentItem` object.
26
+
27
+ ```ruby
28
+ require 'govuk_taxonomy_helpers'
29
+
30
+ content_item = Services.publishing_api.get_content("c75c541-403f-4cb1-9b34-4ddde816a80d")
31
+ expanded_links = Services.publishing_api.get_expanded_links("c75c541-403f-4cb1-9b34-4ddde816a80d")
32
+
33
+ taxonomy = GovukTaxonomyHelpers::LinkedContentItem.from_publishing_api(
34
+ content_item: content_item,
35
+ expanded_links: expanded_links
36
+ )
37
+ ```
38
+
39
+ A `LinkedContentItem` built from a taxon can access all *narrower term* taxons below it and all *broader term* taxons above it.
40
+
41
+ A taxon may have many child taxons, but can only have one or zero parents.
42
+
43
+ ```ruby
44
+ taxon.children
45
+ # => [LinkedContentItem(title: child-1-id, ...), LinkedContentItem(title: child-2, ...)]
46
+
47
+ taxon.parent
48
+ # => LinkedContentItem(title: root, ...)
49
+
50
+ taxon.breadcrumb_trail
51
+ # => [LinkedContentItem(title: root, ...), LinkedContentItem(title: taxon, ...)]
52
+ ```
53
+
54
+ A `LinkedContentItem` built from an content_item that isn't a taxon can access all taxons associated with it.
55
+
56
+ ```ruby
57
+ content_item.taxons
58
+ # => [LinkedContentItem(title: taxon, ...),
59
+ # LinkedContentItem(title: another-taxon, ...)]
60
+
61
+ content_item.taxons_with_ancestors
62
+ # => [LinkedContentItem(title: root, ...),
63
+ # LinkedContentItem(title: taxon-parent, ...),
64
+ # LinkedContentItem(title: taxon, ...),
65
+ # LinkedContentItem(title: another-taxon, ...)]
66
+ ```
67
+
68
+ ## Nomenclature
69
+
70
+ - **Taxonomy**: The hierarchy of topics used to classify content by subject on GOV.UK. Not all content is tagged to the taxonomy. We are rolling out the taxonomy and navigation theme-by-theme.
71
+ - **Taxon**: Any topic within the taxonomy.
72
+ - **Content Item**: A distinct version of a document. A taxon is also a type of content item.
73
+
74
+ ## Documentation
75
+
76
+ To run a Yard server locally to preview documentation, run:
77
+
78
+ $ bundle exec yard server --reload
79
+
80
+ ## Contributing
81
+
82
+ 1. Fork it ( https://github.com/alphagov/govuk_taxonomy_helpers/fork )
83
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
84
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
85
+ 4. Push to the branch (`git push origin my-new-feature`)
86
+ 5. Create a new Pull Request
87
+
88
+ ## License
89
+
90
+ [MIT License](LICENCE.txt)
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'govuk_taxonomy_helpers/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "govuk_taxonomy_helpers"
8
+ spec.version = GovukTaxonomyHelpers::VERSION
9
+ spec.authors = ["Government Digital Service"]
10
+ spec.email = ["govuk-dev@digital.cabinet-office.gov.uk"]
11
+ spec.summary = "Parses the taxonomy of GOV.UK into a browseable tree structure."
12
+ spec.homepage = "https://github.com/alphagov/govuk_taxonomy_helpers"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.6"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+
23
+ spec.add_development_dependency "rspec", "~> 3.5"
24
+ spec.add_development_dependency "govuk-lint", "~> 2.0.0"
25
+ spec.add_development_dependency "pry-byebug", "~> 3.4"
26
+ spec.add_development_dependency "yard", "~> 0.9.8"
27
+ end
@@ -0,0 +1,3 @@
1
+ require "govuk_taxonomy_helpers/version"
2
+ require "govuk_taxonomy_helpers/publishing_api_response"
3
+ require "govuk_taxonomy_helpers/linked_content_item"
@@ -0,0 +1,111 @@
1
+ module GovukTaxonomyHelpers
2
+ # A LinkedContentItem can be anything that has a content store representation
3
+ # on GOV.UK.
4
+ #
5
+ # It can be used with "taxon" content items (a topic in the taxonomy) or
6
+ # other document types that link to taxons.
7
+ #
8
+ # Taxon instances can have an optional parent and any number of child taxons.
9
+ class LinkedContentItem
10
+ extend Forwardable
11
+ attr_reader :title, :content_id, :base_path, :children, :internal_name
12
+ attr_accessor :parent
13
+ attr_reader :taxons
14
+ def_delegators :tree, :map, :each
15
+
16
+ # @param title [String] the user facing name for the content item
17
+ # @param base_path [String] the relative URL, starting with a leading "/"
18
+ # @param content_id [UUID] unique identifier of the content item
19
+ # @param internal_name [String] an internal name for the content item
20
+ def initialize(title:, base_path:, content_id:, internal_name: nil)
21
+ @title = title
22
+ @internal_name = internal_name
23
+ @content_id = content_id
24
+ @base_path = base_path
25
+ @children = []
26
+ @taxons = []
27
+ end
28
+
29
+ # Add a LinkedContentItem as a child of this one
30
+ def <<(child_node)
31
+ child_node.parent = self
32
+ @children << child_node
33
+ end
34
+
35
+ # Get taxons in the taxon's branch of the taxonomy.
36
+ #
37
+ # @return [Array] all taxons in this branch of the taxonomy, including the content item itself
38
+ def tree
39
+ return [self] if @children.empty?
40
+
41
+ @children.each_with_object([self]) do |child, tree|
42
+ tree.concat(child.tree)
43
+ end
44
+ end
45
+
46
+
47
+ # Get descendants of a taxon
48
+ #
49
+ # @return [Array] all taxons in this branch of the taxonomy, excluding the content item itself
50
+ def descendants
51
+ tree.tap(&:shift)
52
+ end
53
+
54
+ # Get ancestors of a taxon
55
+ #
56
+ # @return [Array] all taxons in the path from the root of the taxonomy to the parent taxon
57
+ def ancestors
58
+ if parent.nil?
59
+ []
60
+ else
61
+ parent.ancestors + [parent]
62
+ end
63
+ end
64
+
65
+ # Get a breadcrumb trail for a taxon
66
+ #
67
+ # @return [Array] all taxons in the path from the root of the taxonomy to this taxon
68
+ def breadcrumb_trail
69
+ ancestors + [self]
70
+ end
71
+
72
+ # Get all linked taxons and their ancestors
73
+ #
74
+ # @return [Array] all taxons that this content item can be found in
75
+ def taxons_with_ancestors
76
+ taxons.flat_map(&:breadcrumb_trail)
77
+ end
78
+
79
+ # @return [Integer] the number of taxons in this branch of the taxonomy
80
+ def count
81
+ tree.count
82
+ end
83
+
84
+ # @return [Boolean] whether this taxon is the root of its taxonomy
85
+ def root?
86
+ parent.nil?
87
+ end
88
+
89
+ # @return [Integer] the number of taxons between this taxon and the taxonomy root
90
+ def depth
91
+ return 0 if root?
92
+ 1 + parent.depth
93
+ end
94
+
95
+ # Link this content item to a taxon
96
+ #
97
+ # @param taxon_node [LinkedContentItem] A taxon content item
98
+ def add_taxon(taxon_node)
99
+ taxons << taxon_node
100
+ end
101
+
102
+ # @return [String] the string representation of the content item
103
+ def inspect
104
+ if internal_name.nil?
105
+ "LinkedContentItem(title: '#{title}', content_id: '#{content_id}', base_path: '#{base_path}')"
106
+ else
107
+ "LinkedContentItem(title: '#{title}', internal_name: '#{internal_name}', content_id: '#{content_id}', base_path: '#{base_path}')"
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,101 @@
1
+ require 'govuk_taxonomy_helpers/linked_content_item'
2
+
3
+ module GovukTaxonomyHelpers
4
+ class LinkedContentItem
5
+ # Extract a LinkedContentItem from publishing api response data.
6
+ #
7
+ # @param content_item [Hash] Publishing API `get_content` response hash
8
+ # @param expanded_links [Hash] Publishing API `get_expanded_links` response hash
9
+ # @return [LinkedContentItem]
10
+ # @see http://www.rubydoc.info/gems/gds-api-adapters/GdsApi/PublishingApiV2#get_content-instance_method
11
+ # @see http://www.rubydoc.info/gems/gds-api-adapters/GdsApi%2FPublishingApiV2:get_expanded_links
12
+ def self.from_publishing_api(content_item:, expanded_links:)
13
+ PublishingApiResponse.new(
14
+ content_item: content_item,
15
+ expanded_links: expanded_links,
16
+ ).linked_content_item
17
+ end
18
+ end
19
+
20
+ class PublishingApiResponse
21
+ attr_accessor :linked_content_item
22
+
23
+ # @param content_item [Hash] Publishing API `get_content` response hash
24
+ # @param expanded_links [Hash] Publishing API `get_expanded_links` response hash
25
+ def initialize(content_item:, expanded_links:)
26
+ @linked_content_item = LinkedContentItem.new(
27
+ title: content_item["title"],
28
+ internal_name: content_item["details"]["internal_name"],
29
+ content_id: content_item["content_id"],
30
+ base_path: content_item["base_path"]
31
+ )
32
+
33
+ add_expanded_links(expanded_links)
34
+ end
35
+
36
+ private
37
+
38
+ def add_expanded_links(expanded_links_response)
39
+ child_taxons = expanded_links_response["expanded_links"]["child_taxons"]
40
+ parent_taxons = expanded_links_response["expanded_links"]["parent_taxons"]
41
+ taxons = expanded_links_response["expanded_links"]["taxons"]
42
+
43
+ if !child_taxons.nil?
44
+ child_taxons.each do |child|
45
+ linked_content_item << parse_nested_child(child)
46
+ end
47
+ end
48
+
49
+ if !parent_taxons.nil?
50
+ # Assume no taxon has multiple parents
51
+ single_parent = parent_taxons.first
52
+
53
+ parse_nested_parent(single_parent) << linked_content_item
54
+ end
55
+
56
+ if !taxons.nil?
57
+ taxons.each do |taxon|
58
+ taxon_node = parse_nested_parent(taxon)
59
+ linked_content_item.add_taxon(taxon_node)
60
+ end
61
+ end
62
+ end
63
+
64
+ def parse_nested_child(nested_item)
65
+ nested_linked_content_item = LinkedContentItem.new(
66
+ title: nested_item["title"],
67
+ internal_name: nested_item["details"]["internal_name"],
68
+ content_id: nested_item["content_id"],
69
+ base_path: nested_item["base_path"]
70
+ )
71
+
72
+ child_taxons = nested_item["links"]["child_taxons"]
73
+
74
+ if !child_taxons.nil?
75
+ child_taxons.each do |child|
76
+ nested_linked_content_item << parse_nested_child(child)
77
+ end
78
+ end
79
+
80
+ nested_linked_content_item
81
+ end
82
+
83
+ def parse_nested_parent(nested_item)
84
+ nested_linked_content_item = LinkedContentItem.new(
85
+ title: nested_item["title"],
86
+ internal_name: nested_item["details"]["internal_name"],
87
+ content_id: nested_item["content_id"],
88
+ base_path: nested_item["base_path"]
89
+ )
90
+
91
+ parent_taxons = nested_item["links"]["parent_taxons"]
92
+
93
+ if !parent_taxons.nil?
94
+ single_parent = parent_taxons.first
95
+ parse_nested_parent(single_parent) << nested_linked_content_item
96
+ end
97
+
98
+ nested_linked_content_item
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,3 @@
1
+ module GovukTaxonomyHelpers
2
+ VERSION = "0.0.1".freeze
3
+ end
@@ -0,0 +1,158 @@
1
+ require_relative 'spec_helper'
2
+ require 'govuk_taxonomy_helpers/linked_content_item'
3
+
4
+ RSpec.describe GovukTaxonomyHelpers::LinkedContentItem do
5
+ let(:root_node) { GovukTaxonomyHelpers::LinkedContentItem.new(title: "root-id", content_id: "abc", base_path: "/root-id") }
6
+ let(:child_node_1) { GovukTaxonomyHelpers::LinkedContentItem.new(title: "child-1-id", content_id: "abc", base_path: "/child-1-id") }
7
+
8
+ describe "#<<(child_node)" do
9
+ it "makes one node the child of another node" do
10
+ root_node << child_node_1
11
+
12
+ expect(root_node.tree).to include child_node_1
13
+ expect(child_node_1.parent).to eq root_node
14
+ end
15
+ end
16
+
17
+ describe "#tree" do
18
+ context "given a node with a tree of successors" do
19
+ it "returns an array representing a pre-order traversal of the tree" do
20
+ child_node_2 = GovukTaxonomyHelpers::LinkedContentItem.new(title: "child-2-id", content_id: "abc", base_path: "/child-2-id")
21
+ child_node_3 = GovukTaxonomyHelpers::LinkedContentItem.new(title: "child-3-id", content_id: "abc", base_path: "/child-3-id")
22
+
23
+ root_node << child_node_1
24
+ child_node_1 << child_node_3
25
+ child_node_1 << child_node_2
26
+
27
+ expect(root_node.tree.count).to eq 4
28
+ expect(root_node.tree.first).to eq root_node
29
+ expect(root_node.tree.map(&:title)).to eq %w(root-id child-1-id child-3-id child-2-id)
30
+ expect(child_node_1.tree.map(&:title)).to eq %w(child-1-id child-3-id child-2-id)
31
+ end
32
+ end
33
+
34
+ context "given a single node" do
35
+ it "returns an array containing only that node" do
36
+ expect(root_node.tree.map(&:title)).to eq %w(root-id)
37
+ end
38
+ end
39
+ end
40
+
41
+ describe "#root?" do
42
+ before do
43
+ root_node << child_node_1
44
+ end
45
+
46
+ it "returns true when a node is the root" do
47
+ expect(root_node.root?).to be
48
+ end
49
+
50
+ it "returns false when a node is not the root" do
51
+ expect(child_node_1.root?).to_not be
52
+ end
53
+ end
54
+
55
+ describe "#depth" do
56
+ it "returns the depth of the node in its tree" do
57
+ child_node_2 = GovukTaxonomyHelpers::LinkedContentItem.new(title: "child-2-id", content_id: "abc", base_path: "/child-2-id")
58
+ root_node << child_node_1
59
+ child_node_1 << child_node_2
60
+
61
+ expect(root_node.depth).to eq 0
62
+ expect(child_node_1.depth).to eq 1
63
+ expect(child_node_2.depth).to eq 2
64
+ end
65
+ end
66
+
67
+ describe "#count" do
68
+ it "returns the total number of nodes in the tree" do
69
+ root_node << child_node_1
70
+
71
+ expect(root_node.count).to eq 2
72
+ end
73
+ end
74
+
75
+ context "taxon with ancestors" do
76
+ let(:child_node_2) do
77
+ GovukTaxonomyHelpers::LinkedContentItem.new(
78
+ title: "child-2-id",
79
+ content_id: "abc",
80
+ base_path: "/child-2-id"
81
+ )
82
+ end
83
+
84
+ before do
85
+ root_node << child_node_1
86
+ child_node_1 << child_node_2
87
+ end
88
+
89
+ describe "#breadcrumb_trail" do
90
+ it "includes the ancestors plus the content item itself" do
91
+ expect(child_node_2.breadcrumb_trail.map(&:title)).to eq %w(root-id child-1-id child-2-id)
92
+ end
93
+
94
+ it "is just contains itself for the root node" do
95
+ expect(root_node.breadcrumb_trail.map(&:title)).to eq %w(root-id)
96
+ end
97
+ end
98
+
99
+ describe "#ancestors" do
100
+ it "includes the ancestors but not the content item itself" do
101
+ expect(child_node_2.ancestors.map(&:title)).to eq %w(root-id child-1-id)
102
+ end
103
+
104
+ it "is the reverse of #descendants" do
105
+ have_descendant_node = lambda do |ancestor|
106
+ ancestor.descendants.include?(child_node_2)
107
+ end
108
+
109
+ expect(child_node_2.ancestors).to all(satisfy(&have_descendant_node))
110
+ end
111
+
112
+ it "is an empty array for the root node" do
113
+ expect(root_node.ancestors).to be_empty
114
+ end
115
+ end
116
+
117
+ describe "#taxons" do
118
+ let(:content_item) do
119
+ GovukTaxonomyHelpers::LinkedContentItem.new(
120
+ title: "content",
121
+ content_id: "abc",
122
+ base_path: "/content"
123
+ )
124
+ end
125
+
126
+ it "includes only the directly linked taxons" do
127
+ content_item.add_taxon(child_node_2)
128
+
129
+ expect(content_item.taxons.map(&:title)).to eq ["child-2-id"]
130
+ end
131
+ end
132
+
133
+ describe "#taxons_with_ancestors" do
134
+ let(:another_taxon) do
135
+ GovukTaxonomyHelpers::LinkedContentItem.new(
136
+ title: "another-taxon",
137
+ content_id: "abc",
138
+ base_path: "/another-taxon"
139
+ )
140
+ end
141
+
142
+ let(:content_item) do
143
+ GovukTaxonomyHelpers::LinkedContentItem.new(
144
+ title: "content",
145
+ content_id: "abc",
146
+ base_path: "/content"
147
+ )
148
+ end
149
+
150
+ it "includes all of the taxons and all of their anscestors" do
151
+ content_item.add_taxon(child_node_2)
152
+ content_item.add_taxon(another_taxon)
153
+
154
+ expect(content_item.taxons_with_ancestors.map(&:title).sort).to eq %w(another-taxon child-1-id child-2-id root-id)
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,343 @@
1
+ require_relative 'spec_helper'
2
+ require 'govuk_taxonomy_helpers'
3
+
4
+ RSpec.describe GovukTaxonomyHelpers::PublishingApiResponse do
5
+ let(:content_item) do
6
+ {
7
+ "content_id" => "64aadc14-9bca-40d9-abb4-4f21f9792a05",
8
+ "base_path" => "/taxon",
9
+ "title" => "Taxon",
10
+ "details" => {
11
+ "internal_name" => "My lovely taxon"
12
+ }
13
+ }
14
+ end
15
+
16
+ let(:linked_content_item) do
17
+ GovukTaxonomyHelpers::LinkedContentItem.from_publishing_api(
18
+ content_item: content_item,
19
+ expanded_links: expanded_links
20
+ )
21
+ end
22
+
23
+ context "content item with multiple levels of descendants" do
24
+ let(:expanded_links) do
25
+ grandchild_1 = {
26
+ "content_id" => "84aadc14-9bca-40d9-abb4-4f21f9792a05",
27
+ "base_path" => "/grandchild-1",
28
+ "title" => "Grandchild 1",
29
+ "details" => {
30
+ "internal_name" => "GC 1",
31
+ },
32
+ "links" => {}
33
+ }
34
+
35
+ grandchild_2 = {
36
+ "content_id" => "94aadc14-9bca-40d9-abb4-4f21f9792a05",
37
+ "base_path" => "/grandchild-2",
38
+ "title" => "Grandchild 2",
39
+ "details" => {
40
+ "internal_name" => "GC 2",
41
+ },
42
+ "links" => {}
43
+ }
44
+
45
+ child_1 = {
46
+ "content_id" => "74aadc14-9bca-40d9-abb4-4f21f9792a05",
47
+ "base_path" => "/child-1",
48
+ "title" => "Child 1",
49
+ "details" => {
50
+ "internal_name" => "C 1",
51
+ },
52
+ "links" => {
53
+ "child_taxons" => [
54
+ grandchild_1,
55
+ grandchild_2
56
+ ]
57
+ }
58
+ }
59
+
60
+ {
61
+ "content_id" => "64aadc14-9bca-40d9-abb4-4f21f9792a05",
62
+ "expanded_links" => {
63
+ "child_taxons" => [child_1]
64
+ }
65
+ }
66
+ end
67
+
68
+ it "parses titles" do
69
+ expect(linked_content_item.title).to eq("Taxon")
70
+ expect(linked_content_item.children.map(&:title)).to eq(['Child 1'])
71
+ expect(linked_content_item.children.first.children.map(&:title)).to eq(["Grandchild 1", "Grandchild 2"])
72
+ end
73
+
74
+ it "parses internal names" do
75
+ expect(linked_content_item.internal_name).to eq("My lovely taxon")
76
+ expect(linked_content_item.children.map(&:internal_name)).to eq(['C 1'])
77
+ expect(linked_content_item.children.first.children.map(&:internal_name)).to eq(["GC 1", "GC 2"])
78
+ end
79
+ end
80
+
81
+ context "content item with no descendants" do
82
+ let(:expanded_links) do
83
+ {
84
+ "content_id" => "64aadc14-9bca-40d9-abb4-4f21f9792a05",
85
+ "expanded_links" => {}
86
+ }
87
+ end
88
+
89
+ it "parses each level of taxons" do
90
+ expect(linked_content_item.title).to eq("Taxon")
91
+ expect(linked_content_item.children).to be_empty
92
+ end
93
+ end
94
+
95
+ context "content item with children but no grandchildren" do
96
+ let(:expanded_links) do
97
+ child_1 = {
98
+ "content_id" => "74aadc14-9bca-40d9-abb4-4f21f9792a05",
99
+ "base_path" => "/child-1",
100
+ "title" => "Child 1",
101
+ "details" => {
102
+ "internal_name" => "C 1",
103
+ },
104
+ "links" => {}
105
+ }
106
+
107
+ child_2 = {
108
+ "content_id" => "84aadc14-9bca-40d9-abb4-4f21f9792a05",
109
+ "base_path" => "/child-2",
110
+ "title" => "Child 2",
111
+ "details" => {
112
+ "internal_name" => "C 2",
113
+ },
114
+ "links" => {}
115
+ }
116
+
117
+ {
118
+ "content_id" => "64aadc14-9bca-40d9-abb4-4f21f9792a05",
119
+ "expanded_links" => {
120
+ "child_taxons" => [child_1, child_2]
121
+ }
122
+ }
123
+ end
124
+
125
+ it "parses each level of taxons" do
126
+ expect(linked_content_item.title).to eq("Taxon")
127
+ expect(linked_content_item.children.map(&:title)).to eq(["Child 1", "Child 2"])
128
+ expect(linked_content_item.children.map(&:children)).to all(be_empty)
129
+ end
130
+ end
131
+
132
+ context "content item with parents and grandparents" do
133
+ let(:expanded_links) do
134
+ grandparent_1 = {
135
+ "content_id" => "84aadc14-9bca-40d9-abb4-4f21f9792a05",
136
+ "base_path" => "/grandparent-1",
137
+ "title" => "Grandparent 1",
138
+ "details" => {
139
+ "internal_name" => "GP 1",
140
+ },
141
+ "links" => {}
142
+ }
143
+
144
+ parent_1 = {
145
+ "content_id" => "74aadc14-9bca-40d9-abb4-4f21f9792a05",
146
+ "base_path" => "/parent-1",
147
+ "title" => "Parent 1",
148
+ "details" => {
149
+ "internal_name" => "P 1",
150
+ },
151
+ "links" => {
152
+ "parent_taxons" => [
153
+ grandparent_1
154
+ ]
155
+ }
156
+ }
157
+
158
+ {
159
+ "content_id" => "64aadc14-9bca-40d9-abb4-4f21f9792a05",
160
+ "expanded_links" => {
161
+ "parent_taxons" => [parent_1]
162
+ }
163
+ }
164
+ end
165
+
166
+ it "parses the ancestors" do
167
+ expect(linked_content_item.title).to eq("Taxon")
168
+ expect(linked_content_item.parent.title).to eq("Parent 1")
169
+ expect(linked_content_item.ancestors.map(&:title)).to eq(["Grandparent 1", "Parent 1"])
170
+ end
171
+ end
172
+
173
+
174
+ context "content item with parents and no grandparents" do
175
+ let(:expanded_links) do
176
+ parent_1 = {
177
+ "content_id" => "74aadc14-9bca-40d9-abb4-4f21f9792a05",
178
+ "base_path" => "/parent-1",
179
+ "title" => "Parent 1",
180
+ "details" => {
181
+ "internal_name" => "P 1",
182
+ },
183
+ "links" => {}
184
+ }
185
+
186
+ {
187
+ "content_id" => "64aadc14-9bca-40d9-abb4-4f21f9792a05",
188
+ "expanded_links" => {
189
+ "parent_taxons" => [parent_1]
190
+ }
191
+ }
192
+ end
193
+
194
+ it "parses the ancestors" do
195
+ expect(linked_content_item.title).to eq("Taxon")
196
+ expect(linked_content_item.parent.title).to eq("Parent 1")
197
+ expect(linked_content_item.ancestors.map(&:title)).to eq(["Parent 1"])
198
+ end
199
+ end
200
+
201
+ context "content item with no parents" do
202
+ let(:expanded_links) do
203
+ {
204
+ "content_id" => "64aadc14-9bca-40d9-abb4-4f21f9792a05",
205
+ "expanded_links" => {}
206
+ }
207
+ end
208
+
209
+ it "parses the ancestors" do
210
+ expect(linked_content_item.title).to eq("Taxon")
211
+ expect(linked_content_item.parent).to be_nil
212
+ expect(linked_content_item.ancestors.map(&:title)).to be_empty
213
+ end
214
+ end
215
+
216
+ context "content item with multiple parents" do
217
+ let(:expanded_links) do
218
+ parent_1 = {
219
+ "content_id" => "74aadc14-9bca-40d9-abb4-4f21f9792a05",
220
+ "base_path" => "/parent-1",
221
+ "title" => "Parent 1",
222
+ "details" => {
223
+ "internal_name" => "P 1",
224
+ },
225
+ "links" => {}
226
+ }
227
+
228
+ parent_2 = {
229
+ "content_id" => "84aadc14-9bca-40d9-abb4-4f21f9792a05",
230
+ "base_path" => "/parent-2",
231
+ "title" => "Parent 2",
232
+ "details" => {
233
+ "internal_name" => "P 2",
234
+ },
235
+ "links" => {}
236
+ }
237
+
238
+ {
239
+ "content_id" => "64aadc14-9bca-40d9-abb4-4f21f9792a05",
240
+ "expanded_links" => {
241
+ "parent_taxons" => [parent_1, parent_2]
242
+ }
243
+ }
244
+ end
245
+
246
+ it "uses only the first parent" do
247
+ expect(linked_content_item.title).to eq("Taxon")
248
+ expect(linked_content_item.parent.title).to eq("Parent 1")
249
+ expect(linked_content_item.ancestors.map(&:title)).to eq(["Parent 1"])
250
+ end
251
+ end
252
+
253
+ context "a content item tagged to multiple taxons" do
254
+ let(:expanded_links) do
255
+ grandparent_1 = {
256
+ "content_id" => "22aadc14-9bca-40d9-abb4-4f21f9792a05",
257
+ "base_path" => "/grandparent-1",
258
+ "title" => "Grandparent 1",
259
+ "details" => {
260
+ "internal_name" => "GP 1",
261
+ },
262
+ "links" => {}
263
+ }
264
+
265
+ parent_1 = {
266
+ "content_id" => "11aadc14-9bca-40d9-abb4-4f21f9792a05",
267
+ "base_path" => "/parent-1",
268
+ "title" => "Parent 1",
269
+ "details" => {
270
+ "internal_name" => "P 1",
271
+ },
272
+ "links" => {
273
+ "parent_taxons" => [grandparent_1]
274
+ }
275
+ }
276
+
277
+ taxon_1 = {
278
+ "content_id" => "00aadc14-9bca-40d9-abb4-4f21f9792a05",
279
+ "base_path" => "/this-is-a-taxon",
280
+ "title" => "Taxon 1",
281
+ "details" => {
282
+ "internal_name" => "T 1",
283
+ },
284
+ "links" => {
285
+ "parent_taxons" => [parent_1]
286
+ }
287
+ }
288
+
289
+ grandparent_2 = {
290
+ "content_id" => "03aadc14-9bca-40d9-abb4-4f21f9792a05",
291
+ "base_path" => "/grandparent-2",
292
+ "title" => "Grandparent 2",
293
+ "details" => {
294
+ "internal_name" => "GP 2",
295
+ },
296
+ "links" => {}
297
+ }
298
+
299
+ parent_2 = {
300
+ "content_id" => "02aadc14-9bca-40d9-abb4-4f21f9792a05",
301
+ "base_path" => "/parent-2",
302
+ "title" => "Parent 2",
303
+ "details" => {
304
+ "internal_name" => "P 2",
305
+ },
306
+ "links" => {
307
+ "parent_taxons" => [grandparent_2]
308
+ }
309
+ }
310
+
311
+ taxon_2 = {
312
+ "content_id" => "01aadc14-9bca-40d9-abb4-4f21f9792a05",
313
+ "base_path" => "/this-is-also-a-taxon",
314
+ "title" => "Taxon 2",
315
+ "details" => {
316
+ "internal_name" => "T 2",
317
+ },
318
+ "links" => {
319
+ "parent_taxons" => [parent_2]
320
+ }
321
+ }
322
+
323
+ {
324
+ "content_id" => "64aadc14-9bca-40d9-abb4-4f21f9792a05",
325
+ "expanded_links" => {
326
+ "taxons" => [taxon_1, taxon_2]
327
+ }
328
+ }
329
+ end
330
+
331
+ it "parses the taxons and their ancestors" do
332
+ expect(linked_content_item.parent).to be_nil
333
+ expect(linked_content_item.taxons.map(&:title)).to eq(["Taxon 1", "Taxon 2"])
334
+ expect(linked_content_item.taxons_with_ancestors.map(&:title).sort).to eq(
335
+ [
336
+ "Grandparent 1", "Grandparent 2",
337
+ "Parent 1", "Parent 2",
338
+ "Taxon 1", "Taxon 2",
339
+ ]
340
+ )
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,99 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # The `.rspec` file also contains a few flags that are not defaults but that
16
+ # users commonly want.
17
+ #
18
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
19
+
20
+ require 'pry-byebug'
21
+
22
+ RSpec.configure do |config|
23
+ # rspec-expectations config goes here. You can use an alternate
24
+ # assertion/expectation library such as wrong or the stdlib/minitest
25
+ # assertions if you prefer.
26
+ config.expect_with :rspec do |expectations|
27
+ # This option will default to `true` in RSpec 4. It makes the `description`
28
+ # and `failure_message` of custom matchers include text for helper methods
29
+ # defined using `chain`, e.g.:
30
+ # be_bigger_than(2).and_smaller_than(4).description
31
+ # # => "be bigger than 2 and smaller than 4"
32
+ # ...rather than:
33
+ # # => "be bigger than 2"
34
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
35
+ end
36
+
37
+ # rspec-mocks config goes here. You can use an alternate test double
38
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
39
+ config.mock_with :rspec do |mocks|
40
+ # Prevents you from mocking or stubbing a method that does not exist on
41
+ # a real object. This is generally recommended, and will default to
42
+ # `true` in RSpec 4.
43
+ mocks.verify_partial_doubles = true
44
+ end
45
+
46
+ # The settings below are suggested to provide a good initial experience
47
+ # with RSpec, but feel free to customize to your heart's content.
48
+ =begin
49
+ # These two settings work together to allow you to limit a spec run
50
+ # to individual examples or groups you care about by tagging them with
51
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
52
+ # get run.
53
+ config.filter_run :focus
54
+ config.run_all_when_everything_filtered = true
55
+
56
+ # Allows RSpec to persist some state between runs in order to support
57
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
58
+ # you configure your source control system to ignore this file.
59
+ config.example_status_persistence_file_path = "spec/examples.txt"
60
+
61
+ # Limits the available syntax to the non-monkey patched syntax that is
62
+ # recommended. For more details, see:
63
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
64
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
65
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
66
+ config.disable_monkey_patching!
67
+
68
+ # This setting enables warnings. It's recommended, but in some cases may
69
+ # be too noisy due to issues in dependencies.
70
+ config.warnings = true
71
+
72
+ # Many RSpec users commonly either run the entire suite or an individual
73
+ # file, and it's useful to allow more verbose output when running an
74
+ # individual spec file.
75
+ if config.files_to_run.one?
76
+ # Use the documentation formatter for detailed output,
77
+ # unless a formatter has already been configured
78
+ # (e.g. via a command-line flag).
79
+ config.default_formatter = 'doc'
80
+ end
81
+
82
+ # Print the 10 slowest examples and example groups at the
83
+ # end of the spec run, to help surface which specs are running
84
+ # particularly slow.
85
+ config.profile_examples = 10
86
+
87
+ # Run specs in random order to surface order dependencies. If you find an
88
+ # order dependency and want to debug it, you can fix the order by providing
89
+ # the seed, which is printed after each run.
90
+ # --seed 1234
91
+ config.order = :random
92
+
93
+ # Seed global randomization in this process using the `--seed` CLI option.
94
+ # Setting this allows you to use `--seed` to deterministically reproduce
95
+ # test failures related to randomization by passing the same `--seed` value
96
+ # as the one that triggered the failure.
97
+ Kernel.srand config.seed
98
+ =end
99
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: govuk_taxonomy_helpers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Government Digital Service
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: govuk-lint
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 2.0.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 2.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: yard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.9.8
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.9.8
97
+ description:
98
+ email:
99
+ - govuk-dev@digital.cabinet-office.gov.uk
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".rubocop.yml"
107
+ - ".ruby-version"
108
+ - ".yardopts"
109
+ - CHANGELOG.md
110
+ - Gemfile
111
+ - Jenkinsfile
112
+ - LICENCE.txt
113
+ - README.md
114
+ - Rakefile
115
+ - govuk_taxonomy_helpers.gemspec
116
+ - lib/govuk_taxonomy_helpers.rb
117
+ - lib/govuk_taxonomy_helpers/linked_content_item.rb
118
+ - lib/govuk_taxonomy_helpers/publishing_api_response.rb
119
+ - lib/govuk_taxonomy_helpers/version.rb
120
+ - spec/linked_content_item_spec.rb
121
+ - spec/publishing_api_response_spec.rb
122
+ - spec/spec_helper.rb
123
+ homepage: https://github.com/alphagov/govuk_taxonomy_helpers
124
+ licenses:
125
+ - MIT
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubyforge_project:
143
+ rubygems_version: 2.6.8
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: Parses the taxonomy of GOV.UK into a browseable tree structure.
147
+ test_files:
148
+ - spec/linked_content_item_spec.rb
149
+ - spec/publishing_api_response_spec.rb
150
+ - spec/spec_helper.rb