resource-struct 0.1.0 → 0.3.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: 07376a21fe1fd7e877cdd8755e5a269e8336d1ac9906cf046e2aa39947938117
4
- data.tar.gz: 070d252877489353d147fc93ee84d3ab0e74608f24d82d6d08163e9acd65c97f
3
+ metadata.gz: baaac467d70f8f08aed004cc989e61b19f1ba5c8af5d6c70e32157431ac5cce2
4
+ data.tar.gz: 640b07696f3f7332587859afdbe742fc6ea08e33fc6563d4e1ebc4d3430abb28
5
5
  SHA512:
6
- metadata.gz: b72654d78fbdb3df534c058a4e1df047f0ed8c40a063dbfc77ff869b8123d370f34277878354fe5d537802637fec52490d89374428e1bd5a937455bf59b4f0a1
7
- data.tar.gz: c3673ddd8220bc52a130ccfd5a512dd1f03dd3c7bfcadde3d28b075ba6b2619f2d8bfa346c853fbe20103ae35630cc6f8a6224bd6d67335a0a31ec63a16abfea
6
+ metadata.gz: c47966d7c7c3b96993594514eaaa21f879ad7c03f96a893d15255867491d990de7779dffb7c01dbf5189a2ac77992fd4f049a7601d4c5b42523a6b619fda27e9
7
+ data.tar.gz: 2a129ceadc0026557fca3e10834eca614f00f7415e282b39b7ea29473176cb8996ed67ef2b4682f45c45bd88e85b25745e00e0139928b7d5c01277420e949301
data/.rubocop.yml CHANGED
@@ -15,6 +15,15 @@ Layout/LineLength:
15
15
  Metrics/MethodLength:
16
16
  Max: 20
17
17
 
18
+ Metrics/AbcSize:
19
+ Max: 25
20
+
21
+ Metrics/CyclomaticComplexity:
22
+ Max: 15
23
+
24
+ Metrics/PerceivedComplexity:
25
+ Max: 15
26
+
18
27
  Naming/FileName:
19
28
  Enabled: true
20
29
  Exclude:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2022-01-05
4
+ ### Fix
5
+ - Support nil meaning empty hash as first argument to `FlexStruct` and `StrictStruct`
6
+
7
+ ## [0.3.0] - 2022-01-04
8
+ ### Feature
9
+ - Support for `as_json` and `to_json`
10
+ - Support for `#[]=`, allowing modification on LooseStruct
11
+ - Support for `JSON.parse(STR, object_class: ResourceStruct::FlexStruct)`
12
+ - Refactor common code between LooseStruct and FirmStruct into `ResourceStruct::Extension::IndifferentLookup`
13
+ - No longer support wrong arity for method based access patterns
14
+ - Rename `LooseStruct` -> `FlexStruct`; `FirmStruct` -> `StrictStruct`
15
+
16
+ ## [0.2.1] - 2022-01-01
17
+ ### Fix
18
+ - Correct handling of #== operator on Structs with hashes
19
+
20
+ ## [0.2.0] - 2022-01-01
21
+ ### Changed
22
+ - Indifferent access support for input hash (support for symbol-based hashes in initializer)
23
+
3
24
  ## [0.1.0] - 2021-12-14
4
25
 
5
26
  - Initial release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- resource-struct (0.1.0)
4
+ resource-struct (0.3.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -48,6 +48,8 @@ GEM
48
48
 
49
49
  PLATFORMS
50
50
  x86_64-darwin-18
51
+ x86_64-darwin-19
52
+ x86_64-linux
51
53
 
52
54
  DEPENDENCIES
53
55
  rake (~> 13.0)
@@ -58,4 +60,4 @@ DEPENDENCIES
58
60
  rubocop-rspec
59
61
 
60
62
  BUNDLED WITH
61
- 2.2.33
63
+ 2.3.4
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,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.1.0"
4
+ VERSION = "0.3.1"
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.1.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Riedler
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-16 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:
@@ -55,7 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
55
56
  - !ruby/object:Gem::Version
56
57
  version: '0'
57
58
  requirements: []
58
- rubygems_version: 3.1.4
59
+ rubygems_version: 3.1.6
59
60
  signing_key:
60
61
  specification_version: 4
61
62
  summary: Ruby structs for resource responses
@@ -1,102 +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[___convert_key(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
- @hash == other.instance_variable_get(:@hash)
59
- end
60
-
61
- def [](key, *sub_keys)
62
- ckey = ___convert_key(key)
63
-
64
- result =
65
- if @ro_struct.key?(ckey)
66
- @ro_struct[ckey]
67
- else
68
- @ro_struct[ckey] = ___convert_value(@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?(___convert_key(key))
96
- end
97
-
98
- def ___convert_key(key)
99
- key.is_a?(::Symbol) ? key.to_s : key
100
- end
101
- end
102
- end
@@ -1,99 +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[___convert_key(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
- @hash == other.instance_variable_get(:@hash)
56
- end
57
-
58
- def [](key, *sub_keys)
59
- ckey = ___convert_key(key)
60
-
61
- result =
62
- if @ro_struct.key?(ckey)
63
- @ro_struct[ckey]
64
- else
65
- @ro_struct[ckey] = ___convert_value(@hash[ckey])
66
- end
67
-
68
- return result if sub_keys.empty?
69
-
70
- return unless result
71
-
72
- raise TypeError, "#{result.class.name} does not have #dig method" unless result.respond_to?(:dig)
73
-
74
- result.dig(*sub_keys)
75
- end
76
- alias dig []
77
-
78
- private
79
-
80
- def ___convert_value(value)
81
- case value
82
- when ::Array
83
- value.map { |v| ___convert_value(v) }.freeze
84
- when Hash
85
- self.class.new(value)
86
- else
87
- value
88
- end
89
- end
90
-
91
- def ___key?(key)
92
- @hash.key?(___convert_key(key))
93
- end
94
-
95
- def ___convert_key(key)
96
- key.is_a?(::Symbol) ? key.to_s : key
97
- end
98
- end
99
- end