Hook up a portful implementation of GH search

This commit is contained in:
Richard Feldman
2016-03-06 07:56:26 -08:00
parent e9b33a36ef
commit 01e2bd1b66
16 changed files with 572 additions and 18 deletions

14
8/README.md Normal file
View File

@@ -0,0 +1,14 @@
Stage 5
=======
To run tests:
```bash
npm test
```
To engage Auto-Rebuilding:
```bash
npm run watch
```

19
8/elm-package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"version": "1.0.0",
"summary": "Like GitHub, but for Elm stuff.",
"repository": "https://github.com/rtfeldman/elm-workshop.git",
"license": "BSD-3-Clause",
"source-directories": [
"src"
],
"exposed-modules": [],
"dependencies": {
"NoRedInk/elm-check": "3.0.0 <= v < 4.0.0",
"elm-lang/core": "3.0.0 <= v < 4.0.0",
"evancz/elm-effects": "2.0.0 <= v < 3.0.0",
"evancz/elm-html": "4.0.0 <= v < 5.0.0",
"evancz/elm-http": "3.0.0 <= v < 4.0.0",
"evancz/start-app": "2.0.0 <= v < 3.0.0"
},
"elm-version": "0.16.0 <= v < 0.17.0"
}

2
8/github.js Normal file

File diff suppressed because one or more lines are too long

34
8/index.html Normal file
View File

@@ -0,0 +1,34 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>ElmHub</title>
<script type="text/javascript" src="github.js"></script>
<script type="text/javascript" src="elm.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="elm-landing-pad"></div>
</body>
<script type="text/javascript">
var github = new Github();
var app = Elm.embed(
Elm.Main,
document.getElementById("elm-landing-pad"),
{githubResponse: []});
app.ports.githubSearch.subscribe(function(query) {
console.log("Searching for", query);
var search = github.getSearch(query);
search.repositories({}, function (err, repositories) {
console.log("Got response", repositories);
app.ports.githubResponse.send(repositories);
});
});
</script>
</html>

25
8/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "elm-hub",
"version": "1.0.0",
"description": "Like GitHub, but for Elm stuff.",
"scripts": {
"build": "elm-make src/Main.elm --output elm.js",
"watch": "../../node_modules/.bin/elm-live src/Main.elm --open -- --output=elm.js",
"test": "node test.js",
"install": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/rtfeldman/elm-workshop.git"
},
"author": "Richard Feldman",
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/rtfeldman/elm-workshop/issues"
},
"homepage": "https://github.com/rtfeldman/elm-workshop#readme",
"devDependencies": {
"elm-live": "2.0.4",
"elm-test": "0.16.1-alpha3"
}
}

147
8/src/ElmHub.elm Normal file
View File

