simple-immutable 1.0.5 → 1.1.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: 573eb118f7b46e8ecf7b89a0932cc1eba443a06d43572b2364c86a4d9ea1138d
4
- data.tar.gz: 535879ee9c6697d0d106c809c56f92b84adb0e431bd5329acaf6fb33669cdb49
3
+ metadata.gz: 5816a5f9400fa98154de17883d4198c56e2355fcad70063c535ee2574d9cb47c
4
+ data.tar.gz: 975f96b46d5a8ff3d4f918e49b14554eb9146b6bc5c3485918cf471a9f78a6ff
5
5
  SHA512:
6
- metadata.gz: '0475929a5809411bb352e8d1e7e87edbdf9301b6a6da44267a06993b60eed5939a46dacddf27d2eb5391e5bb707cba160bb9096116e4232bea584c7f2f373453'
7
- data.tar.gz: b920bc63f731e455d53c4ff93fdcf8f9ca8dd6254505bd42340749bbdfda6b77a2bc61cd7a51aa351b2c0a8907064e17e954eb07cbecf65424946ac3f737249d
6
+ metadata.gz: 469829ef61658e63f55b4a6c3e776ab963253af632be7656c4c79ac1a6ecd28803b99fd8851565eac840751194bfb7a7b7a407045b5925927fd0bb035fc00ea3
7
+ data.tar.gz: 6e27e82d85e7069a38a70474d0f3fc7172efe0b9f18a94e3ae1f186e059abeeb72ae61f8e5be64f91f6c8830bb5058eb4e5992bba6379055e33c699018e4bb16
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.5
1
+ 1.1.1
@@ -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
@@ -32,41 +42,85 @@ class Simple::Immutable
32
42
 
33
43
  # adds to_json support.
34
44
  def as_json(opts)
35
- @hsh.as_json(opts)
45
+ data = SELF.raw_data(self)
46
+ data.as_json(opts)
36
47
  end
37
48
 
38
49
  private
39
50
 
40
- def initialize(hsh)
51
+ def initialize(hsh, fallback = nil)
41
52
  @hsh = hsh
53
+ @fallback = fallback
42
54
  end
43
55
 
44
- def method_missing(sym, *args, &block)
45
- if args.empty? && !block
46
- begin
47
- value = @hsh.fetch(sym.to_sym) { @hsh.fetch(sym.to_s) }
48
- return SELF.create(value)
49
- rescue KeyError
50
- # STDERR.puts "Missing attribute #{sym} for Immutable w/#{@hsh.inspect}"
51
- 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)
52
64
  end
65
+ else
66
+ yield
53
67
  end
68
+ end
54
69
 
55
- 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
56
85
  end
57
86
 
58
87
  public
59
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
+
60
101
  def inspect
61
- "<#{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
62
107
  end
63
108
 
64
109
  def respond_to_missing?(method_name, include_private = false)
65
- @hsh.key?(method_name.to_sym) ||
66
- @hsh.key?(method_name.to_s) ||
67
- 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
68
121
  end
69
122
 
123
+ # rubocop:disable Style/OptionalBooleanParameter
70
124
  def respond_to?(sym, include_all = false)
71
125
  super || @hsh.key?(sym.to_s) || @hsh.key?(sym.to_sym)
72
126
  end
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
@@ -21,5 +21,5 @@ Gem::Specification.new do |gem|
21
21
  # executables are used for development purposes only
22
22
  gem.executables = []
23
23
 
24
- gem.required_ruby_version = '~> 2.3'
24
+ gem.required_ruby_version = '>= 2.5'
25
25
  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.5
4
+ version: 1.1.1
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-23 00:00:00.000000000 Z
12
+ date: 2022-07-24 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: {}
@@ -42,18 +44,19 @@ require_paths:
42
44
  - lib
43
45
  required_ruby_version: !ruby/object:Gem::Requirement
44
46
  requirements:
45
- - - "~>"
47
+ - - ">="
46
48
  - !ruby/object:Gem::Version
47
- version: '2.3'
49
+ version: '2.5'
48
50
  required_rubygems_version: !ruby/object:Gem::Requirement
49
51
  requirements:
50
52
  - - ">="
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