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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5cfc9f52cea679fdbd39e375fd930f28c9f87f09a636fab4765053c0a5bec84
4
- data.tar.gz: 02fd5931b21822ea69643ef8ed47069d055b38a3d15a39b41b70b81829c046bf
3
+ metadata.gz: 48f61927e96d8c9e566b3563b4691589f99bab2c18a4ebe0ce4cd0377f123bac
4
+ data.tar.gz: bed00faa7540d1d80ca66c1cb9a742199c91f81cbeeb461826021195295bf11a
5
5
  SHA512:
6
- metadata.gz: 63e3e7ec587ac6fa99438bd7419b886ea4d0bb2eb8d74763d3de8c60200f8a6c701bb3e9e4e0b2074aee8c36cb8717346602f792adc0d616b26808eaffa8a4de
7
- data.tar.gz: e856a2199351f45be79f0abab0a0eac31af2424b9e6fe9370e215617ba358ce3199558c5e35ffa0813d45a345ed9a24081e91829e5ba67062914db088d226357
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
  [![Continuous Integration](https://github.com/bdurand/safe_object_as_json/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/safe_object_as_json/actions/workflows/continuous_integration.yml)
2
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+ [![Gem Version](https://badge.fury.io/rb/safe_object_as_json.svg)](https://badge.fury.io/rb/safe_object_as_json)
3
6
 
4
- # Safe Object As JSON
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.0
1
+ 1.0.1
@@ -1,55 +1,95 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/all"
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 it's instance variables.
14
- # If there is a circular reference within an object hierarchy, then duplicate
15
- # objects will be omitted in order to avoid infinite recursion.
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
- if respond_to?(:to_hash)
18
- to_hash.as_json(options)
19
- else
20
- # The default as_json serializer serializes the instance variables as name value pairs.
21
- # In order to prevent infinite recursion, we keep track of the object already used
22
- # in the serialization and omit them if they are included recursively.
23
- references = (Thread.current[:object_as_json_references] || Set.new)
24
- begin
25
- Thread.current[:object_as_json_references] = references if references.empty?
26
- references << object_id
27
- hash = {}
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
- values.each do |name, value|
44
- unless references.include?(value.object_id) || value.is_a?(Proc) || value.is_a?(IO)
45
- references << value
46
- hash[name] = (options.nil? ? value.as_json : value.as_json(options.dup))
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
@@ -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("../VERSION", __FILE__)).strip
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(File.expand_path("..", __FILE__)) do
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.add_development_dependency "bundler"
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.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: 2021-07-29 00:00:00.000000000 Z
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
- post_install_message:
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.5'
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: 3.0.3
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