Module: Klenod::Runtime::BundleFormat

Defined in:
lib/klenod/runtime/bundle_format.rb

Constant Summary collapse

MAGIC =
"MODPACK_BUNDLE_V1\n"
FORMAT_VERSION =
1

Class Method Summary collapse

Class Method Details

.bundle_from_payload(payload, source_root: nil) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/klenod/runtime/bundle_format.rb', line 48

def bundle_from_payload(payload, source_root: nil)
  validate_payload!(payload)

  bundle =
    Bundle.new(
      decode_value(payload.fetch("entrypoints")),
      decode_modules(payload.fetch("modules")),
      decode_assets(payload.fetch("assets")),
      source_root: decode_value(payload["source_root"])
    )
  bundle.source_root = source_root if source_root
  bundle
end

.decode_assets(payload) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/klenod/runtime/bundle_format.rb', line 166

def decode_assets(payload)
  expect_hash!(payload, "assets").to_h do |output_path, asset_payload|
    asset_payload = expect_hash!(asset_payload, "asset #{output_path}")
    [
      output_path,
      AssetSpec.new(
        asset_payload.fetch("logical_name"),
        asset_payload.fetch("content_hash"),
        asset_payload.fetch("output_path"),
        asset_payload.fetch("content_type"),
        decode_value(asset_payload.fetch("metadata"))
      )
    ]
  end
end

.decode_import(payload) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
# File 'lib/klenod/runtime/bundle_format.rb', line 139

def decode_import(payload)
  payload = expect_hash!(payload, "import")
  case payload.fetch("type")
  when "import_spec"
    ImportSpec.new(payload.fetch("target_id"), decode_value(payload["value"]), payload.fetch("eager"))
  when "module_id"
    payload.fetch("target_id")
  else
    raise BundleFormatError, "Unknown import type: #{payload["type"].inspect}"
  end
end

.decode_imports(payload) ⇒ Object



118
119
120
# File 'lib/klenod/runtime/bundle_format.rb', line 118

def decode_imports(payload)
  expect_hash!(payload, "imports").to_h { |name, import| [name, decode_import(import)] }
end

.decode_modules(payload) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/klenod/runtime/bundle_format.rb', line 96

def decode_modules(payload)
  expect_hash!(payload, "modules").to_h do |id, spec_payload|
    spec_payload = expect_hash!(spec_payload, "module #{id}")
    [
      id,
      ModuleSpec.new(
        spec_payload.fetch("id"),
        spec_payload.fetch("source_path"),
        spec_payload.fetch("source"),
        decode_imports(spec_payload.fetch("imports")),
        decode_source_map(spec_payload["source_map"], spec_payload.fetch("source")),
        spec_payload.fetch("version"),
        spec_payload.fetch("constant_name")
      )
    ]
  end
end

.decode_source_map(payload, output) ⇒ Object



194
195
196
197
198
199
200
201
202
203
# File 'lib/klenod/runtime/bundle_format.rb', line 194

def decode_source_map(payload, output)
  return nil unless payload

  payload = expect_hash!(payload, "source map")
  marks =
    expect_hash!(payload.fetch("marks_by_output_line"), "source map marks").to_h do |line, source_line|
      [line.to_i, SourceMap::Mark.new(source_line)]
    end
  SourceMap::SourceMap.new(payload.fetch("input"), output, marks.freeze)
end

.decode_value(value) ⇒ Object



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/klenod/runtime/bundle_format.rb', line 228

def decode_value(value)
  case value
  when nil, true, false, String, Integer, Float
    value
  when Array
    value.map { |item| decode_value(item) }
  when Hash
    case value["__klenod_type"]
    when "symbol"
      value.fetch("value").to_sym
    when "hash"
      value.fetch("entries").to_h do |key, hash_value|
        [decode_value(key), decode_value(hash_value)]
      end
    when "default_import"
      DefaultImport.new(value.fetch("name").to_sym)
    else
      value.to_h { |key, hash_value| [key, decode_value(hash_value)] }
    end
  else
    raise BundleFormatError, "Cannot decode bundle value: #{value.class}"
  end
end

.dump(bundle) ⇒ Object



19
20
21
# File 'lib/klenod/runtime/bundle_format.rb', line 19

def dump(bundle)
  MAGIC + JSON.generate(payload_for(bundle))
end

.encode_assets(assets) ⇒ Object



151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/klenod/runtime/bundle_format.rb', line 151

def encode_assets(assets)
  assets.to_h do |output_path, asset|
    [
      output_path.to_s,
      {
        "logical_name" => asset.logical_name,
        "content_hash" => asset.content_hash,
        "output_path" => asset.output_path,
        "content_type" => asset.content_type,
        "metadata" => encode_value(asset.)
      }
    ]
  end
