newsmlg2 0.1.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.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +99 -0
  4. data/.github/workflows/performance.yml +25 -0
  5. data/.github/workflows/rake.yml +19 -0
  6. data/.github/workflows/release.yml +34 -0
  7. data/.gitignore +17 -0
  8. data/.rubocop.yml +51 -0
  9. data/CHANGELOG.md +31 -0
  10. data/CLAUDE.md +77 -0
  11. data/Gemfile +15 -0
  12. data/LICENSE +24 -0
  13. data/README.adoc +135 -0
  14. data/Rakefile +15 -0
  15. data/docs/Gemfile +6 -0
  16. data/docs/_config.yml +4 -0
  17. data/docs/index.md +15 -0
  18. data/docs/lychee.toml +4 -0
  19. data/docs/usage.md +36 -0
  20. data/lib/newsmlg2/base/administrative_metadata_group.rb +40 -0
  21. data/lib/newsmlg2/base/arbitrary_value_attributes.rb +13 -0
  22. data/lib/newsmlg2/base/authority_attributes.rb +14 -0
  23. data/lib/newsmlg2/base/common_power_attributes.rb +17 -0
  24. data/lib/newsmlg2/base/concept_definition_group.rb +25 -0
  25. data/lib/newsmlg2/base/concept_relationships_group.rb +22 -0
  26. data/lib/newsmlg2/base/confirmation_status_attributes.rb +13 -0
  27. data/lib/newsmlg2/base/deprecated_link_attributes.rb +14 -0
  28. data/lib/newsmlg2/base/descriptive_metadata_group.rb +54 -0
  29. data/lib/newsmlg2/base/entity_details_group.rb +28 -0
  30. data/lib/newsmlg2/base/flex_attributes.rb +13 -0
  31. data/lib/newsmlg2/base/i18n_attributes.rb +14 -0
  32. data/lib/newsmlg2/base/item_management_group.rb +59 -0
  33. data/lib/newsmlg2/base/media_content_characteristics1.rb +22 -0
  34. data/lib/newsmlg2/base/news_content_attributes.rb +18 -0
  35. data/lib/newsmlg2/base/news_content_characteristics.rb +30 -0
  36. data/lib/newsmlg2/base/news_content_type_attributes.rb +17 -0
  37. data/lib/newsmlg2/base/persistent_edit_attributes.rb +13 -0
  38. data/lib/newsmlg2/base/qualifying_attributes.rb +13 -0
  39. data/lib/newsmlg2/base/quantify_attributes.rb +13 -0
  40. data/lib/newsmlg2/base/ranking_attributes.rb +13 -0
  41. data/lib/newsmlg2/base/recurrence_rule_attributes.rb +18 -0
  42. data/lib/newsmlg2/base/target_resource_attributes.rb +20 -0
  43. data/lib/newsmlg2/base/time_validity_attributes.rb +13 -0
  44. data/lib/newsmlg2/base.rb +35 -0
  45. data/lib/newsmlg2/builder/node.rb +125 -0
  46. data/lib/newsmlg2/builder.rb +69 -0
  47. data/lib/newsmlg2/catalog.rb +38 -0
  48. data/lib/newsmlg2/catalog_cache.rb +49 -0
  49. data/lib/newsmlg2/catalog_ref.rb +13 -0
  50. data/lib/newsmlg2/catalog_store.rb +66 -0
  51. data/lib/newsmlg2/catalogs/catalog.IPTC-G2-Standards_32.xml +620 -0
  52. data/lib/newsmlg2/catalogs/catalog.IPTC-G2-Standards_33.xml +625 -0
  53. data/lib/newsmlg2/catalogs/catalog.IPTC-G2-Standards_34.xml +626 -0
  54. data/lib/newsmlg2/catalogs/catalog.IPTC-G2-Standards_35.xml +635 -0
  55. data/lib/newsmlg2/catalogs/catalog.IPTC-G2-Standards_36.xml +644 -0
  56. data/lib/newsmlg2/catalogs/catalog.IPTC-G2-Standards_37.xml +661 -0
  57. data/lib/newsmlg2/catalogs/catalog.IPTC-G2-Standards_38.xml +674 -0
  58. data/lib/newsmlg2/catalogs/catalog.IPTC-G2-Standards_39.xml +682 -0
  59. data/lib/newsmlg2/catalogs/catalog.IPTC-G2-Standards_40.xml +724 -0
  60. data/lib/newsmlg2/catalogs/catalog.IPTC-G2-Standards_41.xml +729 -0
  61. data/lib/newsmlg2/configuration.rb +61 -0
  62. data/lib/newsmlg2/document.rb +123 -0
  63. data/lib/newsmlg2/errors.rb +18 -0
  64. data/lib/newsmlg2/i18n.rb +20 -0
  65. data/lib/newsmlg2/items/any_item.rb +180 -0
  66. data/lib/newsmlg2/items/catalog_item.rb +24 -0
  67. data/lib/newsmlg2/items/concept_item.rb +23 -0
  68. data/lib/newsmlg2/items/content_meta.rb +73 -0
  69. data/lib/newsmlg2/items/item_ref.rb +13 -0
  70. data/lib/newsmlg2/items/knowledge_item.rb +68 -0
  71. data/lib/newsmlg2/items/news_item.rb +130 -0
  72. data/lib/newsmlg2/items/news_message.rb +142 -0
  73. data/lib/newsmlg2/items/package_item.rb +82 -0
  74. data/lib/newsmlg2/items/part_meta.rb +68 -0
  75. data/lib/newsmlg2/items/planning_item.rb +160 -0
  76. data/lib/newsmlg2/nar_model.rb +76 -0
  77. data/lib/newsmlg2/nar_namespace.rb +10 -0
  78. data/lib/newsmlg2/nitf_namespace.rb +9 -0
  79. data/lib/newsmlg2/same_as_scheme.rb +15 -0
  80. data/lib/newsmlg2/scheme.rb +30 -0
  81. data/lib/newsmlg2/types/approximate_date_time_prop_type.rb +14 -0
  82. data/lib/newsmlg2/types/bag.rb +16 -0
  83. data/lib/newsmlg2/types/block_types.rb +62 -0
  84. data/lib/newsmlg2/types/concept.rb +36 -0
  85. data/lib/newsmlg2/types/concept_id.rb +17 -0
  86. data/lib/newsmlg2/types/concept_name_type.rb +17 -0
  87. data/lib/newsmlg2/types/content_meta_types.rb +152 -0
  88. data/lib/newsmlg2/types/date_opt_time_prop_type.rb +12 -0
  89. data/lib/newsmlg2/types/date_time_or_null_prop_type.rb +12 -0
  90. data/lib/newsmlg2/types/date_time_prop_type.rb +12 -0
  91. data/lib/newsmlg2/types/event_details.rb +213 -0
  92. data/lib/newsmlg2/types/ext_property.rb +21 -0
  93. data/lib/newsmlg2/types/facet.rb +17 -0
  94. data/lib/newsmlg2/types/flex1_prop_type.rb +32 -0
  95. data/lib/newsmlg2/types/flex_location_prop_type.rb +43 -0
  96. data/lib/newsmlg2/types/flex_party_prop_type.rb +49 -0
  97. data/lib/newsmlg2/types/flex_prop2_type.rb +26 -0
  98. data/lib/newsmlg2/types/flex_prop_type.rb +17 -0
  99. data/lib/newsmlg2/types/geo.rb +92 -0
  100. data/lib/newsmlg2/types/hierarchy_info.rb +17 -0
  101. data/lib/newsmlg2/types/ids.rb +28 -0
  102. data/lib/newsmlg2/types/inline_types.rb +56 -0
  103. data/lib/newsmlg2/types/intl_string_type.rb +17 -0
  104. data/lib/newsmlg2/types/intl_string_type2.rb +14 -0
  105. data/lib/newsmlg2/types/item_management.rb +74 -0
  106. data/lib/newsmlg2/types/link.rb +40 -0
  107. data/lib/newsmlg2/types/object_details.rb +40 -0
  108. data/lib/newsmlg2/types/party_details.rb +258 -0
  109. data/lib/newsmlg2/types/poi.rb +79 -0
  110. data/lib/newsmlg2/types/qcode_prop_type.rb +12 -0
  111. data/lib/newsmlg2/types/qual_prop_type.rb +23 -0
  112. data/lib/newsmlg2/types/qual_rel_prop_type.rb +18 -0
  113. data/lib/newsmlg2/types/related_concept.rb +71 -0
  114. data/lib/newsmlg2/types/rights.rb +84 -0
  115. data/lib/newsmlg2/types/ruby_annotation.rb +55 -0
  116. data/lib/newsmlg2/types/same_as.rb +17 -0
  117. data/lib/newsmlg2/types/truncated_date_time_prop_type.rb +13 -0
  118. data/lib/newsmlg2/types/versioned_string_type.rb +10 -0
  119. data/lib/newsmlg2/types/xml_base.rb +12 -0
  120. data/lib/newsmlg2/types/xml_lang.rb +12 -0
  121. data/lib/newsmlg2/types.rb +166 -0
  122. data/lib/newsmlg2/utils.rb +42 -0
  123. data/lib/newsmlg2/version.rb +5 -0
  124. data/lib/newsmlg2.rb +181 -0
  125. data/lib/tasks/performance.rake +72 -0
  126. data/newsmlg2.gemspec +34 -0
  127. metadata +185 -0
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Newsmlg2
4
+ class Builder
5
+ # Wraps one model instance and exposes a DSL method per lutaml-model
6
+ # attribute. Methods are generated at wrap time from the attribute
7
+ # metadata, so unknown methods fail with a normal NoMethodError.
8
+ class Node
9
+ def initialize(model)
10
+ @model = model
11
+ define_attribute_methods
12
+ end
13
+
14
+ attr_reader :model
15
+
16
+ # Runs a block against this node and returns the underlying model.
17
+ def apply(&block)
18
+ instance_eval(&block) if block
19
+ @model
20
+ end
21
+
22
+ private
23
+
24
+ def define_attribute_methods
25
+ @model.class.attributes.each_key do |name|
26
+ next if respond_to?(name, false) || respond_to?(name)
27
+
28
+ define_singleton_method(name) do |value = UNSET, **attrs, &block|
29
+ set_attribute(name, value, attrs, block)
30
+ end
31
+
32
+ singular = singular_of(name)
33
+ next unless singular && collection?(name)
34
+
35
+ define_singleton_method(singular) do |value = UNSET, **attrs, &block|
36
+ set_attribute(name, value, attrs, block)
37
+ end
38
+ end
39
+ end
40
+
41
+ # python-newsmlg2 parity: singular accessor appending to a plural
42
+ # collection attribute (names -> name, keywords -> keyword,
43
+ # libraries -> library).
44
+ def singular_of(name)
45
+ s = name.to_s
46
+ if s.end_with?('ies')
47
+ "#{s[0..-4]}y"
48
+ elsif s.end_with?('s') && !s.end_with?('ss')
49
+ s[0..-2]
50
+ end
51
+ end
52
+
53
+ def private_method_defined?(name)
54
+ respond_to?(name, true) && !respond_to?(name)
55
+ end
56
+
57
+ def set_attribute(name, value, attrs, block)
58
+ child =
59
+ if attrs.any? && !value.equal?(UNSET)
60
+ build_child(name, **attrs, text: value.to_s)
61
+ elsif attrs.any?
62
+ build_child(name, **attrs)
63
+ elsif !value.equal?(UNSET)
64
+ coerce(name, value)
65
+ else
66
+ build_child(name)
67
+ end
68
+
69
+ child = self.class.new(child).apply(&block) if block && child.is_a?(NarModel)
70
+ assign(name, child)
71
+ child
72
+ end
73
+
74
+ def build_child(name, **attrs)
75
+ type = attribute_type(name)
76
+ raise ArgumentError, "attribute #{name} is a plain value" if type == String
77
+
78
+ type.new(**attrs)
79
+ end
80
+
81
+ def coerce(name, value)
82
+ type = attribute_type(name)
83
+ return value if type == String || (!value.is_a?(String) && !value.is_a?(Numeric))
84
+
85
+ # A String (or number) assigned to a content-bearing model type
86
+ # wraps into the type's text content (python-newsmlg2's
87
+ # "located.name = 'Berlin'" convenience).
88
+ type.new(text: value.to_s)
89
+ end
90
+
91
+ def text_typed?(name)
92
+ type = attribute_type(name)
93
+ type != String
94
+ end
95
+
96
+ def assign(name, child)
97
+ if collection?(name)
98
+ current = @model.send(name)
99
+ if current.nil?
100
+ @model.send(:"#{name}=", [child])
101
+ else
102
+ current << child
103
+ end
104
+ else
105
+ @model.send(:"#{name}=", child)
106
+ end
107
+ end
108
+
109
+ def collection?(name)
110
+ attribute = @model.class.attributes[name]
111
+ attribute.options[:collection] == true
112
+ rescue StandardError
113
+ false
114
+ end
115
+
116
+ def attribute_type(name)
117
+ attribute = @model.class.attributes[name]
118
+ type = attribute.type
119
+ type.is_a?(Class) ? type : String
120
+ rescue StandardError
121
+ String
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Newsmlg2
4
+ # Generic, reflection-driven builder DSL: node methods are generated from
5
+ # each model's lutaml-model attribute metadata, so every current and
6
+ # future Newsmlg2 model is buildable without per-class builder code.
7
+ #
8
+ # doc = Newsmlg2.build_news_item(guid: "...", lang: "en-GB") do |item|
9
+ # item.item_meta do |meta|
10
+ # meta.item_class qcode: "ninat:text"
11
+ # meta.provider qcode: "nprov:acme" do |p|
12
+ # p.name "Acme News Agency"
13
+ # end
14
+ # meta.version_created "2026-08-30T12:00:00+00:00"
15
+ # end
16
+ # item.content_meta do |cm|
17
+ # cm.urgency 2
18
+ # cm.headline "Volcano erupts"
19
+ # cm.subject qcode: "medtop:20000962" do |s|
20
+ # s.name "Volcano"
21
+ # end
22
+ # end
23
+ # end
24
+ #
25
+ # doc.to_xml # => Newsmlg2::Document
26
+ class Builder
27
+ UNSET = Object.new.freeze
28
+
29
+ autoload :Node, 'newsmlg2/builder/node'
30
+
31
+ class << self
32
+ # Builds any item model and returns a Document.
33
+ def build(klass, attributes = {}, &block)
34
+ attributes = attributes.transform_keys(lang: :xml_lang)
35
+ model = klass.new(attributes)
36
+ Builder::Node.new(model).apply(&block) if block
37
+ Document.new(model)
38
+ end
39
+
40
+ def build_news_item(**attributes, &)
41
+ build(Newsmlg2::NewsItem, attributes, &)
42
+ end
43
+
44
+ def build_package_item(**attributes, &)
45
+ build(Newsmlg2::PackageItem, attributes, &)
46
+ end
47
+
48
+ def build_concept_item(**attributes, &)
49
+ build(Newsmlg2::ConceptItem, attributes, &)
50
+ end
51
+
52
+ def build_knowledge_item(**attributes, &)
53
+ build(Newsmlg2::KnowledgeItem, attributes, &)
54
+ end
55
+
56
+ def build_catalog_item(**attributes, &)
57
+ build(Newsmlg2::CatalogItem, attributes, &)
58
+ end
59
+
60
+ def build_planning_item(**attributes, &)
61
+ build(Newsmlg2::PlanningItem, attributes, &)
62
+ end
63
+
64
+ def build_news_message(**attributes, &)
65
+ build(Newsmlg2::NewsMessage, attributes, &)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Newsmlg2
4
+ # A local or remote catalog: a set of scheme alias-to-URI declarations.
5
+ class Catalog < Newsmlg2::NarModel
6
+ include Newsmlg2::Base::CommonPowerAttributes
7
+ include Newsmlg2::Base::AuthorityAttributes
8
+
9
+ xml_attributes additionalinfo: 'additionalInfo', url: 'url',
10
+ guid: 'guid', version: 'version'
11
+ xml_element :titles, xml: 'title',
12
+ type: Newsmlg2::Types::Label1Type, collection: true
13
+ xml_element :schemes, xml: 'scheme',
14
+ type: Newsmlg2::Scheme, collection: true
15
+
16
+ xml do
17
+ element 'catalog'
18
+ end
19
+
20
+ def get_scheme_for_alias(alias_name)
21
+ schemes.find { |scheme| scheme.alias_attr == alias_name }
22
+ end
23
+
24
+ def get_scheme_for_uri(uri)
25
+ schemes.find { |scheme| scheme.uri == uri }
26
+ end
27
+
28
+ def length
29
+ schemes.length
30
+ end
31
+
32
+ def to_s
33
+ return '<Catalog>' if titles.empty?
34
+
35
+ "<Catalog \"#{titles.first}\">"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Newsmlg2
4
+ # Maps remote IPTC catalog URLs to the catalog XML files bundled with this
5
+ # gem (under lib/newsmlg2/catalogs), so catalogRef resolution never touches
6
+ # the network.
7
+ module CatalogCache
8
+ CATALOGS_PATH = File.join(__dir__, 'catalogs')
9
+
10
+ URL_TO_FILE = {
11
+ 'http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_32.xml' => 'catalog.IPTC-G2-Standards_32.xml',
12
+ 'http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_33.xml' => 'catalog.IPTC-G2-Standards_33.xml',
13
+ 'http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_34.xml' => 'catalog.IPTC-G2-Standards_34.xml',
14
+ 'http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_35.xml' => 'catalog.IPTC-G2-Standards_35.xml',
15
+ 'http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_36.xml' => 'catalog.IPTC-G2-Standards_36.xml',
16
+ 'http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_37.xml' => 'catalog.IPTC-G2-Standards_37.xml',
17
+ 'http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_38.xml' => 'catalog.IPTC-G2-Standards_38.xml',
18
+ 'http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_39.xml' => 'catalog.IPTC-G2-Standards_39.xml',
19
+ 'http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_40.xml' => 'catalog.IPTC-G2-Standards_40.xml',
20
+ 'http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_41.xml' => 'catalog.IPTC-G2-Standards_41.xml'
21
+ }.freeze
22
+
23
+ class << self
24
+ # The bundled file path for a remote catalog URL, or nil when unknown.
25
+ def bundled_path_for(url)
26
+ file = URL_TO_FILE[url]
27
+ return nil unless file
28
+
29
+ File.join(CATALOGS_PATH, file)
30
+ end
31
+
32
+ # Reads the bundled catalog XML for a remote catalog URL, or nil.
33
+ def read(url)
34
+ path = bundled_path_for(url)
35
+ return nil unless path
36
+
37
+ File.read(path)
38
+ end
39
+
40
+ # Parses the bundled catalog for a remote URL into a Catalog.
41
+ def load(url)
42
+ xml = read(url)
43
+ return nil unless xml
44
+
45
+ Newsmlg2::Catalog.from_xml(xml)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Newsmlg2
4
+ # A reference to a remote catalog: a hyperlink to a set of scheme alias
5
+ # declarations.
6
+ class CatalogRef < Newsmlg2::NarModel
7
+ xml_attributes :title, :href
8
+
9
+ xml do
10
+ element 'catalogRef'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Newsmlg2
4
+ # The set of catalogs known to one NewsML-G2 document: inline <catalog>
5
+ # elements plus catalogs resolved from <catalogRef> hrefs (IPTC standard
6
+ # catalogs resolve to the files bundled with this gem). Owns scheme alias
7
+ # and URI lookups used for qcode expansion.
8
+ class CatalogStore
9
+ include Enumerable
10
+
11
+ def initialize
12
+ @catalogs = []
13
+ end
14
+
15
+ attr_reader :catalogs
16
+
17
+ def add_catalog(catalog)
18
+ catalogs << catalog
19
+ self
20
+ end
21
+ alias << add_catalog
22
+
23
+ def add_catalog_ref(href)
24
+ catalog = Newsmlg2::CatalogCache.load(href)
25
+ add_catalog(catalog) if catalog
26
+ end
27
+
28
+ def each(&)
29
+ catalogs.each(&)
30
+ end
31
+
32
+ def length
33
+ catalogs.length
34
+ end
35
+ alias size length
36
+
37
+ def [](index)
38
+ catalogs[index]
39
+ end
40
+
41
+ def get_scheme_for_alias(alias_name)
42
+ schemes.each do |scheme|
43
+ return scheme if scheme.alias_attr == alias_name
44
+ end
45
+ raise AliasNotFoundInCatalogs,
46
+ 'no catalog of this document declares the scheme alias ' \
47
+ "'#{alias_name}'"
48
+ end
49
+
50
+ def get_scheme_for_uri(uri)
51
+ schemes.each do |scheme|
52
+ return scheme if scheme.uri == uri
53
+ end
54
+ raise UriNotFoundInCatalogs,
55
+ "no catalog of this document declares the scheme URI '#{uri}'"
56
+ end
57
+
58
+ def schemes
59
+ catalogs.flat_map(&:schemes)
60
+ end
61
+
62
+ def to_s
63
+ "#<Newsmlg2::CatalogStore #{length} catalog(s)>"
64
+ end
65
+ end
66
+ end