├── .gitignore ├── README.md ├── elm-package.json ├── example ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── Main.elm ├── config.ru ├── elm-package.json └── server.rb └── src └── JsonApi └── Http.elm /.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff/* 2 | elm.js 3 | 4 | /example/elm-stuff/* 5 | /example/index.html 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-jsonapi-http 2 | HTTP negotiation with JSON API-compliant servers. Intended for use with [elm-jsonapi](https://github.com/noahzgordon/elm-jsonapi). 3 | 4 | See the documentation at: http://package.elm-lang.org/packages/noahzgordon/elm-jsonapi-http/latest 5 | 6 | ## Usage 7 | 8 | For a live usage example, see the `/example` directory in the project repo. 9 | 10 | **Note: The example app requires you to run a Sinatra server, meaning you will need Ruby and Rack installed.** 11 | 1. Compile the elm file: `cd example && elm make Main.elm` 12 | 2. Open the resulting html file `[open|tee|firefox] index.html` 13 | 3. Run the sinatra server (and make sure it runs on port 9292 if that's not your default): `rackup -p 9292` 14 | 15 | 16 | ```elm 17 | -- from `noahzgordon/elm-jsonapi` 18 | import JsonApi.Resources 19 | -- from `noahzgordon/elm-jsonapi-http` 20 | import JsonApi.Http 21 | 22 | type alias Model = 23 | { protagonist : Maybe Character 24 | } 25 | 26 | 27 | type alias Character = 28 | { firstName : String 29 | , lastName : String 30 | } 31 | 32 | 33 | characterDecoder : Json.Decode.Decoder Character 34 | characterDecoder = 35 | Json.Decode.map2 Character 36 | (Json.Decode.field "first-name" Json.Decode.string) 37 | (Json.Decode.field "last-name" Json.Decode.string) 38 | 39 | 40 | getProtagonist : Cmd Message 41 | getProtagonist = 42 | JsonApi.Http.getPrimaryResource "http://localhost:9292/luke" 43 | |> Http.send ProtagonistLoaded 44 | 45 | 46 | update message model = 47 | case message of 48 | ProtagonistLoaded (Ok resource) -> 49 | ( { model | protagonist = JsonApi.Resources.attributes characterDecoder resource |> Result.toMaybe }, Cmd.none) 50 | 51 | ProtagonistLoaded (Err error) -> 52 | Debug.log ("Remember to start the server on port 9292! " ++ toString e) (model, Cmd.none) 53 | 54 | ``` 55 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.1", 3 | "summary": "HTTP server negotiation for JSON API-compliant servers.", 4 | "repository": "https://github.com/noahzgordon/elm-jsonapi-http.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "./src" 8 | ], 9 | "exposed-modules": [ 10 | "JsonApi.Http" 11 | ], 12 | "dependencies": { 13 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 14 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 15 | "noahzgordon/elm-jsonapi": "2.0.3 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.18.0 <= v < 0.19.0" 18 | } 19 | -------------------------------------------------------------------------------- /example/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.0 2 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | ruby "2.4.0" 4 | 5 | gem "sinatra" 6 | gem "json" 7 | gem "jsonapi-serializers" 8 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (5.1.1) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | tzinfo (~> 1.1) 9 | concurrent-ruby (1.0.5) 10 | i18n (0.8.4) 11 | json (2.1.0) 12 | jsonapi-serializers (1.0.0) 13 | activesupport 14 | minitest (5.10.2) 15 | mustermann (1.0.0) 16 | rack (2.0.3) 17 | rack-protection (2.0.0) 18 | rack 19 | sinatra (2.0.0) 20 | mustermann (~> 1.0) 21 | rack (~> 2.0) 22 | rack-protection (= 2.0.0) 23 | tilt (~> 2.0) 24 | thread_safe (0.3.6) 25 | tilt (2.0.7) 26 | tzinfo (1.2.3) 27 | thread_safe (~> 0.1) 28 | 29 | PLATFORMS 30 | ruby 31 | 32 | DEPENDENCIES 33 | json 34 | jsonapi-serializers 35 | sinatra 36 | 37 | RUBY VERSION 38 | ruby 2.4.0p0 39 | 40 | BUNDLED WITH 41 | 1.14.6 42 | -------------------------------------------------------------------------------- /example/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Html 4 | import Http 5 | import Html.Events exposing (onClick) 6 | import Platform.Cmd exposing ((!)) 7 | import Platform.Sub exposing (Sub) 8 | import Json.Decode 9 | import JsonApi 10 | import JsonApi.Http 11 | import JsonApi.Resources 12 | import Debug 13 | 14 | 15 | main = 16 | Html.program 17 | { init = (initialModel, Cmd.none) 18 | , update = update 19 | , subscriptions = \_ -> Sub.none 20 | , view = view 21 | } 22 | 23 | 24 | type Message 25 | = GetProtagonist 26 | | ProtagonistLoaded (Result Http.Error JsonApi.Resource) 27 | 28 | 29 | view model = 30 | Html.div [] 31 | [ Html.p [] [ Html.text "A long time ago in a galaxy far, far away..." ] 32 | , renderProtagonist model 33 | ] 34 | 35 | 36 | renderProtagonist model = 37 | case model.protagonist of 38 | Nothing -> 39 | Html.button [ Html.Events.onClick GetProtagonist ] [ Html.text "Get Initial Model" ] 40 | 41 | Just character -> 42 | Html.p [] [ Html.text (character.firstName ++ " " ++ character.lastName) ] 43 | 44 | 45 | update message model = 46 | case message of 47 | GetProtagonist -> 48 | ( model, getProtagonist ) 49 | 50 | ProtagonistLoaded (Ok resource) -> 51 | ( { model | protagonist = JsonApi.Resources.attributes characterDecoder resource |> Result.toMaybe }, Cmd.none) 52 | 53 | ProtagonistLoaded (Err error) -> 54 | Debug.log ("Remember to start the server on port 9292! " ++ toString e) (model, Cmd.none) 55 | 56 | 57 | type alias Model = 58 | { protagonist : Maybe Character 59 | } 60 | 61 | 62 | type alias Character = 63 | { firstName : String 64 | , lastName : String 65 | } 66 | 67 | 68 | characterDecoder : Json.Decode.Decoder Character 69 | characterDecoder = 70 | Json.Decode.map2 Character 71 | (Json.Decode.field "first-name" Json.Decode.string) 72 | (Json.Decode.field "last-name" Json.Decode.string) 73 | 74 | 75 | initialModel = 76 | { protagonist = Nothing 77 | } 78 | 79 | 80 | getProtagonist : Cmd Message 81 | getProtagonist = 82 | JsonApi.Http.getPrimaryResource "http://localhost:9292/luke" 83 | |> Http.send ProtagonistLoaded 84 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | require './server' 2 | 3 | run ExampleServer 4 | -------------------------------------------------------------------------------- /example/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 12 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 13 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 14 | "noahzgordon/elm-jsonapi": "2.2.1 <= v < 3.0.0", 15 | "noahzgordon/elm-jsonapi-http": "2.0.0 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.18.0 <= v < 0.19.0" 18 | } 19 | -------------------------------------------------------------------------------- /example/server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'json' 3 | require 'jsonapi-serializers' 4 | 5 | class Character 6 | def initialize(id:, first_name:, last_name:) 7 | @id, @first_name, @last_name = id, first_name, last_name 8 | end 9 | 10 | attr_reader :id, :first_name, :last_name 11 | end 12 | 13 | class CharacterSerializer 14 | include JSONAPI::Serializer 15 | 16 | attribute :first_name 17 | attribute :last_name 18 | end 19 | 20 | class ExampleServer < Sinatra::Base 21 | options "*" do 22 | response.headers["Allow"] = "HEAD,GET,PUT,DELETE,OPTIONS" 23 | response.headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Cache-Control, Accept" 24 | response.headers["Access-Control-Allow-Origin"] = "*" 25 | 200 26 | end 27 | 28 | before do 29 | content_type 'application/vnd.api+json' 30 | headers['Access-Control-Allow-Origin'] = '*' 31 | end 32 | 33 | get '/luke' do 34 | character = Character.new(id: 'luke', first_name: 'Luke', last_name: 'Skywalker') 35 | JSONAPI::Serializer.serialize(character).to_json 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /src/JsonApi/Http.elm: -------------------------------------------------------------------------------- 1 | module JsonApi.Http 2 | exposing 3 | ( getDocument 4 | , getPrimaryResource 5 | , getPrimaryResourceCollection 6 | ) 7 | 8 | {-| A library for requesting resources from JSON API-compliant servers. 9 | Intended to be used in conjunction with `elm-jsonapi`, which provides 10 | serializers and helper functions. 11 | 12 | @docs getDocument, getPrimaryResource, getPrimaryResourceCollection 13 | -} 14 | 15 | import JsonApi 16 | import JsonApi.Decode exposing (document) 17 | import JsonApi.Documents 18 | import Http exposing (Request) 19 | import Task exposing (Task) 20 | import Json.Decode exposing (Decoder) 21 | 22 | 23 | {-| Retreives a JSON API document from the given endpoint. 24 | -} 25 | getDocument : String -> Request JsonApi.Document 26 | getDocument url = 27 | get url extractDocument 28 | 29 | 30 | {-| Retreives the JSON API resource from the given endpoint. 31 | If there the payload is malformed or there is no singleton primary resource, 32 | the error type will be BadPayload. 33 | -} 34 | getPrimaryResource : String -> Request JsonApi.Resource 35 | getPrimaryResource url = 36 | get url (extractDocument >> Result.andThen JsonApi.Documents.primaryResource) 37 | 38 | 39 | {-| Retreives the JSON API resource collection from the given endpoint. 40 | If there the payload is malformed or there is no primary resource collection, 41 | the error type will be BadPayload. 42 | -} 43 | getPrimaryResourceCollection : String -> Request (List JsonApi.Resource) 44 | getPrimaryResourceCollection url = 45 | get url (extractDocument >> Result.andThen JsonApi.Documents.primaryResourceCollection) 46 | 47 | 48 | extractDocument : Http.Response String -> Result String JsonApi.Document 49 | extractDocument { body } = 50 | Json.Decode.decodeString JsonApi.Decode.document body 51 | 52 | 53 | get : String -> (Http.Response String -> Result String a) -> Request a 54 | get url handler = 55 | Http.request 56 | { method = "GET" 57 | , headers = 58 | [ Http.header "Accept" "application/vnd.api+json" ] 59 | , url = url 60 | , body = Http.emptyBody 61 | , expect = Http.expectStringResponse handler 62 | , timeout = Nothing 63 | , withCredentials = False 64 | } 65 | --------------------------------------------------------------------------------