resource-struct 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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