Skip to content

Commit 5084e38

Browse files
committed
represent yaml mappings as erlang maps, remove sort_mappings feature as it is no longer needed
1 parent c0ec227 commit 5084e38

8 files changed

+101
-125
lines changed

README.md

+31-30
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ This application loads [YAML](http://en.wikipedia.org/wiki/Yaml) files into Erla
88
* The tag `!atom` for explicitely tagging values as atoms.
99
* An `implicit_atoms` mode to interpret values that look atom-ish as atoms.
1010
* Customizable schemas via callback modules.
11+
* Mappings returned as native Erlang R17 maps. (passes all unit tests on experimental maps branch, edoc currently broken)
1112

1213
This application embeds the C yaml parser "[libyaml](http://pyyaml.org/wiki/LibYAML)" which is compiled as a NIF.
1314

1415
# Example
1516

1617
###The yaml file...
1718

18-
# demo1.yaml
19-
---
19+
# demo1.yaml
20+
---
2021
receipt: Oz-Ware Purchase Invoice
2122
date: 2007-08-06
2223
customer:
@@ -57,25 +58,25 @@ This application embeds the C yaml parser "[libyaml](http://pyyaml.org/wiki/LibY
5758

5859
3> yaml:load_file("test/yaml/demo1.yaml", [implicit_atoms]).
5960

60-
{ok,[[{customer,[{family,<<"Gale">>},{given,<<"Dorothy">>}]},
61-
{items,[[{descrip,<<"Water Bucket (Filled)">>},
62-
{price,1.47},
63-
{quantity,4},
64-
{part_no,<<"A4786">>}],
65-
[{descrip,<<"High Heeled \"Ruby\" Slippers">>},
66-
{price,100.27},
67-
{size,8},
68-
{quantity,1},
69-
{part_no,<<"E1628">>}]]},
70-
{receipt,<<"Oz-Ware Purchase Invoice">>},
71-
{date,<<"2007-08-06">>},
72-
{specialDelivery,<<"Follow the Yellow Brick Road to the Emerald City. Pay no attention to the ma"...>>},
73-
{ship_to,[{street,<<"123 Tornado Alley\nSuite 16\n">>},
74-
{state,<<"KS">>},
75-
{city,<<"East Centerville">>}]},
76-
{bill_to,[{street,<<"123 Tornado Alley\nSuite 16\n">>},
77-
{state,<<"KS">>},
78-
{city,<<"East Centerville">>}]}]]}
61+
{ok,[#{bill_to => #{city => <<"East Centerville">>,
62+
state => <<"KS">>,
63+
street => <<"123 Tornado Alley\nSuite 16\n">>},
64+
customer => #{family => <<"Gale">>,given => <<"Dorothy">>},
65+
date => <<"2007-08-06">>,
66+
items => [#{descrip => <<"Water Bucket (Filled)">>,
67+
part_no => <<"A4786">>,
68+
price => 1.47,
69+
quantity => 4},
70+
#{descrip => <<"High Heeled \"Ruby\" Slippers">>,
71+
part_no => <<"E1628">>,
72+
price => 100.27,
73+
quantity => 1,
74+
size => 8}],
75+
receipt => <<"Oz-Ware Purchase Invoice">>,
76+
ship_to => #{city => <<"East Centerville">>,
77+
state => <<"KS">>,
78+
street => <<"123 Tornado Alley\nSuite 16\n">>},
79+
specialDelivery => <<"Follow the Yellow Brick Road to the Emerald City. Pay no attention to the ma"...>>}]}
7980

8081

8182
# Installation
@@ -97,14 +98,12 @@ Play with it..
9798

