resource-struct 0.2.0 → 0.3.2

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: f9513a73bc3c6331f8174bd02c30c72fcbc655fb2e00c8d88cb1cd80fa3c60ae
4
- data.tar.gz: 3d1355e235d443b74098434f453a482d090790a25f57c65ce8a57f931813863a
3
+ metadata.gz: a61800df441acd7f512f708b4830412edf391047be95e29e8c3e002d8684caf4
4
+ data.tar.gz: dc26ed248afa3f648cc4369225bb402eccf28d74f9770ff1a8f39de088009897
5
5
  SHA512:
6
- metadata.gz: 88f21e99bce0cf3620cf0644f240176af22bd64f99a51781c293cd407467062cbe784d50274115e4214443801f34d10c79858a27910e9bf54556356e5e95d753
7
- data.tar.gz: bfa0b4576f987a5a5a7d39d62abcc7ac4f00fd6f2d27c8bb20d124fbf092914bab0212c8c4987451a86d47e3c06d31ab3452c777f0f25cb29a128e6ca048e3cf
6
+ metadata.gz: cd6e55f3a2cd859a37e6fef45485c322f83c8bcbfb6e1df97f4d69e594d77ddbff42668a07bec7438aa0483382e52d26f4ede84162be611cf98ca40b0be3b8b3
7
+ data.tar.gz: 4ff5a6edcb9dd6e07c4c3c5744d6e56aaefc14593d3acf6cd7fa9aeee25210656595d7ba926f3e0884265328604b01f5eba35ece222233f248244097813b2beb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.2] - 2022-01-05
4
+ ### Fix
5
+ - Support for proper marshalling of object
6
+
7
+ ## [0.3.1] - 2022-01-05
8
+ ### Fix
9
+ - Support nil meaning empty hash as first argument to `FlexStruct` and `StrictStruct`
10
+
11
+ ## [0.3.0] - 2022-01-04
12
+ ### Feature
13
+ - Support for `as_json` and `to_json`
14
+ - Support for `#[]=`, allowing modification on LooseStruct
15
+ - Support for `JSON.parse(STR, object_class: ResourceStruct::FlexStruct)`
16
+ - Refactor common code between LooseStruct and FirmStruct into `ResourceStruct::Extension::IndifferentLookup`
17
+ - No longer support wrong arity for method based access patterns
18
+ - Rename `LooseStruct` -> `FlexStruct`; `FirmStruct` -> `StrictStruct`
19
+
20
+ ## [0.2.1] - 2022-01-01
21
+ ### Fix
22
+ - Correct handling of #== operator on Structs with hashes
23
+
3
24
  ## [0.2.0] - 2022-01-01
4
25
  ### Changed
5
26
  - Indifferent access support for input hash (support for symbol-based hashes in initializer)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- resource-struct (0.2.0)
