safe_object_as_json 1.0.0 → 1.0.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +6 -3
- data/VERSION +1 -1
- data/lib/safe_object_as_json.rb +75 -35
- data/safe_object_as_json.gemspec +9 -6
- metadata +9 -24
- /data/{MIT-LICENSE → MIT-LICENSE.txt} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48f61927e96d8c9e566b3563b4691589f99bab2c18a4ebe0ce4cd0377f123bac
|
|
4
|
+
data.tar.gz: bed00faa7540d1d80ca66c1cb9a742199c91f81cbeeb461826021195295bf11a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca75249f62a5c2a817efd9abeee452b404d47e66e8a14ea59ad5118743b4506a9c8482007f2d1f6a2cf9e789893de9202e272cd50a7d1caea800609e4e6c6715
|
|
7
|
+
data.tar.gz: 629028c82c38d5de8d248dd3bda12f5a808dbb4eed33e915c4b6fbda391890fe84fc0cf205017a98cd4607c92e8e148c2ade89743a47a5898bf5addc3180aefc
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 1.0.1
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Fixed reference tracking so that the internal thread local state is always cleaned up after the outermost call to `Object#as_json`. Previously serialized values were retained in a thread local variable forever, leaking memory and potentially omitting values from unrelated `as_json` calls on the same thread.
|
|
11
|
+
- Circular references through objects that implement `to_hash` no longer raise `SystemStackError`; the circular reference is now serialized as `nil`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Only require the parts of ActiveSupport that are actually used instead of `active_support/all`.
|
|
15
|
+
|
|
7
16
|
## 1.0.0
|
|
8
17
|
|
|
9
18
|
### Added
|
data/README.md
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
# Safe Object As JSON
|
|
2
|
+
|
|
1
3
|
[](https://github.com/bdurand/safe_object_as_json/actions/workflows/continuous_integration.yml)
|
|
2
4
|
[](https://github.com/testdouble/standard)
|
|
5
|
+
[](https://badge.fury.io/rb/safe_object_as_json)
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
This gem provides an enhancement to the implementation for `as_json` on the core `Object` class in the ActiveSiupport library. The implementation provided by ActiveSupport dumps the instance variables as name value pairs in a Hash. However, this is susceptible to infinite recursion when dumping an object that maintains references to other objects that then maintain back references to the original object.
|
|
7
|
+
This gem provides an enhancement to the implementation for `as_json` on the core `Object` class in the ActiveSupport library. The implementation provided by ActiveSupport dumps the instance variables as name value pairs in a Hash. However, this is susceptible to infinite recursion when dumping an object that maintains references to other objects that then maintain back references to the original object.
|
|
7
8
|
|
|
8
9
|
The fix provided by this gem maintains a state of the current stack of objects being used to construct the `as_json` hash. If an object has already been referenced in the current call to `Object#as_json`, it is left out of the hash in lieu of having it raise a stack level too deep error.
|
|
9
10
|
|
|
10
11
|
It also omits any `Proc` or `IO` references since these are inherently not serializable.
|
|
11
12
|
|
|
13
|
+
Objects that implement `to_hash` are serialized from that hash just like in ActiveSupport. If such an object is circularly referenced from within its own hash representation, the circular reference is serialized as `null`.
|
|
14
|
+
|
|
12
15
|
## Usage
|
|
13
16
|
|
|
14
17
|
No changes are needed to use this gem. It will just replace the method definition of `Object#as_json`. It will not impact any class that defines its own `as_json` or `to_hash` method which includes all the core Ruby classes (String, Numeric, Array, Hash) as well as ActiveModel classes.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.1
|
data/lib/safe_object_as_json.rb
CHANGED
|
@@ -1,55 +1,95 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "set"
|
|
4
|
+
require "active_support"
|
|
5
|
+
require "active_support/core_ext/object/json"
|
|
6
|
+
require "active_support/core_ext/object/instance_variables"
|
|
7
|
+
require "active_support/core_ext/hash/except"
|
|
4
8
|
|
|
5
9
|
module SafeObjectAsJson
|
|
6
10
|
# ActiveSupport 7 is adding support for filtering hash values with :only and :except options.
|
|
7
11
|
SUPPORT_FILTERING = (ActiveSupport.version.canonical_segments.first > 6)
|
|
8
12
|
|
|
9
13
|
VERSION = File.read(File.expand_path("../VERSION", __dir__)).chomp
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Yield the set of object ids currently being serialized on this thread. The set is
|
|
17
|
+
# created on the outermost call and always removed when that call finishes so that
|
|
18
|
+
# no state leaks between top level calls to Object#as_json.
|
|
19
|
+
def track_references
|
|
20
|
+
references = Thread.current[:object_as_json_references]
|
|
21
|
+
if references
|
|
22
|
+
yield references
|
|
23
|
+
else
|
|
24
|
+
references = Set.new
|
|
25
|
+
Thread.current[:object_as_json_references] = references
|
|
26
|
+
begin
|
|
27
|
+
yield references
|
|
28
|
+
ensure
|
|
29
|
+
Thread.current[:object_as_json_references] = nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
10
34
|
end
|
|
11
35
|
|
|
12
36
|
class Object
|
|
13
|
-
# Converts any object to JSON by creating a hash out of
|
|
14
|
-
# If there is a circular reference within an object hierarchy, then duplicate
|
|
15
|
-
#
|
|
37
|
+
# Converts any object to JSON by creating a hash out of its instance variables.
|
|
38
|
+
# If there is a circular reference within an object hierarchy, then the duplicate
|
|
39
|
+
# reference will be omitted in order to avoid infinite recursion.
|
|
40
|
+
#
|
|
41
|
+
# @param [Hash] options
|
|
42
|
+
# @return [Object]
|
|
16
43
|
def as_json(options = nil)
|
|
17
|
-
|
|
18
|
-
to_hash
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# Apply :only and :except filter from the options to the hash of instance variables.
|
|
30
|
-
values = instance_values
|
|
31
|
-
if options && SafeObjectAsJson::SUPPORT_FILTERING
|
|
32
|
-
only_attr = options[:only]
|
|
33
|
-
if only_attr
|
|
34
|
-
values = values.slice(*Array(only_attr).map(&:to_s))
|
|
35
|
-
else
|
|
36
|
-
except_attr = options[:except]
|
|
37
|
-
if except_attr
|
|
38
|
-
values = values.except(*Array(except_attr).map(&:to_s))
|
|
39
|
-
end
|
|
44
|
+
SafeObjectAsJson.track_references do |references|
|
|
45
|
+
if respond_to?(:to_hash)
|
|
46
|
+
# Register this object while its hash representation is being serialized so a
|
|
47
|
+
# circular reference back to it serializes as nil instead of recursing forever.
|
|
48
|
+
# A distinct key is used so that the registration of this object's id by a
|
|
49
|
+
# caller serializing it as a value is not mistaken for a circular reference.
|
|
50
|
+
if references.add?([:to_hash, object_id])
|
|
51
|
+
begin
|
|
52
|
+
to_hash.as_json(options)
|
|
53
|
+
ensure
|
|
54
|
+
references.delete([:to_hash, object_id])
|
|
40
55
|
end
|
|
41
56
|
end
|
|
57
|
+
else
|
|
58
|
+
# The default as_json serializer serializes the instance variables as name value
|
|
59
|
+
# pairs. In order to prevent infinite recursion, we keep track of the objects
|
|
60
|
+
# already being serialized and omit them if they are referenced recursively.
|
|
61
|
+
added = references.add?(object_id)
|
|
62
|
+
begin
|
|
63
|
+
hash = {}
|
|
42
64
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
# Apply :only and :except filter from the options to the hash of instance variables.
|
|
66
|
+
values = instance_values
|
|
67
|
+
if options && SafeObjectAsJson::SUPPORT_FILTERING
|
|
68
|
+
only_attr = options[:only]
|
|
69
|
+
if only_attr
|
|
70
|
+
values = values.slice(*Array(only_attr).map(&:to_s))
|
|
71
|
+
else
|
|
72
|
+
except_attr = options[:except]
|
|
73
|
+
if except_attr
|
|
74
|
+
values = values.except(*Array(except_attr).map(&:to_s))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
values.each do |name, value|
|
|
80
|
+
next if references.include?(value.object_id) || value.is_a?(Proc) || value.is_a?(IO)
|
|
81
|
+
|
|
82
|
+
references << value.object_id
|
|
83
|
+
begin
|
|
84
|
+
hash[name] = (options.nil? ? value.as_json : value.as_json(options.dup))
|
|
85
|
+
ensure
|
|
86
|
+
references.delete(value.object_id)
|
|
87
|
+
end
|
|
47
88
|
end
|
|
89
|
+
hash
|
|
90
|
+
ensure
|
|
91
|
+
references.delete(object_id) if added
|
|
48
92
|
end
|
|
49
|
-
hash
|
|
50
|
-
ensure
|
|
51
|
-
references.delete(object_id)
|
|
52
|
-
Thread.current[:object_as_json_references] = nil if references.empty?
|
|
53
93
|
end
|
|
54
94
|
end
|
|
55
95
|
end
|
data/safe_object_as_json.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Gem::Specification.new do |spec|
|
|
2
2
|
spec.name = "safe_object_as_json"
|
|
3
|
-
spec.version = File.read(File.expand_path("
|
|
3
|
+
spec.version = File.read(File.expand_path("VERSION", __dir__)).strip
|
|
4
4
|
spec.authors = ["Brian Durand"]
|
|
5
5
|
spec.email = ["bbdurand@gmail.com"]
|
|
6
6
|
|
|
@@ -8,6 +8,12 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.homepage = "https://github.com/bdurand/safe_object_as_json"
|
|
9
9
|
spec.license = "MIT"
|
|
10
10
|
|
|
11
|
+
spec.metadata = {
|
|
12
|
+
"homepage_uri" => spec.homepage,
|
|
13
|
+
"source_code_uri" => spec.homepage,
|
|
14
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
# Specify which files should be added to the gem when it is released.
|
|
12
18
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
13
19
|
ignore_files = %w[
|
|
@@ -19,9 +25,8 @@ Gem::Specification.new do |spec|
|
|
|
19
25
|
bin/
|
|
20
26
|
gemfiles/
|
|
21
27
|
spec/
|
|
22
|
-
web_ui.png
|
|
23
28
|
]
|
|
24
|
-
spec.files = Dir.chdir(
|
|
29
|
+
spec.files = Dir.chdir(__dir__) do
|
|
25
30
|
`git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
|
|
26
31
|
end
|
|
27
32
|
|
|
@@ -29,7 +34,5 @@ Gem::Specification.new do |spec|
|
|
|
29
34
|
|
|
30
35
|
spec.add_dependency "activesupport"
|
|
31
36
|
|
|
32
|
-
spec.
|
|
33
|
-
|
|
34
|
-
spec.required_ruby_version = ">= 2.5"
|
|
37
|
+
spec.required_ruby_version = ">= 2.6"
|
|
35
38
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: safe_object_as_json
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activesupport
|
|
@@ -24,21 +23,6 @@ dependencies:
|
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
25
|
version: '0'
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: bundler
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - ">="
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0'
|
|
34
|
-
type: :development
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - ">="
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
41
|
-
description:
|
|
42
26
|
email:
|
|
43
27
|
- bbdurand@gmail.com
|
|
44
28
|
executables: []
|
|
@@ -46,7 +30,7 @@ extensions: []
|
|
|
46
30
|
extra_rdoc_files: []
|
|
47
31
|
files:
|
|
48
32
|
- CHANGELOG.md
|
|
49
|
-
- MIT-LICENSE
|
|
33
|
+
- MIT-LICENSE.txt
|
|
50
34
|
- README.md
|
|
51
35
|
- VERSION
|
|
52
36
|
- lib/safe_object_as_json.rb
|
|
@@ -54,8 +38,10 @@ files:
|
|
|
54
38
|
homepage: https://github.com/bdurand/safe_object_as_json
|
|
55
39
|
licenses:
|
|
56
40
|
- MIT
|
|
57
|
-
metadata:
|
|
58
|
-
|
|
41
|
+
metadata:
|
|
42
|
+
homepage_uri: https://github.com/bdurand/safe_object_as_json
|
|
43
|
+
source_code_uri: https://github.com/bdurand/safe_object_as_json
|
|
44
|
+
changelog_uri: https://github.com/bdurand/safe_object_as_json/blob/main/CHANGELOG.md
|
|
59
45
|
rdoc_options: []
|
|
60
46
|
require_paths:
|
|
61
47
|
- lib
|
|
@@ -63,15 +49,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
63
49
|
requirements:
|
|
64
50
|
- - ">="
|
|
65
51
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: '2.
|
|
52
|
+
version: '2.6'
|
|
67
53
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
54
|
requirements:
|
|
69
55
|
- - ">="
|
|
70
56
|
- !ruby/object:Gem::Version
|
|
71
57
|
version: '0'
|
|
72
58
|
requirements: []
|
|
73
|
-
rubygems_version:
|
|
74
|
-
signing_key:
|
|
59
|
+
rubygems_version: 4.0.3
|
|
75
60
|
specification_version: 4
|
|
76
61
|
summary: Drop in replacement for the Object#as_json implementation in ActiveSupport,
|
|
77
62
|
but with logic to handle circular references between objects to avoid infinite recursion.
|
|
File without changes
|