I'm having some trouble decoding optional fields from a JSON string. I am trying to decode "plannings" and a planning can be of two types, a normal planning, or a flex planning. If it is a normal planning, it will have the a planning_id, if it is a flex planning, it will have a flexplanning_id. In the record in which I will store plannings, both planningId and fiexplanningId are of type Maybe Int.
type alias Planning =
{ time : String
, planningId : Maybe Int
, groupId : Int
, groupName : String
, flex : Bool
, flexplanningId : Maybe Int
, employeeTimeslotId : Maybe Int
, employeeId : Int
}
And here is the decoder I use:
planningDecoder : Decoder Planning
planningDecoder =
decode Planning
|> required "time" string
|> optional "planning_id" (nullable int) Nothing
|> required "group_id" int
|> required "group_name" string
|> required "flex" bool
|> optional "employee_timeslot_id" (nullable int) Nothing
|> optional "flexplanning_id" (nullable int) Nothing
|> required "employee_id" int
However, the decoder isn't accurately decoding and storing the data from JSON. Here is an example. This is one piece of the string returned by a request made by my application:
"monday": [
{
"time": "07:00 - 17:00",
"planning_id": 6705,
"group_name": "De rode stip",
"group_id": 120,
"flex": false,
"employee_timeslot_id": 1302,
"employee_id": 120120
},
{
"time": "07:00 - 17:00",
"group_name": "vakantie groep",
"group_id": 5347,
"flexplanning_id": 195948,
"flex": true,
"employee_id": 120120
}
],
This, however, is the result of the decoder:
{ monday = [
{ time = "07:00 - 17:00"
, planningId = Just 6705
, groupId = 120
, groupName = "De rode stip"
, flex = False, flexplanningId = Just 1302
, employeeTimeslotId = Nothing
, employeeId = 120120 }
,{ time = "07:00 - 17:00"
, planningId = Nothing
, groupId = 5347
, groupName = "vakantie groep"
, flex = True
, flexplanningId = Nothing
, employeeTimeslotId = Just 195948
, employeeId = 120120
}
],
As you can see, in the JSON, there are two plannings, one with a planning_id and the other with a flexplanning_id. However, in the record produced by the decoder, the first planning has both a planningId and a flexplanningId, whereas the second has neither.
You need to flip these two lines in the decoder to match the order in which they are defined:
|> optional "employee_timeslot_id" (nullable int) Nothing
|> optional "flexplanning_id" (nullable int) Nothing
They are defined in this order:
, flexplanningId : Maybe Int
, employeeTimeslotId : Maybe Int
Related
What am I doing wrong that is causing the value reported by onInput to be a character behind?
For example, type "mil" in the text field to filter to the mileposts row. Then delete it back to nothing and you'll see its still filtering mileposts (also see the browser console to see that value is still "m" even thought the text field is visibly "")
module Main exposing (main)
import Browser
import Html exposing (Html, a, button, div, input, li, span, text, ul)
import Html.Attributes exposing (checked, class, classList, placeholder, style, type_, value)
import Html.Events exposing (custom, onBlur, onClick, onFocus, onInput)
import Json.Decode as Json
type alias Layer =
{ name : String
, description : String
, selected : Bool
}
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
type alias Model =
{ open : Bool
, layers : List Layer
, filtered : List Layer
, searchText : String
, highlightedIndex : Int
}
init : () -> ( Model, Cmd Msg )
init _ =
let
layers =
[ { name = "Parcels", description = "Show parcel lines", selected = False }
, { name = "Mileposts", description = "Show Mile post markers", selected = False }
]
in
( { open = False, layers = layers, filtered = layers, searchText = "", highlightedIndex = 0 }, Cmd.none )
type Msg
= Open
| Close
| Change String
| Up
| Down
| Toggle
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
let
lastIndex =
List.length model.filtered - 1
in
case msg of
Open ->
( { model | open = True }, Cmd.none )
Close ->
( { model | open = False }, Cmd.none )
Change value ->
let
filtered =
model.layers
|> List.filter
(\{ name } ->
let
_ =
Debug.log "name" name
_ =
Debug.log "searchText" model.searchText
in
String.contains (String.toLower model.searchText) (String.toLower name) |> Debug.log "contains"
)
in
( { model | searchText = value, filtered = filtered }, Cmd.none )
Up ->
if model.highlightedIndex == 0 then
( { model | highlightedIndex = lastIndex }, Cmd.none )
else
( { model | highlightedIndex = model.highlightedIndex - 1 }, Cmd.none )
Down ->
if model.highlightedIndex == lastIndex then
( { model | highlightedIndex = 0 }, Cmd.none )
else
( { model | highlightedIndex = model.highlightedIndex + 1 }, Cmd.none )
Toggle ->
let
highlightedLayer =
model.filtered
|> List.indexedMap Tuple.pair
|> List.filterMap
(\( idx, layer ) ->
if idx == model.highlightedIndex then
Just layer
else
Nothing
)
updatedFiltered =
model.filtered
|> List.indexedMap
(\idx layer ->
if idx == model.highlightedIndex then
{ layer | selected = not layer.selected }
else
layer
)
updatedLayers =
model.layers
|> List.map
(\layer ->
if [ layer ] == highlightedLayer then
{ layer | selected = not layer.selected }
else
layer
)
in
( { model | filtered = updatedFiltered, layers = updatedLayers }, Cmd.none )
view model =
div []
[ span [ class "mapboxgl-ctrl-geocoder--icon mapboxgl-ctrl-geocoder--icon-search" ]
[ span [ class "ds-badge ds-badge--red ds-badge--circle", style "margin-top" "-2px" ] [ text "3" ]
]
, input
[ type_ "text"
, class "mapboxgl-ctrl-geocoder--input"
, placeholder "Search layers"
, value model.searchText
, onFocus Open
, style "padding-left" "45px"
, onInput Change
--, onKey [(38, Up), (40, Down), (13, Toggle)]
]
[]
, if model.open then
div [ class "suggestions-wrapper" ]
[ ul [ class "suggestions", style "display" "block" ]
(model.filtered
|> List.indexedMap
(\idx { name, description, selected } ->
li [ classList [ ( "active", model.highlightedIndex == idx ) ] ]
[ a []
[ div [ class "mapboxgl-ctrl-geocoder--suggestion flex flex-column" ]
[ div [] [ input [ type_ "checkbox", style "margin-top" "5px", checked selected ] [] ]
, div [ class "ml-1" ]
[ div [ class "mapboxgl-ctrl-geocoder--suggestion-title" ] [ text name ]
, div [ class "mapboxgl-ctrl-geocoder--suggestion-address" ] [ text description ]
]
]
]
]
)
)
]
else
text ""
]
onKey : List ( Int, Msg ) -> Html.Attribute Msg
onKey codes =
let
isEnterKey keyCode =
case codes |> List.filter (\( code, _ ) -> code == keyCode) of
[ ( _, msg ) ] ->
Json.succeed
{ message = msg
, stopPropagation = True
, preventDefault = True
}
_ ->
Json.fail "silent failure :)"
in
custom "keydown" <|
Json.andThen isEnterKey Html.Events.keyCode
options =
{ stopPropagation = True
, preventDefault = True
}
https://ellie-app.com/fcgHCF2z5sza1
The problem is here, when handling the Change message from onInput:
Change value ->
let
filtered =
model.layers
|> List.filter
(\{ name } ->
let
_ =
Debug.log "name" name
_ =
Debug.log "searchText" model.searchText
in
String.contains (String.toLower model.searchText) (String.toLower name) |> Debug.log "contains"
)
in
( { model | searchText = value, filtered = filtered }, Cmd.none )
You're using model.searchText to filter the list, binding the result to filtered, then updating model with the new searchText and filtered list. model.searchText still has the previous value when you're filtering. Use value instead when filtering, then it works as expected.
I basically have a little select dropdown like this:
viewDropdown : Model -> Html Msg
viewDropdown model =
let
options =
[ ( "", "-- Select --" )
, ( "not_available", "Unavailable" )
, ( "available", "Available" )
]
buildOption ( k, v ) =
option [ value k, selected (k == model.isAvailable) ] [ text v ]
viewOptions =
options
|> List.map
(\( k, v ) ->
buildOption ( k, v )
)
in
div [ class "styled-select" ]
[ select
[ on "change" (Decode.map (UpdateAvailability) targetValue)
]
viewOptions
]
If the user selects "Available", a modal pops up and they are prompted to confirm. If they hit "Cancel", I want the Select dropdown's value to reset to the value of "". This is not the case and although my model reflects the a Nothing val, the dropdown selection option is still on "Available". Any idea on what I can do to reset the DOM state?
While this doesn't answer why your code doesn't work, it seems that if you add the step of confirming the selection, then code will work:
Ellie example, with full code below: https://ellie-app.com/3P5TTM9YqVWa1
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text, option, select)
import Html.Events exposing (onClick, on, targetValue)
import Html.Attributes exposing (value, selected, class)
import Json.Decode as Decode
import Task
type alias Model =
{ isAvailable : String , showConfirm: Bool }
init : Model
init =
{ isAvailable = "-", showConfirm = False }
type Msg
= UpdateAvailability String
| ConfirmYes
| Reset
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateAvailability v ->
{ model | isAvailable = v, showConfirm = (v == "available") }
ConfirmYes ->
{ model | showConfirm = False }
Reset ->
{ model | isAvailable = "-", showConfirm = False }
viewDropdown : Model -> Html Msg
viewDropdown model =
let
options =
[ ( "-", "-- Select --" )
, ( "not_available", "Unavailable" )
, ( "available", "Available" )
]
buildOption ( k, v ) =
option [ value k, selected (k == model.isAvailable) ] [ text v ]
viewOptions = List.map buildOption options
in
div []
[ select
[ on "change" (Decode.map (UpdateAvailability) targetValue) ]
viewOptions
]
viewConfirm model =
if model.showConfirm then
div []
[ text "Really available?"
, button [ onClick ConfirmYes ] [ text "Yes" ]
, button [ onClick Reset ] [ text "No" ]
]
else
div [] []
view : Model -> Html Msg
view model =
div []
[ viewDropdown model
, viewConfirm model
, button [ onClick Reset ] [ text "Reset" ]
]
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
Turns out, adding a value attribute to the select html will override the option selected state, so this is pretty important if you need your select input to be in sync with model state.
https://ellie-app.com/3NZgYQYKv2Fa1
Are you setting model.isAvailable to "" when hitting the close button? Your code should work as is https://ellie-app.com/3NYRvgwkHWPa1
I'm trying to implement a drag and drop program, using boundingClientRect from the DOM package to get the dimensions of the element to be moved, and position from the Mouse to track the movement of the mouse when dragging.
The program works fine before I have scrolled, but when I scroll down, the dragging element appears higher in the view than before I clicked it. What I suspect is happening is, boundingClientRect gets the position of the element relative to the viewpoint, and then I use those values to set the top and left values. However, top and left are relative to the document or to a parent element. However, I have no idea what I could use instead of or in addition to boundingClientRect to get the left and top values relative to the document or parent element.
Here's the code, it's probably clearer than my rambling.
type alias Model =
{ movableItemsList : List Item
, originalMovableItems : List Item
, movingItem : Maybe ( Item, Rectangle )
, receivingItemsList : List Item
, updatedItemsList : List ( Item, Rectangle )
, drag : Maybe Drag
, scrollTop : Float
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
DragAndDelete deleteMsg xy movingItem movingRectangle ->
model
! [ command (DragStart xy movingItem movingRectangle)
, command (deleteMsg movingItem)
]
DragStart xy selectedItem movingRectangle ->
let
movingItem =
List.head (List.filter (\i -> i.id == selectedItem.id) model.originalMovableItems)
|> Maybe.withDefault (Item "" "" 0 "")
in
{ model
| drag = Just (Drag xy xy)
, movingItem = Just ( movingItem, movingRectangle )
}
! []
DragAt xy ->
{ model
| drag =
(Maybe.map (\{ start } -> Drag start xy) model.drag)
}
! []
DragEnd _ ->
{ model
| movingItem = Nothing
, drag = Nothing
}
! []
DeleteFromUpdatedList movingItem ->
let
isKeepable iteratingItem =
iteratingItem.id /= movingItem.id
updatedItemsData =
List.filter (\( i, _ ) -> isKeepable i) model.updatedItemsList
in
{ model
| updatedItemsList = updatedItemsData
}
! []
DeleteFromMovableList movingItem ->
let
isKeepable iteratingItem =
iteratingItem.id /= movingItem.id
movableItemsData =
List.filter isKeepable model.movableItemsList
in
{ model
| movableItemsList = movableItemsData
}
! []
UpdateReceivingItemsOnOverlap receivingRectangle receivingItem ->
let
receivingItemsData =
if (checkOverlap (getCurrentMovingRectangle model) receivingRectangle) then
List.map (\i -> updateItemColor i receivingItem) model.receivingItemsList
else
model.receivingItemsList
in
{ model | receivingItemsList = receivingItemsData } ! []
RestoreReceivingItemsListColor _ ->
let
receivingItemsData =
List.map restoreReceivingItemColor model.receivingItemsList
in
{ model | receivingItemsList = receivingItemsData } ! []
AddValues receivingRectangle receivingItem ->
let
movingItem =
movingItemMaybe model.movingItem
updatedItemsData =
if (checkOverlap (getCurrentMovingRectangle model) receivingRectangle) then
( { movingItem
| value = receivingItem.value + movingItem.value
, color = "#1A6B0D"
}
, receivingRectangle
)
:: model.updatedItemsList
else
model.updatedItemsList
in
{ model
| updatedItemsList = updatedItemsData
}
! [ command (DeleteFromMovableList movingItem)
]
RestoreListContent ->
let
movingItem =
movingItemMaybe model.movingItem
listItems =
movingItem :: model.movableItemsList
in
{ model | movableItemsList = listItems } ! []
getCurrentMovingRectangle : Model -> Rectangle
getCurrentMovingRectangle model =
let
movingItemTuple =
Maybe.withDefault ( Item "" "" 0 "0", Rectangle 0 0 0 0 ) model.movingItem
( _, movingRect ) =
movingItemTuple
in
case model.drag of
Nothing ->
movingRect
Just { start, current } ->
Rectangle
(movingRect.top + toFloat (current.y - start.y))
(movingRect.left + toFloat (current.x - start.x))
(movingRect.width)
(movingRect.height)
-- VIEW
view : Model -> Html Msg
view model =
div
[]
[ receivingAndUpdatedItemsLayersDiv model
, movableItemsListDiv model
, if model.movingItem /= Nothing then
movingItemDiv model
else
div [] []
]
receivingAndUpdatedItemsLayersDiv : Model -> Html Msg
receivingAndUpdatedItemsLayersDiv model =
div
[ style [ ( "position", "relative" ) ] ]
[ div
[ style
[ ( "position", "relative" )
, ( "top", "10px" )
, ( "left", "80px" )
]
]
[ div
[ style
[ ( "z-index", "3" )
, ( "position", "absolute" )
]
, attribute "class" "drag-here-overlay"
]
(List.map receivingItemOverlay model.receivingItemsList)
, div
[ style
[ ( "z-index", "0" )
, ( "position", "absolute" )
]
, attribute "class" "drag-here-underlay"
]
(List.map receivingItemUnderlay model.receivingItemsList)
]
, div
[]
[ div
[ style
[ ( "position", "absolute" )
, ( "z-index", "1" )
]
, attribute "class" "drag-here-updated"
]
(List.map updatedItemUnderlay model.updatedItemsList)
, div
[ style
[ ( "position", "absolute" )
, ( "z-index", "4" )
]
]
(List.map updatedItemOverlay model.updatedItemsList)
]
]
movableItemsListDiv : Model -> Html Msg
movableItemsListDiv model =
div
[ style
[ ( "position", "relative" )
, ( "top", "10px" )
, ( "left", "800px" )
]
]
(List.map movableItemDiv model.movableItemsList)
updatedItemUnderlay : ( Item, Rectangle ) -> Html Msg
updatedItemUnderlay ( item, rectangle ) =
div
[ attribute "class" "drag-here-updated-underlay-item"
, sharedStyles
, style
[ ( "background-color", item.color )
, ( "border", "1px solid #000" )
, ( "position", "absolute" )
, ( "left", px rectangle.left )
, ( "top", px rectangle.top )
]
]
[ text item.text
, br [] []
, text (toString item.value)
]
updatedItemOverlay : ( Item, Rectangle ) -> Html Msg
updatedItemOverlay ( item, rectangle ) =
div
[ onDragStart DeleteFromUpdatedList item
, attribute "class" "drag-here-updated-overlay-item"
, sharedStyles
, style
[ ( "background-color", "transparent" )
, ( "position", "absolute" )
, ( "left", px rectangle.left )
, ( "top", px rectangle.top )
]
]
[]
receivingItemUnderlay : Item -> Html Msg
receivingItemUnderlay item =
div
[ attribute "class" "drag-here-underlay-item"
, sharedStyles
, style
[ ( "background-color", item.color )
-- , ( "border", "1px solid #1A6B0D" )
]
]
[ text item.text
, br [] []
, text (toString item.value)
]
receivingItemOverlay : Item -> Html Msg
receivingItemOverlay item =
div
[ on "mouseenter" (Decode.map (\d -> UpdateReceivingItemsOnOverlap d item) (DOM.target DOM.boundingClientRect))
, on "mouseleave" (Decode.map (\d -> RestoreReceivingItemsListColor d) (DOM.target DOM.boundingClientRect))
, on "mouseup" (Decode.map (\d -> AddValues d item) (DOM.target DOM.boundingClientRect))
, attribute "class" "drag-here-overlay-item"
, sharedStyles
, style
[ ( "background-color", "transparent" ) ]
]
[]
movableItemDiv : Item -> Html Msg
movableItemDiv item =
div
[ onDragStart DeleteFromMovableList item
, attribute "id" ("drag-me " ++ toString item.value)
, sharedStyles
, style
[ ( "background-color", item.color )
, ( "border", "1px solid #DD0848" )
, ( "position", "relative" )
]
]
[ text "Drag Me!"
, br [] []
, text (toString item.value)
]
movingItemDiv : Model -> Html Msg
movingItemDiv model =
let
movingItem =
movingItemMaybe model.movingItem
realRectangle =
getCurrentMovingRectangle model
in
div
[ onMouseUp RestoreListContent
, sharedStyles
, style
[ ( "background-color", "#FF3C8C" )
, ( "border", "1px solid #DD0848" )
, ( "position", "absolute" )
, ( "top", px (realRectangle.top) )
, ( "left", px (realRectangle.left) )
, ( "z-index", "2" )
]
]
[ text movingItem.text
, br [] []
, text (toString movingItem.value)
]
sharedStyles : Attribute a
sharedStyles =
style
[ ( "width", "100px" )
, ( "height", "100px" )
, ( "border-radius", "4px" )
, ( "color", "white" )
, ( "justify-content", "center" )
, ( "align-items", "center" )
, ( "display", "flex" )
]
onDragStart : (Item -> Msg) -> Item -> Attribute Msg
onDragStart deleteMsg item =
on "mousedown"
(Mouse.position
`Decode.andThen`
(\posit ->
DOM.target DOM.boundingClientRect
`Decode.andThen`
(\rect ->
Decode.succeed (DragAndDelete deleteMsg posit item rect)
)
)
)
px : countable -> String
px number =
toString number ++ "px"
So, as you can see, when one clicks a movableItemDiv, the model's drag and movingItem fields are updated with the position of the mouse and the dimensions (Rectangle) of the movableItem respectively. However, these dimensions are relative to the viewpoint. movingItemDiv then calls getCurrentMovingRectangle, which sets the left and top styles of movingItemDiv according to the dimensions of the movingItem and the drag in the model. Because the dimensions of the movingItem are based on dimensions of the movableItemDiv relative to the viewpoint, not relative to the document, while the values set for the top and left values of the movingItemDiv establish the position of the element relative to the document (or the parent element, I'm not sure to be honest), the movingItemDiv is not positioned correctly. I hope this is clear!
Updated to elm-0.18
Below is a quick and dirty example of a list with draggable items
(which you can copy to elm-lang.org/try to see it in action)
each item has a relative positioning
transform: translate() is used to position the item being dragged
we do not know the absolute position of the item, but we do know how much it has moved relative to its (unknown) starting position.
Next step would be to determine if we are over a drop-zone when drag ends.
To calculate, you would need to know:
the relative position of your drop zones compared to the top-left corner of your list container
the size (width, height) of each drop zone
the original position of the item being dragged relative to top-left of the list container
for that, you would need to know the actual height of each item (I always used a fixed height on each item)
the amount of scroll in the list container (using Dom.y from elm-lang/dom)
Hope this will help you in the right direction!
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (on)
import Json.Decode as Json
import Mouse exposing (Position)
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
type alias Model =
{ position : Position
, items : List String
, drag : Maybe Drag
}
type alias Drag =
{ id : Int
, start : Position
, current : Position
}
init : ( Model, Cmd Msg )
init =
Model
(Position 200 200)
[ "Apples", "Bananas", "Cherries", "Dades" ]
Nothing
! []
-- UPDATE
type Msg
= DragStart Int Position
| DragAt Position
| DragEnd Position
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
( updateHelp msg model, Cmd.none )
updateHelp : Msg -> Model -> Model
updateHelp msg ({position, items, drag} as model) =
case msg of
DragStart id xy ->
Model position items (Just (Drag id xy xy))
DragAt xy ->
Model position items (Maybe.map (\{id, start} -> Drag id start xy) drag)
DragEnd _ ->
Model position items Nothing
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
case model.drag of
Nothing ->
Sub.none
Just _ ->
Sub.batch [ Mouse.moves DragAt, Mouse.ups DragEnd ]
-- VIEW
(=>) = (,)
view : Model -> Html Msg
view model =
div []
<| List.indexedMap (itemView model) model.items
itemView : Model -> Int -> String -> Html Msg
itemView model index item =
let
zIndex =
case model.drag of
Just {id} ->
if index == id then
"99"
else
"0"
Nothing ->
"0"
in
div
[ onMouseDown index
, style
[ "background-color" => "#3C8D2F"
, "border" => "2px solid orange"
, "cursor" => "move"
, "position"=> "relative"
, "transform" => (getOffset model index)
, "z-index" => zIndex
, "width" => "100px"
, "height" => "100px"
, "border-radius" => "4px"
, "color" => "white"
, "display" => "flex"
, "align-items" => "center"
, "justify-content" => "center"
, "user-select" => "none"
]
]
[ text item
]
px : Int -> String
px number =
toString number ++ "px"
getOffset : Model -> Int -> String
getOffset {position, items, drag} index =
case drag of
Nothing ->
translate 0 0
Just {id, start,current} ->
if index == id then
translate (current.x - start.x) (current.y - start.y)
else
translate 0 0
translate : Int -> Int -> String
translate x y =
"translate(" ++ toString x ++ "px , " ++ toString y ++ "px)"
onMouseDown : Int -> Attribute Msg
onMouseDown id =
on "mousedown" (Json.map (DragStart id) Mouse.position)
Probably it is a beginner's question. I have a JSON data format that holds polymorphic records and I need to parse it. These are vertices or edges of a graph
{
"records": [{
"id": 0,
"object": {
"id": "vertex1"
}
}, {
"id": 1,
"object": {
"id": "vertex2"
}
}, {
"id": 2,
"object": {
"from": "vertex1",
"to": "vertex2"
}
}]
}
As you can see they all have id, but vertices and edges have different record structures.
I tried to find something on parsing such structures, but the only thing I found was Handling records with shared substructure in Elm, but I cannot translate the answer to Elm 0.17 (a simple renaming of data to type did not help)
In general there are 2 challenges:
defining a polymorphic record
decode JSON dynamically into a vertex or an edge
This is how far I got:
type alias RecordBase =
{ id : Int
}
type Records = List (Record RecordBase)
type Record o =
VertexRecord o
| EdgeRecord o
type alias VertexRecord o =
{ o | object : {
id : Int
}
}
type alias EdgeRecord o =
{ o | object : {
from : Int
, to : Int
}
}
but the compiler complains with
Naming multiple top-level values VertexRecord makes things
ambiguous.
Apparently union already defined the VertexRecord and EdgeRecord types.
I really don't know how to proceed from here. All suggestions are most welcome.
Since you have the label id in multiple places and of multiple types, I think it makes things a little cleaner to have type aliases and field names that indicate each id's purpose.
Edit 2016-12-15: Updated to elm-0.18
type alias RecordID = Int
type alias VertexID = String
type alias VertexContents =
{ vertexID : VertexID }
type alias EdgeContents =
{ from : VertexID
, to : VertexID
}
Your Record type doesn't actually need to include the field name of object anywhere. You can simply use a union type. Here is an example. You could shape this a few different ways, the important part to understand is fitting both types of data in as a single Record type.
type Record
= Vertex RecordID VertexContents
| Edge RecordID EdgeContents
You could define a function that returns the recordID given either a vertex or edge like so:
getRecordID : Record -> RecordID
getRecordID r =
case r of
Vertex recordID _ -> recordID
Edge recordID _ -> recordID
Now, onto decoding. Using Json.Decode.andThen, you can decode the common record ID field, then pass the JSON off to another decoder to get the rest of the contents:
recordDecoder : Json.Decoder Record
recordDecoder =
Json.field "id" Json.int
|> Json.andThen \recordID ->
Json.oneOf [ vertexDecoder recordID, edgeDecoder recordID ]
vertexDecoder : RecordID -> Json.Decoder Record
vertexDecoder recordID =
Json.object2 Vertex
(Json.succeed recordID)
(Json.object1 VertexContents (Json.at ["object", "id"] Json.string))
edgeDecoder : RecordID -> Json.Decoder Record
edgeDecoder recordID =
Json.object2 Edge
(Json.succeed recordID)
(Json.object2 EdgeContents
(Json.at ["object", "from"] Json.string)
(Json.at ["object", "to"] Json.string))
recordListDecoder : Json.Decoder (List Record)
recordListDecoder =
Json.field "records" Json.list recordDecoder
Putting it all together, you can decode your example like this:
import Html exposing (text)
import Json.Decode as Json
main =
text <| toString <| Json.decodeString recordListDecoder testData
testData =
"""
{
"records": [{
"id": 0,
"object": {
"id": "vertex1"
}
}, {
"id": 1,
"object": {
"id": "vertex2"
}
}, {
"id": 2,
"object": {
"from": "vertex1",
"to": "vertex2"
}
}]
}
"""
I'm trying to get all the values of a certain attribute from a json array.
Considering the following json, I'm trying to get all the types e.g. iPhone,home
{
"firstName": "John",
"lastName" : "doe",
"age" : 26,
"address" :
{
"streetAddress": "naist street",
"city" : "Nara",
"postalCode" : "630-0192"
},
"phoneNumbers":
[
{
"type" : "iPhone",
"number": "0123-4567-8888"
},
{
"type" : "home",
"number": "0123-4567-8910"
}
]
}
I am using $.phoneNumbers[*].type which seems to work fine on online parsers
but when I'm using it in big query:
select json_extract(my_column,'$.phoneNumbers[*].type')
from my_table
I get:
JSONPath parse error at: [*].type
You can write a Javascript UDF to do the extraction:
SELECT JSON_EXTRACT('[1,2,3]', '$[*]') parsed
Error: Unsupported operator in JSONPath: *
UDF alternative:
#standardSQL
CREATE TEMPORARY FUNCTION parseJson(libs STRING)
RETURNS ARRAY<INT64>
LANGUAGE js AS """
try {
return JSON.parse(libs);
} catch (e) {
return [];
}
""";
SELECT parseJson('[1,2,3]') parsed
More complex example:
#standardSQL
CREATE TEMPORARY FUNCTION parseJson(libs STRING)
RETURNS ARRAY<STRUCT<x INT64, y INT64, z INT64>>
LANGUAGE js AS """
try {
return JSON.parse(libs);
} catch (e) {
return [];
}
""";
SELECT parseJson(JSON_EXTRACT('{"a":[{"x":1},{"y":2},{"z":3}]}', '$.a')) parsed
(inspired by: https://discuss.httparchive.org/t/javascript-library-detection/955)
json_extract cannot return REPEATED field, it can only do one match - hence no support for *
Yet another interesting (I hope) solution for BigQuery Standard SQL
Can be easily adjusted to whatever specific needs are
#standardSQL
CREATE TEMPORARY FUNCTION parseJson(data STRING)
RETURNS ARRAY<STRUCT<parent STRING, item STRING, key STRING, value STRING>>
LANGUAGE js AS """
x = JSON.parse(data); z = []; processKey(x, '');
function processKey(node, parent) {
if (parent !== '') {parent += '.'};
Object.keys(node).map(function(key) {
value = node[key].toString();
if (!value.startsWith('[object Object]')) {
var q = {}; var arr = parent.split('.');
q.parent = arr[0]; q.item = arr[1];
q.key = key; q.value = value;
z.push(q);
} else {
processKey(node[key], parent + key);
};
});
}; return z;
""";
WITH t AS (
SELECT """ {
"firstName": "John",
"lastName" : "doe",
"age" : 26,
"address" : {
"streetAddress": "naist street", "city" : "Nara", "postalCode" : "630-0192" },
"phoneNumbers": [
{ "type" : "iPhone", "number": "0123-4567-8888"},
{ "type" : "home", "number": "0123-4567-8910"},
{ "type" : "work", "number": "0123-4567-7777"}]
} """ AS info
)
SELECT parent, item, key, value FROM t, UNNEST(parseJson(info))
WHERE parent = 'phoneNumbers' AND key = 'type'