simple-immutable 1.0.3 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d55dfd0cc8370b9bb68d8c1923770bc248d2eb4a2a71fe89717585366e17da78
4
- data.tar.gz: 50baec5315bdd9a6b6b9cfa389fa8cda388047b38f517f6ca7571d5a96de8926
3
+ metadata.gz: 4ecf8356b446062c2cc5eb9237759cb91eb8a91cba3f311b527f2c73aa675bd7
4
+ data.tar.gz: 2f6c6952068ff100208296f0715d7f8b444b34deb673c285c3fe24d34be38bf8
5
5
  SHA512:
6
- metadata.gz: 59c0ce8e1ae74f98e4c8b13cf6d1642182f83440a2eb111b1df853748adc4a21635900011da503f00d02990b2a5895a54fcde1fc66001b2679ed3169a719db47
7
- data.tar.gz: beb2b47f7096c2e3bf98a1b7bbf4c4a50360a992a4ae96a213512ce31ff1e00cf7106351fedc118846d0f9463c5f82ed04d8bad85bc79200106d1d32ea92965f
6
+ metadata.gz: e71dff987fe8ee9853348389d6d1f262bda8355e88ef0dd8847585ce803adeae1de65c2153df6904bf4727fd32f4baddc3cef72bdd6e6b973126927938a07a9e
7
+ data.tar.gz: daf98517ec14374466755c6075a5ee2b009f63cb245f213b39d39c98915ff8597ba6a1ba7dfa961a1ada6785bde8720e907a19fa6dfda8b405ae7e7e893b8885
data/.rubocop.yml CHANGED
@@ -30,3 +30,6 @@ Style/NumericPredicate:
30
30
 
31
31
  Metrics/MethodLength:
32
32
  Max: 20
33
+
34
+ Lint/IneffectiveAccessModifier:
35
+ Enabled: false
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in {gemname}.gemspec
4
4
  gemspec
5
5
 
6
- gem "rubocop", "~> 0.87.1"
6
+ gem "rubocop", "~> 1.20"
data/Makefile CHANGED
@@ -1,9 +1,11 @@
1
- default: test
1
+ default: test rubocop
2
2
 
3
- .PHONY: test
3
+ .PHONY: test rubocop release
4
4
 
5
5
  test:
6
- ruby test/immutable_test.rb
6
+ scripts/test
7
+
8
+ rubocop:
7
9
  bin/rubocop
8
10
 
9
11
  release:
data/README.md CHANGED
@@ -17,11 +17,11 @@ Turns a nested data structure into a immutable ruby object implementing dot and
17
17
  name: "grandchildname"
18
18
  }
19
19
  },
