resource-struct 0.2.1 → 0.3.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: 3435cd92f2c4374cfd84a3411fd5a9d42fdd80f3e9ef1a13b847d302ba3fd313
4
- data.tar.gz: 79ffe2f000b766de16130742bfd1a087222bf2d6c9ebf70dc9b328f2cdeb3394
3
+ metadata.gz: 1c1790e36a2600fddc519f1f9fdd47a2bdf9f07003f7205f0584be5455867446
4
+ data.tar.gz: f916a4b4a6bb2c27d727d9914732c5a33ce35617246814bfbe8e9743150c89c1
5
5
  SHA512:
6
- metadata.gz: 6c74aece00d81f98c1cd6bc7b42cd4d6fdf43a60493cd247bc30f949e844cc048b28853c33f73acc18bc3a2fa6ad70bbe9438ec3a7ab56086cc5f73ca0db3de7
7
- data.tar.gz: 413595f3fc61327671dc6c499477f7208bd8d6095e7b429e276c5b9082b4dc670bab7b40424f7ef4d4bb2a7cc8850e40809010258c007916cab0cb6141e6566b
6
+ metadata.gz: c2db5d94ea2c816e8bb7a2279709380e43f6a7f0a1f1e7ceb3c027e1f07d5b5ffc308014eeef7ef44bd815ca8f9d9a43c46540af890a60c8f515e2e477536841
7
+ data.tar.gz: eb0426c1c8667be29487125496681cba12c2b25fabc68475675500ea997aec898639cd826ce2370f888f9d0ffca9427ad6e348de3713eeb9d67bed3e1b350605
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2022-01-04
4
+ ### Feature
5
+ - Support for `as_json` and `to_json`
6
+ - Support for `#[]=`, allowing modification on LooseStruct
7
+ - Support for `JSON.parse(STR, object_class: ResourceStruct::FlexStruct)`
8
+ - Refactor common code between LooseStruct and FirmStruct into `ResourceStruct::Extension::IndifferentLookup`
9
+ - No longer support wrong arity for method based access patterns
10
+ - Rename `LooseStruct` -> `FlexStruct`; `FirmStruct` -> `StrictStruct`
11
+
3
12
  ## [0.2.1] - 2022-01-01
4
13
  ### Fix
5
14
  - Correct handling of #== operator on Structs with hashes
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- resource-struct (0.2.1)
4
+ resource-struct (0.3.0)
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
 
@@ -0,0 +1,93 @@
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
+ private
55
+
56
+ def ___convert_value(value)
57
+ case value
58
+ when ::Array
59
+ value.map { |v| ___convert_value(v) }.freeze
60
+ when Hash
61
+ self.class.new(value)
62
+ else
63
+ value
64
+ end
65
+ end
66
+
67
+ def ___key?(key)
68
+ @hash.key?(key) || @hash.key?(___convert_key(key))
69
+ end
70
+
71
+ def ___convert_key(key)
72
+ key.is_a?(::Symbol) ? key.to_s : key
73
+ end
74
+
75
+ def ___all_keys_equal(other)
76
+ return false unless @hash.count == other.count
77
+
78
+ @hash.reduce(true) do |acc, (k, _)|
79
+ value = self[k]
80
+ if other.key?(k)
81
+ acc && value == other[k]
82
+ elsif k.is_a?(String)
83
+ ck = k.to_sym
84
+ acc && other.key?(ck) && value == other[ck]
85
+ else
86
+ ck = ___convert_key(k)
87
+ acc && other.key?(ck) && value == other[ck]
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ 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.1"
4
+ VERSION = "0.3.0"
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.1
4
+ version: 0.3.0
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,122 +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, _)|
109
- value = self[k]
110
- if other.key?(k)
111
- acc && value == other[k]
112
- elsif k.is_a?(String)
113
- ck = k.to_sym
114
- acc && other.key?(ck) && value == other[ck]
115
- else
116
- ck = ___convert_key(k)
117
- acc && other.key?(ck) && value == other[ck]
118
- end
119
- end
120
- end
121
- end
122
- end
@@ -1,119 +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, _)|
106
- value = self[k]
107
- if other.key?(k)
108
- acc && value == other[k]
109
- elsif k.is_a?(String)
110
- ck = k.to_sym
111
- acc && other.key?(ck) && value == other[ck]
112
- else
113
- ck = ___convert_key(k)
114
- acc && other.key?(ck) && value == other[ck]
115
- end
116
- end
117
- end
118
- end
119
- end