-
Notifications
You must be signed in to change notification settings - Fork 6
Refactor: Unify XML Generation to Fix Language Scoping Bug #416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| class LanguageTopicsXmlGenerator | ||
| def initialize(language, provider: nil, **args) | ||
| @language = language | ||
| @provider = provider | ||
| @args = args | ||
| end | ||
|
|
||
| def perform | ||
| doc = Ox::Document.new(version: "1.0") | ||
| root = Ox::Element.new("cmes") | ||
| doc << root | ||
|
|
||
| grouped_by_provider.each do |provider, topics| | ||
| root << provider_xml(provider, topics) | ||
| end | ||
|
|
||
| Ox.dump(doc) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| attr_reader :language, :provider, :args | ||
|
|
||
| def grouped_by_provider | ||
| topics_scope.group_by(&:provider) | ||
| end | ||
|
|
||
| def provider_xml(provider, topics) | ||
| Ox::Element.new("content_provider").tap do |provider_element| | ||
| provider_element[:name] = provider.name | ||
| build_year_nodes(provider_element, topics) | ||
| end | ||
| end | ||
|
|
||
| def build_year_nodes(parent_element, topics) | ||
| topics.group_by { |t| t.published_at.year } | ||
| .sort_by { |year, _| -year } | ||
| .each do |year, topics_in_year| | ||
| parent_element << year_xml(year, topics_in_year) | ||
| end | ||
| end | ||
|
|
||
| def year_xml(year, topics_in_year) | ||
| Ox::Element.new("topic_year").tap do |year_element| | ||
| year_element[:year] = year.to_s | ||
| topics_in_year.group_by { |t| t.published_at.strftime("%m_%B") } | ||
| .sort_by { |month_label, _| month_label } | ||
| .each do |month_label, topics_in_month| | ||
| year_element << month_xml(month_label, topics_in_month) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def month_xml(month_label, topics_in_month) | ||
| Ox::Element.new("topic_month").tap do |month_element| | ||
| month_element[:month] = month_label | ||
| topics_in_month.each { |topic| month_element << topic_xml(topic) } | ||
| end | ||
| end | ||
|
|
||
| def topic_xml(topic) | ||
| Ox::Element.new("title").tap do |title_element| | ||
| title_element[:name] = topic.title | ||
| title_element << (Ox::Element.new("topic_id") << topic.id.to_s) | ||
| title_element << (Ox::Element.new("counter") << "0") | ||
| title_element << (Ox::Element.new("topic_volume") << topic.published_at.year.to_s) | ||
| title_element << (Ox::Element.new("topic_issue") << topic.published_at.month.to_s) | ||
| title_element << files_xml(topic.documents) | ||
| title_element << (Ox::Element.new("topic_author") << (Ox::Element.new("topic_author_1") << " ")) | ||
| title_element << (Ox::Element.new("topic_tags") << topic.current_tags_list.join(", ")) | ||
| end | ||
| end | ||
|
|
||
| def files_xml(documents) | ||
| Ox::Element.new("topic_files").tap do |files| | ||
| files[:files] = "Files" | ||
| documents.reject { |doc| doc.content_type == "video/mp4" } | ||
| .each_with_index do |document, index| | ||
| files << Ox::Element.new("file_name_#{index + 1}").tap do |file_name| | ||
| file_name[:file_size] = document.byte_size | ||
| file_name << document.filename.to_s | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def topics_scope | ||
| scope = @provider ? @provider.topics : Topic | ||
| scope = scope.where(language_id: language.id) | ||
|
|
||
| scope = scope.where("published_at > ?", 1.month.ago) if args.fetch(:recent, false) | ||
|
|
||
| scope.includes(:provider, { taggings: :tag }, { documents_attachments: :blob }) | ||
| .order(published_at: :desc) | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| sample text |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| video content | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add small video file instead? |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| require "rails_helper" | ||
|
|
||
| RSpec.describe LanguageTopicsXmlGenerator do | ||
| let(:language) { Language.find_by(name: "en") } | ||
|
|
||
| # This spec provides a high-confidence check that the refactored service | ||
| # is a safe replacement for the legacy implementation by asserting that | ||
| # their XML outputs are semantically identical. | ||
| it "produces an XML output identical to the corrected legacy generator" do | ||
| # Arrange: Build a consistent data set for both generators. | ||
| XmlTestDataBuilder.xml_scenario | ||
| .for_language(name: "en") | ||
| .for_provider(name: "Health Corp") | ||
| .with_topic( | ||
| title: "Topic A - Jan 2023", | ||
| published_at: Date.new(2023, 1, 15), | ||
| documents: [ | ||
| { filename: "report.pdf", content_type: "application/pdf" }, | ||
| { filename: "video.mp4", content_type: "video/mp4" }, | ||
| ], | ||
| tags: [ "flu", "vaccine" ] | ||
| ) | ||
| .with_topic( | ||
| title: "Topic C - Feb 2022", | ||
| published_at: Date.new(2022, 2, 10), | ||
| tags: [ "diabetes", "research" ] | ||
| ) | ||
| .for_provider(name: "Wellness Inc") | ||
| .with_topic( | ||
| title: "Topic D - Jan 2023", | ||
| published_at: Date.new(2023, 1, 5) | ||
| ) | ||
| .build! | ||
|
|
||
| # Patch the single buggy method in the legacy implementation for this test. | ||
| # This allows us to use the actual legacy classes but with the critical | ||
| # language-scoping logic fixed, ensuring a valid comparison. | ||
| # allow_any_instance_of(XmlGenerator::SingleProvider).to receive(:topic_scope) do |instance, provider| | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this commented code here?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this all works we can just join all in a single spec file for the new class
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove this commented code then? And why we can't join all right now? |
||
| # # Re-implement the method with the correct logic. The `instance` passed | ||
| # # here is the XmlGenerator::AllProviders object, which holds the context. | ||
| # language = instance.instance_variable_get(:@language) | ||
| # args = instance.instance_variable_get(:@args) | ||
|
|
||
| # scope = provider.topics.where(language_id: language.id) | ||
| # scope = scope.where("published_at > ?", 1.month.ago) if args.fetch(:recent, false) | ||
| # scope | ||
| # .select(:id, :title, :published_at, :language_id, :provider_id) | ||
| # .includes(:language, { taggings: :tag }, { documents_attachments: :blob }) | ||
| # .order(published_at: :desc) | ||
| # end | ||
|
|
||
|
|
||
| # Act: Generate XML from both the new and (patched) legacy services. | ||
| new_xml = LanguageTopicsXmlGenerator.new(language).perform | ||
| legacy_xml = XmlGenerator::AllProviders.new(language).perform | ||
|
|
||
| # Assert: Parse and normalize both XML outputs to ensure they are identical. | ||
| # Comparing parsed documents is more robust than string comparison as it | ||
| # ignores insignificant whitespace and attribute ordering differences. | ||
| new_doc = Nokogiri::XML(new_xml) { |config| config.noblanks } | ||
| legacy_doc = Nokogiri::XML(legacy_xml) { |config| config.noblanks } | ||
|
|
||
| expect(new_doc.to_xml).to eq(legacy_doc.to_xml) | ||
| end | ||
| context "when the :recent option is true" do | ||
| let(:generator) { described_class.new(language, recent: true) } | ||
|
|
||
| before do | ||
| XmlTestDataBuilder.xml_scenario | ||
| .for_language(name: "en") | ||
| .for_provider(name: "Health Corp") | ||
| .with_topic( | ||
| title: "Recent Topic", | ||
| published_at: 2.weeks.ago | ||
| ) | ||
| .with_topic( | ||
| title: "Old Topic", | ||
| published_at: 2.months.ago | ||
| ) | ||
| .build! | ||
| end | ||
|
|
||
| it "includes only topics published within the last month" do | ||
| doc = Nokogiri::XML(generator.perform) | ||
|
|
||
| recent_topic_node = doc.at_xpath("//title[@name='Recent Topic']") | ||
| old_topic_node = doc.at_xpath("//title[@name='Old Topic']") | ||
|
|
||
| expect(recent_topic_node).not_to be_nil | ||
| expect(old_topic_node).to be_nil | ||
| end | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like this decomposition into several methods!