Module: Vendor::Colrapi

Defined in:
lib/vendor/colrapi.rb,
lib/vendor/colrapi/name_translation.rb

Overview

A middle-layer wrapper between Colrapi and TaxonWorks

Defined Under Namespace

Modules: NameTranslation

Constant Summary collapse

DATASETS =
{
  col: '3LR', # The Human edited compilation
  col_extended: '3LXR'  # Human plus algorithmic extensions
}.freeze
COL_ACCEPTED_STATUSES =

CoL usage statuses under which a name is in use.

['accepted', 'provisionally accepted'].freeze
COL_FOLLOWED_STATUSES =

CoL usage statuses resolve_accepted_names follows to their accepted name.

['synonym', 'ambiguous synonym'].freeze
COL_CODE_LOOKUP_MAP =

Maps a CoL nomenclatural code string to the TaxonWorks rank-name lookup (rank name => rank_class string) for that code.

Lookups can return nil, see COL_CODE_TO_NOMENCLATURE_CODE.

{
  'zoological' => ::ICZN_LOOKUP,
  'botanical'  => ::ICN_LOOKUP,
  'bacterial'  => ::ICNP_LOOKUP,
  'virus'      => ::ICVCN_LOOKUP
}.freeze
COL_CODE_TO_NOMENCLATURE_CODE =

Maps a CoL nomenclatural code string to the TaxonWorks nomenclature_code symbol (NomenclaturalRank.nomenclatural_code / rank_class.nomenclatural_code) it corresponds to.

Lookups can return nil: CoL may give no code for a name (name.code absent), or a code not mapped here (e.g. 'cultivars', 'phytosociological'). Callers must treat nil as "code unknown", not as a mismatch.

{
  'zoological' => :iczn,
  'botanical'  => :icn,
  'bacterial'  => :icnp,
  'virus'      => :icvcn
}.freeze
COL_CODE_ACRONYMS =

CoL's acronym for each of its nomenclatural codes (from its /vocab/nomCode; CoL gives phyto and phylo the same one).

{
  'zoological' => 'ICZN',
  'botanical' => 'ICN',
  'bacterial' => 'ICNP',
  'virus' => 'ICVCN',
  'cultivars' => 'ICNCP',
  'phyto' => 'ICPN',
  'phylo' => 'ICPN'
}.freeze

Class Method Summary collapse

Class Method Details

.above_genus_group?(rank_name, col_code) ⇒ Boolean

True when rank_name resolves to FamilyGroup or above (HigherClassificationGroup) — i.e. strictly coarser than GenusGroup — for the given CoL nomenclatural code. Unresolvable ranks are treated as not-above (false).

Returns:

  • (Boolean)


605
606
607
608
# File 'lib/vendor/colrapi.rb', line 605

def self.above_genus_group?(rank_name, col_code)
  rank_class = rank_class_for(rank_name, col_code:)
  rank_class ? ::FAMILY_AND_ABOVE_RANK_NAMES.include?(rank_class) : false
end

.ancestors(dataset_id, taxon_id) ⇒ Array<Hash>

Returns the ancestor classification chain for a CoL taxon.

Uses Colrapi.taxon with subresource: 'classification'. Response is an Array of hashes with keys: 'id', 'name' (String, not hash), 'authorship', 'rank', 'label', 'labelHtml'.

Only valid for backbone datasets (DATASETS, DATASETS). For external datasets use ancestors_via_parent_id instead.

Parameters:

  • dataset_id (String) —

    the backbone dataset taxon_id belongs to

  • taxon_id (String) —

    CoL taxon ID (e.g. '6MB3T')

Returns:

  • (Array<Hash>)


242
243
244
245
246
247
248
249
# File 'lib/vendor/colrapi.rb', line 242

def self.ancestors(dataset_id, taxon_id)
  ::Colrapi.taxon(
    dataset_id, taxon_id: taxon_id, subresource: 'classification'
  )
rescue => e
  Rails.logger.warn "Vendor::Colrapi.ancestors error: #{e.message}"
  []
end

.ancestors_via_parent_id(dataset_id, taxon_id, max_depth: 20) ⇒ Array<Hash>

Builds an ancestor chain for external/denormed datasets by following the parentId field of successive taxon records.

External datasets (like the Mammal Diversity Database) are ingested into ChecklistBank without a pre-built classification subresource. Instead, each nameusage record carries a parentId pointing to the immediate parent within the same dataset.

