marshal-md 0.1.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.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MarshalMd
4
+ class ObjectRegistry
5
+ def initialize
6
+ @id_to_anchor = {} # object_id => "obj_1"
7
+ @anchor_to_obj = {} # "obj_1" => object
8
+ @counter = 0
9
+ end
10
+
11
+ # Dump mode: register an object, return its anchor name
12
+ def register(obj)
13
+ oid = obj.object_id
14
+ return @id_to_anchor[oid] if @id_to_anchor.key?(oid)
15
+
16
+ @counter += 1
17
+ anchor = "obj_#{@counter}"
18
+ @id_to_anchor[oid] = anchor
19
+ anchor
20
+ end
21
+
22
+ def anchor_for(obj)
23
+ @id_to_anchor[obj.object_id]
24
+ end
25
+
26
+ def registered?(obj)
27
+ @id_to_anchor.key?(obj.object_id)
28
+ end
29
+
30
+ # Load mode: store anchor -> object mapping
31
+ def store(anchor, obj)
32
+ @anchor_to_obj[anchor] = obj
33
+ end
34
+
35
+ def resolve(anchor)
36
+ unless @anchor_to_obj.key?(anchor)
37
+ raise ArgumentError, "undefined reference: *#{anchor}"
38
+ end
39
+
40
+ @anchor_to_obj[anchor]
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../marshal-md"
4
+
5
+ module MarshalMd
6
+ module Patch
7
+ BINARY_MARSHAL_HEADER = "\x04\x08".b.freeze
8
+
9
+ def self.apply!
10
+ ::Marshal.class_eval do
11
+ class << self
12
+ alias_method :binary_dump, :dump
13
+ alias_method :binary_load, :load
14
+
15
+ def dump(obj, *args)
16
+ if args.first.respond_to?(:write)
17
+ io = args.first
18
+ md = MarshalMd.dump(obj)
19
+ io.write(md)
20
+ io
21
+ else
22
+ MarshalMd.dump(obj)
23
+ end
24
+ end
25
+
26
+ def load(source, *args)
27
+ data = source.respond_to?(:read) ? source.read : source
28
+ if data.b[0, 2] == MarshalMd::Patch::BINARY_MARSHAL_HEADER
29
+ binary_load(data, *args)
30
+ else
31
+ MarshalMd.load(data)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ MarshalMd::Patch.apply!
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MarshalMd
4
+ VERSION = "0.1.0"
5
+ end
data/lib/marshal-md.rb ADDED
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "marshal-md/version"
4
+ require_relative "marshal-md/object_registry"
5
+ require_relative "marshal-md/dumper"
6
+ require_relative "marshal-md/loader"
7
+
8
+ module MarshalMd
9
+ # Match Marshal.dump signature: dump(obj [, io] [, limit])
10
+ def self.dump(obj, *args)
11
+ io = nil
12
+ limit = -1
13
+
14
+ args.each do |arg|
15
+ if arg.respond_to?(:write)
16
+ io = arg
17
+ elsif arg.is_a?(Integer)
18
+ limit = arg
19
+ end
20
+ end
21
+
22
+ md = Dumper.new(obj, limit: limit).dump
23
+ if io
24
+ io.write(md)
25
+ io
26
+ else
27
+ md
28
+ end
29
+ end
30
+
31
+ # Match Marshal.load signature: load(source [, proc] [, freeze: false])
32
+ def self.load(source, proc = nil, freeze: false)
33
+ md = source.respond_to?(:read) ? source.read : source
34
+ obj = Loader.new(md).load
35
+ obj = apply_proc_recursive(obj, proc) if proc
36
+ deep_freeze(obj) if freeze
37
+ obj
38
+ end
39
+
40
+ class << self
41
+ private
42
+
43
+ # Apply proc to all deserialized objects, bottom-up.
44
+ # The proc return value replaces the original in parent containers.
45
+ # Uses an identity map so shared references get the same replacement.
46
+ def apply_proc_recursive(obj, proc, visited = {}.compare_by_identity)
47
+ begin
48
+ return visited[obj] if visited.key?(obj)
49
+ rescue TypeError
50
+ # Immediate values
51
+ end
52
+
53
+ # Process children first (bottom-up)
54
+ case obj
55
+ when Array
56
+ obj.each_with_index { |el, i| obj[i] = apply_proc_recursive(el, proc, visited) }
57
+ when Hash
58
+ obj.each { |k, v| obj[k] = apply_proc_recursive(v, proc, visited) }
59
+ end
60
+
61
+ result = proc.call(obj)
62
+
63
+ begin
64
+ visited[obj] = result
65
+ rescue TypeError
66
+ # Immediate values
67
+ end
68
+
69
+ result
70
+ end
71
+
72
+ def deep_freeze(obj, visited = {}.compare_by_identity)
73
+ return if obj.frozen?
74
+
75
+ begin
76
+ return if visited.key?(obj)
77
+ visited[obj] = true
78
+ rescue TypeError
79
+ # Immediate values
80
+ end
81
+
82
+ case obj
83
+ when Array
84
+ obj.each { |el| deep_freeze(el, visited) }
85
+ when Hash
86
+ obj.each { |k, v| deep_freeze(k, visited); deep_freeze(v, visited) }
87
+ when String
88
+ # freeze string
89
+ else
90
+ obj.instance_variables.each do |iv|
91
+ deep_freeze(obj.instance_variable_get(iv), visited)
92
+ end
93
+ end
94
+
95
+ obj.freeze
96
+ end
97
+ end
98
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: marshal-md
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - twokidsCarl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ description: Serialize Ruby objects to readable Markdown format. API-compatible with
28
+ Ruby's built-in Marshal. Passes the CRuby official Marshal test suite.
29
+ email:
30
+ - carl@anz.io
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - LICENSE
36
+ - README.md
37
+ - lib/marshal-md.rb
38
+ - lib/marshal-md/dumper.rb
39
+ - lib/marshal-md/loader.rb
40
+ - lib/marshal-md/object_registry.rb
41
+ - lib/marshal-md/patch.rb
42
+ - lib/marshal-md/version.rb
43
+ homepage: https://twokidscarl.github.io/marshal-md
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ homepage_uri: https://twokidscarl.github.io/marshal-md
48
+ source_code_uri: https://github.com/twokidsCarl/marshal-md
49
+ changelog_uri: https://github.com/twokidsCarl/marshal-md/releases
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 2.5.0
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.5.22
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: Human-readable Ruby Marshal alternative using Markdown
69
+ test_files: []