20
- "children": [
20
+ "children" => [
21
21
  "anna",
22
22
  "arthur",
23
23
  {
24
- action: {
24
+ "action" => {
25
25
  keep_your_mouth_shut: true
26
26
  }
27
27
  }
@@ -36,3 +36,14 @@ Turns a nested data structure into a immutable ruby object implementing dot and
36
36
  imm.children.foo # NoMethodError (undefined method `foo' for #<Array:0x00007fb90f1be390>)
37
37
  imm.children.first.foo # NoMethodError (undefined method `foo' for "anna":String)
38
38
 
39
+ If an attribute is not defined in the immutable (or in any sub-immutable) we raise a NameError
40
+
41
+ imm.unknown # NameError (unknown immutable attribute 'unknown')
42
+
43
+ One can check for an attribute by appending a "?". If the attribute is not defined this returns +nil+ instead of raising an error.
44
+
45
+ imm.a? # 'a-value'
46
+ imm.unknown? # nil
47
+ imm.foo?.bar? # also nil
48
+
49
+ When building an immutable one can provide a "fallback" hash in addition to the returned hash. If a key does not exist in the passed in data it will be fetched from the "fallback" data. This is rarely useful, but can provide effectove optimization in some cases.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.3
1
+ 1.1.0
@@ -6,14 +6,17 @@ class Simple::Immutable
6
6
 
7
7
  # turns an object, which can be a hash or array of hashes, arrays, and scalars
8
8
  # into an object which you can use to access with dot methods.
9
- def self.create(object, max_depth = 8)
9
+ def self.create(object, max_depth: 8, null_record: nil, fallback: nil)
10
+ # Note that null_record is deprecated.
11
+ fallback ||= null_record
12
+
10
13
  case object
11
14
  when Array
12
15
  raise ArgumentError, "Object nested too deep (or inner loop?)" if max_depth < 0
13
16
 
14
- object.map { |obj| create obj, max_depth - 1 }
17
+ object.map { |obj| create obj, fallback: fallback, max_depth: max_depth - 1 }
15
18
  when Hash
16
- new(object)
19
+ new(object, fallback)
17
20
  else
18
21
  object
19
22
  end
@@ -22,7 +25,14 @@ class Simple::Immutable
22
25
  def self.raw_data(immutable)
23
26
  case immutable
24
27
  when SELF
25
- immutable.instance_variable_get :@hsh
28
+ hsh = immutable.instance_variable_get :@hsh
29
+ fallback = immutable.instance_variable_get :@fallback
30
+
31
+ if fallback
32
+ fallback.merge(hsh)
33
+ else
34
+ hsh
35
+ end
26
36
  when Array
27
37
  immutable.map { |e| raw_data(e) }
28
38
  else
@@ -30,39 +40,88 @@ class Simple::Immutable
30
40
  end
31
41
  end
32
42
 
43
+ # adds to_json support.
44
+ def as_json(opts)
45
+ data = SELF.raw_data(self)
46
+ data.as_json(opts)
47
+ end
48
+
33
49
  private
34
50
 
35
- def initialize(hsh)
51
+ def initialize(hsh, fallback = nil)
36
52
  @hsh = hsh
53
+ @fallback = fallback
37
54
  end
38
55
 
39
- def method_missing(sym, *args, &block)
40
- if args.empty? && !block
41
- begin
42
- value = @hsh.fetch(sym.to_sym) { @hsh.fetch(sym.to_s) }
43
- return SELF.create(value)
44
- rescue KeyError
45
- # STDERR.puts "Missing attribute #{sym} for Immutable w/#{@hsh.inspect}"
46
- nil
56
+ # rubycop:disable Lint/IneffectiveAccessModifier
57
+
58
+ # fetches key from a hsh, regardless of it being a Symbol or a String.
59
+ # Yields the block if the key cannot be found.
60
+ def self.fetch_symbol_or_string_from_hash(hsh, key, &block)
61
+ if hsh
62
+ hsh.fetch(key.to_sym) do
63
+ hsh.fetch(key.to_s, &block)
47
64
  end
65
+ else
66
+ yield
48
67
  end
68
+ end
49
69
 
50
- super
70
+ def method_missing(sym, *args, &block)
71
+ raise ArgumentError, "Immutable: called attribute method with arguments" unless args.empty?
72
+ raise ArgumentError, "Immutable: called attribute method with arguments" if block
73
+
74
+ if sym.end_with?("?")
75
+ # If the symbol ends in "?" we do not raise a NameError, but instead
76
+ # returns nil if the attribute is not known.
77
+ sym = sym[0...-1]
78
+
79
+ # Note that sym is now a String. However, we are String/Symbol agnostic
80
+ # (in fetch_symbol_or_string_from_hash), so this is ok.
81
+ fetch_attribute!(sym, raise_when_missing: false)
82
+ else
83
+ fetch_attribute!(sym, raise_when_missing: true)
84
+ end
51
85
  end
52
86
 
53
87
  public
54
88
 
89
+ def fetch_attribute!(sym, raise_when_missing:)
90
+ value = SELF.fetch_symbol_or_string_from_hash(@hsh, sym) do
91
+ SELF.fetch_symbol_or_string_from_hash(@fallback, sym) do
92
+ raise NameError, "unknown immutable attribute '#{sym}'" if raise_when_missing
93
+
94
+ nil
95
+ end
96
+ end
97
+
98
+ SELF.create(value)
99
+ end
100
+
55
101
  def inspect
56
- "<#{self.class.name}: #{@hsh.inspect}>"
102
+ if @fallback
103
+ "<#{self.class.name}: #{@hsh.inspect}, w/fallback: #{@fallback.inspect}>"
104
+ else
105
+ "<#{self.class.name}: #{@hsh.inspect}>"
106
+ end
57
107
  end
58
108
 
59
109
  def respond_to_missing?(method_name, include_private = false)
60
- @hsh.key?(method_name.to_sym) ||
61
- @hsh.key?(method_name.to_s) ||
62
- super
110
+ return true if method_name.end_with?("?")
111
+
112
+ key = method_name.to_sym
113
+ return true if @hsh.key?(key)
114
+ return true if @fallback&.key?(key)
115
+
116
+ key = method_name.to_s
117
+ return true if @hsh.key?(key)
118
+ return true if @fallback&.key?(key)
119
+
120
+ super
63
121
  end
64
122
 
65
- def respond_to?(sym)
123
+ # rubocop:disable Style/OptionalBooleanParameter
124
+ def respond_to?(sym, include_all = false)
66
125
  super || @hsh.key?(sym.to_s) || @hsh.key?(sym.to_sym)
67
126
  end
68
127
 
data/scripts/test ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ Dir.glob("test/**/*_test.rb").sort.each do |file|
3
+ load file
4
+ end
@@ -16,11 +16,11 @@ class Simple::Immutable::TestCase < Test::Unit::TestCase
16
16
  name: "grandchildname"
17
17
  }
18
18
  },
19
- "children": [
19
+ "children" => [
20
20
  "anna",
21
21
  "arthur",
22
22
  {
23
- action: {
23
+ "action" => {
24
24
  keep_your_mouth_shut: true
25
25
  }
26
26
  }
@@ -60,6 +60,22 @@ class Simple::Immutable::TestCase < Test::Unit::TestCase
60
60
  assert_equal true, immutable.children[2].action.keep_your_mouth_shut
61
61
  end
62
62
 
63
+ def test_child_access_w_question_mark
64
+ child = immutable.child?
65
+ assert_kind_of(Immutable, child)
66
+ assert_equal "childname", immutable.child.name?
67
+ assert_equal "grandchildname", immutable.child?.grandchild?.name?
68
+ end
69
+
70
+ def test_array_access_w_question_mark
71
+ assert_kind_of(Array, immutable.children?)
72
+ assert_equal 3, immutable.children?.length
73
+ assert_equal "anna", immutable.children?[0]
74
+
75
+ assert_kind_of(Immutable, immutable.children?[2])
76
+ assert_equal true, immutable.children[2].action?.keep_your_mouth_shut?
77
+ end
78
+
63
79
  def test_base_class
64
80
  assert_nothing_raised do
65
81
  immutable.object_id
@@ -67,28 +83,30 @@ class Simple::Immutable::TestCase < Test::Unit::TestCase
67
83
  end
68
84
 
69
85
  def test_missing_keys
70
- assert_raise(NoMethodError) do
86
+ assert_raise(NameError) do
71
87
  immutable.foo
72
88
  end
73
89
  end
74
90
 
75
91
  def test_skip_when_args_or_block
76
- # raise NoMethodError when called with arguments
77
- assert_raise(NoMethodError) do
92
+ # raise ArgumentError when called with arguments
93
+ assert_raise(ArgumentError) do
78
94
  immutable.a(1, 2, 3)
79
95
  end
96
+ assert_raise(ArgumentError) do
97
+ immutable.a(one: 1)
98
+ end
80
99
 
81
- # raise NoMethodError when called with a block argument
82
- assert_raise(NoMethodError) do
100
+ # raise ArgumentError when called with a block argument
101
+ assert_raise(ArgumentError) do
83
102
  immutable.a { :dummy }
84
103
  end
85
104
  end
86
105
 
87
106
  def test_raw_data
88
- immutable = Immutable.create(hsh)
89
107
  expected = hsh
90
108
 
91
109
  assert_equal(expected, Immutable.raw_data(immutable))
92
- assert_equal(expected[:children], Immutable.raw_data(immutable.children))
110
+ assert_equal(expected["children"], Immutable.raw_data(immutable.children))
93
111
  end
94
112
  end
@@ -0,0 +1,63 @@
1
+ # rubocop:disable Metrics/AbcSize
2
+
3
+ require "test-unit"
4
+ require_relative "../lib/simple-immutable"
5
+
6
+ class Simple::Immutable::WithFallbackTestCase < Test::Unit::TestCase
7
+ Immutable = ::Simple::Immutable
8
+
9
+ def hsh
10
+ {
11
+ a: "a-value",
12
+ "b": "b-value",
13
+ "child": {
14
+ name: "childname",
15
+ grandchild: {
16
+ name: "grandchildname"
17
+ }
18
+ },
19
+ "children" => [
20
+ "anna",
21
+ "arthur",
22
+ {
23
+ "action" => {
24
+ keep_your_mouth_shut: true
25
+ }
26
+ }
27
+ ]
28
+ }
29
+ end
30
+
31
+ def fallback
32
+ {
33
+ foo: "foo-value",
34
+ "bar" => "bar-value"
35
+ }
36
+ end
37
+
38
+ def immutable
39
+ Immutable.create hsh, fallback: fallback
40
+ end
41
+
42
+ def test_hash_access
43
+ assert_equal "a-value", immutable.a
44
+ assert_equal "b-value", immutable.b
45
+ end
46
+
47
+ def test_fallback_access
48
+ assert_equal "foo-value", immutable.foo
49
+ assert_equal "bar-value", immutable.bar
50
+ end
51
+
52
+ def test_missing_keys
53
+ assert_raise(NameError) do
54
+ immutable.unknown
55
+ end
56
+ end
57
+
58
+ def test_raw_data
59
+ # raw data does contain fallback
60
+ expected = hsh.merge(fallback)
61
+ assert_equal(expected, Immutable.raw_data(immutable))
62
+ end
63
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple-immutable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - radiospiel
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-07-08 00:00:00.000000000 Z
12
+ date: 2021-10-06 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Immutable ruby objects implementing dot and [] accessors.
15
15
  email: eno@radiospiel.org
@@ -31,8 +31,10 @@ files:
31
31
  - lib/simple/immutable.rb
32
32
  - scripts/release
33
33
  - scripts/release.rb
34
+ - scripts/test
34
35
  - simple-immutable.gemspec
35
36
  - test/immutable_test.rb
37
+ - test/immutable_w_fallback_test.rb
36
38
  homepage: http://github.com/radiospiel/simple-immutable
37
39
  licenses: []
38
40
  metadata: {}
@@ -51,9 +53,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
53
  - !ruby/object:Gem::Version
52
54
  version: '0'
53
55
  requirements: []
54
- rubygems_version: 3.0.6
56
+ rubygems_version: 3.1.4
55
57
  signing_key:
56
58
  specification_version: 4
57
59
  summary: Immutable ruby objects
58
60
  test_files:
59
61
  - test/immutable_test.rb
62
+ - test/immutable_w_fallback_test.rb