end

.encode_import(import) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/klenod/runtime/bundle_format.rb', line 122

def encode_import(import)
  case import
  when ImportSpec
    {
      "type" => "import_spec",
      "target_id" => import.target_id,
      "value" => encode_value(import.value),
      "eager" => import.eager
    }
  else
    {
      "type" => "module_id",
      "target_id" => import.to_s
    }
  end
end

.encode_imports(imports) ⇒ Object



114
115
116
# File 'lib/klenod/runtime/bundle_format.rb', line 114

def encode_imports(imports)
  imports.to_h { |name, import| [name.to_s, encode_import(import)] }
end

.encode_modules(modules) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/klenod/runtime/bundle_format.rb', line 79

def encode_modules(modules)
  modules.to_h do |id, spec|
    [
      id.to_s,
      {
        "id" => spec.id,
        "source_path" => spec.source_path,
        "source" => spec.source,
        "imports" => encode_imports(spec.imports),
        "source_map" => encode_source_map(spec.source_map),
        "version" => spec.version,
        "constant_name" => spec.constant_name
      }
    ]
  end
end

.encode_source_map(source_map) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
# File 'lib/klenod/runtime/bundle_format.rb', line 182

def encode_source_map(source_map)
  return nil unless source_map

  {
    "input" => source_map.input,
    "marks_by_output_line" =>
      source_map.marks_by_output_line.to_h do |line, mark|
        [line.to_s, mark.line]
      end
  }
end

.encode_value(value) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/klenod/runtime/bundle_format.rb', line 205

def encode_value(value)
  case value
  when nil, true, false, String, Integer, Float
    value
  when Symbol
    {"__klenod_type" => "symbol", "value" => value.to_s}
  when Array
    value.map { |item| encode_value(item) }
  when Hash
    {
      "__klenod_type" => "hash",
      "entries" =>
        value.map do |key, hash_value|
          [encode_value(key), encode_value(hash_value)]
        end
    }
  when DefaultImport
    {"__klenod_type" => "default_import", "name" => value.name.to_s}
  else
    raise BundleFormatError, "Cannot encode bundle value: #{value.class}"
  end
end

.expect_hash!(value, label) ⇒ Object

Raises:



252
253
254
255
256
# File 'lib/klenod/runtime/bundle_format.rb', line 252

def expect_hash!(value, label)
  return value if value.is_a?(Hash)

  raise BundleFormatError, "Malformed Klenod bundle payload: #{label} must be an object"
end

.load(source, source_root: nil) ⇒ Object



23
24
25
# File 'lib/klenod/runtime/bundle_format.rb', line 23

def load(source, source_root: nil)
  load_bytes(read_source(source), source_root: source_root)
end

.load_bytes(bytes, source_root: nil) ⇒ Object



27
28
29
30
31
32
33
34
35
# File 'lib/klenod/runtime/bundle_format.rb', line 27

def load_bytes(bytes, source_root: nil)
  raise BundleFormatError, "Invalid Klenod bundle header" unless bytes.start_with?(MAGIC)

  body = bytes.byteslice(MAGIC.bytesize, bytes.bytesize - MAGIC.bytesize)
  payload = JSON.parse(body)
  bundle_from_payload(payload, source_root: source_root)
rescue JSON::ParserError => error
  raise BundleFormatError, "Invalid Klenod bundle JSON: #{error.message}"
end

.payload_for(bundle) ⇒ Object



37
38
39
40
41
42
43
44
45
46
# File 'lib/klenod/runtime/bundle_format.rb', line 37

def payload_for(bundle)
  {
    "format_version" => FORMAT_VERSION,
    "runtime_version" => Runtime::VERSION,
    "source_root" => encode_value(bundle.source_root),
    "entrypoints" => encode_value(bundle.entrypoints),
    "modules" => encode_modules(bundle.modules),
    "assets" => encode_assets(bundle.assets)
  }
end

.read_source(source) ⇒ Object



62
63
64
# File 'lib/klenod/runtime/bundle_format.rb', line 62

def read_source(source)
  source.respond_to?(:read) ? source.read : File.binread(source)
end

.validate_payload!(payload) ⇒ Object

Raises:



66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/klenod/runtime/bundle_format.rb', line 66

def validate_payload!(payload)
  raise BundleFormatError, "Malformed Klenod bundle payload" unless payload.is_a?(Hash)

  version = payload["format_version"]
  unless version == FORMAT_VERSION
    raise BundleFormatError, "Unsupported Klenod bundle format version: #{version.inspect}"
  end

  %w[runtime_version source_root entrypoints modules assets].each do |key|
    raise BundleFormatError, "Malformed Klenod bundle payload: missing #{key}" unless payload.key?(key)
  end
end