recursive_case_indifferent_ostruct 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b398b7f98e4169257f4811888715dd82fd43b4b2
4
+ data.tar.gz: f44fee346580aec8bc67449f40223967852f3bc6
5
+ SHA512:
6
+ metadata.gz: e23e8daeca037f194a719201a486c509462c0e0abaa5bb52f8eb8e392075e885b96e2229195860b1bfc63338ff44202124cab26d8cc2d232bb4f1255cca5c399
7
+ data.tar.gz: fde5b277fe47328995e2f6d762f2ea14c11ed1bb41f1b63566807b032541d16673c454737140c1a088773397b527bccf98c65b764aa54221882f5a28433ddc7e
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Recursive Case Indifferent Ostruct
2
+
3
+ Takes a hash of snake, camel, train, or kabab case and make its
4
+ attributes accessible with any other case.
5
+
6
+ The main application is for working with JSON from an API and standardizing
7
+ to a Ruby-centric underscore/snake case interface.
8
+
9
+ ##Usage
10
+
11
+ Gemfile
12
+
13
+ ```ruby
14
+ gem "recursive_case_indifferent_ostruct"
15
+ ````
16
+
17
+
18
+ ## Examples
19
+ ```ruby
20
+ user = RecursiveCaseIndifferentOstruct.new({
21
+ "firstName" => "Tommy",
22
+ "first-name" => "Bobby",
23
+ "LAST_NAME" => "Johnson",
24
+ "Birth-Place" => "Springfield",
25
+ "father" => {
26
+ "age" 72
27
+ },
28
+ "siblings" => [
29
+ {relation: "brother", age: 12}
30
+ ]
31
+ }, :lower_camel)
32
+
33
+ # access attributes like so
34
+ user.first_name # "Tommy"
35
+ user.last_name # "Johnson"
36
+ user.father.age # 72
37
+ user.siblings[0].age # 12
38
+
39
+ user.birth_place = "New York" # set fields
40
+ user.first_name = "Ken" # sets {"firstName" => "Ken", "first-name" => "Ken"}
41
+ user.mothers_maiden = "Woods" # Since case is mixed in that hash, use default :lower_camel
42
+
43
+
44
+ user.to_h
45
+ # {
46
+ # "firstName" => "Ken",
47
+ # "first-name" => "Bobby",
48
+ # "LAST_NAME" => "Johnson",
49
+ # "Birth-Place" => "New York",
50
+ # "father" => {
51
+ # "age" 72
52
+ # },
53
+ # "siblings" => [
54
+ # {relation: "brother", age: 12}
55
+ # ],
56
+ # "mothersMaiden" => "Woods",
57
+ # }
58
+ ```
59
+
60
+
61
+ ### Case Matching
62
+ Since the key matching is fuzzy, should there be two attributes with
63
+ the same name but different case (`first-name` and `firstName`), the
64
+ library will return first value that it finds. For an assignment it
65
+ will assign all matching attributes to the new value.
66
+
67
+ ### Default Case
68
+ Should you assign an attribute to the hash that does not already exist
69
+ the library will try to figure out which case to use based on other
70
+ keys in the hash. If it cannot determine which case is being used
71
+ it will fall back to `RecursiveCaseIndifferentOstruct::DEFAULT_CASE=:snake`
72
+ which you could set to override. Another option is to pass the default
73
+ case on initialization `RecursiveCaseIndifferentOstruct.new(hash, :snake)`.
74
+
75
+ Available options:
76
+ * `:snake` *this_is_snake*
77
+ * `:lower_camel` *thisIsLowerCamel*
78
+ * `:upper_camel` *ThisIsUpperCamel*
79
+ * `:kabab` *this-is-kabab*
80
+ * `:train` *This-Is-Train*
81
+
82
+ If you need to assign a value to a key that has an odd case you can
83
+ use a string with the bracket syntax like so: `json["PI:Value"] = 3.14`.
84
+ The value can still be accessed via `json.pi_value`.
85
+
86
+
87
+ ### Hash Methods
88
+ `RecursiveCaseIndifferentOstruct` will pass methods along to the hash
89
+ if the method is not defined and there is not attribute matching that
90
+ name. However, all the hash methods operate on the original hash, so
91
+ `#has_key?`, `#each`, `#fetch`, etc, will not be case-indifferent.
92
+
93
+ ```ruby
94
+ json = RecursiveCaseIndifferentOstruct.new({
95
+ ID: 123,
96
+ userName: "pax"
97
+ })
98
+
99
+ json.has_key?(:user_name) # false
100
+ json.has_key?("userName") # true
101
+ json.fetch(:id) # nil
102
+ json.keys # [:ID, :userName]
103
+
104
+ ```
105
+
106
+
107
+ ## Tests
108
+ `rake test`
109
+
110
+
@@ -0,0 +1,167 @@
1
+
2
+ class RecursiveCaseIndifferentOstruct
3
+ DEFAULT_CASE=:snake
4
+ attr_accessor :default_case
5
+
6
+ def initialize(hash={}, casing=nil)
7
+ @default_case = casing || DEFAULT_CASE
8
+ @hash = hash
9
+ end
10
+
11
+ def to_h
12
+ @hash
13
+ end
14
+
15
+ def [](key)
16
+ handle_get(key)
17
+ end
18
+
19
+ def []=(key, value)
20
+ handle_assignment(key, value, true)
21
+ end
22
+
23
+ def method_missing(method_sym, *args, &block)
24
+ method_name = method_sym.to_s
25
+ is_assignment = method_name.slice(-1) == '='
26
+ is_get = !is_assignment && args.length == 0 && !@hash.respond_to?(method_name)
27
+
28
+ # remove = if an assignment
29
+ if is_assignment
30
+ method_name = method_name.slice(0, method_name.length - 1)
31
+ end
32
+
33
+ if is_assignment
34
+ return handle_assignment(method_name, args.first)
35
+ elsif is_get
36
+ return handle_get(method_name)
37
+ else
38
+ @hash.send(method_name, *args, &block)
39
+ end
40
+
41
+ # if they called a function and passed some args, then
42
+ # raise no method error
43
+ #super
44
+ end
45
+
46
+ def case_being_used
47
+ # TODO: If all nil then scan for keys in nested hashes
48
+ cases_found = @hash.keys.map do |key|
49
+ if !key.to_s.match(/^[a-z]+\-[a-z]/).nil? # kabab
50
+ :kabab
51
+ elsif key.to_s.include?('_')# snake
52
+ :snake
53
+ elsif !key.to_s.match(/^[a-z]+[A-Z]/).nil? # lower camel
54
+ :lower_camel
55
+ elsif !key.to_s.match(/^[A-Z][a-z]+[A-Z]/).nil? # upper camel
56
+ :upper_camel
57
+ elsif !key.to_s.match(/^[A-Z][a-z]+\-[A-Z]/).nil? # Train-Case
58
+ :train
59
+ else
60
+ # Could just be one word in that case who knows what case it is
61
+ nil
62
+ end
63
+ end
64
+
65
+ if cases_found.compact.uniq.size == 1
66
+ cases_found.compact.first
67
+ else
68
+ return @default_case
69
+ end
70
+
71
+ end
72
+
73
+ def find_matching_keys(to_find)
74
+ @hash.keys.select do |key|
75
+ self.class.clean_key(to_find) == self.class.clean_key(key)
76
+ end
77
+ end
78
+
79
+ # string for comparison
80
+ def self.clean_key(key)
81
+ key.to_s.downcase.gsub(/[\W_]/, '')
82
+ end
83
+
84
+ private
85
+ def handle_assignment(method_name, value, allow_override=false)
86
+ # assign all matching keys
87
+ keys = find_matching_keys(method_name)
88
+
89
+ # assigning a new value
90
+ if keys.empty?
91
+ # allow custom case via []=
92
+ if method_name.is_a?(String) && allow_override
93
+ @hash[method_name] = value
94
+ else
95
+ @hash[ensure_default_case(method_name)] = value
96
+ end
97
+
98
+ end
99
+
100
+ keys.each do |key|
101
+ @hash[key] = value
102
+ end
103
+
104
+ value
105
+ end
106
+
107
+ def handle_get(method_name)
108
+ keys = find_matching_keys(method_name)
109
+
110
+ value = @hash[keys.first]
111
+
112
+ if value.is_a?(Array)
113
+ # Make any hashes in the array an ostruct
114
+ value.map do |item|
115
+ item.is_a?(Hash) ? RecursiveCaseIndifferentOstruct.new(item, @default_case) : item
116
+ end
117
+ elsif value.is_a?(Hash)
118
+ RecursiveCaseIndifferentOstruct.new(value, @default_case)
119
+ else
120
+ value
121
+ end
122
+ end
123
+
124
+
125
+ def ensure_default_case(key)
126
+ # Pull out the words/numbers from the key name
127
+ # find: lower camel UPCASE numbers
128
+ re = /( (?:[a-z]+) | (?:[A-Z][a-z]+) | (?:[A-Z]+) | [0-9]+ )/x
129
+ words = key.to_s.scan(re).map do |word|
130
+ word = word.first
131
+ if word.scan(/[A-Z]/).size < 2
132
+ word.downcase
133
+ else
134
+ word # it's an acronym in whichcase lets leave it.
135
+ end
136
+ end
137
+
138
+ if @default_case == :snake
139
+ words.join('_')
140
+
141
+ elsif @default_case == :kabab
142
+ words.join('-')
143
+
144
+ elsif @default_case == :lower_camel
145
+ words.map.with_index do |w, i|
146
+ if i > 0
147
+ w[0] = w[0].upcase
148
+ w
149
+ else
150
+ w
151
+ end
152
+ end.join
153
+
154
+ elsif @default_case == :upper_camel
155
+ words.map do |w|
156
+ w[0] = w[0].upcase
157
+ end.join
158
+
159
+ elsif @default_case == :train
160
+ words.map do |w|
161
+ w[0] = w[0].upcase
162
+ end.join('-')
163
+ end
164
+
165
+ end
166
+
167
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: recursive_case_indifferent_ostruct
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Tyler Roberts
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: See github.
14
+ email: code@polar-concepts.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - lib/recursive_case_indifferent_ostruct.rb
21
+ homepage: http://github.com/bdevel/recursive_case_indifferent_ostruct
22
+ licenses:
23
+ - MIT
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.4.5
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: Access JSON or Hashes attributes without regards to snake_case, camelCase,
45
+ or kabab-case.
46
+ test_files: []