Module: Abqari::PublicationNormaliser

Defined in:
lib/abqari/publication_normaliser.rb

Overview

Normalises publication / bundle / series frontmatter into the canonical Folio-aligned shape documented in docs/publications.md. Runs once at frontmatter-parse time so the rest of the engine sees one vocabulary.

Today the normalisers do small, contained work:

- `PublicationNormaliser` expands `price: "$16.00"` shorthand
into the canonical `{ cents:, currency:, formatted: }` Hash
so authors can write the prettier form without losing the
Folio-aligned shape downstream.
- `BundleNormaliser` and `SeriesNormaliser` do the same price
expansion. Bundle / series use `name` (not `title`); no
translation needed — frontmatter already writes `name`.

Adding a new shorthand later: each normaliser is a single function that takes a hash and returns a hash. Keep them idempotent so a frontmatter block already in canonical form passes through unchanged.

Constant Summary collapse

CURRENCY_SYMBOLS =

Symbol → ISO currency code. Falls back to USD for unknown symbols (matches Folio's serializer default).

{
  '$'  => 'USD',
  'A$' => 'AUD',
  'C$' => 'CAD',
  'NZ$' => 'NZD',
  'S$' => 'SGD',
  '£'  => 'GBP',
  ''  => 'EUR',
  '¥'  => 'JPY'
}.freeze
SYMBOL_ORDER =

Order matters — longest symbols first so 'A$' / 'S$' / 'NZ$' / 'C$' match before the bare '$'.

CURRENCY_SYMBOLS.keys.sort_by { |s| -s.length }
PRICE_RE =
/\A\s*(?<symbol>#{SYMBOL_ORDER.map { |s| Regexp.escape(s) }.join('|')})?\s*(?<amount>\d+(?:\.\d+)?)\s*\z/

Class Method Summary collapse

Class Method Details

.call(frontmatter, collection) ⇒ Object

Top-level entry point. Dispatches per collection. Returns the frontmatter hash with shorthand fields expanded; original input not mutated.

Publications, bundles, AND series all get price normalisation. Folio doesn't supply prices on series (series in Folio are pure sequences, no commerce), but a native author CAN set price: "$28.00" on a series to render a "buy the whole series" button — and the rest of the engine reads fm.dig('price', 'formatted'), so the shorthand needs to expand here regardless of source.



55
56
57
58
59
60
61
62
# File 'lib/abqari/publication_normaliser.rb', line 55

def call(frontmatter, collection)
  return frontmatter unless frontmatter.is_a?(Hash)

  case collection
  when 'publications', 'bundles', 'series' then normalise_with_price(frontmatter)
  else frontmatter
  end
end

.expand_price(raw) ⇒ Object

Expand a string price into the canonical Hash shape:

"$16.00"  => { cents: 1600, currency: 'USD', formatted: '$16.00' }
"12"      => { cents: 1200, currency: 'USD', formatted: '$12.00' }
"£9.99"   => { cents: 999,  currency: 'GBP', formatted: '£9.99'  }

Returns nil if the string doesn't parse (caller leaves the frontmatter value as-is, so the operator sees their typo surface downstream — e.g. JSON-LD will omit the offer).



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
# File 'lib/abqari/publication_normaliser.rb', line 82

def expand_price(raw)
  return nil if raw.nil? || raw.to_s.strip.empty?

  raw_str = raw.to_s.strip
  match = PRICE_RE.match(raw_str)
  return nil unless match

  symbol = match[:symbol]
  currency = CURRENCY_SYMBOLS[symbol] || 'USD'
  amount = match[:amount].to_f
  cents  = (amount * 100).round
  # When the author wrote a bare number ("12"), supply a default
  # formatted form so display code doesn't see "12" with no
  # currency marker. The symbol is the canonical one for the
  # inferred currency.
  formatted = if symbol
                raw_str
              else
                default_symbol = CURRENCY_SYMBOLS.invert[currency] || '$'
                "#{default_symbol}#{format('%.2f', amount)}"
              end

  {
    'cents'     => cents,
    'currency'  => currency,
    'formatted' => formatted
  }
end

.normalise_with_price(frontmatter) ⇒ Object



64
65
66
67
68
69
70
71
72
# File 'lib/abqari/publication_normaliser.rb', line 64

def normalise_with_price(frontmatter)
  return frontmatter unless frontmatter['price']
  return frontmatter if frontmatter['price'].is_a?(Hash)

  expanded = expand_price(frontmatter['price'])
  return frontmatter unless expanded

  frontmatter.merge('price' => expanded)
end