asciichem 0.22.0 → 0.23.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: d1aef5e42c0840b8bff4d291220e84f76f9536a9dba1cca3445deacb696388b1
4
- data.tar.gz: 92e28e623dfb7494080a9ee80d3545fa5a593d83d545775d890b7a9a5594c0bd
3
+ metadata.gz: a97d83231f84e4dde6c03c18f88ae7c34a30779e2550ca3f859e91bc58365f84
4
+ data.tar.gz: dbb27614ede6ba6987f09942465624b8d84dda4041444f63326982d84c365bdd
5
5
  SHA512:
6
- metadata.gz: bab5532c9007d1a5bd2079b792a66458d411f48af5ac59f04ad9e4a9150c7b53844639e67a46494dac7e6ae05f3451904a8fbe927b711803184749f00ce40e7e
7
- data.tar.gz: c0a9cb7f9f01e25dcce78f69894c23c8d5fe75d2d21fd20828d2daafa133186ebeb46d2aeb09bfc4246f43f851a3b982c4691cd013c40c56085634ca54f9db01
6
+ metadata.gz: 47d0e9bf10643d6a7fb24779d1baaaf1382e4c11b1b43375e1543b7cacb5169ea94784db8c1ab70a2f117590966bc96ca4809c994fd83f6295ca63fa56bf3c01
7
+ data.tar.gz: 84386eb43091bbcde85bf7ed8bc428544865bd87f985e2b986d2578c5cb2e25a11d0ef39b880bcd781a5da6b061e4a6501d18a02bd2cf3016da369dd72afd365
data/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to AsciiChem are documented here.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.23.0] - 2026-09-12
7
+
8
+ ### Added
9
+ - `AsciiChem::Resolver` — multi-source identifier resolution (TODO.impl
10
+ 38): opt-in adapter registry, one substance per source with
11
+ provenance, cross-check Conflict on InChIKey disagreement, TTL
12
+ cache in the user cache dir.
13
+ - `Resolver::PubChem` — keyless PUG-REST adapter with recorded
14
+ offline fixtures; CAS via the name namespace (TODO.impl 39).
15
+ - CLI `resolve` (networked, cached, `--refresh`) and offline
16
+ `validate` for identifier annotations (TODO.impl 41).
17
+
6
18
  ## [0.22.0] - 2026-09-12
7
19
 
8
20
  ### Added