@@ -0,0 +1,147 @@
module ElmHub (..) where
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import StartApp
import Http
import Task exposing (Task)
import Effects exposing (Effects)
import Json.Decode exposing (Decoder, (:=))
import Json.Encode
import Signal exposing (Address)
searchFeed : Address String -> String -> Task x Action
searchFeed address query =
let
-- See https://developer.github.com/v3/search/#example for how to customize!
url =
"https://api.github.com/search/repositories?q="
++ query
++ "+language:elm&sort=stars&order=desc"
task =
Signal.send address query
|> Task.map (\_ -> DoNothing)
in
Task.onError task (\_ -> Task.succeed DoNothing)
responseDecoder : Decoder (List SearchResult)
responseDecoder =
"items" := Json.Decode.list searchResultDecoder
searchResultDecoder : Decoder SearchResult
searchResultDecoder =
Json.Decode.object3
SearchResult
("id" := Json.Decode.int)
("full_name" := Json.Decode.string)
("stargazers_count" := Json.Decode.int)
type alias Model =
{ query : String
, results : List SearchResult
}
type alias SearchResult =
{ id : ResultId
, name : String
, stars : Int
}
type alias ResultId =
Int
initialModel : Model
initialModel =
{ query = "tutorial"
, results = []
}
view : Address Action -> Model -> Html
view address model =
div
[ class "content" ]
[ header
[]
[ h1 [] [ text "ElmHub" ]
, span [ class "tagline" ] [ text "Like GitHub, but for Elm things." ]
]
, input [ class "search-query", onInput address SetQuery, defaultValue model.query ] []
, button [ class "search-button", onClick address Search ] [ text "Search" ]
, ul
[ class "results" ]
(List.map (viewSearchResult address) model.results)
]
onInput address wrap =
on "input" targetValue (\val -> Signal.message address (wrap val))
defaultValue str =
property "defaultValue" (Json.Encode.string str)
viewSearchResult : Address Action -> SearchResult -> Html
viewSearchResult address result =
li
[]
[ span [ class "star-count" ] [ text (toString result.stars) ]
, a
[ href ("https://github.com/" ++ result.name)
, class "result-name"
, target "_blank"
]
[ text result.name ]
, button
[ class "hide-result", onClick address (HideById result.id) ]
[ text "X" ]
]
type Action
= Search
| SetQuery String
| HideById ResultId
| SetResults (List SearchResult)
| DoNothing
update : Address String -> Action -> Model -> ( Model, Effects Action )
update searchAddress action model =
case action of
Search ->
( model, Effects.task (searchFeed searchAddress model.query) )
SetQuery query ->
( { model | query = query }, Effects.none )
SetResults results ->
let
newModel =
{ model | results = results }
in
( newModel, Effects.none )
HideById idToHide ->
let
newResults =
model.results
|> List.filter (\{ id } -> id /= idToHide)
newModel =
{ model | results = newResults }
in
( newModel, Effects.none )
DoNothing ->
( model, Effects.none )

58
8/src/Main.elm Normal file
View File

@@ -0,0 +1,58 @@
module Main (..) where
import StartApp
import ElmHub exposing (..)
import Effects exposing (Effects)
import Task exposing (Task)
import Html exposing (Html)
import Signal
import Json.Encode
import Json.Decode
main : Signal Html
main =
app.html
app : StartApp.App Model
app =
StartApp.start
{ view = view
, update = update search.address
, init = ( initialModel, Effects.task (searchFeed search.address initialModel.query) )
, inputs = [ responseActions ]
}
port tasks : Signal (Task Effects.Never ())
port tasks =
app.tasks
search : Signal.Mailbox String
search =
Signal.mailbox ""
port githubSearch : Signal String
port githubSearch =
search.signal
responseActions : Signal Action
responseActions =
Signal.map decodeGithubResponse githubResponse
decodeGithubResponse : Json.Encode.Value -> Action
decodeGithubResponse value =
case Json.Decode.decodeValue responseDecoder value of
Ok results ->
SetResults results
Err _ ->
DoNothing
port githubResponse : Signal Json.Encode.Value

91
8/style.css Normal file
View File

@@ -0,0 +1,91 @@
.content {
width: 960px;
margin: 0 auto;
padding: 30px;
font-family: Helvetica, Arial, serif;
}
header {
position: relative;
padding: 6px 12px;
background-color: rgb(96, 181, 204);
}
h1 {
color: white;
font-weight: normal;
margin: 0;
}
.tagline {
color: #eee;
position: absolute;
right: 16px;
top: 12px;
font-size: 24px;
font-style: italic;
}
.results {
list-style-image: url('http://img-cache.cdn.gaiaonline.com/76bd5c99d8f2236e9d3672510e933fdf/http://i278.photobucket.com/albums/kk81/d3m3nt3dpr3p/Tiny-Star-Icon.png');
list-style-position: inside;
padding: 0;
}
.results li {
font-size: 18px;
margin-bottom: 16px;
}
.star-count {
font-weight: bold;
}
.result-name {
color: rgb(96, 181, 204);
margin-left: 16px;
text-decoration: none;
}
.result-name:hover{
text-decoration: underline;
}
.search-query {
padding: 8px;
font-size: 24px;
margin-bottom: 18px;
margin-top: 36px;
}
.search-button {
padding: 8px 16px;
font-size: 24px;
color: white;
border: 1px solid #ccc;
background-color: rgb(96, 181, 204);
margin-left: 12px
}
.search-button:hover {
color: rgb(96, 181, 204);
background-color: white;
}
.hide-result {
background-color: transparent;
border: 0;
font-weight: bold;
font-size: 18px;
margin-left: 18px;
cursor: pointer;
}
.hide-result:hover {
color: rgb(96, 181, 204);
}
button:focus, input:focus {
outline: none;
}

