| 79 | | # Splits the given key into several pieces. Example keys are 'name', 'person[name]', |
|---|
| 80 | | # 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned. |
|---|
| 81 | | # 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', ''] |
|---|
| 82 | | def split_key(key) |
|---|
| 83 | | if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key |
|---|
| 84 | | keys = [$1] |
|---|
| 85 | | |
|---|
| 86 | | keys.concat($2[1..-2].split('][')) |
|---|
| 87 | | keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings |
|---|
| 88 | | |
|---|
| 89 | | keys |
|---|
| 90 | | else |
|---|
| 91 | | [key] |
|---|
| 92 | | end |
|---|
| 93 | | end |
|---|
| 94 | | |
|---|
| | 120 | |
|---|
| | 121 | class QueryStringScanner < StringScanner |
|---|
| | 122 | |
|---|
| | 123 | attr_reader :top, :parent, :result |
|---|
| | 124 | |
|---|
| | 125 | def initialize(string) |
|---|
| | 126 | super(string) |
|---|
| | 127 | end |
|---|
| | 128 | |
|---|
| | 129 | KEY_REGEXP = %r{([^\[\]=&]+)} |
|---|
| | 130 | BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} |
|---|
| | 131 | |
|---|
| | 132 | # Parse the query string |
|---|
| | 133 | def parse |
|---|
| | 134 | @result = {} |
|---|
| | 135 | until eos? |
|---|
| | 136 | # Parse each & delimited chunk |
|---|
| | 137 | @parent, @top = nil, result |
|---|
| | 138 | |
|---|
| | 139 | # First scan the bare key |
|---|
| | 140 | key = scan(KEY_REGEXP) or (skip_term and next) |
|---|
| | 141 | key = post_key_check(key) |
|---|
| | 142 | |
|---|
| | 143 | # Then scan as many nestings as present |
|---|
| | 144 | until check(/\=/) || eos? |
|---|
| | 145 | r = scan(BRACKETED_KEY_REGEXP) or (skip_term and break) |
|---|
| | 146 | key = self[1] |
|---|
| | 147 | key = post_key_check(key) |
|---|
| | 148 | end |
|---|
| | 149 | |
|---|
| | 150 | # Scan the value if we see an = |
|---|
| | 151 | if scan %r{=} |
|---|
| | 152 | value = scan(/[^\&]+/) # scan_until doesn't handle \Z |
|---|
| | 153 | value = CGI.unescape(value) if value # May be nil when eos? |
|---|
| | 154 | bind key, value |
|---|
| | 155 | end |
|---|
| | 156 | scan %r/\&+/ # Ignore multiple adjacent &'s |
|---|
| | 157 | |
|---|
| | 158 | end |
|---|
| | 159 | |
|---|
| | 160 | return result |
|---|
| | 161 | end |
|---|
| | 162 | |
|---|
| | 163 | # Skip over the current term by scanning past the next &, or to |
|---|
| | 164 | # then end of the string if there is no next & |
|---|
| | 165 | def skip_term |
|---|
| | 166 | scan_until(%r/\&+/) || scan(/.+/) |
|---|
| | 167 | end |
|---|
| | 168 | |
|---|
| | 169 | # After we see a key, we must look ahead to determine our next action. Cases: |
|---|
| | 170 | # |
|---|
| | 171 | # [] follows the key. Then the value must be an array. |
|---|
| | 172 | # = follows the key. (A value comes next) |
|---|
| | 173 | # & or the end of string follows the key. Then the key is a flag. |
|---|
| | 174 | # otherwise, a hash follows the key. |
|---|
| | 175 | def post_key_check(key) |
|---|
| | 176 | if eos? || check(/\&/) # a& or a\Z indicates a is a flag. |
|---|
| | 177 | bind key, nil # Curiously enough, the flag's value is nil |
|---|
| | 178 | nil |
|---|
| | 179 | elsif scan(/\[\]/) # a[b][] indicates that b is an array |
|---|
| | 180 | container key, Array |
|---|
| | 181 | nil |
|---|
| | 182 | elsif check(/\[[^\]]/) # a[b] indicates that a is a hash |
|---|
| | 183 | container key, Hash |
|---|
| | 184 | nil |
|---|
| | 185 | else # Presumably an = sign is next. |
|---|
| | 186 | key |
|---|
| | 187 | end |
|---|
| | 188 | end |
|---|
| | 189 | |
|---|
| | 190 | # Add a container to the stack. |
|---|
| | 191 | # |
|---|
| | 192 | def container(key, klass) |
|---|
| | 193 | raise TypeError if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) |
|---|
| | 194 | value = bind(key, klass.new) |
|---|
| | 195 | raise TypeError unless value.is_a? klass |
|---|
| | 196 | push value |
|---|
| | 197 | end |
|---|
| | 198 | |
|---|
| | 199 | # Push a value onto the 'stack', which is actually only the top 2 items. |
|---|
| | 200 | def push(value) |
|---|
| | 201 | @parent, @top = @top, value |
|---|
| | 202 | end |
|---|
| | 203 | |
|---|
| | 204 | # Bind a key (which may be nil for items in an array) to the provided value. |
|---|
| | 205 | def bind(key, value) |
|---|
| | 206 | if top.is_a? Array |
|---|
| | 207 | if key |
|---|
| | 208 | if top[-1].is_a?(Hash) && ! top[-1].key?(key) |
|---|
| | 209 | top[-1][key] = value |
|---|
| | 210 | else |
|---|
| | 211 | top << {key => value} |
|---|
| | 212 | push top.last |
|---|
| | 213 | end |
|---|
| | 214 | else |
|---|
| | 215 | top << value |
|---|
| | 216 | end |
|---|
| | 217 | elsif top.is_a? Hash |
|---|
| | 218 | key = CGI.unescape(key) |
|---|
| | 219 | if top.key?(key) && parent.is_a?(Array) |
|---|
| | 220 | parent << (@top = {}) |
|---|
| | 221 | end |
|---|
| | 222 | return top[key] ||= value |
|---|
| | 223 | else |
|---|
| | 224 | # Do nothing? |
|---|
| | 225 | end |
|---|
| | 226 | return value |
|---|
| | 227 | end |
|---|
| | 228 | end |
|---|
| | 229 | |
|---|