@@ -399,7 +411,8 @@ This project follows [Semantic Versioning](https://semver.org/).
399
411
  `version`.
400
412
  - Comprehensive RSpec suite with round-trip conformance.
401
413
 
402
- [Unreleased]: https://github.com/asciichem/asciichem-ruby/compare/v0.22.0...HEAD
414
+ [Unreleased]: https://github.com/asciichem/asciichem-ruby/compare/v0.23.0...HEAD
415
+ [0.23.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.22.0...v0.23.0
403
416
  [0.22.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.21.0...v0.22.0
404
417
  [0.21.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.20.0...v0.21.0
405
418
  [0.20.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.19.0...v0.20.0
data/lib/asciichem/cli.rb CHANGED
@@ -86,6 +86,61 @@ module AsciiChem
86
86
  "asciichem #{command.usage}"
87
87
  end
88
88
 
89
+ desc "resolve --cas X | --name X | ...", "Resolve a substance from a source (network; cached)"
90
+ method_option :cas, type: :string, desc: "CAS registry number"
91
+ method_option :name, type: :string, desc: "Substance name"
92
+ method_option :cid, type: :string, desc: "PubChem CID"
93
+ method_option :inchikey, type: :string, desc: "InChIKey"
94
+ method_option :smiles, type: :string, desc: "SMILES"
95
+ method_option :source, type: :string, default: "pubchem", desc: "Resolver source"
96
+ method_option :refresh, type: :boolean, default: false, desc: "Bypass the cache"
97
+ method_option :format, aliases: "-t", type: :string, default: "model-json",
98
+ desc: "Output: model-json | text | smiles"
99
+ def resolve
100
+ convention, value = %i[cas name cid inchikey smiles]
101
+ .filter_map { |k| [k, options[k.to_s]] if options[k.to_s] }
102
+ .first
103
+ raise AsciiChem::Error, "give one of --cas/--name/--cid/--inchikey/--smiles" unless value
104
+
105
+ convention = { cas: "cas", name: "name", cid: "pubchem-cid",
106
+ inchikey: "inchikey", smiles: "smiles" }.fetch(convention)
107
+ substance = AsciiChem::Resolver[options[:source]].new.resolve(
108
+ value: value, convention: convention, refresh: options[:refresh]
109
+ )
110
+ raise AsciiChem::Error, "#{options[:source]} does not know #{value.inspect}" unless substance
111
+
112
+ puts case options[:format].to_s
113
+ when "text" then substance.preferred_name.to_s
114
+ when "smiles" then substance.identifier_value("canonical-smiles").to_s
115
+ else substance.to_model_json
116
+ end
117
+ rescue AsciiChem::Error => e
118
+ warn "Resolve error: #{e.message}"
119
+ exit 3
120
+ end
121
+
122
+ desc "validate -i INPUT", "Offline identifier validation"
123
+ method_option :input, aliases: "-i", type: :string, required: true
124
+ def validate
125
+ formula = AsciiChem.parse(options[:input])
126
+ annotations = formula.nodes.grep(AsciiChem::Model::Molecule).flat_map(&:identifiers)
127
+ if annotations.empty?
128
+ puts "no identifier annotations found"
129
+ return
130
+ end
131
+ annotations.each do |identifier|
132
+ known = AsciiChem::Identifiers.known?(identifier.convention)
133
+ valid = known && AsciiChem::Identifiers.valid?(identifier.convention, identifier.value)
134
+ status = known ? (valid ? "ok" : "INVALID") : "unknown convention"
135
+ puts format("%-12s %-40s %s", identifier.convention, identifier.value, status)
136
+ end
137
+ exit 1 if annotations.any? { |i| AsciiChem::Identifiers.known?(i.convention) &&
138
+ !AsciiChem::Identifiers.valid?(i.convention, i.value) }
139
+ rescue AsciiChem::ParseError => e
140
+ warn "Parse error: #{e.message}"
141
+ exit 1
142
+ end
143
+
89
144
  private
90
145
 
91
146
  def read_source
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiChem
4
+ module Resolver
5
+ # A source adapter. Subclasses register themselves:
6
+ #
7
+ # class MySource < Adapter
8
+ # source_name :mysource
9
+ # supports "name", "inchikey"
10
+ # def fetch_record(...); end
11
+ # end
12
+ #
13
+ # `resolve` handles caching around `fetch_record`; the transport
14
+ # (`fetch`) is injectable so specs run offline.
15
+ class Adapter
16
+ class << self
17
+ # Declares the source name and registers the adapter (called
18
+ # from the class body — `inherited` fires before the body
19
+ # runs, so it cannot see the subclass configuration).
20
+ def source_name(name = nil)
21
+ return @source_name if name.nil?
22
+
23
+ @source_name = name.to_s
24
+ AsciiChem::Resolver.register(@source_name, self)
25
+ end
26
+
27
+ def supports(*conventions)
28
+ @supported_conventions = conventions.map(&:to_s)
29
+ end
30
+
31
+ def supported_conventions
32
+ @supported_conventions || []
33
+ end
34
+ end
35
+
36
+ def supports?(convention)
37
+ self.class.supported_conventions.include?(convention.to_s)
38
+ end
39
+
40
+ # Cache-aware resolution. `fetch` must answer #get(url) -> body
41
+ # String (NetFetch by default; specs inject recorded responses).
42
+ def resolve(value:, convention:, fetch: nil, cache: nil, refresh: false)
43
+ cache ||= Cache.default
44
+ fetch ||= NetFetch.new
45
+ key = cache.key_for(self.class.source_name, convention, value)
46
+
47
+ unless refresh
48
+ cached = cache.read(key)
49
+ return cached if cached
50
+ end
51
+
52
+ substance = fetch_record(value: value, convention: convention, fetch: fetch)
53
+ .tap { |s| s&.identifiers&.each { |i| i.provenance ||= default_provenance } }
54
+ cache.write(key, substance) if substance
55
+ substance
56
+ end
57
+
58
+ # Not-found is nil, not an error — sources legitimately answer
59
+ # "unknown substance" during multi-source resolution.
60
+ def fetch_record(**)
61
+ raise NotImplementedError
62
+ end
63
+
64
+ def default_provenance
65
+ Provenance.new(
66
+ source: self.class.source_name,
67
+ retrieved_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
68
+ attribution: attribution
69
+ )
70
+ end
71
+
72
+ def attribution
73
+ nil
74
+ end
75
+ end
76
+
77
+ # Minimal stdlib HTTP transport. No runtime gem dependencies.
78
+ class NetFetch
79
+ def get(url, limit = 3)
80
+ raise Error, "too many redirects" if limit.zero?
81
+
82
+ uri = URI(url)
83
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
84
+ open_timeout: 5, read_timeout: 10) do |http|
85
+ http.request(Net::HTTP::Get.new(uri.request_uri, "User-Agent" => "asciichem-resolver"))
86
+ end
87
+
88
+ case response
89
+ when Net::HTTPSuccess then response.body
90
+ when Net::HTTPRedirection then get(URI.join(uri, response["location"]).to_s, limit - 1)
91
+ when Net::HTTPNotFound then nil
92
+ else
93
+ raise Error, "HTTP #{response.code}: #{response.message}"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ module AsciiChem
8
+ module Resolver
9
+ # Resolution cache in the USER cache dir (never the gem install
10
+ # dir): entries carry source, retrieved-at, attribution, TTL.
11
+ class Cache
12
+ DEFAULT_TTL = 7 * 24 * 60 * 60 # chemistry identifiers are stable
13
+
14
+ attr_reader :dir
15
+
16
+ def self.default_dir
17
+ base = ENV.fetch("XDG_CACHE_HOME", nil) || File.join(Dir.home, ".cache")
18
+ File.join(base, "asciichem", "resolver")
19
+ end
20
+
21
+ def initialize(dir: self.class.default_dir, ttl: DEFAULT_TTL)
22
+ @dir = dir
23
+ @ttl = ttl
24
+ end
25
+
26
+ def self.default
27
+ @default ||= new
28
+ end
29
+
30
+ def key_for(source, convention, value)
31
+ Digest::SHA256.hexdigest("#{source}:#{convention}:#{value}")
32
+ end
33
+
34
+ def read(key)
35
+ path = File.join(dir, "#{key}.json")
36
+ return nil unless File.file?(path)
37
+
38
+ entry = JSON.parse(File.read(path))
39
+ return nil if stale?(entry)
40
+
41
+ Substance.from_model_json(JSON.generate(entry["substance"]))
42
+ rescue JSON::ParserError, StandardError
43
+ nil
44
+ end
45
+
46
+ def write(key, substance)
47
+ FileUtils.mkdir_p(dir)
48
+ File.write(File.join(dir, "#{key}.json"), JSON.pretty_generate(
49
+ cached_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
50
+ ttl_seconds: @ttl,
51
+ substance: JSON.parse(substance.to_model_json)
52
+ ))
53
+ substance
54
+ end
55
+
56
+ private
57
+
58
+ def stale?(entry)
59
+ cached_at = Time.parse(entry["cached_at"]) rescue nil
60
+ return true unless cached_at
61
+
62
+ Time.now.utc - cached_at > entry.fetch("ttl_seconds", @ttl)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "json"
5
+ require "net/https"
6
+ require "uri"
7
+
8
+ module AsciiChem
9
+ module Resolver
10
+ # PubChem source adapter (TODO.impl 39; TODO.v2 07 Layer 2).
11
+ # PubChem is the default source: keyless PUG-REST, permissive
12
+ # terms, and the bulk mirror of Common Chemistry's validated CAS
13
+ # RNs (CAS numbers are indexed as names). Politeness: one request
14
+ # per resolution, 5/10s timeouts, no retries — PubChem asks for
15
+ # max 5 requests/second.
16
+ #
17
+ # Citation profile (TODO.v2 08): publisher NCBI, per-access
18
+ # versioning (retrievedAt provenance), compound link
19
+ # https://pubchem.ncbi.nlm.nih.gov/compound/{cid}.
20
+ class PubChem < Adapter
21
+ source_name :pubchem
22
+ supports "name", "cas", "pubchem-cid", "inchikey", "inchi", "smiles", "canonical-smiles"
23
+
24
+ BASE = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound"
25
+ PROPERTIES = %w[CanonicalSMILES IsomericSMILES MolecularFormula
26
+ MolecularWeight IUPACName InChI InChIKey].freeze
27
+ private_constant :BASE, :PROPERTIES
28
+
29
+ def fetch_record(value:, convention:, fetch:)
30
+ body = fetch.get(url_for(value, convention))
31
+ return nil unless body
32
+
33
+ props = JSON.parse(body).dig("PropertyTable", "Properties", 0)
34
+ return nil unless props
35
+
36
+ substance_from(props)
37
+ rescue JSON::ParserError => e
38
+ raise Error, "pubchem returned malformed JSON: #{e.message}"
39
+ end
40
+
41
+ def attribution
42
+ "PubChem, U.S. National Library of Medicine"
43
+ end
44
+
45
+ private
46
+
47
+ # CAS numbers are indexed as PubChem names (Common Chemistry
48
+ # mirror), so both map to the name namespace.
49
+ def url_for(value, convention)
50
+ namespace = { "name" => "name", "cas" => "name", "pubchem-cid" => "cid",
51
+ "inchikey" => "inchikey", "inchi" => "inchi",
52
+ "smiles" => "smiles", "canonical-smiles" => "smiles" }.fetch(convention.to_s)
53
+ "#{BASE}/#{namespace}/#{CGI.escape(value.to_s)}/property/#{PROPERTIES.join(',')}/JSON"
54
+ end
55
+
56
+ def substance_from(props)
57
+ provenance = default_provenance
58
+ identifiers = []
59
+ identifiers << Identifier.new(value: props["CID"].to_s, convention: "pubchem-cid", provenance: provenance) if props["CID"]
60
+ add_identifier(identifiers, props, "InChIKey", "inchikey", provenance)
61
+ add_identifier(identifiers, props, "InChI", "inchi", provenance)
62
+ add_identifier(identifiers, props, "CanonicalSMILES", "canonical-smiles", provenance)
63
+
64
+ Substance.new(
65
+ preferred_name: props["IUPACName"],
66
+ formula: props["MolecularFormula"],
67
+ molecular_weight: props["MolecularWeight"]&.to_f,
68
+ identifiers: identifiers,
69
+ properties: [
70
+ Property.new(name: "molecular-formula", value: props["MolecularFormula"], provenance: provenance),
71
+ Property.new(name: "molecular-weight", value: props["MolecularWeight"],
72
+ units: "g/mol", provenance: provenance)
73
+ ].compact,
74
+ # Best effort: PubChem canonical SMILES may use constructs
75
+ # outside the supported subset (stereo); then the SMILES
76
+ # stays as an identifier and structure is nil — no silent loss.
77
+ structure: parse_structure(props["CanonicalSMILES"])
78
+ )
79
+ end
80
+
81
+ def add_identifier(list, props, key, convention, provenance)
82
+ return unless props[key]
83
+
84
+ list << Identifier.new(value: props[key], convention: convention, provenance: provenance)
85
+ end
86
+
87
+ def parse_structure(smiles)
88
+ return nil unless smiles
89
+
90
+ AsciiChem.parse_smiles(smiles).nodes.first
91
+ rescue AsciiChem::ParseError
92
+ nil
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiChem
4
+ module Resolver
5
+ # Domain value objects for resolution results. These are data
6
+ # carriers; (de)serialization to the canonical wire form goes
7
+ # through the lutaml-model Wire classes only.
8
+ Provenance = Struct.new(:source, :retrieved_at, :source_version, :attribution, keyword_init: true)
9
+ Identifier = Struct.new(:value, :convention, :provenance, keyword_init: true)
10
+ Property = Struct.new(:name, :value, :units, :provenance, keyword_init: true)
11
+
12
+ # A resolved substance: identity + provenance + best-effort
13
+ # structure. `structure` is a Model::Molecule when the source's
14
+ # SMILES parses within the supported subset; otherwise nil and the
15
+ # SMILES stays available as an identifier — nothing is dropped.
16
+ class Substance
17
+ attr_reader :preferred_name, :synonyms, :formula, :molecular_weight,
18
+ :identifiers, :properties, :structure
19
+
20
+ def initialize(preferred_name: nil, synonyms: [], formula: nil,
21
+ molecular_weight: nil, identifiers: [], properties: [],
22
+ structure: nil)
23
+ @preferred_name = preferred_name
24
+ @synonyms = synonyms
25
+ @formula = formula
26
+ @molecular_weight = molecular_weight
27
+ @identifiers = identifiers
28
+ @properties = properties
29
+ @structure = structure
30
+ end
31
+
32
+ def identifier_value(convention)
33
+ identifiers.find { |i| i.convention == convention }&.value
34
+ end
35
+
36
+ def provenance
37
+ identifiers.first&.provenance
38
+ end
39
+
40
+ # Canonical wire form (asciichem-model v1 substance-record).
41
+ def to_model_json
42
+ Wire::SubstanceRecord.new(
43
+ type: "substance-record",
44
+ preferred_name: preferred_name,
45
+ synonyms: synonyms,
46
+ formula: formula,
47
+ molecular_weight: molecular_weight,
48
+ identifiers: identifiers.map do |i|
49
+ Wire::ProvenancedIdentifier.new(
50
+ identifier: Wire::Identifier.new(type: "identifier", value: i.value, convention: i.convention),
51
+ provenance: provenance_wire(i.provenance)
52
+ )
53
+ end,
54
+ properties: properties.map do |p|
55
+ Wire::ProvenancedProperty.new(
56
+ name: p.name, value: p.value, units: p.units,
57
+ provenance: provenance_wire(p.provenance)
58
+ )
59
+ end
60
+ ).to_json
61
+ end
62
+
63
+ # Rebuilds a Substance from the wire form (the cache path).
64
+ def self.from_model_json(json)
65
+ wire = Wire::SubstanceRecord.from_json(json)
66
+ new(
67
+ preferred_name: wire.preferred_name,
68
+ synonyms: Array(wire.synonyms),
69
+ formula: wire.formula,
70
+ molecular_weight: wire.molecular_weight,
71
+ identifiers: Array(wire.identifiers).map do |pi|
72
+ Identifier.new(
73
+ value: pi.identifier&.value,
74
+ convention: pi.identifier&.convention,
75
+ provenance: provenance_domain(pi.provenance)
76
+ )
77
+ end,
78
+ properties: Array(wire.properties).map do |p|
79
+ Property.new(name: p.name, value: p.value, units: p.units,
80
+ provenance: provenance_domain(p.provenance))
81
+ end
82
+ )
83
+ end
84
+
85
+ def self.provenance_domain(wire)
86
+ return nil unless wire
87
+
88
+ Provenance.new(source: wire.source, retrieved_at: wire.retrieved_at,
89
+ source_version: wire.source_version, attribution: wire.attribution)
90
+ end
91
+
92
+ private
93
+
94
+ def provenance_wire(p)
95
+ return nil unless p
96
+
97
+ Wire::Provenance.new(
98
+ type: "provenance",
99
+ source: p.source, retrieved_at: p.retrieved_at,
100
+ source_version: p.source_version, attribution: p.attribution
101
+ )
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "asciichem/errors"
4
+
5
+ module AsciiChem
6
+ # Resolution turns identifiers into substance records (TODO.v2 07
7
+ # Layer 2; TODO.impl 38). Multi-source by design: every answering
8
+ # source produces its own record with provenance, because the
9
+ # citation track (TODO.v2 08) emits one bibitem per (substance,
10
+ # source) the user chooses to cite.
11
+ #
12
+ # Sources are opt-in adapters that self-register (OCP, same pattern
13
+ # as the linter checks). NC-licensed sources are never registered by
14
+ # default — Common Chemistry ships only after the maintainer
15
+ # sign-off recorded in TODO.v2 07.
16
+ module Resolver
17
+ autoload :Adapter, "asciichem/resolver/adapter"
18
+ autoload :Cache, "asciichem/resolver/cache"
19
+ autoload :PubChem, "asciichem/resolver/pubchem"
20
+ autoload :Substance, "asciichem/resolver/substance"
21
+
22
+ # Raised when sources disagree on substance identity (cross-check
23
+ # mode). The message lists the disagreement — never hidden.
24
+ class Conflict < AsciiChem::Error; end
25
+
26
+ class << self
27
+ def adapters
28
+ @adapters ||= {}
29
+ end
30
+
31
+ def register(name, adapter)
32
+ adapters[name.to_s] = adapter
33
+ end
34
+
35
+ def [](name)
36
+ adapters.fetch(name.to_s)
37
+ rescue KeyError
38
+ raise Error, "unknown resolver source #{name.inspect} (registered: #{adapters.keys.sort.join(', ')})"
39
+ end
40
+
41
+ # Resolves against every opt-in source, returning one Substance
42
+ # per source that answers (nil entries dropped). Sources answer
43
+ # in registration order.
44
+ def resolve_all(value:, convention:, sources: nil, fetch: nil, cache: nil, refresh: false)
45
+ selected = sources ? Array(sources).map { |s| self[s] } : adapters.values
46
+ selected.filter_map do |adapter_class|
47
+ adapter = adapter_class.new
48
+ next unless adapter.supports?(convention)
49
+
50
+ adapter.resolve(value: value, convention: convention, fetch: fetch, cache: cache, refresh: refresh)
51
+ end
52
+ end
53
+
54
+ # First answering source wins.
55
+ def resolve(**args)
56
+ resolve_all(**args).first
57
+ end
58
+
59
+ # Cross-check mode: all sources must agree on the primary key
60
+ # (InChIKey) when more than one source provides one.
61
+ def resolve!(**args)
62
+ substances = resolve_all(**args)
63
+ keys = substances.filter_map { |s| s.identifier_value("inchikey") }.uniq
64
+ if keys.length > 1
65
+ raise Conflict,
66
+ "sources disagree on substance identity (InChIKeys: #{keys.join(' vs ')})"
67
+ end
68
+ substances.first
69
+ end
70
+ end
71
+
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
75
+ constants.each { |c| const_get(c) unless c == :Conflict }
76
+ end
77
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciiChem
4
- VERSION = "0.22.0"
4
+ VERSION = "0.23.0"
5
5
  end
data/lib/asciichem.rb CHANGED
@@ -25,6 +25,7 @@ module AsciiChem
25
25
  autoload :Molfile, "asciichem/molfile"
26
26
  autoload :Parser, "asciichem/parser"
27
27
  autoload :PeriodicTable, "asciichem/periodic_table"
28
+ autoload :Resolver, "asciichem/resolver"
28
29
  autoload :RingBonds, "asciichem/ring_bonds"
29
30
  autoload :Smiles, "asciichem/smiles"
30
31
  autoload :Structure, "asciichem/structure"
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.22.0
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -228,6 +228,11 @@ files:
228
228
  - lib/asciichem/molfile/writer.rb
229
229
  - lib/asciichem/parser.rb
230
230
  - lib/asciichem/periodic_table.rb
231
+ - lib/asciichem/resolver.rb
232
+ - lib/asciichem/resolver/adapter.rb
233
+ - lib/asciichem/resolver/cache.rb
234
+ - lib/asciichem/resolver/pubchem.rb
235
+ - lib/asciichem/resolver/substance.rb
231
236
  - lib/asciichem/ring_bonds.rb
232
237
  - lib/asciichem/smiles.rb
233
238
  - lib/asciichem/smiles/parser.rb