10
8/test.js Normal file
View File

@@ -0,0 +1,10 @@
var fs = require("fs");
var execSync = require("child_process").execSync;
var path = require("path");
var testDir = path.join(__dirname, "test");
var binPath = path.join(__dirname, "..", "..", "node_modules", ".bin");
var elmTestPath = path.join(binPath, "elm-test");
var elmMakePath = path.join(binPath, "elm-make");
execSync(elmMakePath + " TestRunner.elm --yes --output /dev/null", { cwd: testDir });
execSync(elmTestPath + " TestRunner.elm", { cwd: testDir, stdio: "inherit" });

15
8/test/TestRunner.elm Normal file
View File

@@ -0,0 +1,15 @@
module Main where
import Signal exposing (Signal)
import ElmTest exposing (consoleRunner)
import Console exposing (IO, run)
import Task
import Tests
console : IO ()
console = consoleRunner Tests.all
port runner : Signal (Task.Task x ())
port runner = run console

64
8/test/Tests.elm Normal file
View File

@@ -0,0 +1,64 @@
module Tests (..) where
import ElmTest exposing (..)
import ElmHub exposing (responseDecoder)
import Json.Decode as Decode
import Json.Encode as Encode
import Check exposing (Claim, Evidence, check, claim, that, is, for)
import Check.Producer exposing (..)
import Check.Test exposing (evidenceToTest)
import String
import ElmHub exposing (..)
import Random
all : Test
all =
suite
"Decoding responses from GitHub"
[ test "they can decode empty responses"
<| let
emptyResponse =
"""{ "items": [] }"""
in
assertEqual
(Decode.decodeString responseDecoder emptyResponse)
(Ok [])
, test "they can decode responses with results in them"
<| let
response =
"""{ "items": [
{ "id": 5, "full_name": "foo", "stargazers_count": 42 },
{ "id": 3, "full_name": "bar", "stargazers_count": 77 }
] }"""
in
assertEqual
(Decode.decodeString responseDecoder response)
(Ok
[ { id = 5, name = "foo", stars = 42 }
, { id = 3, name = "bar", stars = 77 }
]
)
, (claim "they can decode individual search results"
`that` (\( id, name, stars ) -> encodeAndDecode id name stars)
`is` (\( id, name, stars ) -> Ok (SearchResult id name stars))
`for` tuple3 ( int, string, int )
)
|> check 100 defaultSeed
|> evidenceToTest
]
encodeAndDecode : Int -> String -> Int -> Result String SearchResult
encodeAndDecode id name stars =
[ ( "id", Encode.int id )
, ( "full_name", Encode.string name )
, ( "stargazers_count", Encode.int stars )
]
|> Encode.object
|> Encode.encode 0
|> Decode.decodeString searchResultDecoder
defaultSeed =
Random.initialSeed 42

22
8/test/elm-package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"version": "1.0.0",
"summary": "Like GitHub, but for Elm stuff.",
"repository": "https://github.com/rtfeldman/elm-workshop.git",
"license": "BSD-3-Clause",
"source-directories": [
".",
"../src"
],
"exposed-modules": [],
"dependencies": {
"NoRedInk/elm-check": "3.0.0 <= v < 4.0.0",
"deadfoxygrandpa/elm-test": "3.1.1 <= v < 4.0.0",
"elm-lang/core": "3.0.0 <= v < 4.0.0",
"evancz/elm-effects": "2.0.0 <= v < 3.0.0",
"evancz/elm-html": "4.0.0 <= v < 5.0.0",
"evancz/elm-http": "3.0.0 <= v < 4.0.0",
"evancz/start-app": "2.0.0 <= v < 3.0.0",
"laszlopandy/elm-console": "1.0.3 <= v < 2.0.0"
},
"elm-version": "0.16.0 <= v < 0.17.0"
}

