sd_struct 0.0.3 → 0.1.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 +4 -4
- data/.rspec +2 -0
- data/CHANGELOG.md +13 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/README.md +32 -13
- data/Rakefile +1 -1
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/sd_struct/base.rb +225 -0
- data/lib/sd_struct/deep_convert.rb +37 -0
- data/lib/sd_struct/deep_search.rb +112 -0
- data/lib/sd_struct/version.rb +1 -1
- data/lib/sd_struct.rb +3 -233
- data/sd_struct.gemspec +8 -8
- metadata +29 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: db8247a8652fb98a73e935ce9c0737d3bcb60aeb
|
4
|
+
data.tar.gz: e681ebbbe672b041dbe2d84e7e819123c7bfd966
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a46da494946f552eca4dc1912c1d6f96667f933539b9fe4f66d37f530600f52e6e508c7a9c26a4d541f71c433af85dd890008bef1b231af98fe8d30878027b03
|
7
|
+
data.tar.gz: 3987632b1425ef16c1535a740ac68183170a644b9b1122e14ac3e2eab9c9d24c6745c357e957e5fe2266ef452f9a8a172e7bd097afeb9f8c5b2ab96b7c942837
|
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
0.1.0 / 2017-04-19
|
2
|
+
==================
|
3
|
+
* Added warning when changing value to different type and
|
4
|
+
* Added conversion of value assigned to struct if it's a Hash or an Array and deep option is set to true
|
5
|
+
* Improved `find` to be more like xpath with its root and non-root search
|
6
|
+
* Upgraded Bundler
|
7
|
+
* Prepared to use Rspec (no specs yet)
|
8
|
+
* Added changelog
|
9
|
+
|
10
|
+
0.0.3 / 2017-04-09
|
11
|
+
==================
|
12
|
+
|
13
|
+
* Initial working version
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at adrian.setyadi@reebonz.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# SDStruct
|
2
2
|
|
3
|
-
An alternative to OpenStruct that is
|
4
|
-
consuming the passed Hash and transforming it back to Hash or JSON, equipped
|
3
|
+
An alternative to OpenStruct that is more strict in assigning values and deeper
|
4
|
+
in consuming the passed Hash and transforming it back to Hash or JSON, equipped
|
5
5
|
with deep digging capabilities.
|
6
6
|
|
7
7
|
## Usage
|
@@ -48,6 +48,11 @@ sd_struct = JSON.parse(response.body, object_class: SDStruct)
|
|
48
48
|
|
49
49
|
sd_struct["two words"]
|
50
50
|
# => "Foo bar"
|
51
|
+
|
52
|
+
# By the way, you can also use `send` method to access spaced key/field
|
53
|
+
o_struct.send("two words")
|
54
|
+
sd_struct.send("two words")
|
55
|
+
# => "Foo bar"
|
51
56
|
```
|
52
57
|
|
53
58
|
|
@@ -103,26 +108,26 @@ sd_struct
|
|
103
108
|
sd_struct.find('object/a')
|
104
109
|
# => "bau bau"
|
105
110
|
|
106
|
-
sd_struct.find('array/0/one')
|
111
|
+
sd_struct.find('/array/0/one')
|
107
112
|
# => 1
|
108
113
|
|
109
114
|
sd_struct.find('object->a', separator: '->')
|
110
115
|
# => "bau bau"
|
111
116
|
|
112
|
-
sd_struct.find('array
|
117
|
+
sd_struct.find('.array..one', separator: '.')
|
113
118
|
# => 1
|
114
119
|
|
115
120
|
# You can push it to find deeper. It will return the first occurrence of the matched field
|
116
|
-
sd_struct.find('a')
|
121
|
+
sd_struct.find('//a')
|
117
122
|
# => "bau bau"
|
118
123
|
|
119
|
-
sd_struct.find('0/one')
|
124
|
+
sd_struct.find('//0/one')
|
120
125
|
# => 1
|
121
126
|
|
122
|
-
sd_struct.find('one')
|
127
|
+
sd_struct.find('//one')
|
123
128
|
# => 1
|
124
129
|
|
125
|
-
sd_struct.find('four')
|
130
|
+
sd_struct.find('//four')
|
126
131
|
# => nil
|
127
132
|
|
128
133
|
sd_struct.dig_deep(0, :one)
|
@@ -147,8 +152,8 @@ sd_struct
|
|
147
152
|
# => #<SDStruct .object=#<SDStruct .a="bau bau", .c="boo boo">,
|
148
153
|
# .array=[#<SDStruct .one=1, .two=2, .three=3>], ['two words']="Foo bar">
|
149
154
|
|
150
|
-
sd_struct.find('0').one = 0
|
151
|
-
sd_struct.find('0').three = 0
|
155
|
+
sd_struct.find('//0').one = 0
|
156
|
+
sd_struct.find('//0').three = 0
|
152
157
|
sd_struct['two words'] = ""
|
153
158
|
sd_struct
|
154
159
|
# => #<SDStruct .object=#<SDStruct .a="bau bau", .c="boo boo">,
|
@@ -162,7 +167,7 @@ Therefore, keys with those values are excluded from the generated JSON string.
|
|
162
167
|
sd_struct.to_json
|
163
168
|
# => "{\"object\":{\"a\":\"bau bau\",\"c\":\"boo boo\"},\"array\":[{\"two\":2}]}"
|
164
169
|
|
165
|
-
sd_struct.find('0').two = 0
|
170
|
+
sd_struct.find('///0').two = 0
|
166
171
|
sd_struct.to_json
|
167
172
|
# => "{\"object\":{\"a\":\"bau bau\",\"c\":\"boo boo\"}}"
|
168
173
|
```
|
@@ -215,8 +220,9 @@ sd_struct = SDStruct.new(hash)
|
|
215
220
|
|
216
221
|
## Reserved Field Names
|
217
222
|
|
218
|
-
`
|
219
|
-
`
|
223
|
+
`delete_field`, `delete_key`, `dig`, `dig_deep`, `each_pair`, `fields`, `find`,
|
224
|
+
`keys`, `marshal_dump`, `marshal_load`, `new_struct_member`, `non_spaced_keys`,
|
225
|
+
`spaced_keys`, `structurize`, `table`, `to_h`, `to_json`
|
220
226
|
|
221
227
|
## Dependencies
|
222
228
|
|
@@ -238,6 +244,12 @@ Or install it yourself as:
|
|
238
244
|
|
239
245
|
$ gem install activesupport
|
240
246
|
|
247
|
+
In your code:
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
require 'active_support/all'
|
251
|
+
```
|
252
|
+
|
241
253
|
|
242
254
|
## Installation
|
243
255
|
|
@@ -255,6 +267,13 @@ Or install it yourself as:
|
|
255
267
|
|
256
268
|
$ gem install sd_struct
|
257
269
|
|
270
|
+
If you're not using rails, you can choose to only require the minimal version
|
271
|
+
that doesn't have deep search and deep convert capabilities.
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
require 'sd_struct/base'
|
275
|
+
```
|
276
|
+
|
258
277
|
## Contributing
|
259
278
|
|
260
279
|
1. Fork it ( https://github.com/styd/sd_struct/fork )
|
data/Rakefile
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
-
|
2
|
+
task :default => :spec
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "sd_struct"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
# Alternative to OpenStruct that is more strict and go deeper.
|
2
|
+
#
|
3
|
+
# @author Adrian Setyadi
|
4
|
+
#
|
5
|
+
class SDStruct
|
6
|
+
using Module.new {
|
7
|
+
refine Hash do
|
8
|
+
#
|
9
|
+
# Change current Hash object to SDStruct object
|
10
|
+
#
|
11
|
+
# @return [SDStruct] SDStruct object
|
12
|
+
#
|
13
|
+
def to_struct
|
14
|
+
SDStruct.new(to_h.dup)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
refine Array do
|
19
|
+
|
20
|
+
#
|
21
|
+
# Call `to_struct` to an Array to go deeper or to a Hash to change it to SDStruct
|
22
|
+
#
|
23
|
+
# @return [Array<SDStruct,Object>] array of SDStruct or any other objects
|
24
|
+
#
|
25
|
+
def to_struct
|
26
|
+
map{|x| ( x.is_a?(Hash) || x.is_a?(Array) ) ? x.to_struct : x }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
}
|
30
|
+
|
31
|
+
#
|
32
|
+
# Creates a new SDStruct object. By default, the resulting SDStruct object
|
33
|
+
# will have no attributes.
|
34
|
+
#
|
35
|
+
# The optional +hash+, if given, will generate attributes and values (can be
|
36
|
+
# a Hash, an SDStruct or a Struct).
|
37
|
+
# For example:
|
38
|
+
#
|
39
|
+
# require 'sd_struct' # or require 'sd_struct/base'
|
40
|
+
# hash = { "name" => "Matz", "coding language" => :ruby, :age => "old" }
|
41
|
+
# data = SDStruct.new(hash)
|
42
|
+
#
|
43
|
+
# p data # -> #<SDStruct .name="Matz", ['coding language']=:ruby, .age="old">
|
44
|
+
#
|
45
|
+
def initialize(hash = nil, deep = true)
|
46
|
+
@deep = deep
|
47
|
+
@table = {}
|
48
|
+
if hash
|
49
|
+
hash.each_pair do |k, v|
|
50
|
+
@table[new_struct_member(k)] = structurize(v) # @deep is used in this method
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Duplicate an SDStruct object members.
|
57
|
+
#
|
58
|
+
def initialize_copy(orig)
|
59
|
+
super
|
60
|
+
@table = @table.dup
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Provides marshalling support for use by the Marshal library.
|
65
|
+
#
|
66
|
+
def marshal_dump
|
67
|
+
to_h
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Provides marshalling support for use by the Marshal library.
|
72
|
+
#
|
73
|
+
def marshal_load(x)
|
74
|
+
@table = x.map{|a| structurize(a) }.original_to_h
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Yields all attributes (as a symbol) along with the corresponding values
|
79
|
+
# or returns an enumerator if not block is given.
|
80
|
+
# Example:
|
81
|
+
#
|
82
|
+
# require 'sd_struct'
|
83
|
+
# data = SDStruct.new("name" => "Matz", "coding language" => :ruby)
|
84
|
+
# data.each_pair.to_a # => [[:name, "Matz"], ["coding language", :ruby]]
|
85
|
+
#
|
86
|
+
def each_pair
|
87
|
+
return to_enum(__method__) { @table.size } unless block_given?
|
88
|
+
@table.each_pair{|p| yield p}
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Used internally to define field properties
|
93
|
+
#
|
94
|
+
def new_struct_member(name)
|
95
|
+
name = name.to_s.underscore.to_sym unless name[/^[A-Z]|\s+/]
|
96
|
+
unless respond_to?(name)
|
97
|
+
define_singleton_method(name) { @table[name] }
|
98
|
+
define_singleton_method("#{name}=") { |x| @table[name] = x }
|
99
|
+
end
|
100
|
+
name
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Call to struct to a value if it is an Array or a Hash and @deep is true
|
105
|
+
#
|
106
|
+
def structurize(value)
|
107
|
+
( @deep && (value.is_a?(Hash) || value.is_a?(Array)) ) ? value.to_struct : value
|
108
|
+
end
|
109
|
+
|
110
|
+
protected :new_struct_member, :structurize
|
111
|
+
|
112
|
+
|
113
|
+
#
|
114
|
+
# Returns the value of a member.
|
115
|
+
#
|
116
|
+
# person = SDStruct.new('name' => 'Matz', 'lang' => 'ruby')
|
117
|
+
# person[:lang] # => ruby, same as person.lang
|
118
|
+
#
|
119
|
+
def [](name)
|
120
|
+
@table.has_key?(name) ? @table[name] : @table[name.to_s.underscore.to_sym]
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Sets the value of a member.
|
125
|
+
#
|
126
|
+
# person = SDStruct.new('name' => 'Matz', 'lang' => 'python')
|
127
|
+
# person[:lang] = 'ruby' # => equivalent to person.lang = 'ruby'
|
128
|
+
# person.lang # => ruby
|
129
|
+
#
|
130
|
+
def []=(name, value)
|
131
|
+
unless self.[](name).nil? || value.is_a?(self.[](name).class)
|
132
|
+
warn("You're assigning a value with different type as the previous value.")
|
133
|
+
end
|
134
|
+
@table[new_struct_member(name)] = structurize(value)
|
135
|
+
end
|
136
|
+
|
137
|
+
InspectKey = :__inspect_key__ # :nodoc:
|
138
|
+
|
139
|
+
#
|
140
|
+
# Returns a string containing a detailed summary of the keys and values.
|
141
|
+
#
|
142
|
+
def inspect
|
143
|
+
str = "#<#{self.class}"
|
144
|
+
|
145
|
+
ids = (Thread.current[InspectKey] ||= [])
|
146
|
+
if ids.include?(object_id)
|
147
|
+
return str << ' ...>'
|
148
|
+
end
|
149
|
+
|
150
|
+
ids << object_id
|
151
|
+
begin
|
152
|
+
first = true
|
153
|
+
for k,v in @table
|
154
|
+
str << "," unless first
|
155
|
+
first = false
|
156
|
+
str << " #{k[/\s+/] ? "['#{k}']" : ".#{k}"}=#{v.inspect}"
|
157
|
+
end
|
158
|
+
return str << '>'
|
159
|
+
ensure
|
160
|
+
ids.pop
|
161
|
+
end
|
162
|
+
end
|
163
|
+
alias :to_s :inspect
|
164
|
+
|
165
|
+
attr_reader :table
|
166
|
+
protected :table
|
167
|
+
|
168
|
+
def ==(other)
|
169
|
+
return false unless other.kind_of?(self.class)
|
170
|
+
@table == other.table
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# Compares this object and +other+ for equality. An SDStruct is eql? to
|
175
|
+
# +other+ when +other+ is an SDStruct and the two objects' Hash tables are
|
176
|
+
# eql?.
|
177
|
+
#
|
178
|
+
def eql?(other)
|
179
|
+
return false unless other.kind_of?(self.class)
|
180
|
+
@table.eql?(other.table)
|
181
|
+
end
|
182
|
+
|
183
|
+
#
|
184
|
+
# Compute a hash-code for this SDStruct.
|
185
|
+
# Two hashes with the same content will have the same hash code
|
186
|
+
# (and will be eql?).
|
187
|
+
#
|
188
|
+
def hash
|
189
|
+
@table.hash
|
190
|
+
end
|
191
|
+
|
192
|
+
#
|
193
|
+
# Expose keys with space(s)
|
194
|
+
#
|
195
|
+
def spaced_keys
|
196
|
+
@table.keys - non_spaced_keys
|
197
|
+
end
|
198
|
+
|
199
|
+
#
|
200
|
+
# Expose keys without space(s)
|
201
|
+
#
|
202
|
+
def non_spaced_keys
|
203
|
+
methods(false).select{|x| x[/^\S+[^=]$/]}
|
204
|
+
end
|
205
|
+
alias :fields :non_spaced_keys
|
206
|
+
|
207
|
+
#
|
208
|
+
# Expose all keys
|
209
|
+
#
|
210
|
+
def keys
|
211
|
+
@table.keys
|
212
|
+
end
|
213
|
+
|
214
|
+
#
|
215
|
+
# Delete specified field or key
|
216
|
+
#
|
217
|
+
def delete_field(name)
|
218
|
+
sym = name.to_sym
|
219
|
+
@table.delete(sym) do
|
220
|
+
raise NameError.new("no field `#{sym}' in #{self}", sym)
|
221
|
+
end
|
222
|
+
singleton_class.__send__(:remove_method, sym, "#{sym}=")
|
223
|
+
end
|
224
|
+
alias :delete_key :delete_field
|
225
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class SDStruct
|
2
|
+
using Module.new {
|
3
|
+
refine Array do
|
4
|
+
alias :original_to_h :to_h
|
5
|
+
|
6
|
+
def to_h(camelize_keys = false)
|
7
|
+
map{|x| x.respond_to?(:to_h) ? x.to_h(camelize_keys) : x }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
}
|
11
|
+
|
12
|
+
def to_h(opts = {})
|
13
|
+
opts = {
|
14
|
+
camelize_keys: false,
|
15
|
+
exclude_blank_values: false,
|
16
|
+
values_to_exclude: []
|
17
|
+
}.merge(opts)
|
18
|
+
|
19
|
+
@table.map do |k, v|
|
20
|
+
v = v.to_h(opts) if v.is_a?(self.class) || v.is_a?(Array)
|
21
|
+
k = k.to_s.camelize(:lower) if opts[:camelize_keys] && !k[/\s+/]
|
22
|
+
[k, v]
|
23
|
+
end.original_to_h
|
24
|
+
.select{|_,v| opts[:exclude_blank_values] ? v.present? : !v.nil? }
|
25
|
+
.select{|_,v| !v.in?(opts[:values_to_exclude]) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_json(opts = {})
|
29
|
+
opts = {
|
30
|
+
camelize_keys: true,
|
31
|
+
exclude_blank_values: true,
|
32
|
+
values_to_exclude: [0, [""], [{}]]
|
33
|
+
}.merge(opts)
|
34
|
+
|
35
|
+
to_h(opts).to_json
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
class SDStruct
|
2
|
+
using Module.new {
|
3
|
+
refine Array do
|
4
|
+
|
5
|
+
#
|
6
|
+
# Dig deep into array until non-Array and non-Hash primitive data is found
|
7
|
+
#
|
8
|
+
# @param [Symbol] multiple symbols
|
9
|
+
# @return [String,Integer,Float,Boolean,nil] first matched result
|
10
|
+
#
|
11
|
+
def dig_deep(*args)
|
12
|
+
full_args = args.dup
|
13
|
+
parent_key = args.shift
|
14
|
+
result = nil
|
15
|
+
if parent_key.is_a?(Integer)
|
16
|
+
result = dig(parent_key)
|
17
|
+
unless result.nil? || args.length.zero?
|
18
|
+
result = result.dig_deep(*args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
if result.nil?
|
22
|
+
each do |x|
|
23
|
+
if x.respond_to?(:dig_deep) || x.is_a?(Array)
|
24
|
+
result = x.dig_deep(*full_args)
|
25
|
+
end
|
26
|
+
return result unless result.nil?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
return result
|
30
|
+
end
|
31
|
+
end
|
32
|
+
}
|
33
|
+
|
34
|
+
#
|
35
|
+
# Dig deep into Hash until non-Array and non-Hash primitive data is found
|
36
|
+
#
|
37
|
+
# @param [Symbol] multiple symbols
|
38
|
+
# @return [SDStruct,Hash,Array,String,Integer,Float,Boolean,nil] first matched result
|
39
|
+
#
|
40
|
+
def dig_deep(*args)
|
41
|
+
full_args = args.dup
|
42
|
+
parent_key = args.shift
|
43
|
+
result = dig(parent_key)
|
44
|
+
unless result.nil? || args.length.zero?
|
45
|
+
if result.respond_to?(:dig)
|
46
|
+
result = result.dig(*args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
if result.nil?
|
50
|
+
@table.values
|
51
|
+
.select{|v| v.respond_to?(:dig) }
|
52
|
+
.each do |v|
|
53
|
+
if v.respond_to?(:dig_deep) || v.is_a?(Array)
|
54
|
+
result = v.dig_deep(*full_args)
|
55
|
+
end
|
56
|
+
return result unless result.nil?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
return result
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Dig the content of @table which is a hash
|
64
|
+
#
|
65
|
+
# @param [Symbol] multiple symbols
|
66
|
+
# @return [SDStruct,Hash,Array,String,Integer,Float,Boolean,nil] first matched result
|
67
|
+
#
|
68
|
+
def dig(*args)
|
69
|
+
@table.dig(*args)
|
70
|
+
end
|
71
|
+
|
72
|
+
def find(key_str, opts = {})
|
73
|
+
opts = {
|
74
|
+
separator: "/"
|
75
|
+
}.merge(opts)
|
76
|
+
|
77
|
+
sep = Regexp.quote(opts[:separator])
|
78
|
+
|
79
|
+
args = begin
|
80
|
+
key_str.gsub(/^#{sep}(?!#{sep})|#{sep}+$/, '')
|
81
|
+
.split(/#{sep}{2,}/)
|
82
|
+
.map do |ks|
|
83
|
+
ks.split(/#{sep}/)
|
84
|
+
.map do |x|
|
85
|
+
x.strip!
|
86
|
+
if !!x[/\A[-+]?\d+\z/]
|
87
|
+
x.to_i
|
88
|
+
else
|
89
|
+
if x[/^$|\s+/]
|
90
|
+
x
|
91
|
+
else
|
92
|
+
x.underscore.to_sym
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
if !(parent_key = args.shift) # args == [], key_str == ""
|
100
|
+
return
|
101
|
+
else # e.g. args == [[], ..] or [[.., ..], [..]]
|
102
|
+
result = dig(*parent_key) unless parent_key.empty?
|
103
|
+
|
104
|
+
unless args.length.zero?
|
105
|
+
args.each do |a|
|
106
|
+
result = result.dig_deep(*a) rescue result = dig_deep(*a)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
return result
|
111
|
+
end
|
112
|
+
end
|
data/lib/sd_struct/version.rb
CHANGED
data/lib/sd_struct.rb
CHANGED
@@ -1,234 +1,4 @@
|
|
1
1
|
require "sd_struct/version"
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
#
|
6
|
-
# @author Adrian Setyadi
|
7
|
-
#
|
8
|
-
class SDStruct
|
9
|
-
using Module.new {
|
10
|
-
refine Hash do
|
11
|
-
def to_struct
|
12
|
-
SDStruct.new(to_h.dup)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
refine Array do
|
17
|
-
alias :original_to_h :to_h
|
18
|
-
|
19
|
-
def to_h(camelize_keys = false)
|
20
|
-
map{|x| x.respond_to?(:to_h) ? x.to_h(camelize_keys) : x }
|
21
|
-
end
|
22
|
-
|
23
|
-
# Call `to_struct` to an Array to go deeper or to a Hash to change it to SDStruct
|
24
|
-
#
|
25
|
-
# @return [Array<SDStruct,Object>] array of SDStruct or any other objects
|
26
|
-
#
|
27
|
-
def to_struct
|
28
|
-
map{|x| ( x.is_a?(Hash) || x.is_a?(Array) ) ? x.to_struct : x }
|
29
|
-
end
|
30
|
-
|
31
|
-
def dig_deep(*args)
|
32
|
-
full_args = args.dup
|
33
|
-
parent_key = args.shift
|
34
|
-
result = nil
|
35
|
-
if parent_key.is_a?(Integer)
|
36
|
-
result = dig(parent_key)
|
37
|
-
unless result.nil? || args.length.zero?
|
38
|
-
result = result.dig_deep(*args)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
if result.nil?
|
42
|
-
each do |x|
|
43
|
-
if x.respond_to?(:dig_deep) || x.is_a?(Array)
|
44
|
-
result = x.dig_deep(*full_args)
|
45
|
-
end
|
46
|
-
return result unless result.nil?
|
47
|
-
end
|
48
|
-
end
|
49
|
-
return result
|
50
|
-
end
|
51
|
-
end
|
52
|
-
}
|
53
|
-
|
54
|
-
def initialize(hash = nil, deep = true)
|
55
|
-
@table = {}
|
56
|
-
if hash
|
57
|
-
hash.each_pair do |k, v|
|
58
|
-
v = v.to_struct if deep && ( v.is_a?(Hash) || v.is_a?(Array) )
|
59
|
-
@table[new_member(k)] = v
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def initialize_copy(orig)
|
65
|
-
super
|
66
|
-
@table = @table.dup
|
67
|
-
end
|
68
|
-
|
69
|
-
def marshal_dump
|
70
|
-
to_h
|
71
|
-
end
|
72
|
-
|
73
|
-
def marshal_load(x)
|
74
|
-
@table = x.map{|a| ( a.is_a?(Hash) || a.is_a?(Array) ) ? a.to_struct : a }
|
75
|
-
.original_to_h
|
76
|
-
end
|
77
|
-
|
78
|
-
def to_h(opt = {})
|
79
|
-
opt = {
|
80
|
-
camelize_keys: false,
|
81
|
-
exclude_blank_values: false,
|
82
|
-
values_to_exclude: []
|
83
|
-
}.merge(opt)
|
84
|
-
|
85
|
-
@table.map do |k, v|
|
86
|
-
v = v.to_h(opt) if v.is_a?(self.class) || v.is_a?(Array)
|
87
|
-
k = k.to_s.camelize(:lower) if opt[:camelize_keys] && !k[/\s+/]
|
88
|
-
[k, v]
|
89
|
-
end.original_to_h
|
90
|
-
.select{|_,v| opt[:exclude_blank_values] ? v.present? : !v.nil? }
|
91
|
-
.select{|_,v| !v.in?(opt[:values_to_exclude]) }
|
92
|
-
end
|
93
|
-
|
94
|
-
def to_json(opt = {})
|
95
|
-
opt = {
|
96
|
-
camelize_keys: true,
|
97
|
-
exclude_blank_values: true,
|
98
|
-
values_to_exclude: [0, [""], [{}]]
|
99
|
-
}.merge(opt)
|
100
|
-
|
101
|
-
to_h(opt).to_json
|
102
|
-
end
|
103
|
-
|
104
|
-
def new_member(name)
|
105
|
-
name = name.to_s.underscore.to_sym unless name[/\s+/] # contains whitespace
|
106
|
-
unless respond_to?(name)
|
107
|
-
define_singleton_method(name) { @table[name] }
|
108
|
-
define_singleton_method("#{name}=") { |x| @table[name] = x }
|
109
|
-
end
|
110
|
-
name
|
111
|
-
end
|
112
|
-
protected :new_member
|
113
|
-
|
114
|
-
def [](name)
|
115
|
-
@table.has_key?(name) ? @table[name] : @table[name.to_s.underscore.to_sym]
|
116
|
-
end
|
117
|
-
|
118
|
-
def []=(name, value)
|
119
|
-
@table[new_member(name)] = value
|
120
|
-
end
|
121
|
-
|
122
|
-
InspectKey = :__inspect_key__ # :nodoc:
|
123
|
-
|
124
|
-
def inspect
|
125
|
-
str = "#<#{self.class}"
|
126
|
-
|
127
|
-
ids = (Thread.current[InspectKey] ||= [])
|
128
|
-
if ids.include?(object_id)
|
129
|
-
return str << ' ...>'
|
130
|
-
end
|
131
|
-
|
132
|
-
ids << object_id
|
133
|
-
begin
|
134
|
-
first = true
|
135
|
-
for k,v in @table
|
136
|
-
str << "," unless first
|
137
|
-
first = false
|
138
|
-
str << " #{k[/\s+/] ? "['#{k}']" : ".#{k}"}=#{v.inspect}"
|
139
|
-
end
|
140
|
-
return str << '>'
|
141
|
-
ensure
|
142
|
-
ids.pop
|
143
|
-
end
|
144
|
-
end
|
145
|
-
alias :to_s :inspect
|
146
|
-
|
147
|
-
def dig_deep(*args)
|
148
|
-
full_args = args.dup
|
149
|
-
parent_key = args.shift
|
150
|
-
result = dig(parent_key)
|
151
|
-
unless result.nil? || args.length.zero?
|
152
|
-
if result.respond_to?(:dig)
|
153
|
-
result = result.dig(*args)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
if result.nil?
|
157
|
-
@table.values
|
158
|
-
.select{|v| v.respond_to?(:dig) }
|
159
|
-
.each do |v|
|
160
|
-
if v.respond_to?(:dig_deep) || v.is_a?(Array)
|
161
|
-
result = v.dig_deep(*full_args)
|
162
|
-
end
|
163
|
-
return result unless result.nil?
|
164
|
-
end
|
165
|
-
end
|
166
|
-
return result
|
167
|
-
end
|
168
|
-
|
169
|
-
def dig(*args)
|
170
|
-
@table.dig(*args)
|
171
|
-
end
|
172
|
-
|
173
|
-
def find(key_str, opt = {})
|
174
|
-
opt = {
|
175
|
-
separator: "/"
|
176
|
-
}.merge(opt)
|
177
|
-
|
178
|
-
args = key_str.split(opt[:separator])
|
179
|
-
.map do |x|
|
180
|
-
x.strip!
|
181
|
-
if !!(x =~ /\A[-+]?\d+\z/)
|
182
|
-
x.to_i
|
183
|
-
else
|
184
|
-
if x[/\s+/]
|
185
|
-
x
|
186
|
-
else
|
187
|
-
x.underscore.to_sym
|
188
|
-
end
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
result = dig_deep(*args) rescue nil
|
193
|
-
return result
|
194
|
-
end
|
195
|
-
|
196
|
-
attr_reader :table
|
197
|
-
protected :table
|
198
|
-
|
199
|
-
def ==(other)
|
200
|
-
return false unless other.kind_of?(self.class)
|
201
|
-
@table == other.table
|
202
|
-
end
|
203
|
-
|
204
|
-
def eql?(other)
|
205
|
-
return false unless other.kind_of?(self.class)
|
206
|
-
@table.eql?(other.table)
|
207
|
-
end
|
208
|
-
|
209
|
-
def hash
|
210
|
-
@table.hash
|
211
|
-
end
|
212
|
-
|
213
|
-
def spaced_keys
|
214
|
-
@table.keys - non_spaced_keys
|
215
|
-
end
|
216
|
-
|
217
|
-
def non_spaced_keys
|
218
|
-
methods(false).select{|x| x[/^\S+[^=]$/]}
|
219
|
-
end
|
220
|
-
alias :fields :non_spaced_keys
|
221
|
-
|
222
|
-
def keys
|
223
|
-
@table.keys
|
224
|
-
end
|
225
|
-
|
226
|
-
def delete_field(name)
|
227
|
-
sym = name.to_sym
|
228
|
-
@table.delete(sym) do
|
229
|
-
raise NameError.new("no field `#{sym}' in #{self}", sym)
|
230
|
-
end
|
231
|
-
singleton_class.__send__(:remove_method, sym, "#{sym}=")
|
232
|
-
end
|
233
|
-
alias :delete_key :delete_field
|
234
|
-
end
|
2
|
+
require "sd_struct/base"
|
3
|
+
require "sd_struct/deep_convert"
|
4
|
+
require "sd_struct/deep_search"
|
data/sd_struct.gemspec
CHANGED
@@ -8,20 +8,20 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = SDStruct::VERSION
|
9
9
|
spec.authors = ["Adrian Setyadi"]
|
10
10
|
spec.email = ["a.styd@yahoo.com"]
|
11
|
-
spec.summary = %q{
|
12
|
-
spec.description = %q{An alternative to OpenStruct that
|
11
|
+
spec.summary = %q{Strict and Deep Struct}
|
12
|
+
spec.description = %q{An alternative to OpenStruct that more strict in assigning values and deeper in
|
13
13
|
consuming the passed Hash and transforming it back to Hash or JSON, equipped
|
14
14
|
with deep digging capabilities.}
|
15
|
-
spec.homepage = ""
|
16
15
|
spec.license = "MIT"
|
17
16
|
|
18
|
-
spec.files = `git ls-files -z`.split("\x0")
|
19
|
-
|
20
|
-
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
21
20
|
spec.require_paths = ["lib"]
|
22
21
|
|
23
|
-
spec.add_runtime_dependency "activesupport", "
|
22
|
+
spec.add_runtime_dependency "activesupport", "> 4"
|
24
23
|
|
25
|
-
spec.add_development_dependency "bundler", "~> 1.
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
26
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.5"
|
27
27
|
end
|
metadata
CHANGED
@@ -1,22 +1,19 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sd_struct
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adrian Setyadi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-04-
|
11
|
+
date: 2017-04-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '4'
|
20
17
|
- - ">"
|
21
18
|
- !ruby/object:Gem::Version
|
22
19
|
version: '4'
|
@@ -24,9 +21,6 @@ dependencies:
|
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
|
-
- - "~>"
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '4'
|
30
24
|
- - ">"
|
31
25
|
- !ruby/object:Gem::Version
|
32
26
|
version: '4'
|
@@ -36,14 +30,14 @@ dependencies:
|
|
36
30
|
requirements:
|
37
31
|
- - "~>"
|
38
32
|
- !ruby/object:Gem::Version
|
39
|
-
version: '1.
|
33
|
+
version: '1.14'
|
40
34
|
type: :development
|
41
35
|
prerelease: false
|
42
36
|
version_requirements: !ruby/object:Gem::Requirement
|
43
37
|
requirements:
|
44
38
|
- - "~>"
|
45
39
|
- !ruby/object:Gem::Version
|
46
|
-
version: '1.
|
40
|
+
version: '1.14'
|
47
41
|
- !ruby/object:Gem::Dependency
|
48
42
|
name: rake
|
49
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -58,8 +52,22 @@ dependencies:
|
|
58
52
|
- - "~>"
|
59
53
|
- !ruby/object:Gem::Version
|
60
54
|
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.5'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.5'
|
61
69
|
description: |-
|
62
|
-
An alternative to OpenStruct that
|
70
|
+
An alternative to OpenStruct that more strict in assigning values and deeper in
|
63
71
|
consuming the passed Hash and transforming it back to Hash or JSON, equipped
|
64
72
|
with deep digging capabilities.
|
65
73
|
email:
|
@@ -69,14 +77,22 @@ extensions: []
|
|
69
77
|
extra_rdoc_files: []
|
70
78
|
files:
|
71
79
|
- ".gitignore"
|
80
|
+
- ".rspec"
|
81
|
+
- CHANGELOG.md
|
82
|
+
- CODE_OF_CONDUCT.md
|
72
83
|
- Gemfile
|
73
84
|
- LICENSE.txt
|
74
85
|
- README.md
|
75
86
|
- Rakefile
|
87
|
+
- bin/console
|
88
|
+
- bin/setup
|
76
89
|
- lib/sd_struct.rb
|
90
|
+
- lib/sd_struct/base.rb
|
91
|
+
- lib/sd_struct/deep_convert.rb
|
92
|
+
- lib/sd_struct/deep_search.rb
|
77
93
|
- lib/sd_struct/version.rb
|
78
94
|
- sd_struct.gemspec
|
79
|
-
homepage:
|
95
|
+
homepage:
|
80
96
|
licenses:
|
81
97
|
- MIT
|
82
98
|
metadata: {}
|
@@ -99,5 +115,5 @@ rubyforge_project:
|
|
99
115
|
rubygems_version: 2.5.1
|
100
116
|
signing_key:
|
101
117
|
specification_version: 4
|
102
|
-
summary:
|
118
|
+
summary: Strict and Deep Struct
|
103
119
|
test_files: []
|