├── .babelrc ├── .gitignore ├── .postcssrc.yml ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── app.json ├── app ├── .DS_Store ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ └── .keep │ └── stylesheets │ │ └── custom.css.scss ├── controllers │ ├── api_responses_controller.rb │ ├── application_controller.rb │ └── home_controller.rb ├── javascript │ ├── Main.elm │ ├── Request │ │ ├── MainRequest.elm │ │ ├── RequestBasicAuthentication.elm │ │ ├── RequestBody.elm │ │ ├── RequestHeaders.elm │ │ └── RequestParameters.elm │ ├── Response │ │ ├── JSVal.elm │ │ ├── JsonViewer.elm │ │ ├── JsonViewerTypes.elm │ │ └── MainResponse.elm │ ├── Router.elm │ ├── Utils │ │ ├── HttpMethods.elm │ │ ├── HttpUtil.elm │ │ └── Util.elm │ ├── Views │ │ └── NotFound.elm │ ├── images │ │ └── .gitkeep │ └── packs │ │ ├── application.css │ │ └── application.js ├── jobs │ └── purge_record_job.rb ├── models │ ├── api_response.rb │ └── application_record.rb ├── services │ ├── api_request_parser_service.rb │ └── request_service.rb └── views │ ├── api_responses │ └── show.json.jbuilder │ ├── home │ └── index.html.erb │ └── layouts │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb ├── bin ├── bundle ├── delayed_job ├── rails ├── rake ├── setup ├── spring ├── update ├── webpack ├── webpack-dev-server └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml.postgresql ├── database.yml.postgresqlapp ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ ├── staging.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── delayed_job_config.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── scheduled_job.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml ├── spring.rb ├── webpack │ ├── development.js │ ├── environment.js │ ├── production.js │ └── test.js └── webpacker.yml ├── db ├── migrate │ ├── 20171109155254_create_api_responses.rb │ ├── 20171222085456_add_timestamps_to_api_responses.rb │ └── 20180504053823_create_delayed_jobs.rb └── schema.rb ├── elm-package.json ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ └── assets.rake ├── log └── .keep ├── package.json ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── test ├── application_system_test_case.rb ├── controllers │ └── .keep ├── fixtures │ ├── .keep │ └── files │ │ └── .keep ├── helpers │ └── .keep ├── integration │ └── .keep ├── mailers │ └── .keep ├── models │ └── .keep ├── system │ └── .keep └── test_helper.rb ├── tmp └── .keep ├── vendor └── .keep └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": "> 1%", 7 | "uglify": true 8 | }, 9 | "useBuiltIns": true 10 | }] 11 | ], 12 | 13 | "plugins": [ 14 | "syntax-dynamic-import", 15 | "transform-object-rest-spread", 16 | ["transform-class-properties", { "spec": true }] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | /config/database.yml 14 | 15 | # Ignore all logfiles and tempfiles. 16 | /log/* 17 | /tmp/* 18 | !/log/.keep 19 | !/tmp/.keep 20 | 21 | /elm-stuff 22 | /node_modules 23 | /yarn-error.log 24 | 25 | .byebug_history 26 | /public/packs 27 | /public/packs-test 28 | /elm-stuff 29 | /node_modules 30 | logfile 31 | package-lock.json 32 | .vscode 33 | .idea/ -------------------------------------------------------------------------------- /.postcssrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | postcss-smart-import: {} 3 | postcss-cssnext: {} 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.1 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 4 | 5 | ruby '2.5.1' 6 | 7 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 8 | gem 'rails', '~> 5.1.6' 9 | 10 | # Use Puma as the app server 11 | gem 'puma', '~> 3.7' 12 | 13 | # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker 14 | gem 'webpacker' 15 | 16 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 17 | gem 'jbuilder', '~> 2.5' 18 | 19 | # Use PostgreSQL database 20 | gem 'pg' 21 | 22 | # For making HTTP requests outside the app 23 | gem 'rest-client' 24 | 25 | # for deleting records periodically 26 | gem 'delayed_job_active_record' 27 | gem 'scheduled_job' 28 | 29 | group :development, :test do 30 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 31 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 32 | 33 | # For debugging 34 | gem 'pry' 35 | end 36 | 37 | group :development do 38 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 39 | gem 'web-console', '>= 3.3.0' 40 | gem 'listen', '>= 3.0.5', '< 3.2' 41 | 42 | # For booting Rails app and required dependant services 43 | gem 'foreman' 44 | end 45 | 46 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 47 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 48 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.1.6) 5 | actionpack (= 5.1.6) 6 | nio4r (~> 2.0) 7 | websocket-driver (~> 0.6.1) 8 | actionmailer (5.1.6) 9 | actionpack (= 5.1.6) 10 | actionview (= 5.1.6) 11 | activejob (= 5.1.6) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.1.6) 15 | actionview (= 5.1.6) 16 | activesupport (= 5.1.6) 17 | rack (~> 2.0) 18 | rack-test (>= 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.1.6) 22 | activesupport (= 5.1.6) 23 | builder (~> 3.1) 24 | erubi (~> 1.4) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | activejob (5.1.6) 28 | activesupport (= 5.1.6) 29 | globalid (>= 0.3.6) 30 | activemodel (5.1.6) 31 | activesupport (= 5.1.6) 32 | activerecord (5.1.6) 33 | activemodel (= 5.1.6) 34 | activesupport (= 5.1.6) 35 | arel (~> 8.0) 36 | activesupport (5.1.6) 37 | concurrent-ruby (~> 1.0, >= 1.0.2) 38 | i18n (>= 0.7, < 2) 39 | minitest (~> 5.1) 40 | tzinfo (~> 1.1) 41 | arel (8.0.0) 42 | bindex (0.5.0) 43 | builder (3.2.3) 44 | byebug (10.0.2) 45 | coderay (1.1.2) 46 | concurrent-ruby (1.0.5) 47 | crass (1.0.4) 48 | delayed_job (4.1.5) 49 | activesupport (>= 3.0, < 5.3) 50 | delayed_job_active_record (4.1.3) 51 | activerecord (>= 3.0, < 5.3) 52 | delayed_job (>= 3.0, < 5) 53 | domain_name (0.5.20180417) 54 | unf (>= 0.0.5, < 1.0.0) 55 | erubi (1.7.1) 56 | ffi (1.9.23) 57 | foreman (0.84.0) 58 | thor (~> 0.19.1) 59 | globalid (0.4.1) 60 | activesupport (>= 4.2.0) 61 | http-cookie (1.0.3) 62 | domain_name (~> 0.5) 63 | i18n (1.0.1) 64 | concurrent-ruby (~> 1.0) 65 | jbuilder (2.7.0) 66 | activesupport (>= 4.2.0) 67 | multi_json (>= 1.2) 68 | listen (3.1.5) 69 | rb-fsevent (~> 0.9, >= 0.9.4) 70 | rb-inotify (~> 0.9, >= 0.9.7) 71 | ruby_dep (~> 1.2) 72 | loofah (2.2.2) 73 | crass (~> 1.0.2) 74 | nokogiri (>= 1.5.9) 75 | mail (2.7.0) 76 | mini_mime (>= 0.1.1) 77 | method_source (0.9.0) 78 | mime-types (3.1) 79 | mime-types-data (~> 3.2015) 80 | mime-types-data (3.2016.0521) 81 | mini_mime (1.0.0) 82 | mini_portile2 (2.3.0) 83 | minitest (5.11.3) 84 | multi_json (1.13.1) 85 | netrc (0.11.0) 86 | nio4r (2.3.1) 87 | nokogiri (1.8.2) 88 | mini_portile2 (~> 2.3.0) 89 | pg (1.0.0) 90 | pry (0.11.3) 91 | coderay (~> 1.1.0) 92 | method_source (~> 0.9.0) 93 | puma (3.11.4) 94 | rack (2.0.5) 95 | rack-proxy (0.6.4) 96 | rack 97 | rack-test (1.0.0) 98 | rack (>= 1.0, < 3) 99 | rails (5.1.6) 100 | actioncable (= 5.1.6) 101 | actionmailer (= 5.1.6) 102 | actionpack (= 5.1.6) 103 | actionview (= 5.1.6) 104 | activejob (= 5.1.6) 105 | activemodel (= 5.1.6) 106 | activerecord (= 5.1.6) 107 | activesupport (= 5.1.6) 108 | bundler (>= 1.3.0) 109 | railties (= 5.1.6) 110 | sprockets-rails (>= 2.0.0) 111 | rails-dom-testing (2.0.3) 112 | activesupport (>= 4.2.0) 113 | nokogiri (>= 1.6) 114 | rails-html-sanitizer (1.0.4) 115 | loofah (~> 2.2, >= 2.2.2) 116 | railties (5.1.6) 117 | actionpack (= 5.1.6) 118 | activesupport (= 5.1.6) 119 | method_source 120 | rake (>= 0.8.7) 121 | thor (>= 0.18.1, < 2.0) 122 | rake (12.3.1) 123 | rb-fsevent (0.10.3) 124 | rb-inotify (0.9.10) 125 | ffi (>= 0.5.0, < 2) 126 | rest-client (2.0.2) 127 | http-cookie (>= 1.0.2, < 2.0) 128 | mime-types (>= 1.16, < 4.0) 129 | netrc (~> 0.8) 130 | ruby_dep (1.5.0) 131 | scheduled_job (0.2.5) 132 | delayed_job (< 4.2) 133 | delayed_job_active_record (< 4.2) 134 | sprockets (3.7.1) 135 | concurrent-ruby (~> 1.0) 136 | rack (> 1, < 3) 137 | sprockets-rails (3.2.1) 138 | actionpack (>= 4.0) 139 | activesupport (>= 4.0) 140 | sprockets (>= 3.0.0) 141 | thor (0.19.4) 142 | thread_safe (0.3.6) 143 | tzinfo (1.2.5) 144 | thread_safe (~> 0.1) 145 | unf (0.1.4) 146 | unf_ext 147 | unf_ext (0.0.7.5) 148 | web-console (3.6.2) 149 | actionview (>= 5.0) 150 | activemodel (>= 5.0) 151 | bindex (>= 0.4.0) 152 | railties (>= 5.0) 153 | webpacker (3.5.3) 154 | activesupport (>= 4.2) 155 | rack-proxy (>= 0.6.1) 156 | railties (>= 4.2) 157 | websocket-driver (0.6.5) 158 | websocket-extensions (>= 0.1.0) 159 | websocket-extensions (0.1.3) 160 | 161 | PLATFORMS 162 | ruby 163 | 164 | DEPENDENCIES 165 | byebug 166 | delayed_job_active_record 167 | foreman 168 | jbuilder (~> 2.5) 169 | listen (>= 3.0.5, < 3.2) 170 | pg 171 | pry 172 | puma (~> 3.7) 173 | rails (~> 5.1.6) 174 | rest-client 175 | scheduled_job 176 | tzinfo-data 177 | web-console (>= 3.3.0) 178 | webpacker 179 | 180 | RUBY VERSION 181 | ruby 2.5.1p57 182 | 183 | BUNDLED WITH 184 | 1.16.1 185 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: bin/rails db:migrate 2 | web: bin/rails server -p $PORT -e $RAILS_ENV 3 | worker: rake jobs:work -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | rails: ./bin/rails server -p 3333 2 | webpack: ./bin/webpack-dev-server 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApiSnapshot 2 | 3 | Takes a snapshot of the API response and creates a unique URL for each response. 4 | Now you can share that URL in email, slack or anywhere else. 5 | The URL and the data it shows can't be changed. 6 | 7 | If you submit another request then you will get another URL. 8 | 9 | Please note that records are deleted after 30 days. 10 | 11 | ## Technology Stack 12 | 13 | This project is built using Elm and Ruby on Rails. 14 | 15 | ## Local Development Setup 16 | 17 | ``` 18 | cp config/database.yml.postgresql config/database.yml 19 | 20 | ./bin/bundle install 21 | 22 | ./bin/rails db:setup 23 | 24 | ./bin/yarn install 25 | 26 | npm install -g elm 27 | 28 | elm-package install 29 | 30 | ./bin/yarn start 31 | ``` 32 | 33 | Once we see `webpack: Compiled successfully.` message in terminal, 34 | we can visit the app at http://localhost:3333. 35 | 36 | Webpack will automatically compile if a file inside `app/javascript/` directory is modified in development mode. 37 | 38 | ## Heroku Review 39 | 40 | [Heroku Review](https://devcenter.heroku.com/articles/github-integration-review-apps) 41 | is enabled on this application. It means when a PR is sent then heroku 42 | automatically deploys an application for that branch. 43 | 44 | 45 | ## About BigBinary 46 | 47 |  48 | 49 | ApiSnapshot is maintained by [BigBinary](https://www.BigBinary.com). BigBinary is a software consultancy company. We build web and mobile applications using Ruby on Rails, React.js, React Native and Elm. 50 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apisnapshot", 3 | "scripts": {}, 4 | "env": { 5 | "SECRET_KEY_BASE": { 6 | "generator": "secret" 7 | }, 8 | "RACK_ENV": { 9 | "value": "staging" 10 | }, 11 | "RAILS_ENV": { 12 | "value": "staging" 13 | }, 14 | "HEROKU_APP_NAME": { 15 | "required": true 16 | }, 17 | "LOG_LEVEL": { 18 | "value": "DEBUG" 19 | } 20 | }, 21 | "formation": { 22 | "web": { 23 | "quantity": 1 24 | } 25 | }, 26 | "addons": [ 27 | { 28 | "plan": "heroku-postgresql", 29 | "options": { 30 | "version": "9.5" 31 | } 32 | } 33 | ], 34 | "buildpacks": [ 35 | { 36 | "url": "heroku/nodejs" 37 | }, 38 | { 39 | "url": "heroku/ruby" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbinary/apisnapshot/c877183223d33b4b384b2b86f931185d7482e802/app/.DS_Store -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbinary/apisnapshot/c877183223d33b4b384b2b86f931185d7482e802/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/stylesheets/custom.css.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbinary/apisnapshot/c877183223d33b4b384b2b86f931185d7482e802/app/assets/stylesheets/custom.css.scss -------------------------------------------------------------------------------- /app/controllers/api_responses_controller.rb: -------------------------------------------------------------------------------- 1 | class ApiResponsesController < ApplicationController 2 | 3 | before_action :load_api_response, only: [:show] 4 | 5 | def show 6 | respond_to do |format| 7 | format.json 8 | end 9 | end 10 | 11 | def create 12 | request_service = RequestService.new(url: api_request_params[:url], 13 | method: api_request_params[:method], 14 | options: options_for_request_service) 15 | request_service.process 16 | 17 | if request_service.errors.present? 18 | render json: request_service, status: 422 19 | else 20 | render json: request_service.api_response, status: 200 21 | end 22 | end 23 | 24 | private 25 | 26 | def load_api_response 27 | unless @api_response = ApiResponse.find_by({token: params[:id]}) 28 | render json: {error: "Invalid Page"}, status: 404 29 | end 30 | end 31 | 32 | def options_for_request_service 33 | parsed_request_params = ApiRequestParserService.new(api_request_params).process 34 | api_request_params.merge(parsed_request_params).to_h 35 | end 36 | 37 | def api_request_params 38 | params.permit(:url, :method, :request_body, 39 | request_headers: [:key, :value], 40 | request_parameters: [:key, :value]) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | 4 | force_ssl if: :ssl_configured? 5 | 6 | def ssl_configured? 7 | !(Rails.env.development? || Rails.env.staging? || Rails.env.test?) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | 3 | def index 4 | render 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /app/javascript/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | import Navigation exposing (Location) 4 | import Router exposing (..) 5 | import Response.MainResponse 6 | import Request.MainRequest 7 | import Request.RequestParameters 8 | import Request.RequestHeaders 9 | import Views.NotFound 10 | import Html exposing (Html, div, ul, li, a, text) 11 | import RemoteData 12 | import Http 13 | import Utils.HttpUtil as HttpUtil exposing (..) 14 | import Utils.HttpMethods as HttpMethods exposing (HttpMethod) 15 | import Tuple exposing (..) 16 | 17 | 18 | -- MODEL -- 19 | 20 | 21 | type alias RequestBasicAuthentication = 22 | { username : String 23 | , password : String 24 | } 25 | 26 | 27 | type alias Model = 28 | { request : Request.MainRequest.Model 29 | , response : Response.MainResponse.Model 30 | , route : Route 31 | } 32 | 33 | 34 | init : Location -> ( Model, Cmd Msg ) 35 | init location = 36 | updateRoute (Router.parseLocation location) 37 | { request = Request.MainRequest.init location 38 | , response = Response.MainResponse.init 39 | , route = HomeRoute 40 | } 41 | 42 | 43 | 44 | -- VIEW -- 45 | 46 | 47 | view : Model -> Html Msg 48 | view model = 49 | let 50 | homeRouteMarkup = 51 | div [] 52 | [ Request.MainRequest.view model.request |> Html.map RequestMsg 53 | , Response.MainResponse.view model.response |> Html.map ResponseMsg 54 | ] 55 | in 56 | case model.route of 57 | Router.HomeRoute -> 58 | homeRouteMarkup 59 | 60 | Router.HitRoute _ -> 61 | homeRouteMarkup 62 | 63 | _ -> 64 | Views.NotFound.view model 65 | 66 | 67 | 68 | -- SUBSCRIPTIONS -- 69 | 70 | 71 | subscriptions : Model -> Sub Msg 72 | subscriptions model = 73 | Sub.none 74 | 75 | 76 | 77 | -- UPDATE -- 78 | 79 | 80 | type Msg 81 | = RequestMsg Request.MainRequest.Msg 82 | | ResponseMsg Response.MainResponse.Msg 83 | | OnSubmitResponse (RemoteData.WebData Response) 84 | | OnLocationChange Location 85 | | OnHitFetchResponse (RemoteData.WebData Response) 86 | 87 | 88 | navigateToHitPermalinkCommand : Maybe String -> Cmd Msg 89 | navigateToHitPermalinkCommand maybeToken = 90 | case maybeToken of 91 | Just token -> 92 | Navigation.newUrl ("#/hits/" ++ token) 93 | 94 | Nothing -> 95 | Cmd.none 96 | 97 | 98 | update : Msg -> Model -> ( Model, Cmd Msg ) 99 | update msg model = 100 | case msg of 101 | RequestMsg phrMsg -> 102 | let 103 | areRequestParametersValid = 104 | Request.RequestParameters.valid (Request.MainRequest.getRequestParameters model.request) 105 | 106 | areRequestHeadersValid = 107 | Request.RequestHeaders.valid (Request.MainRequest.getRequestHeaders model.request) 108 | 109 | shouldSubmit = 110 | (Request.MainRequest.isUrlValid (Request.MainRequest.getRequestUrl model.request)) && areRequestParametersValid && areRequestHeadersValid 111 | 112 | ( updatedRequest, updatedResponse, subCmd ) = 113 | case phrMsg of 114 | Request.MainRequest.Submit -> 115 | let 116 | ( uReq, _ ) = 117 | Request.MainRequest.update (Request.MainRequest.SetError (not shouldSubmit)) model.request 118 | 119 | ( uRes, subCmd ) = 120 | case shouldSubmit of 121 | True -> 122 | ( first <| 123 | (Response.MainResponse.update (Response.MainResponse.UpdateResponse RemoteData.Loading) model.response) 124 | , requestCommand model 125 | ) 126 | 127 | _ -> 128 | ( model.response, Cmd.none ) 129 | in 130 | ( uReq, uRes, subCmd ) 131 | 132 | _ -> 133 | let 134 | ( uReq, _ ) = 135 | Request.MainRequest.update phrMsg model.request 136 | in 137 | ( uReq, model.response, Cmd.none ) 138 | in 139 | ( { model | request = updatedRequest, response = updatedResponse }, subCmd ) 140 | 141 | ResponseMsg phrMsg -> 142 | let 143 | ( updatedResponse, subCmd ) = 144 | Response.MainResponse.update phrMsg model.response 145 | in 146 | ( { model | response = updatedResponse }, Cmd.map ResponseMsg subCmd ) 147 | 148 | OnLocationChange location -> 149 | updateRoute (parseLocation location) model 150 | 151 | OnHitFetchResponse response -> 152 | let 153 | ( updatedRequest, _ ) = 154 | Request.MainRequest.update (Request.MainRequest.OnHitFetchResponse response) model.request 155 | 156 | ( updatedResponse, _ ) = 157 | Response.MainResponse.update (Response.MainResponse.UpdateResponse response) model.response 158 | 159 | updatedModel = 160 | case response of 161 | RemoteData.Success successResponse -> 162 | { model 163 | | request = updatedRequest 164 | , response = updatedResponse 165 | } 166 | 167 | _ -> 168 | model 169 | in 170 | updatedModel ! [] 171 | 172 | OnSubmitResponse response -> 173 | let 174 | cmd = 175 | case response of 176 | RemoteData.Success successResponse -> 177 | HttpUtil.decodeTokenFromResponse successResponse 178 | |> navigateToHitPermalinkCommand 179 | 180 | _ -> 181 | Cmd.none 182 | 183 | ( updatedResponse, _ ) = 184 | Response.MainResponse.update (Response.MainResponse.UpdateResponse response) model.response 185 | in 186 | { model | response = updatedResponse } ! [ cmd ] 187 | 188 | 189 | updateRoute : Route -> Model -> ( Model, Cmd Msg ) 190 | updateRoute route model = 191 | let 192 | cmd = 193 | case route of 194 | HitRoute token -> 195 | fetchHitDataCommand token 196 | 197 | _ -> 198 | Cmd.none 199 | 200 | ( response, request ) = 201 | if cmd == Cmd.none then 202 | ( first <| 203 | Response.MainResponse.update (Response.MainResponse.UpdateResponse RemoteData.NotAsked) model.response 204 | , first <| 205 | (Request.MainRequest.update Request.MainRequest.EmptyRequest model.request) 206 | ) 207 | else 208 | ( first <| 209 | Response.MainResponse.update (Response.MainResponse.UpdateResponse RemoteData.Loading) model.response 210 | , model.request 211 | ) 212 | in 213 | { model | route = route, request = request, response = response } ! [ cmd ] 214 | 215 | 216 | requestCommand : Model -> Cmd Msg 217 | requestCommand model = 218 | let 219 | requestPath = 220 | "/api_responses" 221 | 222 | requestBody = 223 | Http.jsonBody (Request.MainRequest.encodeRequest model.request) 224 | 225 | request = 226 | HttpUtil.buildRequest requestPath HttpMethods.Post requestBody 227 | in 228 | RemoteData.sendRequest request 229 | |> Cmd.map OnSubmitResponse 230 | 231 | 232 | fetchHitDataCommand : String -> Cmd Msg 233 | fetchHitDataCommand token = 234 | let 235 | requestPath = 236 | "/api_responses/" ++ token 237 | 238 | request = 239 | HttpUtil.buildRequest requestPath HttpMethods.Get Http.emptyBody 240 | in 241 | RemoteData.sendRequest request 242 | |> Cmd.map OnHitFetchResponse 243 | 244 | 245 | 246 | -- MAIN -- 247 | 248 | 249 | main : Program Never Model Msg 250 | main = 251 | Navigation.program OnLocationChange 252 | { view = view 253 | , init = init 254 | , update = update 255 | , subscriptions = subscriptions 256 | } 257 | -------------------------------------------------------------------------------- /app/javascript/Request/MainRequest.elm: -------------------------------------------------------------------------------- 1 | module Request.MainRequest exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Encode exposing (..) 5 | import Json.Decode 6 | import Utils.HttpMethods as HttpMethods exposing (HttpMethod, avaialableHttpMethodsString, parse) 7 | import RemoteData 8 | import Utils.Util as Util exposing (..) 9 | import Html exposing (..) 10 | import Html.Attributes exposing (..) 11 | import Html.Events exposing (..) 12 | import Utils.HttpUtil as HttpUtil exposing (..) 13 | import Request.RequestParameters as RequestParameters exposing (..) 14 | import Request.RequestHeaders as RequestHeaders exposing (..) 15 | import Request.RequestBody as RequestBody exposing (requestBodyEncoder, RequestBodyType, Msg, update) 16 | import Request.RequestBasicAuthentication as RequestBasicAuthentication exposing (..) 17 | 18 | 19 | -- MODEL -- 20 | 21 | 22 | type alias Model = 23 | { request : Request 24 | , showErrors : Bool 25 | } 26 | 27 | 28 | init location = 29 | { request = emptyRequest 30 | , showErrors = False 31 | } 32 | 33 | 34 | 35 | -- UPDATE -- 36 | 37 | 38 | type Msg 39 | = Submit 40 | | SetError Bool 41 | | ChangeUrl String 42 | | MoreActionsDropdownChange DropDownAction 43 | | HttpMethodsDropdownChange String 44 | | RequestHeaderMsg RequestHeaders.Msg 45 | | RequestParameterMsg RequestParameters.Msg 46 | | RequestBodyMsg RequestBody.Msg 47 | | RequestBasicAuthenticationMsg RequestBasicAuthentication.Msg 48 | | OnHitFetchResponse (RemoteData.WebData HttpUtil.Response) 49 | | EmptyRequest 50 | 51 | 52 | update : Msg -> Model -> ( Model, Cmd Msg ) 53 | update msg model = 54 | case msg of 55 | SetError err -> 56 | ( { model | showErrors = err }, Cmd.none ) 57 | 58 | ChangeUrl newUrl -> 59 | ( changeUrl model newUrl, Cmd.none ) 60 | 61 | MoreActionsDropdownChange selectedOption -> 62 | case selectedOption of 63 | DDAddParameter -> 64 | let 65 | currentRequest = 66 | model.request 67 | 68 | ( newParameters, subCmd ) = 69 | RequestParameters.update RequestParameters.AddRequestParameter currentRequest.requestParameters 70 | 71 | newRequest = 72 | { currentRequest | requestParameters = newParameters } 73 | in 74 | ( { model | request = newRequest }, Cmd.map RequestParameterMsg subCmd ) 75 | 76 | DDAddHeader -> 77 | let 78 | currentRequest = 79 | model.request 80 | 81 | ( newHeaders, subCmd ) = 82 | RequestHeaders.update RequestHeaders.AddRequestHeader currentRequest.requestHeaders 83 | 84 | newRequest = 85 | { currentRequest | requestHeaders = newHeaders } 86 | in 87 | ( { model | request = newRequest }, Cmd.map RequestHeaderMsg subCmd ) 88 | 89 | DDAddBody -> 90 | let 91 | currentRequest = 92 | model.request 93 | 94 | newRequest = 95 | { currentRequest | requestBody = Just RequestBody.emptyBody } 96 | in 97 | ( { model | request = newRequest }, Cmd.none ) 98 | 99 | DDAddBasicAuthentication -> 100 | let 101 | currentRequest = 102 | model.request 103 | 104 | newRequest = 105 | { currentRequest | basicAuthentication = Just RequestBasicAuthentication.empty } 106 | in 107 | ( { model | request = newRequest }, Cmd.none ) 108 | 109 | HttpMethodsDropdownChange selectedHttpMethodString -> 110 | let 111 | currentRequest = 112 | model.request 113 | 114 | newRequest = 115 | { currentRequest | httpMethod = parse selectedHttpMethodString } 116 | in 117 | ( { model | request = newRequest }, Cmd.none ) 118 | 119 | RequestParameterMsg rpsMsg -> 120 | let 121 | currentRequest = 122 | model.request 123 | 124 | ( newParameters, subCmd ) = 125 | RequestParameters.update rpsMsg currentRequest.requestParameters 126 | 127 | newRequest = 128 | { currentRequest | requestParameters = newParameters } 129 | in 130 | ( { model | request = newRequest }, Cmd.map RequestParameterMsg subCmd ) 131 | 132 | RequestHeaderMsg rhsMsg -> 133 | let 134 | currentRequest = 135 | model.request 136 | 137 | ( newHeaders, subCmd ) = 138 | RequestHeaders.update rhsMsg currentRequest.requestHeaders 139 | 140 | newRequest = 141 | { currentRequest | requestHeaders = newHeaders } 142 | in 143 | ( { model | request = newRequest }, Cmd.map RequestHeaderMsg subCmd ) 144 | 145 | RequestBodyMsg rbsMsg -> 146 | let 147 | currentRequest = 148 | model.request 149 | 150 | ( newBody, subCmd ) = 151 | RequestBody.update rbsMsg currentRequest.requestBody 152 | 153 | newRequest = 154 | { currentRequest | requestBody = newBody } 155 | in 156 | ( { model | request = newRequest }, Cmd.map RequestBodyMsg subCmd ) 157 | 158 | OnHitFetchResponse response -> 159 | let 160 | updatedModel = 161 | case response of 162 | RemoteData.Success successResponse -> 163 | { model 164 | | request = 165 | HttpUtil.decodeHitResponseIntoRequest successResponse 166 | } 167 | 168 | _ -> 169 | model 170 | in 171 | ( updatedModel, Cmd.none ) 172 | 173 | EmptyRequest -> 174 | ( { model | request = emptyRequest }, Cmd.none ) 175 | 176 | RequestBasicAuthenticationMsg rMsg -> 177 | case rMsg of 178 | RemoveBasicAuthentication -> 179 | let 180 | currentRequest = 181 | model.request 182 | 183 | newRequest = 184 | { currentRequest | basicAuthentication = Nothing } 185 | in 186 | ( { model | request = newRequest }, Cmd.none ) 187 | 188 | _ -> 189 | let 190 | currentRequest = 191 | model.request 192 | 193 | currentBa = 194 | currentRequest.basicAuthentication 195 | 196 | ( newRba, _ ) = 197 | RequestBasicAuthentication.update rMsg currentBa 198 | 199 | newRequest = 200 | case currentBa of 201 | Just b -> 202 | { currentRequest | basicAuthentication = newRba } 203 | 204 | Nothing -> 205 | currentRequest 206 | in 207 | ( { model | request = newRequest }, Cmd.none ) 208 | 209 | _ -> 210 | ( model, Cmd.none ) 211 | 212 | 213 | changeUrl : Model -> String -> Model 214 | changeUrl model newUrl = 215 | let 216 | currentRequest = 217 | model.request 218 | in 219 | { model | showErrors = False, request = { currentRequest | url = newUrl } } 220 | 221 | 222 | encodeRequest : Model -> Json.Encode.Value 223 | encodeRequest ({ request } as model) = 224 | let 225 | attributes = 226 | [ ( "url", string request.url ) 227 | , ( "method", string <| HttpMethods.toString <| request.httpMethod ) 228 | , ( "request_parameters", requestParametersEncoder request.requestParameters ) 229 | ] 230 | 231 | addBA atr = 232 | case request.basicAuthentication of 233 | Just ba -> 234 | atr ++ [ ( "username", string ba.username ), ( "password", string ba.password ) ] 235 | 236 | Nothing -> 237 | atr 238 | 239 | addBody atr = 240 | case request.requestBody of 241 | Just rb -> 242 | let 243 | bodyHeader = 244 | case rb.bodyType of 245 | RequestBody.BodyText -> 246 | "text/plain" 247 | 248 | RequestBody.BodyJSON -> 249 | "application/json" 250 | 251 | currentHeaders = 252 | request.requestHeaders 253 | 254 | contentTypeHeader = 255 | { key = "content_type" 256 | , value = bodyHeader 257 | } 258 | 259 | newHeaders = 260 | Dict.insert (Dict.size currentHeaders) contentTypeHeader currentHeaders 261 | in 262 | atr ++ [ ( "request_body", string rb.value ) ] ++ [ ( "request_headers", requestHeadersEncoder newHeaders ) ] 263 | 264 | Nothing -> 265 | atr ++ [ ( "request_headers", requestHeadersEncoder request.requestHeaders ) ] 266 | in 267 | Json.Encode.object (addBA <| addBody <| attributes) 268 | 269 | 270 | getRequestUrl : Model -> String 271 | getRequestUrl model = 272 | model.request.url 273 | 274 | 275 | getRequestHeaders : Model -> RequestHeaders 276 | getRequestHeaders model = 277 | model.request.requestHeaders 278 | 279 | 280 | getRequestParameters : Model -> RequestParameters 281 | getRequestParameters model = 282 | model.request.requestParameters 283 | 284 | 285 | 286 | -- VIEW -- 287 | 288 | 289 | type DropDownAction 290 | = DDAddParameter 291 | | DDAddHeader 292 | | DDAddBody 293 | | DDAddBasicAuthentication 294 | 295 | 296 | view : Model -> Html Msg 297 | view model = 298 | div [ class "row form-controls" ] [ formView model ] 299 | 300 | 301 | formView : Model -> Html Msg 302 | formView ({ request } as model) = 303 | Html.form 304 | [ class "bootstrap-center-form api-req-form__form col" 305 | , onSubmit Submit 306 | , action "javascript:void(0)" 307 | ] 308 | [ div [ class "api-req-form__url-group" ] 309 | [ httpMethodDropdown request.httpMethod 310 | , urlInputField model 311 | , morePullDownMenu 312 | , button [ class "btn btn-primary", type_ "Submit" ] [ text "Send" ] 313 | ] 314 | , requestBasicAuthenticationView request |> Html.map RequestBasicAuthenticationMsg 315 | , requestHeadersView request model.showErrors |> Html.map RequestHeaderMsg 316 | , requestParametersView request model.showErrors |> Html.map RequestParameterMsg 317 | , requestBodyView request model.showErrors |> Html.map RequestBodyMsg 318 | ] 319 | 320 | 321 | isUrlValid : String -> Bool 322 | isUrlValid = 323 | Util.isStringPresent 324 | 325 | 326 | urlInputField : Model -> Html Msg 327 | urlInputField model = 328 | let 329 | defaultClass = 330 | "input form-control required" 331 | 332 | shouldShowError = 333 | model.showErrors && not (isUrlValid model.request.url) 334 | 335 | updatedClass = 336 | if shouldShowError then 337 | defaultClass ++ " is-invalid" 338 | else 339 | defaultClass 340 | 341 | viewValidationError = 342 | if shouldShowError then 343 | div [ class "invalid-feedback" ] [ text "Please enter a URL" ] 344 | else 345 | text "" 346 | in 347 | div [ class "api-req-form__url-control" ] 348 | [ input 349 | [ class updatedClass 350 | , name "url" 351 | , type_ "text" 352 | , placeholder "Enter URL here" 353 | , onInput ChangeUrl 354 | , value model.request.url 355 | ] 356 | [] 357 | , viewValidationError 358 | ] 359 | 360 | 361 | morePullDownMenu = 362 | div [ class "dropdown api-req-form__btn-group btn-group" ] 363 | [ button 364 | [ type_ "button" 365 | , class "btn btn-default dropdown-toggle" 366 | , attribute "data-toggle" "dropdown" 367 | , attribute "aria-haspopup" "true" 368 | , attribute "aria-expanded" "false" 369 | ] 370 | [ text "More" ] 371 | , div 372 | [ class "dropdown-menu dropdown-menu-right", attribute "aria-labelledby" "dropdownMenuButton" ] 373 | [ dropDownItem DDAddBasicAuthentication 374 | , dropDownItem DDAddHeader 375 | , dropDownItem DDAddParameter 376 | , dropDownItem DDAddBody 377 | ] 378 | ] 379 | 380 | 381 | dropDownItem : DropDownAction -> Html Msg 382 | dropDownItem item = 383 | a 384 | [ class "dropdown-item" 385 | , href "javascript:void(0)" 386 | , onClick <| MoreActionsDropdownChange item 387 | ] 388 | [ text <| dropDownActionToString item ] 389 | 390 | 391 | dropDownActionToString : DropDownAction -> String 392 | dropDownActionToString dda = 393 | case dda of 394 | DDAddParameter -> 395 | "Add Parameter" 396 | 397 | DDAddHeader -> 398 | "Add Header" 399 | 400 | DDAddBody -> 401 | "Add Request Body" 402 | 403 | DDAddBasicAuthentication -> 404 | "Add Basic Authentication" 405 | 406 | 407 | requestParametersView : Request -> Bool -> Html RequestParameters.Msg 408 | requestParametersView { requestParameters } showErrors = 409 | if Dict.isEmpty requestParameters then 410 | text "" 411 | else 412 | RequestParameters.view requestParameters showErrors 413 | 414 | 415 | requestHeadersView : Request -> Bool -> Html RequestHeaders.Msg 416 | requestHeadersView { requestHeaders } showErrors = 417 | if Dict.isEmpty requestHeaders then 418 | text "" 419 | else 420 | RequestHeaders.view requestHeaders showErrors 421 | 422 | 423 | requestBasicAuthenticationView : Request -> Html RequestBasicAuthentication.Msg 424 | requestBasicAuthenticationView { basicAuthentication } = 425 | case basicAuthentication of 426 | Nothing -> 427 | text "" 428 | 429 | Just b -> 430 | RequestBasicAuthentication.view b 431 | 432 | 433 | requestBodyView : Request -> Bool -> Html RequestBody.Msg 434 | requestBodyView { requestBody } showErrors = 435 | case requestBody of 436 | Nothing -> 437 | text "" 438 | 439 | Just b -> 440 | RequestBody.view b.value b.bodyType showErrors 441 | 442 | 443 | httpMethodDropdown : HttpMethod -> Html Msg 444 | httpMethodDropdown selectedHttpMethod = 445 | div [] 446 | [ select 447 | [ class "form-control required" 448 | , on "change" <| Json.Decode.map HttpMethodsDropdownChange targetValue 449 | ] 450 | (avaialableHttpMethodsString 451 | |> List.map (HttpMethods.toString selectedHttpMethod |> httpMethodDropdownOption) 452 | ) 453 | ] 454 | 455 | 456 | httpMethodDropdownOption : String -> String -> Html msg 457 | httpMethodDropdownOption selectedMethodString httpMethodString = 458 | option 459 | [ value httpMethodString 460 | , selected (selectedMethodString == httpMethodString) 461 | ] 462 | [ text httpMethodString ] 463 | -------------------------------------------------------------------------------- /app/javascript/Request/RequestBasicAuthentication.elm: -------------------------------------------------------------------------------- 1 | module Request.RequestBasicAuthentication exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (..) 6 | import Json.Encode exposing (..) 7 | 8 | 9 | type alias BasicAuthentication = 10 | { username : String 11 | , password : String 12 | } 13 | 14 | 15 | empty : BasicAuthentication 16 | empty = 17 | { username = "" 18 | , password = "" 19 | } 20 | 21 | 22 | 23 | -- UPDATE -- 24 | 25 | 26 | type Msg 27 | = RemoveBasicAuthentication 28 | | UpdateUsername String 29 | | UpdatePassword String 30 | 31 | 32 | update : Msg -> Maybe BasicAuthentication -> ( Maybe BasicAuthentication, Cmd Msg ) 33 | update msg model = 34 | case msg of 35 | UpdateUsername n -> 36 | let 37 | new = 38 | Maybe.map (\m -> { m | username = n }) model 39 | in 40 | ( new, Cmd.none ) 41 | 42 | UpdatePassword p -> 43 | let 44 | new = 45 | Maybe.map (\m -> { m | password = p }) model 46 | in 47 | ( new, Cmd.none ) 48 | 49 | _ -> 50 | ( model, Cmd.none ) 51 | 52 | 53 | 54 | -- VIEW -- 55 | 56 | 57 | view : BasicAuthentication -> Html Msg 58 | view b = 59 | div [ class "form-group" ] 60 | [ div [ class "form-group__label" ] [ span [] [ text "Basic Authentication" ] ] 61 | , div [ class "aapi-req-form__form-inline" ] 62 | [ div [ class "form-row" ] 63 | [ div [ class "col" ] 64 | [ input 65 | [ type_ "text" 66 | , placeholder "Username" 67 | , class "input form-control api-req-form__input" 68 | , value b.username 69 | , onInput (UpdateUsername) 70 | ] 71 | [] 72 | ] 73 | , div [ class "col" ] 74 | [ input 75 | [ type_ "password" 76 | , placeholder "Password" 77 | , class "input form-control api-req-form__input" 78 | , value b.password 79 | , onInput (UpdatePassword) 80 | ] 81 | [] 82 | ] 83 | , div [ class "col" ] 84 | [ a 85 | [ href "javascript:void(0)" 86 | , class "row__delete" 87 | , onClick (RemoveBasicAuthentication) 88 | ] 89 | [ text "×" ] 90 | ] 91 | ] 92 | ] 93 | ] 94 | -------------------------------------------------------------------------------- /app/javascript/Request/RequestBody.elm: -------------------------------------------------------------------------------- 1 | module Request.RequestBody exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (..) 6 | import Json.Encode exposing (..) 7 | 8 | 9 | -- MODEL -- 10 | 11 | 12 | type RequestBodyType 13 | = BodyJSON 14 | | BodyText 15 | 16 | 17 | type alias RequestBody = 18 | { bodyType : RequestBodyType 19 | , value : String 20 | } 21 | 22 | 23 | emptyBody = 24 | { bodyType = BodyText 25 | , value = "" 26 | } 27 | 28 | 29 | 30 | -- UPDATE -- 31 | 32 | 33 | type Msg 34 | = UpdateRequestBody RequestBody 35 | | RemoveRequestBody 36 | 37 | 38 | update : Msg -> Maybe RequestBody -> ( Maybe RequestBody, Cmd Msg ) 39 | update msg model = 40 | case msg of 41 | UpdateRequestBody val -> 42 | let 43 | requestBody = 44 | Just val 45 | in 46 | ( requestBody, Cmd.none ) 47 | 48 | RemoveRequestBody -> 49 | let 50 | requestBody = 51 | Nothing 52 | in 53 | ( requestBody, Cmd.none ) 54 | 55 | 56 | 57 | -- VIEW -- 58 | 59 | 60 | view bodyString bodyType showErrors = 61 | div [ class "form-row" ] 62 | [ div [ class "form-group__label" ] 63 | [ text "Request Body: " 64 | , select [ class "api-req-form__request-bod", onInput (onSelect bodyString) ] 65 | [ option [ isTextSelected bodyType, value "text" ] [ text "Text" ] 66 | , option [ isJsonSelected bodyType, value "json" ] [ text "JSON" ] 67 | ] 68 | , a [ class "btn devise-links", onClick RemoveRequestBody, href "javascript:void(0)" ] [ text "Remove Request Body" ] 69 | ] 70 | , textarea 71 | [ class "form-control api-req-form__textarea" 72 | , rows 8 73 | , cols 8 74 | , onInput (\val -> UpdateRequestBody { bodyType = bodyType, value = val }) 75 | ] 76 | [ text bodyString ] 77 | ] 78 | 79 | 80 | onSelect strngVal val = 81 | case val of 82 | "text" -> 83 | UpdateRequestBody { bodyType = BodyText, value = strngVal } 84 | 85 | "json" -> 86 | UpdateRequestBody { bodyType = BodyJSON, value = strngVal } 87 | 88 | _ -> 89 | UpdateRequestBody { bodyType = BodyText, value = strngVal } 90 | 91 | 92 | isTextSelected bodyType = 93 | case bodyType of 94 | BodyJSON -> 95 | selected False 96 | 97 | BodyText -> 98 | selected True 99 | 100 | 101 | isJsonSelected bodyType = 102 | case bodyType of 103 | BodyJSON -> 104 | selected True 105 | 106 | BodyText -> 107 | selected False 108 | 109 | 110 | requestBodyEncoder : RequestBody -> Value 111 | requestBodyEncoder rb = 112 | Json.Encode.object 113 | [ ( "bodyType", (requestBodyTypeEncode rb.bodyType) ) 114 | , ( "value", string rb.value ) 115 | ] 116 | 117 | 118 | requestBodyTypeEncode : RequestBodyType -> Value 119 | requestBodyTypeEncode rbt = 120 | case rbt of 121 | BodyJSON -> 122 | string "BodyJSON" 123 | 124 | BodyText -> 125 | string "BodyText" 126 | -------------------------------------------------------------------------------- /app/javascript/Request/RequestHeaders.elm: -------------------------------------------------------------------------------- 1 | module Request.RequestHeaders exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import Json.Encode exposing (..) 8 | import Utils.Util exposing (isStringPresent) 9 | 10 | 11 | -- TYPES -- 12 | 13 | 14 | type alias RequestHeader = 15 | { key : String 16 | , value : String 17 | } 18 | 19 | 20 | type alias Position = 21 | Int 22 | 23 | 24 | type alias RequestHeaders = 25 | Dict Position RequestHeader 26 | 27 | 28 | blankRequestHeader : RequestHeader 29 | blankRequestHeader = 30 | RequestHeader "" "" 31 | 32 | 33 | empty : RequestHeaders 34 | empty = 35 | Dict.empty 36 | 37 | 38 | 39 | -- UPDATE -- 40 | 41 | 42 | type Msg 43 | = AddRequestHeader 44 | | ChangeRequestHeaderAttribute String Int String 45 | | DeleteRequestHeader Int 46 | 47 | 48 | addHeaders requestHeaders = 49 | let 50 | newRequestHeaders = 51 | pushBlank requestHeaders 52 | in 53 | ( newRequestHeaders, Cmd.none ) 54 | 55 | 56 | update : Msg -> RequestHeaders -> ( RequestHeaders, Cmd Msg ) 57 | update msg requestHeaders = 58 | case msg of 59 | AddRequestHeader -> 60 | addHeaders requestHeaders 61 | 62 | ChangeRequestHeaderAttribute label index value -> 63 | let 64 | newRequestHeaders = 65 | case label of 66 | "Name" -> 67 | updateName index value requestHeaders 68 | 69 | "Value" -> 70 | updateValue index value requestHeaders 71 | 72 | _ -> 73 | requestHeaders 74 | in 75 | ( newRequestHeaders, Cmd.none ) 76 | 77 | DeleteRequestHeader index -> 78 | let 79 | newRequestHeaders = 80 | remove index requestHeaders 81 | in 82 | ( newRequestHeaders, Cmd.none ) 83 | 84 | 85 | pushBlank : RequestHeaders -> RequestHeaders 86 | pushBlank requestHeaders = 87 | push blankRequestHeader requestHeaders 88 | 89 | 90 | push : RequestHeader -> RequestHeaders -> RequestHeaders 91 | push requestHeader requestHeaders = 92 | Dict.insert (Dict.size requestHeaders) requestHeader requestHeaders 93 | 94 | 95 | updateName : Position -> String -> RequestHeaders -> RequestHeaders 96 | updateName position newName requestHeaders = 97 | let 98 | requestHeader = 99 | Dict.get position requestHeaders 100 | 101 | newRequestHeader = 102 | case requestHeader of 103 | Just requestHeader_ -> 104 | { requestHeader_ | key = newName } 105 | 106 | Nothing -> 107 | RequestHeader newName "" 108 | in 109 | updateRequestHeader position newRequestHeader requestHeaders 110 | 111 | 112 | updateValue : Position -> String -> RequestHeaders -> RequestHeaders 113 | updateValue position newValue requestHeaders = 114 | let 115 | requestHeader = 116 | Dict.get position requestHeaders 117 | 118 | newRequestHeader = 119 | case requestHeader of 120 | Just requestHeader_ -> 121 | { requestHeader_ | value = newValue } 122 | 123 | Nothing -> 124 | RequestHeader newValue "" 125 | in 126 | updateRequestHeader position newRequestHeader requestHeaders 127 | 128 | 129 | updateRequestHeader : Position -> RequestHeader -> RequestHeaders -> RequestHeaders 130 | updateRequestHeader position newRequestHeader requestHeaders = 131 | Dict.update position (\_ -> Just newRequestHeader) requestHeaders 132 | 133 | 134 | remove : Position -> RequestHeaders -> RequestHeaders 135 | remove position requestHeaders = 136 | Dict.remove position requestHeaders 137 | |> Dict.foldl 138 | (\_ requestHeader newRequestHeaders -> 139 | Dict.insert (Dict.size newRequestHeaders) requestHeader newRequestHeaders 140 | ) 141 | Dict.empty 142 | 143 | 144 | 145 | -- VIEW -- 146 | 147 | 148 | viewRequestHeader : Bool -> Position -> RequestHeader -> Html Msg 149 | viewRequestHeader showErrors position requestHeader = 150 | div [ class "form-row" ] 151 | [ viewRequestHeaderAttribute "Name" position requestHeader.key showErrors 152 | , viewRequestHeaderAttribute "Value" position requestHeader.value showErrors 153 | , div [ class "col" ] 154 | [ a 155 | [ href "javascript:void(0)" 156 | , class "row__delete" 157 | , onClick (DeleteRequestHeader position) 158 | ] 159 | [ text "×" ] 160 | ] 161 | ] 162 | 163 | 164 | viewRequestHeaderAttribute : String -> Position -> String -> Bool -> Html Msg 165 | viewRequestHeaderAttribute label position value_ showErrors = 166 | let 167 | defaultClass = 168 | "input form-control api-req-form__input" 169 | 170 | shouldShowError = 171 | showErrors && not (isStringPresent value_) 172 | 173 | updatedClass = 174 | if shouldShowError then 175 | defaultClass ++ " is-invalid" 176 | else 177 | defaultClass 178 | 179 | viewValidationError = 180 | if shouldShowError then 181 | div [ class "invalid-feedback" ] [ text "Cannot be empty" ] 182 | else 183 | text "" 184 | in 185 | div [ class "col" ] 186 | [ input 187 | [ type_ "text" 188 | , placeholder ("Enter " ++ label) 189 | , class updatedClass 190 | , value value_ 191 | , onInput (ChangeRequestHeaderAttribute label position) 192 | ] 193 | [] 194 | , viewValidationError 195 | ] 196 | 197 | 198 | viewRequestHeaders : RequestHeaders -> Bool -> Html Msg 199 | viewRequestHeaders requestHeaders showErrors = 200 | div [ class "aapi-req-form__form-inline" ] 201 | (requestHeaders 202 | |> Dict.map (viewRequestHeader showErrors) 203 | |> Dict.toList 204 | |> List.map (\( _, viewRequestHeader ) -> viewRequestHeader) 205 | ) 206 | 207 | 208 | view : RequestHeaders -> Bool -> Html Msg 209 | view requestHeaders showErrors = 210 | div [ class "form-group" ] 211 | [ div [ class "form-group__label" ] 212 | [ span [] [ text "Request Headers" ] 213 | , a [ href "javascript:void(0)", class "devise-links", onClick AddRequestHeader ] [ text "Add Header" ] 214 | ] 215 | , viewRequestHeaders requestHeaders showErrors 216 | ] 217 | 218 | 219 | valid : RequestHeaders -> Bool 220 | valid requestHeaders = 221 | requestHeaders 222 | |> Dict.values 223 | |> List.map (\{ key, value } -> isStringPresent key && isStringPresent value) 224 | |> List.member False 225 | |> not 226 | 227 | 228 | 229 | -- ENCODERS 230 | 231 | 232 | requestHeadersEncoder : RequestHeaders -> Value 233 | requestHeadersEncoder requestParamters = 234 | Dict.map requestHeaderEncoder requestParamters 235 | |> Dict.toList 236 | |> List.map (\( key, value ) -> ( toString key, value )) 237 | |> Json.Encode.object 238 | 239 | 240 | requestHeaderEncoder : Int -> RequestHeader -> Value 241 | requestHeaderEncoder index requestHeader = 242 | Json.Encode.object 243 | [ ( "key", string requestHeader.key ) 244 | , ( "value", string requestHeader.value ) 245 | ] 246 | -------------------------------------------------------------------------------- /app/javascript/Request/RequestParameters.elm: -------------------------------------------------------------------------------- 1 | module Request.RequestParameters exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import Json.Encode exposing (..) 8 | import Utils.Util exposing (isStringPresent) 9 | 10 | 11 | type alias RequestParameter = 12 | { key : String 13 | , value : String 14 | } 15 | 16 | 17 | type alias Position = 18 | Int 19 | 20 | 21 | type alias RequestParameters = 22 | Dict Position RequestParameter 23 | 24 | 25 | blankRequestParameter : RequestParameter 26 | blankRequestParameter = 27 | RequestParameter "" "" 28 | 29 | 30 | empty : RequestParameters 31 | empty = 32 | Dict.empty 33 | 34 | 35 | 36 | -- UPDATE -- 37 | 38 | 39 | type Msg 40 | = AddRequestParameter 41 | | ChangeRequestParameterName Int String 42 | | ChangeRequestParameterValue Int String 43 | | DeleteRequestParameter Int 44 | 45 | 46 | addParameters rp = 47 | let 48 | newRequestParameters = 49 | pushBlank rp 50 | in 51 | ( newRequestParameters, Cmd.none ) 52 | 53 | 54 | update msg model = 55 | case msg of 56 | AddRequestParameter -> 57 | addParameters model 58 | 59 | ChangeRequestParameterName index newName -> 60 | let 61 | newRequestParameters = 62 | updateName index newName model 63 | in 64 | ( newRequestParameters, Cmd.none ) 65 | 66 | ChangeRequestParameterValue index newValue -> 67 | let 68 | newRequestParameters = 69 | updateValue index newValue model 70 | in 71 | ( newRequestParameters, Cmd.none ) 72 | 73 | DeleteRequestParameter index -> 74 | let 75 | newRequestParameters = 76 | remove index model 77 | in 78 | ( newRequestParameters, Cmd.none ) 79 | 80 | 81 | pushBlank : RequestParameters -> RequestParameters 82 | pushBlank requestParameters = 83 | push blankRequestParameter requestParameters 84 | 85 | 86 | push : RequestParameter -> RequestParameters -> RequestParameters 87 | push requestParameter requestParameters = 88 | Dict.insert (Dict.size requestParameters) requestParameter requestParameters 89 | 90 | 91 | updateName : Position -> String -> RequestParameters -> RequestParameters 92 | updateName position newName requestParameters = 93 | let 94 | requestParameter = 95 | Dict.get position requestParameters 96 | 97 | newRequestParameter = 98 | case requestParameter of 99 | Just requestParameter_ -> 100 | { requestParameter_ | key = newName } 101 | 102 | Nothing -> 103 | RequestParameter newName "" 104 | in 105 | updateRequestParameter position newRequestParameter requestParameters 106 | 107 | 108 | updateValue : Position -> String -> RequestParameters -> RequestParameters 109 | updateValue position newValue requestParameters = 110 | let 111 | requestParameter = 112 | Dict.get position requestParameters 113 | 114 | newRequestParameter = 115 | case requestParameter of 116 | Just requestParameter_ -> 117 | { requestParameter_ | value = newValue } 118 | 119 | Nothing -> 120 | RequestParameter newValue "" 121 | in 122 | updateRequestParameter position newRequestParameter requestParameters 123 | 124 | 125 | updateRequestParameter : Position -> RequestParameter -> RequestParameters -> RequestParameters 126 | updateRequestParameter position newRequestParameter requestParameters = 127 | Dict.update position (\_ -> Just newRequestParameter) requestParameters 128 | 129 | 130 | remove : Position -> RequestParameters -> RequestParameters 131 | remove position requestParameters = 132 | Dict.remove position requestParameters 133 | |> Dict.foldl 134 | (\_ requestParameter newRequestParameters -> 135 | Dict.insert (Dict.size newRequestParameters) requestParameter newRequestParameters 136 | ) 137 | Dict.empty 138 | 139 | 140 | 141 | -- VIEW -- 142 | 143 | 144 | viewRequestParameter : Bool -> Position -> RequestParameter -> Html Msg 145 | viewRequestParameter showErrors position requestParameter = 146 | div [ class "form-row" ] 147 | [ viewRequestParameterName position requestParameter showErrors 148 | , div [ class "col" ] 149 | [ input 150 | [ type_ "text" 151 | , placeholder "Enter Value" 152 | , class "input form-control api-req-form__input" 153 | , value requestParameter.value 154 | , onInput (ChangeRequestParameterValue position) 155 | ] 156 | [] 157 | ] 158 | , div [ class "col" ] 159 | [ a 160 | [ href "javascript:void(0)" 161 | , class "row__delete" 162 | , onClick (DeleteRequestParameter position) 163 | ] 164 | [ text "×" ] 165 | ] 166 | ] 167 | 168 | 169 | viewRequestParameterName : Position -> RequestParameter -> Bool -> Html Msg 170 | viewRequestParameterName position { key } showErrors = 171 | let 172 | defaultClass = 173 | "input form-control api-req-form__input" 174 | 175 | shouldShowError = 176 | showErrors && not (isStringPresent key) 177 | 178 | updatedClass = 179 | if shouldShowError then 180 | defaultClass ++ " is-invalid" 181 | else 182 | defaultClass 183 | 184 | viewValidationError = 185 | if shouldShowError then 186 | div [ class "invalid-feedback" ] [ text "Cannot be empty" ] 187 | else 188 | text "" 189 | in 190 | div [ class "col" ] 191 | [ input 192 | [ type_ "text" 193 | , placeholder "Enter Name" 194 | , class updatedClass 195 | , value key 196 | , onInput (ChangeRequestParameterName position) 197 | ] 198 | [] 199 | , viewValidationError 200 | ] 201 | 202 | 203 | viewRequestParameters : RequestParameters -> Bool -> Html Msg 204 | viewRequestParameters requestParameters showErrors = 205 | div [ class "aapi-req-form__form-inline" ] 206 | (requestParameters 207 | |> Dict.map (viewRequestParameter showErrors) 208 | |> Dict.toList 209 | |> List.map (\( _, viewRequestParameter ) -> viewRequestParameter) 210 | ) 211 | 212 | 213 | view : RequestParameters -> Bool -> Html Msg 214 | view requestParameters showErrors = 215 | div [ class "form-group" ] 216 | [ div [ class "form-group__label" ] 217 | [ span [] [ text "Request Parameters" ] 218 | , a [ href "javascript:void(0)", class "devise-links", onClick AddRequestParameter ] [ text "Add Parameter" ] 219 | ] 220 | , viewRequestParameters requestParameters showErrors 221 | ] 222 | 223 | 224 | 225 | -- UTILITY FUNCTIONS 226 | 227 | 228 | valid : RequestParameters -> Bool 229 | valid requestParameters = 230 | requestParameters 231 | |> Dict.values 232 | |> List.map (\{ key } -> isStringPresent key) 233 | |> List.member False 234 | |> not 235 | 236 | 237 | 238 | -- ENCODERS 239 | 240 | 241 | requestParametersEncoder : RequestParameters -> Value 242 | requestParametersEncoder requestParamters = 243 | Dict.map requestParameterEncoder requestParamters 244 | |> Dict.toList 245 | |> List.map (\( key, value ) -> ( toString key, value )) 246 | |> Json.Encode.object 247 | 248 | 249 | requestParameterEncoder : Int -> RequestParameter -> Value 250 | requestParameterEncoder index requestParameter = 251 | Json.Encode.object 252 | [ ( "key", string requestParameter.key ) 253 | , ( "value", string requestParameter.value ) 254 | ] 255 | -------------------------------------------------------------------------------- /app/javascript/Response/JSVal.elm: -------------------------------------------------------------------------------- 1 | module Response.JSVal exposing (..) 2 | 3 | import Json.Decode as Decode exposing (Decoder, Value) 4 | 5 | 6 | type JSVal 7 | = JSString String 8 | | JSFloat Float 9 | | JSInt Int 10 | | JSBool Bool 11 | | JSNull 12 | | JSArray (List JSVal) 13 | | JSObject (List ( String, JSVal )) 14 | 15 | 16 | decoder : Decoder JSVal 17 | decoder = 18 | Decode.oneOf 19 | [ Decode.map JSString Decode.string 20 | , Decode.map JSInt Decode.int 21 | , Decode.map JSFloat Decode.float 22 | , Decode.map JSBool Decode.bool 23 | , Decode.null JSNull 24 | , Decode.map JSArray (Decode.list (Decode.lazy <| \_ -> decoder)) 25 | , Decode.map (List.reverse >> JSObject) (Decode.keyValuePairs (Decode.lazy <| \_ -> decoder)) 26 | ] 27 | -------------------------------------------------------------------------------- /app/javascript/Response/JsonViewer.elm: -------------------------------------------------------------------------------- 1 | module Response.JsonViewer exposing (fromJSVal, view, rootNodePath, Msg, init, update, Model, getRootNode) 2 | 3 | {-| JsonViewer transforms the parsed JSON data (`JSVal`) into the 4 | renderable structure `JsonView`, and provides a renderer for it in 5 | which the user can collapse and expand nested values. 6 | 7 | While `JSVal` is a plain tree of data that comes from simply parsing the 8 | incoming JSON response, the `JsonView` structure defined here adds a 9 | unique path to every element, and also transforms both objects and arrays 10 | into a homogeneous collection type so that they can be rendered with the 11 | same code. 12 | 13 | Having a unique path makes it possible to refer unambiguously to any 14 | value in the JSON, which is needed since we don't have object references in 15 | value-based programming. Such an unique path can be used for example, 16 | to maintain a list of collapsed nodes. 17 | 18 | 19 | # Usage 20 | 21 | let 22 | jsonView = (JsonViewer.fromJSVal (JSVal.String "Hello") 23 | in 24 | div [][JsonViewer.view jsonView)] 25 | 26 | -} 27 | 28 | import Html exposing (..) 29 | import Html.Attributes exposing (..) 30 | import Html.Events exposing (..) 31 | import Response.JsonViewerTypes as JsonViewerTypes exposing (..) 32 | import Response.JSVal as JSVal 33 | import Utils.HttpUtil as HttpUtil 34 | import Set 35 | 36 | 37 | type alias Model = 38 | CollapsedNodePaths 39 | 40 | 41 | init : CollapsedNodePaths 42 | init = 43 | Set.empty 44 | 45 | 46 | 47 | -- Indentation in pixels when rendering a nested structure 48 | 49 | 50 | indent : Int 51 | indent = 52 | 16 53 | 54 | 55 | arrowRight : Html msg 56 | arrowRight = 57 | span [ class "JsonView__collapseArrow" ] [ text "▶ " ] 58 | 59 | 60 | arrowDown : Html msg 61 | arrowDown = 62 | span [ class "JsonView__collapseArrow" ] [ text "▼ " ] 63 | 64 | 65 | rootNodePath : NodePath 66 | rootNodePath = 67 | "root" 68 | 69 | 70 | getRootNode : HttpUtil.Response -> CollapsedNodePaths -> Node 71 | getRootNode body collapsedNodePaths = 72 | { jsonVal = 73 | fromJSVal 74 | (HttpUtil.decodeHitResponseBodyIntoJson body) 75 | , nodePath = rootNodePath 76 | , depth = 0 77 | , collapsedNodePaths = collapsedNodePaths 78 | } 79 | 80 | 81 | type alias Node = 82 | { depth : Int 83 | , collapsedNodePaths : CollapsedNodePaths 84 | , nodePath : NodePath 85 | , jsonVal : JsonView 86 | } 87 | 88 | 89 | 90 | ---- Construct a JsonView from plain JSVal ---- 91 | 92 | 93 | mapArrayElements : List JSVal.JSVal -> NodePath -> List JVCollectionElement 94 | mapArrayElements jsValsList parentNodePath = 95 | List.indexedMap 96 | (\index jsValElement -> 97 | let 98 | nodePath = 99 | parentNodePath ++ "." ++ toString index 100 | in 101 | ( nodePath, toString index, fromJSVal_ jsValElement nodePath ) 102 | ) 103 | jsValsList 104 | 105 | 106 | mapObjectElements : List ( ElementKey, JSVal.JSVal ) -> NodePath -> List JVCollectionElement 107 | mapObjectElements jsValsList parentNodePath = 108 | List.map 109 | (\( key, jsVal ) -> 110 | let 111 | nodePath = 112 | parentNodePath ++ "." ++ key 113 | in 114 | ( nodePath, key, fromJSVal_ jsVal nodePath ) 115 | ) 116 | jsValsList 117 | 118 | 119 | fromJSVal_ : JSVal.JSVal -> NodePath -> JsonView 120 | fromJSVal_ jsVal parentNodePath = 121 | case jsVal of 122 | JSVal.JSString string -> 123 | JVString string 124 | 125 | JSVal.JSFloat float -> 126 | JVFloat float 127 | 128 | JSVal.JSInt int -> 129 | JVInt int 130 | 131 | JSVal.JSBool bool -> 132 | JVBool bool 133 | 134 | JSVal.JSNull -> 135 | JVNull 136 | 137 | JSVal.JSArray array -> 138 | JVArray (mapArrayElements array parentNodePath) 139 | 140 | JSVal.JSObject object -> 141 | JVObject (mapObjectElements object parentNodePath) 142 | 143 | 144 | fromJSVal : JSVal.JSVal -> JsonView 145 | fromJSVal jsVal = 146 | fromJSVal_ jsVal rootNodePath 147 | 148 | 149 | 150 | -- UPDATE -- 151 | 152 | 153 | type Msg 154 | = ToggleJsonCollectionView String 155 | 156 | 157 | update : Msg -> Model -> ( Model, Cmd Msg ) 158 | update msg collapsedNodePaths = 159 | case msg of 160 | ToggleJsonCollectionView id -> 161 | let 162 | updatedModel = 163 | if Set.member id collapsedNodePaths then 164 | Set.remove id collapsedNodePaths 165 | else 166 | Set.insert id collapsedNodePaths 167 | in 168 | ( updatedModel, Cmd.none ) 169 | 170 | 171 | 172 | ---- VIEW ---- 173 | 174 | 175 | collectionItemPrefix : Node -> Html Msg 176 | collectionItemPrefix node = 177 | let 178 | { jsonVal, collapsedNodePaths, nodePath } = 179 | node 180 | 181 | isCollapsed = 182 | Set.member nodePath collapsedNodePaths 183 | 184 | render collection caption = 185 | span 186 | [ class "JsonView__collapsible" 187 | , onClick (ToggleJsonCollectionView nodePath) 188 | ] 189 | [ if isCollapsed then 190 | arrowRight 191 | else 192 | arrowDown 193 | , text caption 194 | ] 195 | in 196 | case jsonVal of 197 | JVArray collection -> 198 | render collection "[" 199 | 200 | JVObject collection -> 201 | render collection "{" 202 | 203 | _ -> 204 | Html.text "" 205 | 206 | 207 | collectionItemPostfix : Node -> Html Msg 208 | collectionItemPostfix { jsonVal } = 209 | case jsonVal of 210 | JVArray collection -> 211 | text "]," 212 | 213 | JVObject collection -> 214 | text "}," 215 | 216 | _ -> 217 | text "," 218 | 219 | 220 | collectionItemView : Bool -> Node -> JVCollectionElement -> Html Msg 221 | collectionItemView showPropertyKey parentNode ( nodePath, elementKey, jsonVal ) = 222 | let 223 | node = 224 | { parentNode | jsonVal = jsonVal, nodePath = nodePath } 225 | 226 | propertyKey = 227 | if showPropertyKey then 228 | span 229 | [ class "JsonView__propertyKey" ] 230 | [ text (elementKey ++ ":") ] 231 | else 232 | text "" 233 | in 234 | li [ class "JsonView__collectionItem" ] 235 | [ propertyKey 236 | , collectionItemPrefix node 237 | , view node 238 | , collectionItemPostfix node 239 | ] 240 | 241 | 242 | collectionView : Node -> JVCollection -> Bool -> Html Msg 243 | collectionView parentNode collection showPropertyKey = 244 | let 245 | { collapsedNodePaths, depth, nodePath } = 246 | parentNode 247 | 248 | isCollapsed = 249 | Set.member nodePath collapsedNodePaths 250 | in 251 | if isCollapsed then 252 | Html.text "" 253 | else 254 | ol 255 | [ class "JsonView__collectionItemsList" 256 | , style [ ( "paddingLeft", toString ((depth + 1) * indent) ++ "px" ) ] 257 | ] 258 | (List.map 259 | (collectionItemView showPropertyKey { parentNode | depth = depth + 1 }) 260 | collection 261 | ) 262 | 263 | 264 | view : Node -> Html Msg 265 | view node = 266 | span [ class "json-view" ] [ (view2 node) ] 267 | 268 | 269 | view2 : Node -> Html Msg 270 | view2 node = 271 | case node.jsonVal of 272 | JVString string -> 273 | span [ class "JsonView__string" ] [ text string ] 274 | 275 | JVFloat float -> 276 | span [ class "JsonView__number" ] [ text (toString float) ] 277 | 278 | JVInt int -> 279 | span [ class "JsonView__number" ] [ text (toString int) ] 280 | 281 | JVBool bool -> 282 | span [ class "JsonView__bool" ] [ text (toString bool) ] 283 | 284 | JVNull -> 285 | span [ class "JsonView__null" ] [ text "null" ] 286 | 287 | JVArray array -> 288 | let 289 | rendered = 290 | collectionView node array False 291 | in 292 | if node.depth == 0 then 293 | li [ class "JsonView__collectionItem" ] 294 | [ collectionItemPrefix node 295 | , rendered 296 | , collectionItemPostfix node 297 | ] 298 | else 299 | rendered 300 | 301 | JVObject object -> 302 | let 303 | rendered = 304 | collectionView node object True 305 | in 306 | if node.depth == 0 then 307 | li [ class "JsonView__collectionItem" ] 308 | [ collectionItemPrefix node 309 | , rendered 310 | , collectionItemPostfix node 311 | ] 312 | else 313 | rendered 314 | -------------------------------------------------------------------------------- /app/javascript/Response/JsonViewerTypes.elm: -------------------------------------------------------------------------------- 1 | module Response.JsonViewerTypes exposing (..) 2 | 3 | import Set 4 | 5 | 6 | {-| Set of absolute paths of all collapsed nodes 7 | -} 8 | type alias CollapsedNodePaths = 9 | Set.Set NodePath 10 | 11 | 12 | type JsonView 13 | = JVString String 14 | | JVFloat Float 15 | | JVInt Int 16 | | JVBool Bool 17 | | JVNull 18 | | JVArray JVCollection 19 | | JVObject JVCollection 20 | 21 | 22 | type alias JVCollection = 23 | List JVCollectionElement 24 | 25 | 26 | type alias JVCollectionElement = 27 | -- A collection element could belong to either an Object or an Array. 28 | -- For objects, their `ElementKey` is the key itself, and for arrays it is the element index. 29 | ( NodePath, ElementKey, JsonView ) 30 | 31 | 32 | {-| The key to render for each collection item. 33 | Objects have their own keys; arrays use their index. 34 | -} 35 | type alias ElementKey = 36 | String 37 | 38 | 39 | {-| Unique absolute path for every element in the JSON. 40 | 41 | This is required so that we can record whether a specific node 42 | is expanded or collapsed in the Collapsed set, and seek it out 43 | when rendering the node. 44 | 45 | This is guaranteed to be unique as it encodes the entire path 46 | from the root to itself. 47 | 48 | For example `root.2.name` points to the value whose 49 | key is `name`, of the object that is the second element in the root array. 50 | 51 | -} 52 | type alias NodePath = 53 | String 54 | -------------------------------------------------------------------------------- /app/javascript/Response/MainResponse.elm: -------------------------------------------------------------------------------- 1 | module Response.MainResponse exposing (..) 2 | 3 | import Response.JsonViewer as JsonViewer 4 | import Utils.HttpUtil as HttpUtil exposing (Response) 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Http 8 | import Html.Events exposing (..) 9 | import RemoteData 10 | import Date.Extra as Date 11 | 12 | 13 | -- MODEL -- 14 | 15 | 16 | type ResponseViewing 17 | = Formatted 18 | | Raw 19 | 20 | 21 | type alias Model = 22 | { response : RemoteData.WebData HttpUtil.Response 23 | , responseViewing : ResponseViewing 24 | , jsonViewer : JsonViewer.Model 25 | } 26 | 27 | 28 | init : Model 29 | init = 30 | { response = RemoteData.NotAsked 31 | , responseViewing = Formatted 32 | , jsonViewer = JsonViewer.init 33 | } 34 | 35 | 36 | 37 | -- UPDATE -- 38 | 39 | 40 | type Msg 41 | = SetResponseViewType ResponseViewing 42 | | JsonViewerMsg JsonViewer.Msg 43 | | UpdateResponse (RemoteData.WebData HttpUtil.Response) 44 | 45 | 46 | update : Msg -> Model -> ( Model, Cmd Msg ) 47 | update msg model = 48 | case msg of 49 | SetResponseViewType v -> 50 | ( { model | responseViewing = v }, Cmd.none ) 51 | 52 | JsonViewerMsg jMsg -> 53 | case model.response of 54 | RemoteData.Success response -> 55 | let 56 | ( updatedJv, subCmd ) = 57 | JsonViewer.update jMsg model.jsonViewer 58 | in 59 | ( { model | jsonViewer = updatedJv }, Cmd.map JsonViewerMsg subCmd ) 60 | 61 | _ -> 62 | ( model, Cmd.none ) 63 | 64 | UpdateResponse newResponse -> 65 | ( { model | response = newResponse }, Cmd.none ) 66 | 67 | 68 | 69 | -- VIEW -- 70 | 71 | 72 | view : Model -> Html Msg 73 | view model = 74 | let 75 | content = 76 | case model.response of 77 | RemoteData.NotAsked -> 78 | text "" 79 | 80 | RemoteData.Loading -> 81 | p [ class "Main__loading" ] [ text "Loading..." ] 82 | 83 | RemoteData.Failure error -> 84 | errorMarkup error 85 | 86 | RemoteData.Success response -> 87 | loadedMarkup response model 88 | in 89 | div [ class "row" ] [ div [ class "col" ] [ content ] ] 90 | 91 | 92 | loadedMarkup : Response -> Model -> Html Msg 93 | loadedMarkup response model = 94 | div [] 95 | [ httpStatusMarkup response 96 | , bodyHeadersNavBar 97 | , div [ class "tab-content", id "nav-tabContent" ] 98 | [ headersTabMarkup response 99 | , bodyTabMarkup response model 100 | ] 101 | ] 102 | 103 | 104 | headersTabMarkup : Response -> Html Msg 105 | headersTabMarkup response = 106 | let 107 | headerRow ( key, value ) = 108 | tr [] [ td [] [ strong [] [ text key ] ], td [] [ text value ] ] 109 | in 110 | div [ class "tab-pane fade", id "response-headers" ] 111 | [ table [ class "table" ] 112 | (List.map 113 | headerRow 114 | (HttpUtil.decodeHeadersFromHitResponse response) 115 | ) 116 | ] 117 | 118 | 119 | bodyTabMarkup : Response -> Model -> Html Msg 120 | bodyTabMarkup response model = 121 | div [ class "tab-pane fade show active", id "response-body" ] 122 | [ formattedOrRawView response model ] 123 | 124 | 125 | formattedOrRawView : Response -> Model -> Html Msg 126 | formattedOrRawView response model = 127 | case model.responseViewing of 128 | Raw -> 129 | rawResponseMarkup response 130 | 131 | Formatted -> 132 | formattedResponseMarkup response model 133 | 134 | 135 | formattedResponseMarkup : Response -> Model -> Html Msg 136 | formattedResponseMarkup response model = 137 | h5 [] 138 | [ text "Formatted response" 139 | , a [ class "btn", href "javascript:void(0)", onClick (SetResponseViewType Raw) ] [ text "Switch to raw response" ] 140 | , pre [ class "api-res__res" ] 141 | [ span [ class "block" ] [ JsonViewer.view (JsonViewer.getRootNode response model.jsonViewer) |> Html.map JsonViewerMsg ] ] 142 | ] 143 | 144 | 145 | errorMarkup : Http.Error -> Html Msg 146 | errorMarkup httpError = 147 | case httpError of 148 | Http.BadUrl url -> 149 | p [ class "Error" ] [ text ("Bad Url! " ++ url) ] 150 | 151 | Http.Timeout -> 152 | p [ class "Error" ] [ text "Sorry the request timed out" ] 153 | 154 | Http.NetworkError -> 155 | p [ class "Error" ] [ text "There was a network error." ] 156 | 157 | Http.BadStatus response -> 158 | div [] [ p [ class "Error" ] [ text "Server returned an error." ], httpErrorMarkup response ] 159 | 160 | Http.BadPayload message response -> 161 | div [] [ p [ class "Error" ] [ text ("Bad payload error: " ++ message) ], httpErrorMarkup response ] 162 | 163 | 164 | httpErrorMarkup : Http.Response String -> Html Msg 165 | httpErrorMarkup response = 166 | div [ class "" ] 167 | [ httpStatusMarkup response 168 | , bodyHeadersNavBar 169 | , rawResponseMarkup response 170 | ] 171 | 172 | 173 | httpStatusMarkup : Http.Response String -> Html msg 174 | httpStatusMarkup response = 175 | let 176 | responseCreatedAtMarkup = 177 | case HttpUtil.decodeCreatedAtFromResponse response of 178 | Just dateString -> 179 | p [] 180 | [ span 181 | [ class "api-res-form__label" ] 182 | [ strong [] [ text "Date: " ] 183 | , formatAndLocalizeDatetime dateString |> text 184 | ] 185 | ] 186 | 187 | Nothing -> 188 | Html.text "" 189 | in 190 | div [ class "api-res-form__response" ] 191 | [ h3 [] [ text "Response" ] 192 | , p [] 193 | [ span 194 | [ class "api-res-form__label" ] 195 | [ strong [] [ text "Status: " ] 196 | , HttpUtil.decodeStatusCodeFromResponse response |> toString |> text 197 | ] 198 | ] 199 | , responseCreatedAtMarkup 200 | ] 201 | 202 | 203 | formatAndLocalizeDatetime : String -> String 204 | formatAndLocalizeDatetime dateString = 205 | case Date.fromIsoString dateString of 206 | Just date -> 207 | Date.toFormattedString "ddd MMMM y, h:mm a" date 208 | 209 | Nothing -> 210 | dateString 211 | 212 | 213 | bodyHeadersNavBar : Html msg 214 | bodyHeadersNavBar = 215 | nav [ class "nav nav-tabs api-res__req-tabs", id "body-headers", attribute "role" "bodyheaderslist" ] 216 | [ a 217 | [ class "nav-item nav-link active" 218 | , id "response-body-tab" 219 | , attribute "data-toggle" "tab" 220 | , href "#response-body" 221 | , attribute "role" "tab" 222 | ] 223 | [ text "Body" ] 224 | , a 225 | [ class "nav-item nav-link" 226 | , id "response-headers-tab" 227 | , attribute "data-toggle" "tab" 228 | , href "#response-headers" 229 | , attribute "role" "tab" 230 | ] 231 | [ text "Headers" ] 232 | ] 233 | 234 | 235 | rawResponseMarkup : Http.Response String -> Html Msg 236 | rawResponseMarkup response = 237 | let 238 | parsedResponse = 239 | HttpUtil.decodeResponseBodyToString response 240 | in 241 | div [] 242 | [ h5 [] 243 | [ text "Raw Response" 244 | , a [ class "btn", href "javascript:void(0)", onClick (SetResponseViewType Formatted) ] [ text "Switch to formatted response" ] 245 | ] 246 | , pre [ class "form-control" ] [ text parsedResponse ] 247 | ] 248 | -------------------------------------------------------------------------------- /app/javascript/Router.elm: -------------------------------------------------------------------------------- 1 | module Router exposing (..) 2 | 3 | import Navigation exposing (Location) 4 | import UrlParser exposing (..) 5 | 6 | type Route 7 | = HomeRoute 8 | | HitRoute String 9 | | NotFound 10 | 11 | matchers : Parser (Route -> a) a 12 | matchers = 13 | oneOf 14 | [ map HomeRoute top 15 | , map HitRoute (s "hits" > string) 16 | ] 17 | 18 | 19 | parseLocation : Location -> Route 20 | parseLocation location = 21 | case (parseHash matchers location) of 22 | Just route -> 23 | route 24 | 25 | Nothing -> 26 | NotFound 27 | -------------------------------------------------------------------------------- /app/javascript/Utils/HttpMethods.elm: -------------------------------------------------------------------------------- 1 | module Utils.HttpMethods exposing (..) 2 | 3 | 4 | type HttpMethod 5 | = Get 6 | | Post 7 | | Put 8 | | Patch 9 | | Delete 10 | 11 | 12 | toString : HttpMethod -> String 13 | toString httpMethod = 14 | case httpMethod of 15 | Get -> 16 | "GET" 17 | 18 | Post -> 19 | "POST" 20 | 21 | Put -> 22 | "PUT" 23 | 24 | Patch -> 25 | "PATCH" 26 | 27 | Delete -> 28 | "DELETE" 29 | 30 | 31 | parse : String -> HttpMethod 32 | parse httpMethodString = 33 | case httpMethodString of 34 | "GET" -> 35 | Get 36 | 37 | "POST" -> 38 | Post 39 | 40 | "PUT" -> 41 | Put 42 | 43 | "PATCH" -> 44 | Patch 45 | 46 | "DELETE" -> 47 | Delete 48 | 49 | _ -> 50 | Get 51 | 52 | 53 | avaialableHttpMethods : List HttpMethod 54 | avaialableHttpMethods = 55 | [ Get, Post, Put, Patch, Delete ] 56 | 57 | 58 | avaialableHttpMethodsString : List String 59 | avaialableHttpMethodsString = 60 | List.map toString avaialableHttpMethods 61 | -------------------------------------------------------------------------------- /app/javascript/Utils/HttpUtil.elm: -------------------------------------------------------------------------------- 1 | module Utils.HttpUtil exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Response.JSVal as JSVal 5 | import Json.Decode as JD 6 | import Json.Decode.Pipeline as JP 7 | import Http 8 | import Utils.HttpMethods as HttpMethods exposing (HttpMethod, parse, toString) 9 | import Request.RequestParameters as RequestParameters exposing (..) 10 | import Request.RequestHeaders as RequestHeaders exposing (..) 11 | import Request.RequestBody as RequestHeaders exposing (..) 12 | import Request.RequestBasicAuthentication as RequestBasicAuthentication exposing (..) 13 | 14 | 15 | type alias Response = 16 | Http.Response String 17 | 18 | 19 | type alias Request = 20 | { url : String 21 | , httpMethod : HttpMethod 22 | , requestParameters : RequestParameters 23 | , requestHeaders : RequestHeaders 24 | , basicAuthentication : Maybe BasicAuthentication 25 | , requestBody : Maybe RequestBody 26 | } 27 | 28 | 29 | type alias SRequest = 30 | { url : String 31 | , httpMethod : HttpMethod 32 | , requestParameters : RequestParameters 33 | , requestHeaders : RequestHeaders 34 | , username : Maybe String 35 | , password : Maybe String 36 | , requestBody : Maybe RequestBody 37 | } 38 | 39 | 40 | emptyRequest : Request 41 | emptyRequest = 42 | Request "" 43 | HttpMethods.Get 44 | RequestParameters.empty 45 | RequestHeaders.empty 46 | Nothing 47 | Nothing 48 | 49 | 50 | encodeUrl : String -> RequestParameters -> String 51 | encodeUrl url requestParameters = 52 | requestParameters 53 | |> Dict.values 54 | |> List.filter (\{ key } -> key /= "") 55 | |> List.map (\{ key, value } -> Http.encodeUri key ++ "=" ++ Http.encodeUri value) 56 | |> String.join "&" 57 | |> (++) (url ++ "?") 58 | 59 | 60 | decodeResponseBodyToString : Response -> String 61 | decodeResponseBodyToString hitResponse = 62 | let 63 | result = 64 | JD.decodeString (JD.at [ "response", "response_body" ] JD.string) hitResponse.body 65 | in 66 | case result of 67 | Ok value -> 68 | value 69 | 70 | Err err -> 71 | ("Error: Could not parse body. " ++ err) 72 | 73 | 74 | decodeHitResponseBodyIntoJson : Response -> JSVal.JSVal 75 | decodeHitResponseBodyIntoJson hitResponse = 76 | let 77 | result = 78 | JD.decodeString (JD.at [ "response", "response_body" ] JSVal.decoder) hitResponse.body 79 | 80 | errorParsing message = 81 | JSVal.JSString ("Error: Could not parse body. " ++ message) 82 | 83 | parseString string = 84 | let 85 | result = 86 | JD.decodeString JSVal.decoder string 87 | in 88 | case result of 89 | Ok jsonValue -> 90 | jsonValue 91 | 92 | Err err -> 93 | errorParsing err 94 | in 95 | case result of 96 | Ok jsonValue -> 97 | case jsonValue of 98 | JSVal.JSString stringValue -> 99 | parseString stringValue 100 | 101 | _ -> 102 | jsonValue 103 | 104 | Err err -> 105 | errorParsing err 106 | 107 | 108 | requestParametersDecoder : JD.Decoder RequestParameters 109 | requestParametersDecoder = 110 | JD.keyValuePairs JD.string 111 | |> JD.andThen 112 | (\result -> 113 | result 114 | |> List.map (\( key, value ) -> RequestParameter key value) 115 | |> List.foldl (\item memo -> RequestParameters.push item memo) RequestParameters.empty 116 | |> JD.succeed 117 | ) 118 | 119 | 120 | requestBasicAuthenticationDecoder : JD.Decoder BasicAuthentication 121 | requestBasicAuthenticationDecoder = 122 | JP.decode BasicAuthentication 123 | |> JP.required "username" JD.string 124 | |> JP.required "password" JD.string 125 | 126 | 127 | requestHeadersDecoder : JD.Decoder RequestHeaders 128 | requestHeadersDecoder = 129 | JD.keyValuePairs JD.string 130 | |> JD.andThen 131 | (\result -> 132 | result 133 | |> List.map (\( key, value ) -> RequestHeader key value) 134 | |> List.foldl (\item memo -> RequestHeaders.push item memo) RequestHeaders.empty 135 | |> JD.succeed 136 | ) 137 | 138 | 139 | httpMethodDecoder : JD.Decoder HttpMethod 140 | httpMethodDecoder = 141 | JD.string 142 | |> JD.andThen (\str -> JD.succeed (parse str)) 143 | 144 | 145 | requestDecoder : JD.Decoder SRequest 146 | requestDecoder = 147 | JP.decode SRequest 148 | |> JP.optional "url" JD.string "" 149 | |> JP.optional "method" httpMethodDecoder HttpMethods.Get 150 | |> JP.optional "request_params" requestParametersDecoder RequestParameters.empty 151 | |> JP.optional "request_headers" requestHeadersDecoder RequestHeaders.empty 152 | |> JP.optional "username" (JD.map Just JD.string) Nothing 153 | |> JP.optional "password" (JD.map Just JD.string) Nothing 154 | |> JP.optional "request_body" 155 | (JD.map Just requestBodyDecoder) 156 | Nothing 157 | 158 | 159 | requestBodyDecoder : JD.Decoder RequestBody 160 | requestBodyDecoder = 161 | JP.decode RequestBody 162 | |> JP.required "bodyType" requestBodyTypeDecoder 163 | |> JP.required "value" JD.string 164 | 165 | 166 | requestBodyTypeDecoder : JD.Decoder RequestBodyType 167 | requestBodyTypeDecoder = 168 | JD.string 169 | |> JD.andThen 170 | (\str -> 171 | case str of 172 | "BodyText" -> 173 | JD.succeed BodyText 174 | 175 | "BodyJSON" -> 176 | JD.succeed BodyJSON 177 | 178 | somethingElse -> 179 | JD.fail <| "Unknown type: " ++ somethingElse 180 | ) 181 | 182 | 183 | decodeHitResponseIntoRequest : Response -> Request 184 | decodeHitResponseIntoRequest hitResponse = 185 | let 186 | result = 187 | JD.decodeString requestDecoder hitResponse.body 188 | in 189 | case result of 190 | Ok v -> 191 | let 192 | extractedCreds = 193 | ( v.username, v.password ) 194 | 195 | bA = 196 | case extractedCreds of 197 | ( Nothing, Nothing ) -> 198 | Nothing 199 | 200 | ( Just a, Nothing ) -> 201 | Just { username = a, password = "" } 202 | 203 | ( Nothing, Just a ) -> 204 | Just { username = "", password = a } 205 | 206 | ( Just u, Just p ) -> 207 | Just { username = u, password = p } 208 | in 209 | { url = v.url 210 | , httpMethod = v.httpMethod 211 | , requestParameters = v.requestParameters 212 | , requestHeaders = v.requestHeaders 213 | , basicAuthentication = bA 214 | , requestBody = v.requestBody 215 | } 216 | 217 | Err err -> 218 | emptyRequest 219 | 220 | 221 | decodeHeadersFromHitResponse : Response -> List ( String, String ) 222 | decodeHeadersFromHitResponse hitResponse = 223 | let 224 | decoder = 225 | JD.keyValuePairs JD.string 226 | |> JD.at [ "response", "response_headers" ] 227 | 228 | result = 229 | JD.decodeString decoder hitResponse.body 230 | in 231 | case result of 232 | Ok jsonValue -> 233 | jsonValue 234 | 235 | Err err -> 236 | [] 237 | 238 | 239 | decodeStatusCodeFromResponse : Response -> Int 240 | decodeStatusCodeFromResponse response = 241 | let 242 | decoder = 243 | JD.string |> JD.at [ "response", "response_code" ] 244 | 245 | result = 246 | JD.decodeString decoder response.body 247 | 248 | defaultStatusCode = 249 | 500 250 | in 251 | case result of 252 | Ok statusCode -> 253 | String.toInt statusCode |> Result.withDefault defaultStatusCode 254 | 255 | Err err -> 256 | defaultStatusCode 257 | 258 | 259 | decodeCreatedAtFromResponse : Response -> Maybe String 260 | decodeCreatedAtFromResponse response = 261 | let 262 | decoder = 263 | JD.field "created_at" JD.string 264 | 265 | result = 266 | JD.decodeString decoder response.body 267 | in 268 | case result of 269 | Ok date -> 270 | Just date 271 | 272 | Err err -> 273 | Nothing 274 | 275 | 276 | decodeTokenFromResponse : Response -> Maybe String 277 | decodeTokenFromResponse response = 278 | let 279 | decoder = 280 | JD.field "token" JD.string 281 | 282 | result = 283 | JD.decodeString decoder response.body 284 | in 285 | case result of 286 | Ok token -> 287 | Just token 288 | 289 | Err err -> 290 | Nothing 291 | 292 | 293 | buildRequest : String -> HttpMethod -> Http.Body -> Http.Request (Http.Response String) 294 | buildRequest url httpMethod body = 295 | Http.request 296 | { method = HttpMethods.toString httpMethod 297 | , headers = [] 298 | , url = url 299 | , body = body 300 | , expect = Http.expectStringResponse preserveFullResponse 301 | , timeout = Nothing 302 | , withCredentials = False 303 | } 304 | 305 | 306 | preserveFullResponse : Http.Response String -> Result String (Http.Response String) 307 | preserveFullResponse response = 308 | Ok response 309 | -------------------------------------------------------------------------------- /app/javascript/Utils/Util.elm: -------------------------------------------------------------------------------- 1 | module Utils.Util exposing (..) 2 | 3 | 4 | isStringPresent : String -> Bool 5 | isStringPresent s = 6 | case (String.trim s) of 7 | "" -> 8 | False 9 | 10 | _ -> 11 | True 12 | -------------------------------------------------------------------------------- /app/javascript/Views/NotFound.elm: -------------------------------------------------------------------------------- 1 | module Views.NotFound exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | 6 | 7 | view : model -> Html msg 8 | view _ = 9 | div [ class "col-md-12" ] 10 | [ p [] [ text "Not found" ] 11 | ] 12 | -------------------------------------------------------------------------------- /app/javascript/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbinary/apisnapshot/c877183223d33b4b384b2b86f931185d7482e802/app/javascript/images/.gitkeep -------------------------------------------------------------------------------- /app/javascript/packs/application.css: -------------------------------------------------------------------------------- 1 | @import '~bootstrap/dist/css/bootstrap'; 2 | 3 | body { 4 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 5 | font-size: 14px; 6 | margin: 0; 7 | text-align: left; 8 | color: #444; 9 | } 10 | 11 | * { 12 | &:focus, &:active { 13 | outline: none; 14 | } 15 | } 16 | 17 | a { 18 | color: #3845a5; 19 | 20 | &:hover { 21 | color: #333; 22 | } 23 | } 24 | 25 | strong { 26 | font-weight: 500; 27 | } 28 | 29 | .btn { 30 | cursor: pointer; 31 | 32 | &:focus, &:active { 33 | box-shadow: none !important; 34 | } 35 | 36 | &.btn-primary { 37 | color: #fff; 38 | background-color: #3845a5; 39 | border-color: #3845a5; 40 | } 41 | 42 | &.btn-default { 43 | color: #444; 44 | 45 | &:hover { 46 | color: #333; 47 | } 48 | } 49 | } 50 | 51 | .dropdown-item { 52 | color: #444; 53 | 54 | &:hover { 55 | color: #333; 56 | } 57 | } 58 | 59 | div { 60 | margin: 0 61 | } 62 | 63 | img { 64 | margin: 20px 0; 65 | max-width: 200px; 66 | } 67 | 68 | h3 { 69 | margin-top: 20px; 70 | margin-bottom: 10px; 71 | } 72 | 73 | .navbar { 74 | min-height: 4rem; 75 | margin-bottom: 0; 76 | padding: 0 20px; 77 | background: #3845a5; 78 | box-shadow: none; 79 | border: 0; 80 | position: -webkit-sticky; 81 | position: sticky; 82 | top: 0; 83 | z-index: 1; 84 | 85 | .navbar-brand { 86 | color: #fff; 87 | font-weight: 500; 88 | 89 | &:hover { 90 | color: #fff; 91 | } 92 | } 93 | 94 | .nav-link { 95 | color: #fff; 96 | } 97 | } 98 | 99 | .container { 100 | max-width: 95%; 101 | } 102 | 103 | .main { 104 | margin-top: 40px; 105 | } 106 | 107 | .form-group { 108 | margin-bottom: 15px; 109 | } 110 | 111 | .Main__loading { 112 | margin-top: 20px; 113 | font-size: 24px; 114 | } 115 | 116 | .UrlForm { 117 | margin: 2em 3em 1em 3em; 118 | font-size: 20px; 119 | } 120 | 121 | .UrlForm__httpMethodsDropdown { 122 | margin-right: 1em; 123 | } 124 | 125 | .UrlForm__input { 126 | width: 60%; 127 | margin-right: 1em; 128 | } 129 | 130 | .UrlForm__moreActionsDropdown { 131 | margin-right: 1em; 132 | } 133 | 134 | .RequestParameters { 135 | margin: 0 2em; 136 | padding: 2em; 137 | } 138 | 139 | .RequestParameters__heading { 140 | font-size: 0.8rem; 141 | color: #aaa; 142 | text-transform: uppercase; 143 | } 144 | 145 | .RequestParameters__add { 146 | margin-left: 10em; 147 | } 148 | 149 | .form-row:hover .row__delete { 150 | opacity: 1; 151 | } 152 | 153 | .row__delete { 154 | color: #999; 155 | font-size: 18px; 156 | font-weight: bold; 157 | padding: 10px 5px; 158 | display: block; 159 | opacity: 0; 160 | } 161 | 162 | .row__delete:hover { 163 | color: #000; 164 | text-decoration: none; 165 | } 166 | 167 | .RequestParameters ul { 168 | list-style-type: none; 169 | padding: 0 170 | } 171 | 172 | .RequestParameters li { 173 | margin-bottom: 1em; 174 | } 175 | 176 | .RequestParameters input { 177 | margin-right: 2em; 178 | } 179 | 180 | .Result { 181 | padding: 2em; 182 | margin: 2em; 183 | } 184 | 185 | .Result__jsonView { 186 | font-size: 14px; 187 | font-family: Menlo, monospace; 188 | color: black; 189 | } 190 | 191 | .Error { 192 | color: darkred; 193 | } 194 | 195 | .JsonView__collapsible:hover { 196 | cursor: pointer; 197 | } 198 | 199 | .JsonView__propertyKey { 200 | color: rgb(136, 19, 145); 201 | margin-right: 8px; 202 | } 203 | 204 | .JsonView__number { 205 | color: rgb(28, 0, 207); 206 | } 207 | 208 | .JsonView__string { 209 | white-space: pre; 210 | unicode-bidi: -webkit-isolate; 211 | color: rgb(196, 26, 22); 212 | } 213 | 214 | .JsonView__null { 215 | color: rgb(128, 128, 128); 216 | } 217 | 218 | .JsonView__bool { 219 | color: rgb(170, 13, 145); 220 | } 221 | 222 | .JsonView__collectionGroup { 223 | /* display: flex; */ 224 | } 225 | 226 | .JsonView__collectionItem { 227 | align-items: center; 228 | box-sizing: border-box; 229 | color: rgb(33, 33, 33); 230 | cursor: default; 231 | list-style-type: none; 232 | min-height: 16px; 233 | min-width: 0px; 234 | position: relative; 235 | tab-size: 4; 236 | text-align: left; 237 | text-overflow: ellipsis; 238 | user-select: text; 239 | white-space: nowrap; 240 | word-wrap: break-word; 241 | } 242 | 243 | .JsonView__collectionItemsList { 244 | box-sizing: border-box; 245 | color: rgb(33, 33, 33); 246 | cursor: default; 247 | display: block; 248 | list-style-type: none; 249 | min-height: 0px; 250 | min-width: 0px; 251 | tab-size: 4; 252 | user-select: text; 253 | white-space: pre-wrap; 254 | word-wrap: break-word; 255 | -webkit-margin-after: 0px; 256 | -webkit-margin-before: 0px; 257 | -webkit-margin-end: 0px; 258 | -webkit-margin-start: 0px; 259 | padding-left: 0px; 260 | -webkit-padding-start: 0px; 261 | border-left: 1px dotted; 262 | } 263 | 264 | .JsonView__collapseArrow { 265 | font-size: 10px; 266 | color: #aaa; 267 | } 268 | 269 | .api-request-table { 270 | margin-top: 20px; 271 | } 272 | 273 | html, body { 274 | min-height: 100%; 275 | height: 100%; 276 | font-size: 16px; 277 | } 278 | .navbar { 279 | margin-bottom: 0; 280 | padding: 0 20px; 281 | } 282 | .list-headers { 283 | padding: 5px 15px; 284 | } 285 | .input, .input:focus { 286 | border-width: 0 0 1px 0; 287 | border-radius: 0; 288 | font-size: 16px; 289 | box-shadow: none; 290 | } 291 | .api-req-form__container { 292 | padding: 0; 293 | border-bottom: 1px dashed #ccc; 294 | } 295 | .api-req-form__container .bootstrap-center-form { 296 | text-align: left; 297 | box-shadow: none; 298 | margin-bottom: 0; 299 | } 300 | .api-req-form__title { 301 | margin-bottom: 20px; 302 | } 303 | .api-req-form__auth-check { 304 | margin-right: 5px !important; 305 | } 306 | .api-req-form__auth-form { 307 | height: 50px; 308 | margin-top: 15px; 309 | } 310 | .api-req-form__auth-form .checkbox { 311 | line-height: 34px; 312 | } 313 | .api-req-form__auth-form .form-group { 314 | margin: 0 5px; 315 | } 316 | .api-req-form__form { 317 | border: none; 318 | } 319 | .api-req-form__send-icon { 320 | color: #fff; 321 | margin-right: 10px; 322 | } 323 | .api-req-form__url-group { 324 | display: flex; 325 | align-items: flex-start; 326 | } 327 | .api-req-form__url-control { 328 | flex: 1; 329 | padding-left: 20px; 330 | padding-right: 20px; 331 | width: 600px; 332 | } 333 | .api-req-form__remove-icon { 334 | font-size: 16px; 335 | color: gray; 336 | } 337 | .api-req-form__input { 338 | margin: 5px 20px 5px 0px; 339 | } 340 | .api-res-form__label { 341 | color: gray; 342 | text-transform: uppercase; 343 | font-size: 14px; 344 | } 345 | .api-res-form__response { 346 | padding: 0 10px; 347 | } 348 | .api-req-form__textarea { 349 | margin-top: 15px; 350 | } 351 | .api-req-form__form-inline { 352 | margin-top: 10px; 353 | } 354 | .api-res__res { 355 | background: #fff; 356 | border-color: #eee; 357 | font-size: 14px; 358 | } 359 | .api-req-form__btn-group { 360 | margin-right: 20px; 361 | } 362 | .form-group__label { 363 | font-size: 0.8rem; 364 | margin-top: 20px; 365 | margin-bottom: 0; 366 | color: #aaa; 367 | text-transform: uppercase; 368 | } 369 | .form-group__label .devise-links { 370 | text-transform: capitalize; 371 | font-size: 0.9rem; 372 | margin-left: 10px; 373 | } 374 | .form-group { 375 | margin-bottom: 0; 376 | } 377 | .api-req-form__request-body { 378 | margin-left: 10px; 379 | color: #666; 380 | } 381 | .api-res__req-panel { 382 | border-bottom: 1px dashed #ddd; 383 | } 384 | .api-res__headers { 385 | display: inline-block; 386 | padding-left: 0; 387 | list-style: none; 388 | } 389 | .api-res__req-tabs { 390 | margin-bottom: 10px; 391 | } 392 | .api-res__req-tabs li { 393 | cursor: pointer; 394 | } 395 | .api-res-form__list { 396 | display: inline-block; 397 | vertical-align: top; 398 | list-style: none; 399 | padding-left: 10px; 400 | } 401 | .api-req-form__more-text { 402 | margin-right: 5px; 403 | } 404 | .api-res__header-item-key { 405 | font-weight: bold; 406 | margin-right: 4px; 407 | } 408 | a.glyphicon-star, a.glyphicon-star-empty { 409 | color: gold; 410 | text-decoration: none; 411 | cursor: pointer; 412 | padding-left: 15px; 413 | } 414 | a.glyphicon-star-empty { 415 | color: black; 416 | } 417 | 418 | pre { 419 | white-space: pre-wrap; 420 | } 421 | -------------------------------------------------------------------------------- /app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // Run this example by adding <%= javascript_pack_tag "elm_root" %> to the 2 | // head of your layout file, like app/views/layouts/application.html.erb. 3 | // It will render "Hello Elm!" within the page. 4 | 5 | import 'bootstrap' 6 | import './application.css' 7 | import 'csrf-xhr' 8 | import Elm from '../Main' 9 | 10 | document.addEventListener('DOMContentLoaded', () => { 11 | Elm.Main.embed(document.getElementById('root')) 12 | }) 13 | -------------------------------------------------------------------------------- /app/jobs/purge_record_job.rb: -------------------------------------------------------------------------------- 1 | class PurgeRecordJob 2 | 3 | RETAIN_RECORDS_FOR_IN_DAYS = 30 4 | 5 | include ::ScheduledJob 6 | 7 | def perform 8 | ScheduledJob.logger.info "Going to purge old records" 9 | ApiResponse.where("created_at < ? ", RETAIN_RECORDS_FOR_IN_DAYS.days.ago).delete_all 10 | end 11 | 12 | # run it every one hour 13 | def self.time_to_recur(last_run_at) 14 | last_run_at + 1.hour 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/api_response.rb: -------------------------------------------------------------------------------- 1 | class ApiResponse < ApplicationRecord 2 | has_secure_token 3 | validates :url, :method, presence: true 4 | 5 | def response_body 6 | response['body'] 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end -------------------------------------------------------------------------------- /app/services/api_request_parser_service.rb: -------------------------------------------------------------------------------- 1 | class ApiRequestParserService 2 | 3 | attr_reader :request_parameters, :request_body, :request_headers 4 | 5 | def initialize(parameters) 6 | @request_parameters = parameters[:request_parameters] 7 | @request_body = parameters[:request_body] 8 | @request_headers = parameters[:request_headers] 9 | end 10 | 11 | def process 12 | {request_parameters: parse_request_params, request_headers: parse_request_headers} 13 | end 14 | 15 | private 16 | 17 | def parse_request_params 18 | if request_parameters.present? 19 | parse(request_parameters) 20 | elsif request_body.present? 21 | request_body 22 | else 23 | {} 24 | end 25 | end 26 | 27 | def parse_request_headers 28 | if request_headers.present? 29 | parse(request_headers) 30 | else 31 | {} 32 | end 33 | end 34 | 35 | def parse(request_array) 36 | parsed_hash = {} 37 | request_array.values.each do |request| 38 | parsed_hash[request[:key]] = request[:value] if request[:key] && request[:value] 39 | end 40 | parsed_hash 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/services/request_service.rb: -------------------------------------------------------------------------------- 1 | class RequestService 2 | include ActiveModel::Validations 3 | 4 | attr_reader :url, :username, :password, :method, :response, 5 | :request_parameters, :request_headers, :request_body 6 | attr_accessor :api_response 7 | 8 | validates :url, :method, presence: true 9 | 10 | def initialize(url:, method: nil, options: {}) 11 | @url = url 12 | @method = method || :get 13 | @username = options[:username] 14 | @password = options[:password] 15 | @request_parameters = options[:request_parameters] 16 | @request_body = options[:request_body] 17 | @request_headers = options[:request_headers] 18 | end 19 | 20 | def process 21 | if valid? 22 | begin 23 | @response = RestClient::Request.execute(options) 24 | self.api_response = save_api_response 25 | rescue RestClient::ExceptionWithResponse => e 26 | @response = e.response 27 | self.api_response = save_api_response 28 | rescue URI::InvalidURIError 29 | errors.add(:url, 'Invalid URL') 30 | rescue SocketError => e 31 | errors.add(:url, 'Invalid URL or Domain') 32 | end 33 | end 34 | end 35 | 36 | private 37 | 38 | def save_api_response 39 | ApiResponse.create!({ url: url, 40 | method: method.upcase, 41 | response: response_body, 42 | response_headers: response.headers, 43 | status_code: response.code, 44 | request_headers: request_headers, 45 | request_params: request_parameters_to_save, 46 | username: username, 47 | password: password, 48 | request_body: request_body } 49 | ) 50 | end 51 | 52 | def request_parameters_to_save 53 | request_parameters.is_a?(String) ? JSON.parse(request_parameters) : sanitized_request_parameters 54 | end 55 | 56 | def sanitized_request_parameters 57 | {}.tap do |return_value| 58 | request_parameters.each do |key, value| 59 | return_value[key] = value.is_a?(ActionDispatch::Http::UploadedFile) ? '' : value 60 | end 61 | end 62 | end 63 | 64 | def authorization_options 65 | if username && password 66 | {user: username, password: password} 67 | else 68 | {} 69 | end 70 | end 71 | 72 | def response_body 73 | { 74 | body: response.body.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') 75 | } 76 | end 77 | 78 | def options 79 | {url: url, 80 | method: method, 81 | verify_ssl: false, 82 | payload: request_parameters, 83 | headers: request_headers} 84 | .merge(authorization_options) 85 | end 86 | 87 | end -------------------------------------------------------------------------------- /app/views/api_responses/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @api_response, :url, :method, 2 | :username, :password, :created_at, 3 | :request_params, :request_headers, :request_body 4 | json.response do 5 | json.response_headers @api_response.response_headers.sort.to_h 6 | json.response_body @api_response.response['body'] 7 | json.response_code @api_response.status_code 8 | end 9 | -------------------------------------------------------------------------------- /app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 |
You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |If you are the application owner check the logs for more information.
64 |