Returns entries in the same format as the classification subresource used by ancestors():

{ 'id', 'name' (String, the full scientificName), 'rank', 'authorship',
'label', 'labelHtml' }

Order is distal-first (kingdom before immediate parent), matching ancestors(). !! The starting taxon itself is NOT included; only its ancestors are.

Parameters:

  • dataset_id (String) —

    the external dataset to query

  • taxon_id (String) —

    ID of the starting taxon

  • max_depth (Integer) (defaults to: 20) —

    circuit-breaker against malformed/cyclic data

Returns:

  • (Array<Hash>)


282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/vendor/colrapi.rb', line 282

def self.ancestors_via_parent_id(dataset_id, taxon_id, max_depth: 20)
  chain   = []
  visited = Set.new

  initial = ::Colrapi.taxon(dataset_id, taxon_id: taxon_id)
  return chain if initial.blank?

  current_id = initial['parentId']

  max_depth.times do
    break if current_id.blank? || visited.include?(current_id)
    visited << current_id

    taxon = ::Colrapi.taxon(dataset_id, taxon_id: current_id)

    break if taxon.blank?

    chain << {
      'id'         => current_id,
      'name'       => taxon.dig('name', 'scientificName').to_s,
      'rank'       => taxon.dig('name', 'rank'),
      'authorship' => taxon.dig('name', 'authorship'),
      'label'      => taxon.fetch('label', '').to_s,
      'labelHtml'  => taxon.fetch('labelHtml', '').to_s
    }

    current_id = taxon['parentId']
  end

  # Return distal-first (kingdom before genus) to match the classification
  # subresource order returned by ancestors().
  chain.reverse
rescue => e
  Rails.logger.warn "Vendor::Colrapi.ancestors_via_parent_id error: #{e.message}"
  []
end

.classified_usage(col_result, dataset_id: nil) ⇒ Hash

Builds CoL's own classification chain for a nameusage result — pure CoL data, no TaxonWorks matching.

col_result is a flat nameusage hash as returned by search (no 'usage' wrapper):

{ 'id' => '6MB3T', 'status' => 'accepted',
'name' => { 'scientificName' => 'Homo sapiens', 'rank' => 'species',
            'authorship' => 'Linnaeus, 1758',
            'combinationAuthorship' => { 'authors' => [...], 'year' => '1758' } },
'label' => 'Homo sapiens Linnaeus, 1758', … }

Classification entries from ancestors() have:

{ 'id' => '636X2', 'name' => 'Homo', 'rank' => 'genus', 'label' => 'Homo', … }

Note: in classification entries 'name' is a plain String, not a hash: the full scientific name ('Olea europaea', 'Aedes (Stegomyia)').

Parameters:

  • col_result (Hash) —

    a single entry from search

  • dataset_id (String, nil) (defaults to: nil) —

    the dataset that was searched; falls back to DATASETS

