asciichem 0.23.0 → 0.24.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a97d83231f84e4dde6c03c18f88ae7c34a30779e2550ca3f859e91bc58365f84
4
- data.tar.gz: dbb27614ede6ba6987f09942465624b8d84dda4041444f63326982d84c365bdd
3
+ metadata.gz: d733aab290b1e54dfd0139d75a92888dfb8fc2af088544a07cc4395e523e583f
4
+ data.tar.gz: 5dfe21e949307366a6afbcc94bf2c52f9e9cfedb4a1f328996fbde3a20b5ec99
5
5
  SHA512:
6
- metadata.gz: 47d0e9bf10643d6a7fb24779d1baaaf1382e4c11b1b43375e1543b7cacb5169ea94784db8c1ab70a2f117590966bc96ca4809c994fd83f6295ca63fa56bf3c01
7
- data.tar.gz: 84386eb43091bbcde85bf7ed8bc428544865bd87f985e2b986d2578c5cb2e25a11d0ef39b880bcd781a5da6b061e4a6501d18a02bd2cf3016da369dd72afd365
6
+ metadata.gz: cdeb663f26df5189cd73df0d7c5f6a9ad3918d981d924ec08f1a781728415af474840de566a8d8f1075cb0cdc0d7086f41f92b80250efee6f71ebd11cdc62680
7
+ data.tar.gz: 371e88363eb2e143b2b8a1b3f1a3cbc339d3eb81d5bcd521fdc00d02fd90e6068c769864185198c8e35e5ae6e2fc95b0088885d760844f527118a50469eaa9fb
data/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to AsciiChem are documented here.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.24.0] - 2026-09-13
7
+
8
+ ### Added
9
+ - `AsciiChem::Citation` (TODO.impl 44): one dataset-type Relaton
10
+ bibitem per (substance, source) — the citation track's core
11
+ contract. Source-specific fields live in per-source profiles
12
+ (PubChem, Common Chemistry); every identifier rides along as a
13
+ keyword so citations are machine-checkable. Runtime dependency:
14
+ relaton-bib (classic model).
15
+ - `Resolver::CommonChemistry` (TODO.impl 40, maintainer sign-off
16
+ 2026-09-13): CAS RN lookups against CAS Common Chemistry. CC BY-NC
17
+ 4.0 posture is enforced in code — the adapter never self-registers;
18
+ users opt in explicitly, and every result carries the required
19
+ attribution.
20
+
6
21
  ## [0.23.0] - 2026-09-12
7
22
 
8
23
  ### Added
