Module: Typstify::Data
- Defined in:
- lib/typstify/data.rb
Overview
Turns the data: you pass into the data.json a template reads.
The validation pass exists so that a bad value fails immediately, naming the exact key path, instead of surfacing later as a template that silently renders "#Invoice:0x00007f…" — or, under Rails, as an empty object.
That last case is the subtle one. ActiveSupport defines as_json on
Object itself, returning the instance variables of anything at all, so a
naive respond_to?(:as_json) check would happily serialise a Proc, an IO or
a half-built value object into {} and render a blank invoice. We therefore
accept as_json only from classes that actually define it — models,
serializers, Hash, Array, Time — and reject the generic fallback.
Constant Summary collapse
- PRIMITIVES =
[String, Integer, TrueClass, FalseClass, NilClass].freeze
- GENERIC_AS_JSON_OWNERS =
as_jsoninherited from one of these is ActiveSupport's catch-all, not a deliberate serialization. [Object, Kernel, BasicObject].freeze
Class Method Summary collapse
-
.deliberate_as_json?(value) ⇒ Boolean
True when the object's class defines as_json itself, rather than picking up ActiveSupport's Object-level fallback.
- .deliberate_to_h?(value) ⇒ Boolean
-
.dump(data) ⇒ String
Pretty JSON, ready to write as data.json.
-
.join(path) ⇒ Object
["data", ".line_items", "[0]", ".amount"] => data.line_items.amount.
- .normalize(value, path) ⇒ Object
- .normalize_float(float, path) ⇒ Object
- .normalize_hash(hash, path) ⇒ Object
- .normalize_object(value, path) ⇒ Object
Class Method Details
.deliberate_as_json?(value) ⇒ Boolean
True when the object's class defines as_json itself, rather than picking up ActiveSupport's Object-level fallback.
82 83 84 85 86 87 88 |
# File 'lib/typstify/data.rb', line 82 def deliberate_as_json?(value) return false unless value.respond_to?(:as_json) !GENERIC_AS_JSON_OWNERS.include?(value.method(:as_json).owner) rescue NameError false end |
.deliberate_to_h?(value) ⇒ Boolean
90 91 92 93 94 95 96 |
# File 'lib/typstify/data.rb', line 90 def deliberate_to_h?(value) return false unless value.respond_to?(:to_h) !GENERIC_AS_JSON_OWNERS.include?(value.method(:to_h).owner) rescue NameError false end |
.dump(data) ⇒ String
Returns pretty JSON, ready to write as data.json.
31 32 33 |
# File 'lib/typstify/data.rb', line 31 def dump(data) JSON.pretty_generate(normalize(data, ["data"])) end |
.join(path) ⇒ Object
["data", ".line_items", "[0]", ".amount"] => data.line_items.amount
99 100 101 |
# File 'lib/typstify/data.rb', line 99 def join(path) path.join end |
.normalize(value, path) ⇒ Object
35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/typstify/data.rb', line 35 def normalize(value, path) case value when *PRIMITIVES then value when Symbol then value.to_s when Float then normalize_float(value, path) when Hash then normalize_hash(value, path) when Array then value.each_with_index.map { |v, i| normalize(v, path + ["[#{i}]"]) } when Time, Date then value.iso8601 else normalize_object(value, path) end end |
.normalize_float(float, path) ⇒ Object
58 59 60 61 62 |
# File 'lib/typstify/data.rb', line 58 def normalize_float(float, path) return float if float.finite? raise ArgumentError, "#{join(path)} is #{float}, which JSON cannot represent." end |
.normalize_hash(hash, path) ⇒ Object
47 48 49 50 51 52 53 54 55 56 |
# File 'lib/typstify/data.rb', line 47 def normalize_hash(hash, path) hash.each_with_object({}) do |(key, value), out| unless key.is_a?(String) || key.is_a?(Symbol) || key.is_a?(Numeric) raise ArgumentError, "#{join(path)} has a #{key.class} key (#{key.inspect}); " \ "JSON object keys must be strings, symbols or numbers." end out[key.to_s] = normalize(value, path + [".#{key}"]) end end |
.normalize_object(value, path) ⇒ Object
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/typstify/data.rb', line 64 def normalize_object(value, path) if deliberate_as_json?(value) normalize(value.as_json, path) elsif deliberate_to_h?(value) normalize(value.to_h, path) else raise ArgumentError, <<~MSG.strip #{join(path)} is a #{value.class}, which is not JSON-serializable. Convert it first — a serializer, a class that defines #as_json, or a plain Hash of strings and numbers. Passing it through would put an empty object or an inspect string into your document. MSG end end |