{"action":"created","comment":{"url":"https://api.github.com/repos/manastech/crystal/pulls/comments/40377523","id":40377523,"diff_hunk":"@@ -78,22 +80,188 @@ module HTTP\n       form_builder.to_s\n     end\n \n+    protected getter raw_params\n+    def initialize(@raw_params)\n+    end\n+\n+    def ==(other : self)\n+      self.raw_params == other.raw_params\n+    end\n+\n+    def ==(other)\n+      false\n+    end\n+\n+    # Returns first value for specified param name.\n+    #\n+    # ```\n+    # params[\"email\"]                # => \"john@example.org\"\n+    # params[\"non_existent_param\"]   # KeyError\n+    # ```\n+    def [](name)\n+      raw_params[name].first\n+    end\n+\n+    # Returns true if param with provided name exists.\n+    #\n+    # ```\n+    # params.has_key?(\"email\")       # => true\n+    # params.has_key?(\"garbage\")     # => false\n+    # ```\n+    delegate has_key?, raw_params\n+\n+    # Sets first value for specified param name.\n+    #\n+    # ```\n+    # params[\"item\"] = \"pencil\"\n+    # ```\n+    def []=(name, value)\n+      raw_params[name] ||= [\"\"]\n+      raw_params[name][0] = value\n+    end\n+\n+    # Returns all values for specified param name.\n+    #\n+    # ```\n+    # params.fetch_all(\"item\")       # => [\"pencil\", \"book\", \"workbook\"]\n+    # ```\n+    def fetch_all(name)\n+      raw_params.fetch(name) { [] of String }\n+    end\n+\n+    # Returns first value for specified param name.\n+    #\n+    # ```\n+    # params.fetch(\"email\")                # => \"john@example.org\"\n+    # params.fetch(\"non_existent_param\")   # KeyError\n+    # ```\n+    def fetch(name)\n+      raw_params.fetch(name).first\n+    end\n+\n+    # Returns first value for specified param name. Fallbacks to provided\n+    # default value when there is no such param.\n+    #\n+    # ```\n+    # params.fetch(\"email\", \"none@example.org\")                # => \"john@example.org\"\n+    # params.fetch(\"non_existent_param\", \"default value\")      # => \"default value\"\n+    # ```\n+    def fetch(name, default)\n+      return default unless has_key?(name)\n+      fetch(name)\n+    end\n+\n+    # Returns first value for specified param name. Fallbacks to return value\n+    # of provided block when there is no such param.\n+    #\n+    # ```\n+    # params.fetch(\"email\") { raise InvalidUser(\"email is missing\") }    # InvalidUser \"email is missing\"\n+    # params.fetch(\"non_existent_param\") { \"default computed value\" }    # => \"default computed value\"\n+    # ```\n+    def fetch(name)\n+      return yield unless has_key?(name)\n+      fetch(name)\n+    end\n+\n+    # Appends new value for specified param name. Creates param when there was\n+    # no such param.\n+    #\n+    # ```\n+    # params.add(\"item\", \"keychain\")\n+    # params.fetch_all(\"item\")        # => [\"pencil\", \"book\", \"workbook\", \"keychain\"]\n+    # ```\n+    def add(name, value)\n+      raw_params[name] ||= [] of String\n+      raw_params[name] = [] of String if raw_params[name] == [\"\"]\n+      raw_params[name] << value\n+    end\n+\n+    # Sets all values for specified param name at once.\n+    #\n+    # ```\n+    # params.set_all(\"item\", [\"keychain\", \"keynote\"])\n+    # params.fetch_all(\"item\")        # => [\"keychain\", \"keynote\"]\n+    # ```\n+    def set_all(name, values)\n+      raw_params[name] = values\n+    end\n+\n+    # Allows to iterate over all name-value pairs.\n+    #\n+    # ```\n+    # params.each do |name, value|\n+    #   puts \"#{name} => #{value}\"\n+    # end\n+    #\n+    # # Outputs:\n+    # # email => john@example.org\n+    # # item => keychain\n+    # # item => keynote\n+    # ```\n+    def each\n+      raw_params.each do |name, values|\n+        values.each do |value|\n+          yield(name, value)\n+        end\n+      end\n+    end\n+\n+    # Deletes first value for provided param name. If there are no values left,\n+    # deletes param itself. Returns deleted value.\n+    #\n+    # ```\n+    # params.delete(\"item\")     # => \"keychain\"\n+    # params.fetch_all(\"item\")  # => [\"keynote\"]\n+    #\n+    # params.delete(\"item\")     # => \"keynote\"\n+    # params[\"item\"]            # KeyError\n+    #\n+    # params.delete(\"non_existent_param\")  # KeyError\n+    # ```\n+    def delete(name)\n+      value = raw_params[name].shift\n+      raw_params.delete(name) if raw_params[name].size == 0\n+      value\n+    end\n+\n+    # Deletes all values for provided param name. Returns array of deleted\n+    # values.\n+    #\n+    # ```\n+    # params.delete_all(\"comments\")   # => [\"hello, world!\", \":+1:\"]\n+    # params.has_key?(\"comments\")     # => false\n+    # ```\n+    def delete_all(name)\n+      raw_params.delete(name)\n+    end\n+\n+    # Serializes to string representation as http url encoded form\n+    #\n+    # ```\n+    # params.to_s     # => \"item=keychain&item=keynote&email=john@example.org\"\n+    # ```\n+    def to_s(io)\n+      builder = Builder.new(io)\n+      each do |name, value|\n+        builder.add(name, value)\n+      end\n+    end\n+\n     # :nodoc:\n     struct Builder\n-      def initialize\n-        @string = StringIO.new\n+      def initialize(@io = StringIO.new)\n       end\n \n       def add(key, value)\n-        @string << '&' unless @string.empty?\n-        URI.escape key, @string\n-        @string << '='\n-        URI.escape value, @string if value\n+        @io << '&' unless @io.empty?","path":"src/http/params.cr","position":207,"original_position":207,"commit_id":"0002782d5599527981ed7518d83919c6559cf44f","original_commit_id":"0002782d5599527981ed7518d83919c6559cf44f","user":{"login":"waterlink","id":609046,"avatar_url":"https://avatars.githubusercontent.com/u/609046?v=3","gravatar_id":"","url":"https://api.github.com/users/waterlink","html_url":"https://github.com/waterlink","followers_url":"https://api.github.com/users/waterlink/followers","following_url":"https://api.github.com/users/waterlink/following{/other_user}","gists_url":"https://api.github.com/users/waterlink/gists{/gist_id}","starred_url":"https://api.github.com/users/waterlink/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/waterlink/subscriptions","organizations_url":"https://api.github.com/users/waterlink/orgs","repos_url":"https://api.github.com/users/waterlink/repos","events_url":"https://api.github.com/users/waterlink/events{/privacy}","received_events_url":"https://api.github.com/users/waterlink/received_events","type":"User","site_admin":false},"body":"BTW, if I define type like this `@io :: IO` in the beginning of this struct - it catches the problem at compile time :) Should I leave it like that?","created_at":"2015-09-24T21:46:43Z","updated_at":"2015-09-24T21:46:43Z","html_url":"https://github.com/manastech/crystal/pull/1569#discussion_r40377523","pull_request_url":"https://api.github.com/repos/manastech/crystal/pulls/1569","_links":{"self":{"href":"https://api.github.com/repos/manastech/crystal/pulls/comments/40377523"},"html":{"href":"https://github.com/manastech/crystal/pull/1569#discussion_r40377523"},"pull_request":{"href":"https://api.github.com/repos/manastech/crystal/pulls/1569"}}},"pull_request":{"url":"https://api.github.com/repos/manastech/crystal/pulls/1569","id":45774640,"html_url":"https://github.com/manastech/crystal/pull/1569","diff_url":"https://github.com/manastech/crystal/pull/1569.diff","patch_url":"https://github.com/manastech/crystal/pull/1569.patch","issue_url":"https://api.github.com/repos/manastech/crystal/issues/1569","number":1569,"state":"open","locked":false,"title":"Add HTTP::Params class - convenient wrapper over raw http params","user":{"login":"waterlink","id":609046,"avatar_url":"https://avatars.githubusercontent.com/u/609046?v=3","gravatar_id":"","url":"https://api.github.com/users/waterlink","html_url":"https://github.com/waterlink","followers_url":"https://api.github.com/users/waterlink/followers","following_url":"https://api.github.com/users/waterlink/following{/other_user}","gists_url":"https://api.github.com/users/waterlink/gists{/gist_id}","starred_url":"https://api.github.com/users/waterlink/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/waterlink/subscriptions","organizations_url":"https://api.github.com/users/waterlink/orgs","repos_url":"https://api.github.com/users/waterlink/repos","events_url":"https://api.github.com/users/waterlink/events{/privacy}","received_events_url":"https://api.github.com/users/waterlink/received_events","type":"User","site_admin":false},"body":"This PR adds a convenient wrapper over raw http params `Hash(String, Array(String))`.\r\n\r\nIt allows developer to treat http params like a `Hash(String, String)` in most of the cases. And only if one needs to access `multiple-values-per-parameter` functionality, this wrapper has special methods for that.\r\n\r\nIn one of my next PRs I want to return instance of this class from `HTTP::Request#query_params` and `#post_params` (or `body_params` - still not sure about last name).\r\n\r\n/cc @asterite @waj @jhass ","created_at":"2015-09-23T12:48:10Z","updated_at":"2015-09-24T21:46:43Z","closed_at":null,"merged_at":null,"merge_commit_sha":"fe51948881327283e6d32af320219060c51eae01","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/manastech/crystal/pulls/1569/commits","review_comments_url":"https://api.github.com/repos/manastech/crystal/pulls/1569/comments","review_comment_url":"https://api.github.com/repos/manastech/crystal/pulls/comments{/number}","comments_url":"https://api.github.com/repos/manastech/crystal/issues/1569/comments","statuses_url":"https://api.github.com/repos/manastech/crystal/statuses/0002782d5599527981ed7518d83919c6559cf44f","head":{"label":"waterlink:http/params/make-it-a-convenient-hash-wrapper","ref":"http/params/make-it-a-convenient-hash-wrapper","sha":"0002782d5599527981ed7518d83919c6559cf44f","user":{"login":"waterlink","id":609046,"avatar_url":"https://avatars.githubusercontent.com/u/609046?v=3","gravatar_id":"","url":"https://api.github.com/users/waterlink","html_url":"https://github.com/waterlink","followers_url":"https://api.github.com/users/waterlink/followers","following_url":"https://api.github.com/users/waterlink/following{/other_user}","gists_url":"https://api.github.com/users/waterlink/gists{/gist_id}","starred_url":"https://api.github.com/users/waterlink/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/waterlink/subscriptions","organizations_url":"https://api.github.com/users/waterlink/orgs","repos_url":"https://api.github.com/users/waterlink/repos","events_url":"https://api.github.com/users/waterlink/events{/privacy}","received_events_url":"https://api.github.com/users/waterlink/received_events","type":"User","site_admin":false},"repo":{"id":33809796,"name":"crystal","full_name":"waterlink/crystal","owner":{"login":"waterlink","id":609046,"avatar_url":"https://avatars.githubusercontent.com/u/609046?v=3","gravatar_id":"","url":"https://api.github.com/users/waterlink","html_url":"https://github.com/waterlink","followers_url":"https://api.github.com/users/waterlink/followers","following_url":"https://api.github.com/users/waterlink/following{/other_user}","gists_url":"https://api.github.com/users/waterlink/gists{/gist_id}","starred_url":"https://api.github.com/users/waterlink/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/waterlink/subscriptions","organizations_url":"https://api.github.com/users/waterlink/orgs","repos_url":"https://api.github.com/users/waterlink/repos","events_url":"https://api.github.com/users/waterlink/events{/privacy}","received_events_url":"https://api.github.com/users/waterlink/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/waterlink/crystal","description":"The Crystal Programming Language","fork":true,"url":"https://api.github.com/repos/waterlink/crystal","forks_url":"https://api.github.com/repos/waterlink/crystal/forks","keys_url":"https://api.github.com/repos/waterlink/crystal/keys{/key_id}","collaborators_url":"https://api.github.com/repos/waterlink/crystal/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/waterlink/crystal/teams","hooks_url":"https://api.github.com/repos/waterlink/crystal/hooks","issue_events_url":"https://api.github.com/repos/waterlink/crystal/issues/events{/number}","events_url":"https://api.github.com/repos/waterlink/crystal/events","assignees_url":"https://api.github.com/repos/waterlink/crystal/assignees{/user}","branches_url":"https://api.github.com/repos/waterlink/crystal/branches{/branch}","tags_url":"https://api.github.com/repos/waterlink/crystal/tags","blobs_url":"https://api.github.com/repos/waterlink/crystal/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/waterlink/crystal/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/waterlink/crystal/git/refs{/sha}","trees_url":"https://api.github.com/repos/waterlink/crystal/git/trees{/sha}","statuses_url":"https://api.github.com/repos/waterlink/crystal/statuses/{sha}","languages_url":"https://api.github.com/repos/waterlink/crystal/languages","stargazers_url":"https://api.github.com/repos/waterlink/crystal/stargazers","contributors_url":"https://api.github.com/repos/waterlink/crystal/contributors","subscribers_url":"https://api.github.com/repos/waterlink/crystal/subscribers","subscription_url":"https://api.github.com/repos/waterlink/crystal/subscription","commits_url":"https://api.github.com/repos/waterlink/crystal/commits{/sha}","git_commits_url":"https://api.github.com/repos/waterlink/crystal/git/commits{/sha}","comments_url":"https://api.github.com/repos/waterlink/crystal/comments{/number}","issue_comment_url":"https://api.github.com/repos/waterlink/crystal/issues/comments{/number}","contents_url":"https://api.github.com/repos/waterlink/crystal/contents/{+path}","compare_url":"https://api.github.com/repos/waterlink/crystal/compare/{base}...{head}","merges_url":"https://api.github.com/repos/waterlink/crystal/merges","archive_url":"https://api.github.com/repos/waterlink/crystal/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/waterlink/crystal/downloads","issues_url":"https://api.github.com/repos/waterlink/crystal/issues{/number}","pulls_url":"https://api.github.com/repos/waterlink/crystal/pulls{/number}","milestones_url":"https://api.github.com/repos/waterlink/crystal/milestones{/number}","notifications_url":"https://api.github.com/repos/waterlink/crystal/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/waterlink/crystal/labels{/name}","releases_url":"https://api.github.com/repos/waterlink/crystal/releases{/id}","created_at":"2015-04-12T08:50:14Z","updated_at":"2015-04-21T16:16:15Z","pushed_at":"2015-09-23T20:33:07Z","git_url":"git://github.com/waterlink/crystal.git","ssh_url":"git@github.com:waterlink/crystal.git","clone_url":"https://github.com/waterlink/crystal.git","svn_url":"https://github.com/waterlink/crystal","homepage":"http://crystal-lang.org","size":27547,"stargazers_count":0,"watchers_count":0,"language":"Crystal","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"manastech:master","ref":"master","sha":"9c164d87418cf3a781ff675e1d8bc703867bb5cc","user":{"login":"manastech","id":1354950,"avatar_url":"https://avatars.githubusercontent.com/u/1354950?v=3","gravatar_id":"","url":"https://api.github.com/users/manastech","html_url":"https://github.com/manastech","followers_url":"https://api.github.com/users/manastech/followers","following_url":"https://api.github.com/users/manastech/following{/other_user}","gists_url":"https://api.github.com/users/manastech/gists{/gist_id}","starred_url":"https://api.github.com/users/manastech/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/manastech/subscriptions","organizations_url":"https://api.github.com/users/manastech/orgs","repos_url":"https://api.github.com/users/manastech/repos","events_url":"https://api.github.com/users/manastech/events{/privacy}","received_events_url":"https://api.github.com/users/manastech/received_events","type":"Organization","site_admin":false},"repo":{"id":6887813,"name":"crystal","full_name":"manastech/crystal","owner":{"login":"manastech","id":1354950,"avatar_url":"https://avatars.githubusercontent.com/u/1354950?v=3","gravatar_id":"","url":"https://api.github.com/users/manastech","html_url":"https://github.com/manastech","followers_url":"https://api.github.com/users/manastech/followers","following_url":"https://api.github.com/users/manastech/following{/other_user}","gists_url":"https://api.github.com/users/manastech/gists{/gist_id}","starred_url":"https://api.github.com/users/manastech/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/manastech/subscriptions","organizations_url":"https://api.github.com/users/manastech/orgs","repos_url":"https://api.github.com/users/manastech/repos","events_url":"https://api.github.com/users/manastech/events{/privacy}","received_events_url":"https://api.github.com/users/manastech/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/manastech/crystal","description":"The Crystal Programming Language","fork":false,"url":"https://api.github.com/repos/manastech/crystal","forks_url":"https://api.github.com/repos/manastech/crystal/forks","keys_url":"https://api.github.com/repos/manastech/crystal/keys{/key_id}","collaborators_url":"https://api.github.com/repos/manastech/crystal/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/manastech/crystal/teams","hooks_url":"https://api.github.com/repos/manastech/crystal/hooks","issue_events_url":"https://api.github.com/repos/manastech/crystal/issues/events{/number}","events_url":"https://api.github.com/repos/manastech/crystal/events","assignees_url":"https://api.github.com/repos/manastech/crystal/assignees{/user}","branches_url":"https://api.github.com/repos/manastech/crystal/branches{/branch}","tags_url":"https://api.github.com/repos/manastech/crystal/tags","blobs_url":"https://api.github.com/repos/manastech/crystal/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/manastech/crystal/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/manastech/crystal/git/refs{/sha}","trees_url":"https://api.github.com/repos/manastech/crystal/git/trees{/sha}","statuses_url":"https://api.github.com/repos/manastech/crystal/statuses/{sha}","languages_url":"https://api.github.com/repos/manastech/crystal/languages","stargazers_url":"https://api.github.com/repos/manastech/crystal/stargazers","contributors_url":"https://api.github.com/repos/manastech/crystal/contributors","subscribers_url":"https://api.github.com/repos/manastech/crystal/subscribers","subscription_url":"https://api.github.com/repos/manastech/crystal/subscription","commits_url":"https://api.github.com/repos/manastech/crystal/commits{/sha}","git_commits_url":"https://api.github.com/repos/manastech/crystal/git/commits{/sha}","comments_url":"https://api.github.com/repos/manastech/crystal/comments{/number}","issue_comment_url":"https://api.github.com/repos/manastech/crystal/issues/comments{/number}","contents_url":"https://api.github.com/repos/manastech/crystal/contents/{+path}","compare_url":"https://api.github.com/repos/manastech/crystal/compare/{base}...{head}","merges_url":"https://api.github.com/repos/manastech/crystal/merges","archive_url":"https://api.github.com/repos/manastech/crystal/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/manastech/crystal/downloads","issues_url":"https://api.github.com/repos/manastech/crystal/issues{/number}","pulls_url":"https://api.github.com/repos/manastech/crystal/pulls{/number}","milestones_url":"https://api.github.com/repos/manastech/crystal/milestones{/number}","notifications_url":"https://api.github.com/repos/manastech/crystal/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/manastech/crystal/labels{/name}","releases_url":"https://api.github.com/repos/manastech/crystal/releases{/id}","created_at":"2012-11-27T17:32:32Z","updated_at":"2015-09-24T21:25:48Z","pushed_at":"2015-09-24T21:22:05Z","git_url":"git://github.com/manastech/crystal.git","ssh_url":"git@github.com:manastech/crystal.git","clone_url":"https://github.com/manastech/crystal.git","svn_url":"https://github.com/manastech/crystal","homepage":"http://crystal-lang.org","size":46742,"stargazers_count":3415,"watchers_count":3415,"language":"Crystal","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":254,"mirror_url":null,"open_issues_count":231,"forks":254,"open_issues":231,"watchers":3415,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/manastech/crystal/pulls/1569"},"html":{"href":"https://github.com/manastech/crystal/pull/1569"},"issue":{"href":"https://api.github.com/repos/manastech/crystal/issues/1569"},"comments":{"href":"https://api.github.com/repos/manastech/crystal/issues/1569/comments"},"review_comments":{"href":"https://api.github.com/repos/manastech/crystal/pulls/1569/comments"},"review_comment":{"href":"https://api.github.com/repos/manastech/crystal/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/manastech/crystal/pulls/1569/commits"},"statuses":{"href":"https://api.github.com/repos/manastech/crystal/statuses/0002782d5599527981ed7518d83919c6559cf44f"}}}}