@@ -411,7 +426,8 @@ This project follows [Semantic Versioning](https://semver.org/).
411
426
  `version`.
412
427
  - Comprehensive RSpec suite with round-trip conformance.
413
428
 
414
- [Unreleased]: https://github.com/asciichem/asciichem-ruby/compare/v0.23.0...HEAD
429
+ [Unreleased]: https://github.com/asciichem/asciichem-ruby/compare/v0.24.0...HEAD
430
+ [0.24.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.23.0...v0.24.0
415
431
  [0.23.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.22.0...v0.23.0
416
432
  [0.22.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.21.0...v0.22.0
417
433
  [0.21.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.20.0...v0.21.0
data/asciichem.gemspec CHANGED
@@ -38,6 +38,7 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency "lutaml-model", ">= 0.8", "< 2"
39
39
  spec.add_dependency "nokogiri", "~> 1.16"
40
40
  spec.add_dependency "parslet", "~> 2.0"
41
+ spec.add_dependency "relaton-bib", ">= 0.1", "< 2"
41
42
  spec.add_dependency "plurimath", "~> 0.8"
42
43
  spec.add_dependency "thor", "~> 1.3"
43
44
 
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "relaton_bib"
4
+
5
+ module AsciiChem
6
+ # Citation track (TODO.v2 08; TODO.impl 44): a bibitem is a function
7
+ # of (substance, source) — one dataset-type Relaton bibitem per
8
+ # source the user chooses to cite, because two databases agreeing on
9
+ # a substance are still two different documents.
10
+ #
11
+ # Source-specific fields (link shape, publisher string, docidentifier
12
+ # scheme) live in PROFILES; adding a source is one entry (OCP).
13
+ module Citation
14
+ # Source-specific citation profile. Default fields fall back to
15
+ # the substance's provenance.
16
+ Profile = Struct.new(:publisher, :link_for, :identifier_for, keyword_init: true)
17
+
18
+ PROFILES = {
19
+ "pubchem" => Profile.new(
20
+ publisher: "PubChem, U.S. National Library of Medicine",
21
+ link_for: ->(substance) do
22
+ cid = substance.identifier_value("pubchem-cid")
23
+ "https://pubchem.ncbi.nlm.nih.gov/compound/#{cid}" if cid
24
+ end,
25
+ identifier_for: ->(substance) do
26
+ cid = substance.identifier_value("pubchem-cid")
27
+ "PubChem CID #{cid}" if cid
28
+ end
29
+ ),
30
+ "common_chemistry" => Profile.new(
31
+ publisher: "CAS Common Chemistry",
32
+ link_for: ->(substance) do
33
+ cas = substance.identifier_value("cas")
34
+ "https://commonchemistry.cas.org/detail?cas_rn=#{cas}" if cas
35
+ end,
36
+ identifier_for: ->(substance) do
37
+ cas = substance.identifier_value("cas")
38
+ "CAS RN #{cas}" if cas
39
+ end
40
+ )
41
+ }.freeze
42
+
43
+ DEFAULT_PROFILE = Profile.new(
44
+ publisher: nil,
45
+ link_for: ->(_substance) { nil },
46
+ identifier_for: ->(substance) do
47
+ key = substance.identifiers.first
48
+ "#{key.convention}: #{key.value}" if key
49
+ end
50
+ ).freeze
51
+ private_constant :DEFAULT_PROFILE
52
+
53
+ class << self
54
+ # Builds a Relaton dataset-type bibitem for the substance as
55
+ # cited from its recorded source. Raises when the substance
56
+ # carries no provenance (hand-built, not resolved).
57
+ def bibitem(substance)
58
+ provenance = substance.provenance
59
+ unless provenance&.source
60
+ raise Error, "substance has no provenance - resolve it first (AsciiChem::Resolver)"
61
+ end
62
+
63
+ profile = PROFILES.fetch(provenance.source, DEFAULT_PROFILE)
64
+ RelatonBib::BibliographicItem.new(
65
+ type: "dataset",
66
+ title: [{ type: "main",
67
+ content: "#{title_base(substance)} - #{profile.publisher || provenance.source} substance record" }],
68
+ docid: [RelatonBib::DocumentIdentifier.new(
69
+ id: profile.identifier_for.call(substance) || "#{provenance.source} substance",
70
+ type: provenance.source)],
71
+ contributor: [{ entity: RelatonBib::Organization.new(name: profile.publisher || provenance.source),
72
+ role: [{ type: "publisher" }] }],
73
+ date: [{ type: "accessed", on: accessed_on(provenance) }],
74
+ link: [{ type: "src", content: profile.link_for.call(substance) }].compact,
75
+ keyword: substance.identifiers.map { |i| "#{i.convention}=#{i.value}" }
76
+ )
77
+ end
78
+
79
+ # Convenience: bibitem XML (what a document pipeline embeds).
80
+ def to_xml(substance)
81
+ bibitem(substance).to_xml
82
+ end
83
+
84
+ private
85
+
86
+ def title_base(substance)
87
+ substance.preferred_name || substance.identifier_value("cas") ||
88
+ substance.identifier_value("inchikey") || "Substance"
89
+ end
90
+
91
+ def accessed_on(provenance)
92
+ return provenance.retrieved_at[0, 10] if provenance.retrieved_at
93
+
94
+ Time.now.utc.strftime("%Y-%m-%d")
95
+ end
96
+ end
97
+ end
98
+ end
@@ -17,11 +17,13 @@ module AsciiChem
17
17
  # Declares the source name and registers the adapter (called
18
18
  # from the class body — `inherited` fires before the body
19
19
  # runs, so it cannot see the subclass configuration).
20
- def source_name(name = nil)
20
+ # `register: false` for opt-in sources (NC-licensed): the
21
+ # adapter exists but users must register it explicitly.
22
+ def source_name(name = nil, register: true)
21
23
  return @source_name if name.nil?
22
24
 
23
25
  @source_name = name.to_s
24
- AsciiChem::Resolver.register(@source_name, self)
26
+ AsciiChem::Resolver.register(@source_name, self) if register
25
27
  end
26
28
 
27
29
  def supports(*conventions)
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "json"
5
+
6
+ module AsciiChem
7
+ module Resolver
8
+ # CAS Common Chemistry source adapter (TODO.impl 40; TODO.v2 07).
9
+ #
10
+ # License posture (maintainer sign-off 2026-09-13): Common
11
+ # Chemistry data is CC BY-NC 4.0 — this adapter is OPT-IN and never
12
+ # self-registers; users register it explicitly:
13
+ #
14
+ # AsciiChem::Resolver.register(:common_chemistry,
15
+ # AsciiChem::Resolver::CommonChemistry)
16
+ #
17
+ # Every result carries the required attribution.
18
+ class CommonChemistry < Adapter
19
+ source_name :common_chemistry, register: false
20
+ supports "cas"
21
+
22
+ BASE = "https://commonchemistry.cas.org/api/detail"
23
+
24
+ def fetch_record(value:, convention:, fetch:)
25
+ body = fetch.get("#{BASE}?cas_rn=#{CGI.escape(value.to_s)}")
26
+ return nil unless body
27
+
28
+ data = JSON.parse(body)
29
+ return nil if data.empty?
30
+
31
+ substance_from(data)
32
+ rescue JSON::ParserError => e
33
+ raise Error, "common_chemistry returned malformed JSON: #{e.message}"
34
+ end
35
+
36
+ def attribution
37
+ "CAS Common Chemistry (CC BY-NC 4.0)"
38
+ end
39
+
40
+ private
41
+
42
+ def substance_from(data)
43
+ provenance = default_provenance
44
+ identifiers = [Identifier.new(value: data["rn"], convention: "cas",
45
+ provenance: provenance)]
46
+ add_identifier(identifiers, data, "inchikey", "inchikey", provenance)
47
+ add_identifier(identifiers, data, "smiles", "canonical-smiles", provenance)
48
+ add_identifier(identifiers, data, "inchi", "inchi", provenance)
49
+ add_identifier(identifiers, data, "iupacName", "iupac-name", provenance)
50
+
51
+ Substance.new(
52
+ preferred_name: data["name"],
53
+ formula: data["formula"],
54
+ identifiers: identifiers,
55
+ properties: [
56
+ Property.new(name: "molecular-formula", value: data["formula"], provenance: provenance),
57
+ Property.new(name: "molecular-weight", value: data["molecularMass"]&.to_s,
58
+ units: "g/mol", provenance: provenance)
59
+ ].compact,
60
+ # Best effort within the supported SMILES subset; the SMILES
61
+ # stays as an identifier when stereo keeps it out.
62
+ structure: parse_structure(data["smiles"])
63
+ )
64
+ end
65
+
66
+ def add_identifier(list, data, key, convention, provenance)
67
+ return unless data[key]
68
+
69
+ list << Identifier.new(value: data[key], convention: convention,
70
+ provenance: provenance)
71
+ end
72
+
73
+ def parse_structure(smiles)
74
+ return nil unless smiles
75
+
76
+ AsciiChem.parse_smiles(smiles).nodes.first
77
+ rescue AsciiChem::ParseError
78
+ nil
79
+ end
80
+ end
81
+ end
82
+ end
@@ -17,6 +17,7 @@ module AsciiChem
17
17
  autoload :Adapter, "asciichem/resolver/adapter"
18
18
  autoload :Cache, "asciichem/resolver/cache"
19
19
  autoload :PubChem, "asciichem/resolver/pubchem"
20
+ autoload :CommonChemistry, "asciichem/resolver/common_chemistry"
20
21
  autoload :Substance, "asciichem/resolver/substance"
21
22
 
22
23
  # Raised when sources disagree on substance identity (cross-check
@@ -69,9 +70,10 @@ module AsciiChem
69
70
  end
70
71
  end
71
72
 
72
- # Open, permissive sources register eagerly; opt-in NC sources do
73
- # not self-register (registered manually after sign-off).
74
- adapters # ensure the registry exists before adapter files load
73
+ # Open, permissive sources register eagerly. CommonChemistry is
74
+ # CC BY-NC 4.0: it loads (autoload) but never self-registers —
75
+ # users opt in explicitly via Resolver.register.
76
+ adapters
75
77
  constants.each { |c| const_get(c) unless c == :Conflict }
76
78
  end
77
79
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciiChem
4
- VERSION = "0.23.0"
4
+ VERSION = "0.24.0"
5
5
  end
data/lib/asciichem.rb CHANGED
@@ -10,6 +10,7 @@ require "plurimath"
10
10
  # AsciiChem::Cli.start # CLI dispatch
11
11
  module AsciiChem
12
12
  autoload :Cli, "asciichem/cli"
13
+ autoload :Citation, "asciichem/citation"
13
14
  autoload :Cml, "asciichem/cml"
14
15
  autoload :Error, "asciichem/errors"
15
16
  autoload :ParseError, "asciichem/errors"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciichem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -85,6 +85,26 @@ dependencies:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
87
  version: '2.0'
88
+ - !ruby/object:Gem::Dependency
89
+ name: relaton-bib
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0.1'
95
+ - - "<"
96
+ - !ruby/object:Gem::Version
97
+ version: '2'
98
+ type: :runtime
99
+ prerelease: false
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0.1'
105
+ - - "<"
106
+ - !ruby/object:Gem::Version
107
+ version: '2'
88
108
  - !ruby/object:Gem::Dependency
89
109
  name: plurimath
90
110
  requirement: !ruby/object:Gem::Requirement
@@ -156,6 +176,7 @@ files:
156
176
  - benchmarks/benchmark.rb
157
177
  - exe/asciichem
158
178
  - lib/asciichem.rb
179
+ - lib/asciichem/citation.rb
159
180
  - lib/asciichem/cli.rb
160
181
  - lib/asciichem/cml.rb
161
182
  - lib/asciichem/cml/conditions_extensions.rb
@@ -231,6 +252,7 @@ files:
231
252
  - lib/asciichem/resolver.rb
232
253
  - lib/asciichem/resolver/adapter.rb
233
254
  - lib/asciichem/resolver/cache.rb
255
+ - lib/asciichem/resolver/common_chemistry.rb
234
256
  - lib/asciichem/resolver/pubchem.rb
235
257
  - lib/asciichem/resolver/substance.rb
236
258
  - lib/asciichem/ring_bonds.rb