9899
$ export ERL_LIBS=$(pwd)
99100
$ erl
100-
Erlang R15B01 (erts-5.9.1) [source] [64-bit] [smp:3:3] [async-threads:0] [kernel-poll:false]
101-
102-
Eshell V5.9.1 (abort with ^G)
103-
1> yaml:load_file("test/yaml/demo1.yaml").
104-
{ok,[[{<<"specialDelivery">>,
105-
<<"Follow the Yellow Brick Road to the Emerald City. Pay no attention to the man behind the cur"...>>},
106-
{<<"items">>, ...
107-
101+
Erlang R17A (erts-5.11) [source-16b4dc1] [64-bit] [smp:3:3] [async-threads:10] [hipe] [kernel-poll:false]
102+
103+
Eshell V5.11 (abort with ^G)
104+
1> yaml:load_file("test/yaml/demo1.yaml").
105+
{ok,[#{<<"bill_to">> => #{<<"city">> => <<"East Centerville">>,
106+
<<"state">> => <<"KS">>, ...
108107

109108

110109
# Schemas
@@ -116,7 +115,7 @@ The included schemas are:
116115
* `yaml_schema_failsafe`
117116
* `yaml_schema_json`
118117
* `yaml_schema_core`
119-
* `yaml_schame_erlang` (default)
118+
* `yaml_schema_erlang` (default)
120119

121120
The schema is selected with the `schema` option, for example:
122121

@@ -159,6 +158,8 @@ See the behavior documenation for yaml_schema and the 4 schemas included in this
159158

160159
If you are using erlide, I recommend the eclipse yaml editor [yedit](http://code.google.com/p/yedit/).
161160

161+
Erlang R17 implements a native map type which is used by yamler to represent yaml mappings. If you need to use an older Erlang see the git branch `mapping_as_list`.
162+
162163
# Limitations
163164

164165
* This app does not emit yaml.

src/yaml_compose.erl

+32-51
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@
3333
events :: [yaml_libyaml:event()],
3434
anchors :: dict(),
3535
schema :: atom(),
36-
schema_state :: term(),
37-
sort_mappings :: boolean()
36+
schema_state :: term()
3837
}).
3938
% composer state
4039

@@ -54,8 +53,7 @@ compose(Events, Opts) ->
5453
events = Events,
5554
anchors = dict:new(),
5655
schema = Schema,
57-
schema_state = Schema:init(Opts),
58-
sort_mappings = proplists:get_bool(sort_mappings, Opts)
56+
schema_state = Schema:init(Opts)
5957
},
6058
catch compose_stream(State).
6159

@@ -79,15 +77,15 @@ compose_doc(State0 = #state{events=[{document_start, _,_,_}, {document_end, _,_,
7977
State1 = State0#state{events=Rest},
8078
{ok, null, State1};
8179

82-
% normal document case
80+
% normal document case
8381
compose_doc(State0 = #state{events=[{document_start, _,_,_} | Rest]}) ->
8482
State1 = State0#state{events=Rest},
8583
{{_ResolvedTag, ConstructedValue}, State2} = compose_node(State1),
86-
84+
8785
% remove document_end
8886
[{document_end, _,_,_} | Rest2] = State2#state.events,
8987
State3 = State2#state{events=Rest2},
90-
88+
9189
{ok, ConstructedValue, State3};
9290

9391
% stream end case
@@ -96,8 +94,8 @@ compose_doc(State0 = #state{events=[{stream_end, _,_,_}]}) ->
9694
{stream_end, State1}.
9795

9896

99-
-spec compose_node(#state{}) -> {ynode(), #state{}}.
100-
% handle scalar
97+
-spec compose_node(#state{}) -> {ynode(), #state{}}.
98+
% handle scalar
10199
compose_node(State0 = #state{
102100
events=[Head={scalar, {Anchor,Tag,Value,Style},_,_} |Rest],
103101
schema = Schema,
@@ -165,7 +163,7 @@ compose_node(State0 = #state{
165163

166164

167165

168-
-spec compose_sequence([ynode()], #state{}) -> {sequence(), #state{}}.
166+
-spec compose_sequence([ynode()], #state{}) -> {sequence(), #state{}}.
169167
compose_sequence(ConstructedValues, State0 = #state{
170168
events=[{sequence_end, _,_,_} |Rest]}) ->
171169
State1 = State0#state{events=Rest},
@@ -176,74 +174,57 @@ compose_sequence(ConstructedValues, State0) ->
176174
compose_sequence([ConstructedValue|ConstructedValues], State1).
177175

178176

179-
-spec compose_mapping(#state{}) -> {mapping(), #state{}}.
180-
compose_mapping(State) -> compose_mapping(dict:new(), dict:new(), State).
181-
compose_mapping(Map0, Merge, State0 = #state{
182-
events=[{mapping_end, _,_,_} |Rest]}) ->
177+
-spec compose_mapping(#state{}) -> {mapping(), #state{}}.
178+
compose_mapping(State) -> compose_mapping(#{}, #{}, State).
179+
180+
%% Accumulate plain old mapping entries in Map, and merge entries in Merge.
181+
compose_mapping(Map0, Merge, State0 = #state{events=[{mapping_end, _,_,_} |Rest]}) ->
183182
State1 = State0#state{events=Rest},
184-
183+
185184
% notes from http://yaml.org/type/merge.html
186185
% - merged keys do not overwrite any other keys
187186
% - latter merged keys do not overwrite earlier merged keys
188-
Map1 = dict:merge(fun(_K, V1, _V2) -> V1 end, Map0, Merge),
189-
List0 = dict:to_list(Map1),
190-
List1 = case State0#state.sort_mappings of
191-
true -> lists:keysort(1,List0);
192-
_ -> List0
193-
end,
194-
{ List1, State1 };
187+
{ maps:merge(Merge, Map0), % keys in Map take precendence over Merge
188+
State1 };
195189

196190
compose_mapping(Map0, Merge0, State0) ->
197191
{{ KTag, Key}, State1} = compose_node(State0),
198192
{{ VTag, Value}, State2} = compose_node(State1),
199-
193+
200194
case {KTag, VTag} of
195+
%% add single mapping to merge map
201196
{'tag:yaml.org,2002:merge', 'tag:yaml.org,2002:map'} ->
202-
Merge1 = case catch merge_list_unique(Value, Merge0) of
203-
badarg -> merge_error(hd(State1#state.events));
204-
Else -> Else
205-
end,
197+
Merge1 = maps:merge(Value, Merge0),
206198
compose_mapping(Map0, Merge1, State2);
199+
200+
%% add list of mappings to merge map
207201
{'tag:yaml.org,2002:merge', 'tag:yaml.org,2002:seq'} ->
208-
Merge1 = case catch lists:foldl(fun merge_list_unique/2, Merge0, Value) of
209-
badarg -> merge_error(hd(State1#state.events));
210-
Else -> Else
202+
Merge1 = case catch lists:foldl(fun maps:merge/2, Merge0, Value) of
203+
badmap ->
204+
throw_error("Merge sequence must contain only mappings", hd(State1#state.events));
205+
Else when is_map(Else) ->
206+
Else
211207
end,
212208
compose_mapping(Map0, Merge1, State2);
209+
213210
{'tag:yaml.org,2002:merge', _} ->
214-
merge_error(hd(State1#state.events));
211+
throw_error("Merge value must be mapping or sequence of mappings", hd(State1#state.events));
212+
215213
_ ->
216-
Map1 = case dict:is_key(Key, Map0) of
217-
false -> dict:store(Key,Value,Map0);
214+
Map1 = case maps:is_key(Key, Map0) of
215+
false -> maps:put(Key, Value, Map0);
218216
true -> throw_error("Duplicate key", hd(State0#state.events))
219217
end,
220218
compose_mapping(Map1, Merge0, State2)
221219
end.
222220

223-
merge_error(Event) ->
224-
throw_error("Merge value must be mapping or sequence of mappings", Event).
225-
226-
% merge list of items into dict only if key is not already present
227-
merge_list_unique(List, Dict) when is_list(List) ->
228-
lists:foldl(
229-
fun({K,V}, D) ->
230-
case dict:is_key(K, D) of
231-
false -> dict:store(K,V,D);
232-
true -> D
233-
end;
234-
(_,_D) ->
235-
throw(badarg)
236-
end,
237-
Dict, List);
238-
merge_list_unique(_List, _Dict) -> throw(badarg).
239-
240221
-spec maybe_anchor(Anchor, Value, Event, State) -> #state{}
241222
when
242223
Anchor :: null | binary(),
243224
Value :: term(),
244225
Event :: yaml_libyaml:event(),
245226
State :: #state{}.
246-
227+
247228
maybe_anchor(null, _Value, _Event, State0) -> State0;
248229
maybe_anchor(Anchor, Value, Event, State0=#state{anchors=Anchors0}) ->
249230
case dict:is_key(Anchor, Anchors0) of

test/outputs/test_core.term

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
{ok,[[<<"test file for core schema.">>,<<"a">>,<<"b">>,<<"c">>,
2-
[{<<"x">>,1},
3-
{<<"y">>,[{123,<<"abc">>},{<<"abc">>,123}]},
4-
{<<"z">>,[4,5,<<"pqr">>]}],
2+
#{<<"x">> => 1,<<"y">> => #{123 => <<"abc">>,<<"abc">> => 123},<<"z">> => [4,5,<<"pqr">>]},
53
<<"dddd">>,<<"q">>,<<"r">>,1,<<"2">>,<<"3">>,<<"1">>,<<"2">>,<<"3">>,
64
<<"a">>,<<"b">>,<<"c">>,123,123,123,<<"123">>,<<"123">>,123.4,123.4,
75
123.4,<<"123.4">>,<<"123.4">>,true,false,<<"true">>,<<"false">>,
86
<<"true">>,<<"false">>,true,false,true,false,null,null,<<"spookyspoo">>,
97
<<"spooky spoo">>,null,null,null,<<"nulla">>,
10-
[{<<"floats">>,[3.14159,3.4e4]},{<<"ints">>,[123,291,83]}]]]}.
8+
#{<<"floats">> => [3.14159,3.4e4],<<"ints">> => [123,291,83]}]]}.

test/outputs/test_erlang1.term

+10-12
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
{ok,[[<<"test file for erlang schema.">>,<<"a">>,<<"b">>,<<"c">>,
2-
[{<<"x">>,1},
3-
{<<"y">>,[{123,<<"abc">>},{<<"abc">>,123}]},
4-
{<<"z">>,[4,5,<<"pqr">>]}],
2+
#{<<"x">> => 1,<<"y">> => #{123 => <<"abc">>,<<"abc">> => 123},<<"z">> => [4,5,<<"pqr">>]},
53
<<"dddd">>,<<"q">>,<<"r">>,1,<<"2">>,<<"3">>,<<"1">>,<<"2">>,<<"3">>,
64
<<"a">>,<<"b">>,<<"c">>,123,123,123,<<"123">>,<<"123">>,123.4,123.4,
75
123.4,<<"123.4">>,<<"123.4">>,true,false,<<"true">>,<<"false">>,
86
<<"true">>,<<"false">>,true,false,true,false,null,null,<<"spookyspoo">>,
97
<<"spooky spoo">>,null,null,null,<<"nulla">>,
10-
[{<<"floats">>,[3.14159,3.4e4]},{<<"ints">>,[123,291,83]}],
11-
[{<<"x">>,1},{<<"y">>,2}],
12-
[{<<"x">>,0},{<<"y">>,2}],
13-
[{<<"r">>,10}],
14-
[{<<"r">>,1}],
15-
[{<<"label">>,<<"center/big">>},{<<"r">>,10},{<<"x">>,1},{<<"y">>,2}],
16-
[{<<"label">>,<<"center/big">>},{<<"r">>,10},{<<"x">>,1},{<<"y">>,2}],
17-
[{<<"label">>,<<"center/big">>},{<<"r">>,10},{<<"x">>,1},{<<"y">>,2}],
18-
[{<<"label">>,<<"center/big">>},{<<"r">>,10},{<<"x">>,1},{<<"y">>,2}],
8+
#{<<"floats">> => [3.14159,3.4e4],<<"ints">> => [123,291,83]},
9+
#{<<"x">> => 1,<<"y">> => 2},
10+
#{<<"x">> => 0,<<"y">> => 2},
11+
#{<<"r">> => 10},
12+
#{<<"r">> => 1},
13+
#{<<"label">> => <<"center/big">>,<<"r">> => 10,<<"x">> => 1,<<"y">> => 2},
14+
#{<<"label">> => <<"center/big">>,<<"r">> => 10,<<"x">> => 1,<<"y">> => 2},
15+
#{<<"label">> => <<"center/big">>,<<"r">> => 10,<<"x">> => 1,<<"y">> => 2},
16+
#{<<"label">> => <<"center/big">>,<<"r">> => 10,<<"x">> => 1,<<"y">> => 2},
1917
<<"not an atom">>,<<"not an atom">>,<<"an atom">>,'explicit atom',
2018
<<"an_atom">>,<<"anatom">>,<<"aNA_t@OM@_">>,<<"Not_an_atom">>,
2119
<<"Notanatom\"">>,<<"not_an_atom">>,123,14.3]]}.

test/outputs/test_erlang2.term

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
{ok,[[<<"test file for erlang schema.">>,a,b,c,
2-
[{x,1},{y,[{123,abc},{abc,123}]},{z,[4,5,pqr]}],
2+
#{x => 1,y => #{123 => abc,abc => 123},z => [4,5,pqr]},
33
<<"dddd">>,<<"q">>,r,1,<<"2">>,'3',<<"1">>,<<"2">>,<<"3">>,<<"a">>,
44
<<"b">>,<<"c">>,123,123,123,<<"123">>,<<"123">>,123.4,123.4,123.4,
55
<<"123.4">>,<<"123.4">>,true,false,<<"true">>,<<"false">>,<<"true">>,
66
<<"false">>,true,false,true,false,null,null,spookyspoo,
77
<<"spooky spoo">>,null,null,null,nulla,
8-
[{floats,[3.14159,3.4e4]},{ints,[123,291,83]}],
9-
[{x,1},{y,2}],
10-
[{x,0},{y,2}],
11-
[{r,10}],
12-
[{r,1}],
13-
[{label,<<"center/big">>},{r,10},{x,1},{y,2}],
14-
[{label,<<"center/big">>},{r,10},{x,1},{y,2}],
15-
[{label,<<"center/big">>},{r,10},{x,1},{y,2}],
16-
[{label,<<"center/big">>},{r,10},{x,1},{y,2}],
8+
#{floats => [3.14159,3.4e4],ints => [123,291,83]},
9+
#{x => 1,y => 2},
10+
#{x => 0,y => 2},
11+
#{r => 10},
12+
#{r => 1},
13+
#{label => <<"center/big">>,r => 10,x => 1,y => 2},
14+
#{label => <<"center/big">>,r => 10,x => 1,y => 2},
15+
#{label => <<"center/big">>,r => 10,x => 1,y => 2},
16+
#{label => <<"center/big">>,r => 10,x => 1,y => 2},
1717
<<"not an atom">>,<<"not an atom">>,'an atom','explicit atom',an_atom,
1818
anatom,aNA_t@OM@_,<<"Not_an_atom">>,<<"Notanatom\"">>,<<"not_an_atom">>,
1919
123,14.3]]}.

test/outputs/test_failsafe.term

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{ok,[[<<"test file for failsafe schema, only strings are accepted.">>,<<"a">>,
22
<<"b">>,<<"c">>,
3-
[{<<"x">>,<<"1">>},
4-
{<<"y">>,[{<<"123">>,<<"abc">>},{<<"abc">>,<<"123">>}]},
5-
{<<"z">>,[<<"4">>,<<"5">>,<<"pqr">>]}],
3+
#{<<"x">> => <<"1">>,
4+
<<"y">> => #{<<"123">> => <<"abc">>,<<"abc">> => <<"123">>},
5+
<<"z">> => [<<"4">>,<<"5">>,<<"pqr">>]},
66
<<"dddd">>,<<"q">>,<<"r">>,<<"1">>,<<"2">>,<<"3">>,<<"1">>,<<"2">>,
77
<<"3">>,<<"a">>,<<"b">>,<<"c">>]]}.

test/outputs/test_json.term

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
{ok,[[<<"test file for json schema. Unquoted, untagged strings not permitted">>,
2-
[{<<"x">>,1},
3-
{<<"y">>,[{123,<<"abc">>},{<<"abc">>,123}]},
4-
{<<"z">>,[4,5,<<"pqr">>]}],
2+
#{<<"x">> => 1,<<"y">> => #{123 => <<"abc">>,<<"abc">> => 123},<<"z">> => [4,5,<<"pqr">>]},
53
<<"dddd">>,<<"q">>,<<"r">>,1,<<"2">>,<<"3">>,<<"1">>,<<"2">>,<<"3">>,
64
<<"a">>,<<"b">>,<<"c">>,123,123,123,<<"123">>,<<"123">>,123.4,123.4,
75
123.4,<<"123.4">>,<<"123.4">>,true,false,<<"true">>,<<"false">>,

0 commit comments

Comments
 (0)