2
stages/7/github.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,22 +4,36 @@
<head>
<meta charset="UTF-8">
<title>ElmHub</title>
<script type="text/javascript" src="github.js"></script>
<script type="text/javascript" src="elm.js"></script>
<!-- Uncomment the below line to enable elm-reactor support. -->
<!-- <script type="text/javascript" src="/_reactor/debug.js"></script> -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="elm-landing-pad"></div>
</body>
<script type="text/javascript">
var app = Elm.fullscreen(Elm.Main, {});
// documentation: https://github.com/michael/github
var github = new Github();
// Uncomment this line and comment out the above to enable elm-reactor support.
// var app = Elm.fullscreenDebug("ElmHub", "Main.elm");
var app = Elm.embed(
Elm.Main,
document.getElementById("elm-landing-pad"),
{githubResponse: []});
function searchGithub(query) {
console.log("Searching for", query);
var search = github.getSearch(query);
search.repositories({}, function (err, repositories) {
console.log("Got response", repositories);
// TODO: app.ports.portNameGoesHere.send(repositories);
});
}
// TODO app.ports.portNameGoesHere.subscribe(searchGithub);
</script>
</html>

View File

@@ -12,8 +12,8 @@ import Json.Encode
import Signal exposing (Address)
searchFeed : String -> Task x Action
searchFeed query =
searchFeed : Address String -> String -> Task x Action
searchFeed address query =
let
-- See https://developer.github.com/v3/search/#example for how to customize!
url =
@@ -21,11 +21,13 @@ searchFeed query =
++ query
++ "+language:elm&sort=stars&order=desc"
-- These only talk to JavaScript ports now. They don't
-- actually do any actions themselves.
task =
Http.get responseDecoder url
|> Task.map SetResults
Signal.send address query
|> Task.map (\_ -> DoNothing)
in
Task.onError task (\_ -> Task.succeed (SetResults []))
Task.onError task (\_ -> Task.succeed DoNothing)
responseDecoder : Decoder (List SearchResult)
@@ -113,13 +115,14 @@ type Action
| SetQuery String
| HideById ResultId
| SetResults (List SearchResult)
| DoNothing
update : Action -> Model -> ( Model, Effects Action )
update action model =
update : Address String -> Action -> Model -> ( Model, Effects Action )
update searchAddress action model =
case action of
Search ->
( model, Effects.task (searchFeed model.query) )
( model, Effects.task (searchFeed searchAddress model.query) )
SetQuery query ->
( { model | query = query }, Effects.none )
@@ -141,3 +144,6 @@ update action model =
{ model | results = newResults }
in
( newModel, Effects.none )
DoNothing ->
( model, Effects.none )

View File

@@ -5,6 +5,9 @@ import ElmHub exposing (..)
import Effects exposing (Effects)
import Task exposing (Task)
import Html exposing (Html)
import Signal
import Json.Encode
import Json.Decode
main : Signal Html
@@ -16,12 +19,40 @@ app : StartApp.App Model
app =
StartApp.start
{ view = view
, update = update
, init = ( initialModel, Effects.task (searchFeed initialModel.query) )
, inputs = []
, update = update search.address
, init = ( initialModel, Effects.task (searchFeed search.address initialModel.query) )
, inputs = [ responseActions ]
}
port tasks : Signal (Task Effects.Never ())
port tasks =
app.tasks
search : Signal.Mailbox String
search =
Signal.mailbox ""
port githubSearch : Signal String
port githubSearch =
search.signal
responseActions : Signal Action
responseActions =
Signal.map decodeGithubResponse githubResponse
decodeGithubResponse : Json.Encode.Value -> Action
decodeGithubResponse value =
case Json.Decode.decodeValue responseDecoder value of
Ok results ->
SetResults results
Err _ ->
DoNothing
port githubResponse : Signal Json.Encode.Value