4
+ resource-struct (0.3.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -49,6 +49,7 @@ GEM
49
49
  PLATFORMS
50
50
  x86_64-darwin-18
51
51
  x86_64-darwin-19
52
+ x86_64-linux
52
53
 
53
54
  DEPENDENCIES
54
55
  rake (~> 13.0)
data/README.md CHANGED
@@ -1,8 +1,17 @@
1
- # Resource::Struct
1
+ # ResourceStruct
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/resource/struct`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ ![Continous Integration](https://github.com/AlexRiedler/resource-struct/actions/workflows/default.yml/badge.svg)
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ This is a gem for working with JSON resources from a network source with indifferent and method based access.
6
+
7
+ Instead of overriding Hash implementation, this wraps a Hash with indifferent access (by symbol or string keys).
8
+ This makes it fast at runtime, while still providing the necessary lookup method of choice.
9
+
10
+ There are two types `ResouceStruct::StrictStruct` and `ResourceStruct::FlexStruct`.
11
+
12
+ `ResourceStruct::StrictStruct` provides a way of wrapping a Hash such that accesses to invalid keys will raise an exception through the method lookup method; it also is immutable.
13
+
14
+ `ResouceStruct::FlexStruct` provides a way of wrapping a Hash such that it returns nil instead of raising an exception when the key is not present in the hash.
6
15
 
7
16
  ## Installation
8
17
 
@@ -22,7 +31,37 @@ Or install it yourself as:
22
31
 
23
32
  ## Usage
24
33
 
25
- TODO: Write usage instructions here
34
+ ### StrictStruct
35
+
36
+ ```ruby
37
+ struct = ResourceStruct::StrictStruct.new({ "foo" => 1, "bar" => [{ "baz" => 2 }, 3] })
38
+ struct.foo? # => true
39
+ struct.brr? # => false
40
+ struct.foo # => 1
41
+ struct.bar # => [StrictStruct<{ "baz" => 2 }>, 3]
42
+ struct.brr # => NoMethodError
43
+ struct[:foo] # => 1
44
+ struct[:brr] # => nil
45
+ struct[:bar, 0, :baz] # => 2
46
+ struct[:bar, 0, :brr] # => nil
47
+ ```
48
+
49
+ ### FlexStruct
50
+
51
+ ```ruby
52
+ struct = ResourceStruct::FlexStruct.new({ "foo" => 1, "bar" => [{ "baz" => 2 }, 3] })
53
+
54
+ struct.foo? # => true
55
+ struct.brr? # => false
56
+ struct.foo # => 1
57
+ struct.bar # => [FlexStruct<{ "baz" => 2 }>, 3]
58
+ struct.brr # => nil
59
+ struct[:foo] # => 1
60
+ struct[:brr] # => nil
61
+ struct[:bar, 0, :baz] # => 2
62
+ struct[:bar, 0, :brr] # => nil
63
+ ```
64
+
26
65
 
27
66
  ## Development
28
67
 
data/bin/console CHANGED
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "bundler/setup"
5
- require "resource/struct"
5
+ require "resource-struct"
6
6
 
7
7
  # You can add fixtures and/or initialization code here to make experimenting
8
8
  # with your gem easier. You can also use a different console, if you like.
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module ResourceStruct
6
+ module Extensions
7
+ #
8
+ # Common code between FirmStruct and LooseStruct
9
+ #
10
+ module IndifferentLookup
11
+ extend Forwardable
12
+
13
+ def_delegators :@hash, :to_h, :to_hash, :to_s, :as_json, :to_json
14
+
15
+ def initialize(hash = {})
16
+ @hash = hash || {}
17
+ @ro_struct = {}
18
+
19
+ raise ::ArgumentError, "first argument must be a Hash, found #{@hash.class.name}" unless @hash.is_a?(Hash)
20
+ end
21
+
22
+ def inspect
23
+ "#{self.class.name}<#{@hash.inspect}>"
24
+ end
25
+
26
+ def ==(other)
27
+ other.is_a?(Hash) && ___all_keys_equal(other) ||
28
+ (other.is_a?(LooseStruct) || other.is_a?(FirmStruct)) &&
29
+ ___all_keys_equal(other.instance_variable_get(:@hash))
30
+ end
31
+
32
+ def dig(key, *sub_keys)
33
+ ckey = ___convert_key(key)
34
+
35
+ result =
36
+ if @ro_struct.key?(ckey)
37
+ @ro_struct[ckey]
38
+ elsif key.is_a?(String)
39
+ @ro_struct[ckey] = ___convert_value(@hash[key] || @hash[key.to_sym])
40
+ else
41
+ @ro_struct[ckey] = ___convert_value(@hash[key] || @hash[ckey])
42
+ end
43
+
44
+ return result if sub_keys.empty?
45
+
46
+ return unless result
47
+
48
+ raise TypeError, "#{result.class.name} does not have #dig method" unless result.respond_to?(:dig)
49
+
50
+ result.dig(*sub_keys)
51
+ end
52
+ alias [] dig
53
+
54
+ def marshal_dump
55
+ {
56
+ data: @hash
57
+ }
58
+ end
59
+
60
+ def marshal_load(obj)
61
+ @ro_struct = {}
62
+ @hash = obj[:data]
63
+ end
64
+
65
+ private
66
+
67
+ def ___convert_value(value)
68
+ case value
69
+ when ::Array
70
+ value.map { |v| ___convert_value(v) }.freeze
71
+ when Hash
72
+ self.class.new(value)
73
+ else
74
+ value
75
+ end
76
+ end
77
+
78
+ def ___key?(key)
79
+ @hash.key?(key) || @hash.key?(___convert_key(key))
80
+ end
81
+
82
+ def ___convert_key(key)
83
+ key.is_a?(::Symbol) ? key.to_s : key
84
+ end
85
+
86
+ def ___all_keys_equal(other)
87
+ return false unless @hash.count == other.count
88
+
89
+ @hash.reduce(true) do |acc, (k, _)|
90
+ value = self[k]
91
+ if other.key?(k)
92
+ acc && value == other[k]
93
+ elsif k.is_a?(String)
94
+ ck = k.to_sym
95
+ acc && other.key?(ck) && value == other[ck]
96
+ else
97
+ ck = ___convert_key(k)
98
+ acc && other.key?(ck) && value == other[ck]
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResourceStruct
4
+ #
5
+ # FlexStruct provides a struct by which accessing undefined fields returns nil
6
+ #
7
+ # struct = FlexStruct.new({ "foo" => 1, "bar" => [{ "baz" => 2 }, 3] })
8
+ #
9
+ # struct.foo? # => true
10
+ # struct.brr? # => false
11
+ # struct.foo # => 1
12
+ # struct.bar # => [FlexStruct<{ "baz" => 2 }>, 3]
13
+ # struct.brr # => nil
14
+ # struct[:foo] # => 1
15
+ # struct[:brr] # => nil
16
+ # struct[:bar, 0, :baz] # => 2
17
+ # struct[:bar, 0, :brr] # => nil
18
+ #
19
+ class FlexStruct
20
+ include ::ResourceStruct::Extensions::IndifferentLookup
21
+
22
+ def []=(key, value)
23
+ ckey = ___convert_key(key)
24
+ @ro_struct.delete(ckey)
25
+
26
+ value = value.instance_variable_get(:@hash) if value.is_a?(FlexStruct) || value.is_a?(StrictStruct)
27
+
28
+ if @hash.key?(key)
29
+ @hash[key] = value
30
+ elsif key.is_a?(String) || key.is_a?(Symbol) && @hash.key?(key.to_sym)
31
+ @hash[key.to_sym] = value
32
+ else
33
+ @hash[key] = value
34
+ end
35
+ end
36
+
37
+ def method_missing(name, *args)
38
+ args_length = args.length
39
+ return self[name] if ___key?(name) && args_length.zero?
40
+ return !!self[name[...-1]] if name.end_with?("?") && args_length.zero?
41
+ return self[name[...-1]] = args.first if name.end_with?("=") && args_length == 1
42
+
43
+ nil
44
+ end
45
+
46
+ def respond_to_missing?(_name, _include_private = false)
47
+ true
48
+ end
49
+ end
50
+ LooseStruct = FlexStruct
51
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResourceStruct
4
+ #
5
+ # StrictStruct provides a struct by which accessing undefined fields raises a MethodMissing error.
6
+ # This protects against accessing fields that are not present in API Responses.
7
+ #
8
+ # If you need to check whether a field exists in an api response, you can via name? methods.
9
+ #
10
+ # struct = StrictStruct.new({ "foo" => 1, "bar" => [{ "baz" => 2 }, 3] })
11
+ #
12
+ # struct.foo? # => true
13
+ # struct.brr? # => false
14
+ # struct.foo # => 1
15
+ # struct.bar # => [StrictStruct<{ "baz" => 2 }>, 3]
16
+ # struct.brr # => NoMethodError
17
+ # struct[:foo] # => 1
18
+ # struct[:brr] # => nil
19
+ # struct[:bar, 0, :baz] # => 2
20
+ # struct[:bar, 0, :brr] # => nil
21
+ #
22
+ class StrictStruct
23
+ include ::ResourceStruct::Extensions::IndifferentLookup
24
+
25
+ def method_missing(name, *args, &blk)
26
+ args_length = args.length
27
+ return self[name] if ___key?(name) && args_length.zero?
28
+ return !!self[name[...-1]] if name.end_with?("?") && args_length.zero?
29
+
30
+ super
31
+ end
32
+
33
+ def respond_to_missing?(name, include_private = false)
34
+ ___key?(name) || name.end_with?("?") || super
35
+ end
36
+ end
37
+ FirmStruct = StrictStruct
38
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ResourceStruct
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.2"
5
5
  end
@@ -2,9 +2,16 @@
2
2
 
3
3
  require_relative "resource_struct/version"
4
4
 
5
+ #
6
+ # ResourceStruct
7
+ #
8
+ # includes the factory method ResourceStruct.new(hash, opts)
9
+ # for building the various types of structs provided by the library.
10
+ #
5
11
  module ResourceStruct
6
12
  class Error < StandardError; end
7
13
  end
8
14
 
9
- require_relative "resource_struct/firm_struct"
10
- require_relative "resource_struct/loose_struct"
15
+ require_relative "resource_struct/extensions/indifferent_lookup"
16
+ require_relative "resource_struct/strict_struct"
17
+ require_relative "resource_struct/flex_struct"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resource-struct
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Riedler
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-01 00:00:00.000000000 Z
11
+ date: 2022-01-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Openstruct like access without all the headaches of Hash method overrides
14
14
  etc...
@@ -30,8 +30,9 @@ files:
30
30
  - bin/setup
31
31
  - lib/resource-struct.rb
32
32
  - lib/resource_struct.rb
33
- - lib/resource_struct/firm_struct.rb
34
- - lib/resource_struct/loose_struct.rb
33
+ - lib/resource_struct/extensions/indifferent_lookup.rb
34
+ - lib/resource_struct/flex_struct.rb
35
+ - lib/resource_struct/strict_struct.rb
35
36
  - lib/resource_struct/version.rb
36
37
  homepage: https://github.com/AlexRiedler/resource-struct
37
38
  licenses:
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ResourceStruct
4
- #
5
- # FirmStruct provides a struct by which accessing undefined fields raises a MethodMissing error.
6
- # This protects against accessing fields that are not present in API Responses.
7
- #
8
- # If you need to check whether a field exists in an api response, you can via name? methods.
9
- #
10
- # struct = FirmStruct.new({ "foo" => 1, "bar" => [{ "baz" => 2 }, 3] })
11
- #
12
- # struct.foo? # => true
13
- # struct.brr? # => false
14
- # struct.foo # => 1
15
- # struct.bar # => [FirmStruct<{ "baz" => 2 }>, 3]
16
- # struct.brr # => NoMethodError
17
- # struct[:foo] # => 1
18
- # struct[:brr] # => nil
19
- # struct[:bar, 0, :baz] # => 2
20
- # struct[:bar, 0, :brr] # => nil
21
- #
22
- class FirmStruct
23
- def initialize(hash)
24
- raise ::ArgumentError, "first argument must be a Hash, found #{hash.class.name}" unless hash.is_a?(Hash)
25
-
26
- @hash = hash
27
- @ro_struct = {}
28
- end
29
-
30
- def method_missing(name, *args, &blk)
31
- return self[name] if ___key?(name)
32
- return !!self[name[...-1]] if name.end_with?("?")
33
-
34
- super
35
- end
36
-
37
- def respond_to_missing?(name, include_private = false)
38
- ___key?(name) || name.end_with?("?") || super
39
- end
40
-
41
- def to_h
42
- @hash.to_h
43
- end
44
-
45
- def to_hash
46
- @hash.to_hash
47
- end
48
-
49
- def inspect
50
- "#{self.class.name}<#{@hash.inspect}>"
51
- end
52
-
53
- def to_s
54
- @hash.to_s
55
- end
56
-
57
- def ==(other)
58
- other.is_a?(Hash) && ___all_keys_equal(other) ||
59
- (other.is_a?(LooseStruct) || other.is_a?(FirmStruct)) && ___all_keys_equal(other.instance_variable_get(:@hash))
60
- end
61
-
62
- def dig(key, *sub_keys)
63
- ckey = ___convert_key(key)
64
-
65
- result =
66
- if @ro_struct.key?(ckey)
67
- @ro_struct[ckey]
68
- elsif key.is_a?(String)
69
- @ro_struct[ckey] = ___convert_value(@hash[key] || @hash[key.to_sym])
70
- else
71
- @ro_struct[ckey] = ___convert_value(@hash[key] || @hash[ckey])
72
- end
73
-
74
- return result if sub_keys.empty?
75
-
76
- return unless result
77
-
78
- raise TypeError, "#{result.class.name} does not have #dig method" unless result.respond_to?(:dig)
79
-
80
- result.dig(*sub_keys)
81
- end
82
- alias [] dig
83
-
84
- private
85
-
86
- def ___convert_value(value)
87
- case value
88
- when ::Array
89
- value.map { |v| ___convert_value(v) }.freeze
90
- when Hash
91
- self.class.new(value)
92
- else
93
- value
94
- end
95
- end
96
-
97
- def ___key?(key)
98
- @hash.key?(key) || @hash.key?(___convert_key(key))
99
- end
100
-
101
- def ___convert_key(key)
102
- key.is_a?(::Symbol) ? key.to_s : key
103
- end
104
-
105
- def ___all_keys_equal(other)
106
- return false unless @hash.count == other.count
107
-
108
- @hash.reduce(true) do |acc, (k, v)|
109
- if other.key?(k)
110
- acc && other[k] == v
111
- else
112
- ck = ___convert_key(k)
113
- acc && other.key?(ck) && other[ck] == v
114
- end
115
- end
116
- end
117
- end
118
- end
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ResourceStruct
4
- #
5
- # LooseStruct provides a struct by which accessing undefined fields returns nil
6
- #
7
- # struct = LooseStruct.new({ "foo" => 1, "bar" => [{ "baz" => 2 }, 3] })
8
- #
9
- # struct.foo? # => true
10
- # struct.brr? # => false
11
- # struct.foo # => 1
12
- # struct.bar # => [LooseStruct<{ "baz" => 2 }>, 3]
13
- # struct.brr # => nil
14
- # struct[:foo] # => 1
15
- # struct[:brr] # => nil
16
- # struct[:bar, 0, :baz] # => 2
17
- # struct[:bar, 0, :brr] # => nil
18
- #
19
- class LooseStruct
20
- def initialize(hash)
21
- raise ::ArgumentError, "first argument must be a Hash, found #{hash.class.name}" unless hash.is_a?(Hash)
22
-
23
- @hash = hash
24
- @ro_struct = {}
25
- end
26
-
27
- def method_missing(name, *_args)
28
- return self[name] if ___key?(name)
29
- return !!self[name[...-1]] if name.end_with?("?")
30
-
31
- nil
32
- end
33
-
34
- def respond_to_missing?(_name, _include_private = false)
35
- true
36
- end
37
-
38
- def to_h
39
- @hash.to_h
40
- end
41
-
42
- def to_hash
43
- @hash.to_hash
44
- end
45
-
46
- def inspect
47
- "#{self.class.name}<#{@hash.inspect}>"
48
- end
49
-
50
- def to_s
51
- @hash.to_s
52
- end
53
-
54
- def ==(other)
55
- other.is_a?(Hash) && ___all_keys_equal(other) ||
56
- (other.is_a?(LooseStruct) || other.is_a?(FirmStruct)) && ___all_keys_equal(other.instance_variable_get(:@hash))
57
- end
58
-
59
- def dig(key, *sub_keys)
60
- ckey = ___convert_key(key)
61
-
62
- result =
63
- if @ro_struct.key?(ckey)
64
- @ro_struct[ckey]
65
- elsif key.is_a?(String)
66
- @ro_struct[ckey] = ___convert_value(@hash[key] || @hash[key.to_sym])
67
- else
68
- @ro_struct[ckey] = ___convert_value(@hash[key] || @hash[ckey])
69
- end
70
-
71
- return result if sub_keys.empty?
72
-
73
- return unless result
74
-
75
- raise TypeError, "#{result.class.name} does not have #dig method" unless result.respond_to?(:dig)
76
-
77
- result.dig(*sub_keys)
78
- end
79
- alias [] dig
80
-
81
- private
82
-
83
- def ___convert_value(value)
84
- case value
85
- when ::Array
86
- value.map { |v| ___convert_value(v) }.freeze
87
- when Hash
88
- self.class.new(value)
89
- else
90
- value
91
- end
92
- end
93
-
94
- def ___key?(key)
95
- @hash.key?(key) || @hash.key?(___convert_key(key))
96
- end
97
-
98
- def ___convert_key(key)
99
- key.is_a?(::Symbol) ? key.to_s : key
100
- end
101
-
102
- def ___all_keys_equal(other)
103
- return false unless @hash.count == other.count
104
-
105
- @hash.reduce(true) do |acc, (k, v)|
106
- if other.key?(k)
107
- acc && other[k] == v
108
- else
109
- ck = ___convert_key(k)
110
- acc && other.key?(ck) && other[ck] == v
111
- end
112
- end
113
- end
114
- end
115
- end