Returns:

  • (Hash) —

    extension hash with :col_key, :col_name, :col_status, :col_authorship, :col_year, :col_rank, :col_code, :col_dataset_id, :non_accepted, :accepted (a synonym's accepted name: :col_name, :col_authorship) and :alignment (Array of ancestor hashes, kingdom-first, each with :rank, :col_name, :col_id, :dataset_id, :col_authorship, :above_genus_group, :rank_unknown; empty for a non-accepted name)



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/vendor/colrapi.rb', line 344

def self.classified_usage(col_result, dataset_id: nil)
  col_key = col_result['id']
  col_name = protonym_name_from(col_result['name'])
  col_rank = col_result.dig('name', 'rank')&.downcase
  col_status = col_result['status']
  col_authorship = col_result.dig('name', 'authorship')
  col_year = col_result.dig('name', 'combinationAuthorship', 'year') ||
    col_result.dig('name', 'basionymOrCombinationAuthorship', 'year')

  # CoL nomenclatural code: 'zoological', 'botanical', 'bacterial', 'virus'
  col_code = col_result.dig('name', 'code')

  # Dataset used for the search (target row).
  col_dataset_id = dataset_id.presence || DATASETS[:col]

  # A non-accepted name (synonym, ambiguous synonym, misapplied, ...) gets
  # no alignment. CoL hangs it off its accepted taxon, so the only
  # classification CoL has for it is the accepted name's - which says
  # where the taxon is now, nothing about the name's own genus or family.
  non_accepted = col_status.present? &&
    !COL_ACCEPTED_STATUSES.include?(col_status.downcase)
  accepted_name =
    if col_status == 'synonym' && col_result['accepted']
      {
        col_name: col_result.dig('accepted', 'name', 'scientificName'),
        col_authorship: col_result.dig('accepted', 'name', 'authorship')
      }
    end

  # Ancestors come from the searched dataset, so their ids are its ids.
  ancestor_chain =
    if non_accepted || col_key.blank?
      []
    elsif col_backbone_dataset?(col_dataset_id)
      ancestors(col_dataset_id, col_key)
    else
      ancestors_via_parent_id(col_dataset_id, col_key)
    end

  # Drop 'domain', which CoL's backbone puts above kingdom in every
  # cellular lineage (Eukaryota, Bacteria, Archaea) and TaxonWorks has no
  # rank for. Other ranks TaxonWorks can't resolve (e.g. 'unranked'
  # Viruses, 'parvphylum') are kept and flagged :rank_unknown below, and
  # virus 'realm', above kingdom, is a TaxonWorks (ICVCN) rank.
  # Both ancestor sources are already distal→proximal (kingdom before
  # genus).
  ancestor_chain =
    ancestor_chain.reject { |a| a['rank']&.downcase == 'domain' }

  alignment = ancestor_chain.map do |ancestor|
    rank = ancestor['rank']&.downcase

    {
      rank:,
      # As CoL gives it ("Aedes (Stegomyia)", "Olea europaea").
      col_name: ancestor['name'],
      col_id: ancestor['id'],
      dataset_id: col_dataset_id,
      col_authorship: ancestor['authorship'].presence,
      above_genus_group: above_genus_group?(rank, col_code),
      # e.g. 'parvphylum' - can't be created (ColCreator skips it)
      rank_unknown: rank_class_for(rank, col_code:).nil?
    }
  end

  {
    col_key:, col_name:, col_status:, col_authorship:, col_year:, col_rank:,
    col_code:, col_dataset_id:, alignment:, non_accepted:,
    **(accepted_name ? { accepted: accepted_name} : {})
  }
end

.col_backbone_dataset?(dataset_id) ⇒ Boolean

Returns true when dataset_id refers to one of the CoL backbone datasets that support the classification subresource for ancestor retrieval. External/denormed datasets (e.g. Mammal Diversity Database, dataset 9802) do not have this subresource and require iterative parentId traversal instead.

Parameters:

  • dataset_id (String)

Returns:

  • (Boolean)


258
259
260
# File 'lib/vendor/colrapi.rb', line 258

def self.col_backbone_dataset?(dataset_id)
  DATASETS.values.include?(dataset_id.to_s)
end

.col_rank_sort(rank_name, col_code) ⇒ Object

Maps a CoL rank name ('genus', 'family', …) and CoL nomenclatural code ('zoological', 'botanical', 'bacterial', 'virus') to the TaxonWorks RANK_SORT index. Higher index = more specific rank. Returns nil when unresolvable.

Unlike rank_class_for, doesn't fall back through the other codes: callers compare these indices against each other, so all must come from one code. An unrecognized col_code is treated as zoological.



594
595
596
597
598
599
# File 'lib/vendor/colrapi.rb', line 594

def self.col_rank_sort(rank_name, col_code)
  return nil if rank_name.blank?
  lookup = COL_CODE_LOOKUP_MAP[col_code] || ::ICZN_LOOKUP
  rank_class = lookup[rank_name]
  rank_class ? ::RANK_SORT[rank_class] : nil
end

.datasets(q:, limit: 20) ⇒ Array<Hash>

Searches CoL datasets by name string.

Returns an array of dataset summaries, each containing at least 'id', 'title', and 'alias'.

Parameters:

  • q (String) —

    dataset name search string

  • limit (Integer) (defaults to: 20)

Returns:

  • (Array<Hash>)


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/vendor/colrapi.rb', line 192

def self.datasets(q:, limit: 20)
  # Text search by title/name.
  text_results = begin
    result = ::Colrapi.dataset(q: q, limit: limit)
    (result['result'] || []).map { |d|
      {
        'id' => d['key'].to_s,
        'title' => d['title'],
        'alias' => d['alias']
      }
    }
  rescue => e
    Rails.logger.warn "Vendor::Colrapi.datasets text search error: #{e.message}"
    []
  end

  # Direct lookup by dataset ID — `q` may itself be a key like '3LXR'.
  # Colrapi.dataset(dataset_id:) returns a single hash, not a paged result.
  direct_hit = begin
    d = ::Colrapi.dataset(dataset_id: q)
    d.is_a?(Hash) && d['key'].present? ?
      {
        'id' => d['key'].to_s,
        'title' => d['title'],
        'alias' => d['alias']
      } : nil
  rescue
    nil
  end

  seen = {}
  [direct_hit, *text_results].compact.each_with_object([]) do |d, arr|
    next if seen[d['id']]
    seen[d['id']] = true
    arr << d
  end
end

.name_status(target) ⇒ Hash

What CoL says about the status of a name.

CoL's fuzzy /nameusage/search is queried with NameTranslation.search_term, and its usages are compared to the name by NameTranslation key, not exact string - CoL doesn't write a name the same way TaxonWorks does, or even consistently (see NameTranslation). A usage whose key matches makes the name

  • :accepted - accepted, or a synonym of a name with the same key (e.g. an original combination CoL lists under its own species)
  • :synonym - a synonym of a different name, i.e. outdated
  • :ambiguous_synonym - an ambiguous synonym (several possible targets) in that order of precedence. With no matching usage it's :undeterminable.

A TaxonName is looked up by one of its NameTranslation.forms: an invalid Protonym by its published form (original combination), since CoL lists synonyms as published while TaxonWorks moves a synonym under its valid name's parent (project 13 sample, 2026-10: of 40 synonyms whose two forms differ, CoL had the original combination for 38 and #cached for 7, never #cached alone); anything else by its current form (a Combination, which has none, by its published one). A published form is searched at the rank it was published at.

Parameters:

  • target (TaxonName, String, nil) —

    the name to check: a TaxonName when there is one (searched at its own rank, or the rank its published form was published at), else a name string, searched as a species

    • for a CollectionObject see Tasks::CollectionObjects:: OutdatedNamesHelper#outdated_names_target

Returns:

  • (Hash) —

    { taxonworks_name: String, col_usages: [ { usage: { name:, status: }, accepted: { name:, status: } } ], provisional_status: Symbol }



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/vendor/colrapi.rb', line 53

def self.name_status(target)
  if target.is_a?(::Combination)
    # Only a published form, at the rank it was published at (its finest
    # element), which needn't be its protonym's rank now.
    name = NameTranslation.forms(target)[:published]
    rank = target.protonyms_by_rank.keys.last
  elsif target.is_a?(::TaxonName)
    forms = NameTranslation.forms(target)
    invalid_protonym =
      target.is_a?(::Protonym) && !target.cached_is_valid
    rank = ::RANKS_LOOKUP[target.rank_class.to_s]
    if invalid_protonym && forms[:published]
      # At the rank it was published at, e.g. a species published as a
      # variety, or a subgenus as a genus.
      name = forms[:published]
      rank = published_rank(target) || rank
    else
      name = forms[:current]
    end
  else
    name = target
    # TODO: try harder here.
    rank = 'species'
  end
  key = NameTranslation.name_key(name)

  results =
    if key.present?
      ::Colrapi.nameusage_search(
        q: NameTranslation.search_term(name),
        dataset_id: DATASETS[:col],
        rank:
      )['result']
    end

  statuses = []
  col_usages = (results || []).map do |result|
    usage = result['usage']
    statuses << usage_status(usage, key) if usage_key(usage) == key

    accepted = {}
    if usage['accepted']
      accepted = {
        name: usage.dig('accepted', 'name', 'scientificName'),
        status: usage.dig('accepted', 'status')
      }
    end

    {
      usage: {
        name: usage.dig('name', 'scientificName'),
        status: usage['status']
      },
      accepted:
    }
  end

  provisional_status =
    [:accepted, :synonym, :ambiguous_synonym]
      .find { |status| statuses.include?(status) } || :undeterminable

  { taxonworks_name: name, col_usages:, provisional_status: }
end

.protonym_name_from(name_hash) ⇒ String?

The TaxonWorks Protonym#name for a CoL name: the last epithet of a species-group name, a subgenus's epithet, or a uninomial (genus and above). CoL's scientificName is the full name ("Homo sapiens", "Aedes (Stegomyia)"); CoL parses a subgenus into genus + infragenericEpithet, with no uninomial. Priority: infraspecificEpithet > specificEpithet > infragenericEpithet

uninomial > scientificName fallback.

Parameters:

  • name_hash (Hash, nil) —

    the 'name' sub-hash from a CoL nameusage result

Returns:

  • (String, nil)


511
512
513
514
515
516
517
518
# File 'lib/vendor/colrapi.rb', line 511

def self.protonym_name_from(name_hash)
  return nil if name_hash.nil?
  name_hash['infraspecificEpithet'].presence ||
    name_hash['specificEpithet'].presence ||
    name_hash['infragenericEpithet'].presence ||
    name_hash['uninomial'].presence ||
    name_hash['scientificName']
end

.published_rank(protonym) ⇒ String? (private)

Returns the rank protonym was published at, the finest rank of its original combination; nil without one.

Parameters:

Returns:

  • (String, nil) —

    the rank protonym was published at, the finest rank of its original combination; nil without one



120
121
122
123
124
125
# File 'lib/vendor/colrapi.rb', line 120

def self.published_rank(protonym)
  types = protonym.original_combination_relationships.pluck(:type)
  ::ORIGINAL_COMBINATION_RANKS
    .select { |_rank, type| types.include?(type) }
    .keys.last&.to_s
end

.rank_class_for(rank_string, col_code: nil) ⇒ String?

Maps a human-readable CoL rank string to a TaxonWorks rank_class string.

When col_code is recognized (e.g. 'botanical' → ICN_LOOKUP), only that code's lookup is used: a rank class from another code would fail Protonym's same-code validation against its parent (a zoological 'variety' is unknown, not ICN's). Otherwise falls back through all four codes in order so any rank resolves if possible.

Parameters:

  • rank_string (String, nil) —

    e.g. 'genus', 'family'

  • col_code (String, nil) (defaults to: nil) —

    'zoological', 'botanical', 'bacterial', 'virus'

Returns:

  • (String, nil) —

    a TaxonWorks rank_class string, or nil for an unknown rank



571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# File 'lib/vendor/colrapi.rb', line 571

def self.rank_class_for(rank_string, col_code: nil)
  return nil if rank_string.blank?
  r = rank_string.to_s.downcase

  if (lookup = COL_CODE_LOOKUP_MAP[col_code])
    return lookup[r]
  end

  COL_CODE_LOOKUP_MAP.values.each do |lookup|
    result = lookup[r]
    return result if result
  end

  nil
end

.resolve_accepted_names(name_string, dataset_id: nil) ⇒ Array<Hash>

Resolves a free-text name (e.g. transcribed from a specimen label) to the names currently accepted by the Catalogue of Life, so a caller doesn't have to look up synonymy by hand before searching TaxonWorks.

Every exact hit for name_string is resolved, in CoL's order: an accepted (or provisionally accepted) name to itself, a synonym to its accepted target. Several hits are homonyms - the same name in different codes (Pieris), or a later homonym CoL lists as a synonym of its replacement (Abies alba (Aiton) Michx. -> Picea glauca) - so each is resolved rather than picking one.

An ambiguous synonym is followed too. Its accepted is only one of the name's possible targets - a pro parte synonym (Abax brunnipes Letzner, 1852) is one hit per target - so the caller should say so; each target is still a valid name the user may mean.

Anything else is skipped. A misapplied name also carries an accepted usage, but it's the taxon some author misidentified as the name, not what the name means.

Several hits can resolve to the same accepted name; it's given once, and the hit kept for it (which the caller's "valid name for" note names) is, in order of preference: the accepted name itself, a plain synonym, an ambiguous synonym. Ties go to the hit CoL lists first.

Parameters:

  • name_string (String)
  • dataset_id (String, nil) (defaults to: nil) —

    CoL dataset ID; falls back to the default hardwired ID when nil

Returns:

  • (Array<Hash>) —

    empty when CoL resolves nothing, else each { input:, matched_name:, matched_authorship:, matched_status:, matched_col_key:, valid_name:, valid_col_key:, valid_authorship:, valid_status:, valid_col_result:, dataset_id: }



448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/vendor/colrapi.rb', line 448

def self.resolve_accepted_names(name_string, dataset_id: nil)
  target_dataset_id = dataset_id.presence || DATASETS[:col]
  hits = search(name_string, dataset_id:)['result'] || []

  resolved = hits.filter_map do |hit|
    matched_status = hit['status']
    if COL_FOLLOWED_STATUSES.include?(matched_status)
      accepted = hit['accepted']
    end
    next if accepted.nil? &&
      !COL_ACCEPTED_STATUSES.include?(matched_status.to_s.downcase)

    # The full nameusage hash for the *valid* name - CoL nests the
    # accepted usage (a complete nameusage record, same shape as a search
    # result) right on the synonym hit.
    valid_col_result = accepted || hit

    {
      input: name_string,
      # Full scientific name (e.g. "Homo sapiens"), not the single-word
      # component protonym_name_from extracts for building TW Protonym
      # trees - TaxonName/Otu#cached holds the full combination, so
      # that's what a caller needs to search against.
      matched_name: hit.dig('name', 'scientificName'),
      matched_authorship: hit.dig('name', 'authorship').presence,
      matched_status:,
      matched_col_key: hit['id'],
      valid_name: valid_col_result.dig('name', 'scientificName'),
      valid_col_key: valid_col_result['id'],
      valid_authorship: valid_col_result.dig('name', 'authorship'),
      valid_status:
        accepted ? (accepted['status'] || 'accepted') : matched_status,
      valid_col_result:,
      dataset_id: target_dataset_id
    }
  end

  resolved
    .group_by { |r| r[:valid_col_key] }
    .values
    .map do |same_valid_name|
      same_valid_name.min_by do |r|
        if r[:matched_col_key] == r[:valid_col_key]
          0
        elsif r[:matched_status] == 'synonym'
          1
        else
          2
        end
      end
    end
end

.search(name_string, dataset_id: nil, limit: 20) ⇒ Hash

Searches the Catalogue of Life by name string - the exact string only (/nameusage?q=): no partial names, and a name with or without a subgenus misses the other form (see NameTranslation).

The Colrapi gem takes dataset_id as a positional first argument. Response structure: { 'total' => Integer, 'result' => Array } Each result entry is the name usage itself (not wrapped in 'usage' as nameusage_search entries are), with keys:

'id', 'status', 'name' (hash with 'scientificName', 'rank', 'authorship', …),
'label', 'labelHtml', 'parentId', etc.

Parameters:

  • name_string (String)
  • dataset_id (String, nil) (defaults to: nil) —

    CoL dataset ID; falls back to the default hardwired ID when nil

  • limit (Integer) (defaults to: 20) —

    maximum number of results

Returns:

  • (Hash) —

    raw Colrapi nameusage response (keys 'total', 'result')



176
177
178
179
180
181
182
# File 'lib/vendor/colrapi.rb', line 176

def self.search(name_string, dataset_id: nil, limit: 20)
  target = dataset_id.presence || DATASETS[:col]
  ::Colrapi.nameusage(target, q: name_string, limit:)
rescue => e
  Rails.logger.warn "Vendor::Colrapi.search error: #{e.message}"
  { 'total' => 0, 'result' => [] }
end

.usage_key(usage) ⇒ String? (private)

Returns its comparison key.

Parameters:

  • usage (Hash) —

    a nameusage_search usage

Returns:

  • (String, nil) —

    its comparison key



130
131
132
133
134
# File 'lib/vendor/colrapi.rb', line 130

def self.usage_key(usage)
  NameTranslation.col_name_key(
    usage.dig('name', 'scientificName'), usage.dig('name', 'rank')
  )
end

.usage_status(usage, key) ⇒ Symbol? (private)

Returns what the usage says about the name, see .name_status.

Parameters:

  • usage (Hash) —

    a nameusage_search usage whose key matches key

  • key (String) —

    the TaxonWorks name's comparison key

Returns:

  • (Symbol, nil) —

    what the usage says about the name, see .name_status



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/vendor/colrapi.rb', line 140

def self.usage_status(usage, key)
  case usage['status']
  when *COL_ACCEPTED_STATUSES
    :accepted
  when 'synonym'
    # A synonym of a name with the same key is the same name, so it isn't
    # outdated. CoL lists "Aphrophora ambigua" (the original combination,
    # no subgenus) as a synonym of accepted "Aphrophora (Aphrophora)
    # ambigua".
    accepted_key = NameTranslation.col_name_key(
      usage.dig('accepted', 'name', 'scientificName'),
      usage.dig('accepted', 'name', 'rank')
    )
    accepted_key == key ? :accepted : :synonym
  when 'ambiguous synonym'
    :ambiguous_synonym
  end
end