diff options
| author | Marcos Magueta <maguetamarcos@gmail.com> | 2026-03-04 00:49:09 -0300 |
|---|---|---|
| committer | Marcos Magueta <maguetamarcos@gmail.com> | 2026-03-04 00:49:09 -0300 |
| commit | 295c6a5a1f2b0db9d756ffe7bf413553a0c02737 (patch) | |
| tree | 35b0efd1a99ff7666581c94fc15a3b16bdbb12e3 | |
| parent | a02a0cc86ccc2ca3e05a57debc34ac47379cf3ea (diff) | |
| download | Sakura-295c6a5a1f2b0db9d756ffe7bf413553a0c02737.tar.gz Sakura-295c6a5a1f2b0db9d756ffe7bf413553a0c02737.zip | |
delete xml server
Diffstat (limited to '')
| -rw-r--r-- | src/xml_server.erl | 512 | ||||
| -rw-r--r-- | src/xml_server_app.erl | 28 |
2 files changed, 0 insertions, 540 deletions
diff --git a/src/xml_server.erl b/src/xml_server.erl deleted file mode 100644 index eaa08f9..0000000 --- a/src/xml_server.erl +++ /dev/null @@ -1,512 +0,0 @@ -%%% @doc XML Server for Lazy Relation Consumption -%%% -%%% Provides a TCP server that accepts query plans as Erlang term strings -%%% and returns relation tuples as XML. The server maintains lazy iterators -%%% and allows clients to consume tuples on demand. -%%% -%%% == Protocol == -%%% -%%% Client sends commands as line-delimited strings: -%%% <pre> -%%% QUERY <erlang-term-string> - Execute a query plan, returns session ID -%%% NEXT <session-id> <count> - Get next N tuples from iterator -%%% CLOSE <session-id> - Close iterator and free resources -%%% </pre> -%%% -%%% Server responds with XML: -%%% <pre> -%%% <?xml version="1.0"?> -%%% <response> -%%% <session>abc123</session> -%%% <tuples> -%%% <tuple> -%%% <attribute name="id">1</attribute> -%%% <attribute name="name">Alice</attribute> -%%% </tuple> -%%% </tuples> -%%% </response> -%%% </pre> -%%% -%%% == Usage == -%%% <pre> -%%% % Start server on port 8080 -%%% xml_server:start(8080). -%%% -%%% % Client connects and sends: -%%% % QUERY employees\n -%%% % NEXT <session-id> 10\n -%%% % NEXT <session-id> 10\n -%%% % CLOSE <session-id>\n -%%% </pre> -%%% -%%% @author Nekoma Team -%%% @copyright 2025 - --module(xml_server). - --export([start/1, start/2, stop/0]). --export([init/2, handle_client/2]). - --include("operations.hrl"). - -%%% Public API - -%% @doc Start the XML server on specified port with example database. -%% -%% @param Port TCP port number to listen on -%% @returns {ok, Pid} on success --spec start(pos_integer()) -> {ok, pid()}. -start(Port) -> - DB = repl:example_db(), - start(Port, DB). - -%% @doc Start the XML server on specified port with custom database. -%% -%% @param Port TCP port number to listen on -%% @param DB Database state to use for queries -%% @returns {ok, Pid} on success --spec start(pos_integer(), term()) -> {ok, pid()}. -start(Port, DB) -> - spawn(?MODULE, init, [Port, DB]), - io:format("XML Server started on port ~p~n", [Port]), - {ok, whereis(xml_server)}. - -%% @doc Stop the XML server. -%% -%% @returns ok --spec stop() -> ok. -stop() -> - case whereis(xml_server) of - undefined -> ok; - Pid -> - Pid ! stop, - ok - end. - -%%% Internal Functions - -%% @private -%% @doc Initialize server and start listening. -init(Port, DB) -> - register(xml_server, self()), - {ok, ListenSocket} = gen_tcp:listen(Port, [ - binary, - {packet, line}, - {active, false}, - {reuseaddr, true} - ]), - io:format("Listening for connections...~n"), - accept_loop(ListenSocket, DB). - -%% @private -%% @doc Accept incoming client connections. -accept_loop(ListenSocket, DB) -> - receive - stop -> - gen_tcp:close(ListenSocket), - io:format("Server stopped~n"), - ok - after 0 -> - case gen_tcp:accept(ListenSocket, 1000) of - {ok, ClientSocket} -> - io:format("Client connected: ~p~n", [ClientSocket]), - spawn(?MODULE, handle_client, [ClientSocket, DB]), - accept_loop(ListenSocket, DB); - {error, timeout} -> - accept_loop(ListenSocket, DB); - {error, Reason} -> - io:format("Accept error: ~p~n", [Reason]), - accept_loop(ListenSocket, DB) - end - end. - -%% @private -%% @doc Handle client session with command processing. -handle_client(Socket, DB) -> - Sessions = #{}, - client_loop(Socket, DB, Sessions). - -%% @private -%% @doc Main client command processing loop. -client_loop(Socket, DB, Sessions) -> - case gen_tcp:recv(Socket, 0) of - {ok, Data} -> - Line = binary_to_list(Data), - Command = string:trim(Line), - io:format("Received command: ~s~n", [Command]), - - case parse_command(Command) of - {query, QueryStr} -> - {NewSessions, Response} = handle_query(QueryStr, DB, Sessions), - send_response(Socket, Response), - client_loop(Socket, DB, NewSessions); - - {next, SessionId, Count} -> - {NewSessions, Response} = handle_next(SessionId, Count, Sessions), - send_response(Socket, Response), - client_loop(Socket, DB, NewSessions); - - {close, SessionId} -> - {NewSessions, Response} = handle_close(SessionId, Sessions), - send_response(Socket, Response), - client_loop(Socket, DB, NewSessions); - - {schema} -> - Response = handle_schema(DB), - send_response(Socket, Response), - client_loop(Socket, DB, Sessions); - - {error, Msg} -> - Response = error_response(Msg), - send_response(Socket, Response), - client_loop(Socket, DB, Sessions) - end; - - {error, closed} -> - io:format("Client disconnected~n"), - cleanup_sessions(Sessions), - ok; - - {error, Reason} -> - io:format("Receive error: ~p~n", [Reason]), - cleanup_sessions(Sessions), - ok - end. - -%% @private -%% @doc Parse client command. -parse_command(Command) -> - case string:split(Command, " ", leading) of - ["QUERY", QueryStr] -> - {query, QueryStr}; - ["NEXT", Rest] -> - case string:split(Rest, " ", leading) of - [SessionId, CountStr] -> - try - Count = list_to_integer(CountStr), - {next, SessionId, Count} - catch - _:_ -> {error, "Invalid count"} - end; - _ -> - {error, "Invalid NEXT syntax"} - end; - ["CLOSE", SessionId] -> - {close, SessionId}; - ["SCHEMA"] -> - {schema}; - _ -> - {error, "Unknown command"} - end. - -%% @private -%% @doc Handle QUERY command - execute query and create iterator. -handle_query(QueryStr, DB, Sessions) -> - try - % Parse and evaluate the query plan from string using erl_eval - % This allows parsing of function literals - {ok, Tokens, _} = erl_scan:string(QueryStr ++ "."), - {ok, Exprs} = erl_parse:parse_exprs(Tokens), - {value, QueryPlan, _} = erl_eval:exprs(Exprs, erl_eval:new_bindings()), - - % Prepare and execute query to get ephemeral relation - PreparedPlan = query_planner:prepare(QueryPlan), - EphemeralRelation = query_planner:execute(DB, PreparedPlan), - - % Create actual iterator from ephemeral relation's generator - Iterator = (EphemeralRelation#relation.generator)(), - - % Generate session ID - SessionId = generate_session_id(), - - % Store iterator in sessions - NewSessions = maps:put(SessionId, Iterator, Sessions), - - % Return session ID - Response = session_response(SessionId), - {NewSessions, Response} - catch - Error:Reason:Stack -> - io:format("Query error: ~p:~p~n~p~n", [Error, Reason, Stack]), - {Sessions, error_response(io_lib:format("Query failed: ~p", [Reason]))} - end. - -%% @private -%% @doc Handle NEXT command - fetch next N tuples from iterator. -handle_next(SessionId, Count, Sessions) -> - case maps:get(SessionId, Sessions, undefined) of - undefined -> - {Sessions, error_response("Invalid session ID")}; - Iterator -> - try - % Collect next Count tuples - {Tuples, NewIterator} = collect_n_tuples(Iterator, Count), - - % Update session with new iterator state - NewSessions = maps:put(SessionId, NewIterator, Sessions), - - % Return tuples as XML - Response = tuples_response(Tuples), - {NewSessions, Response} - catch - Error:Reason:Stack -> - io:format("Next error: ~p:~p~n~p~n", [Error, Reason, Stack]), - {Sessions, error_response(io_lib:format("Next failed: ~p", [Reason]))} - end - end. - -%% @private -%% @doc Handle CLOSE command - close iterator and free resources. -handle_close(SessionId, Sessions) -> - case maps:get(SessionId, Sessions, undefined) of - undefined -> - {Sessions, error_response("Invalid session ID")}; - Iterator -> - operations:close_iterator(Iterator), - NewSessions = maps:remove(SessionId, Sessions), - Response = ok_response("Session closed"), - {NewSessions, Response} - end. - -%% @private -%% @doc Handle SCHEMA command - return database schema with all relations and their attributes. -handle_schema(DB) -> - try - % Get all relation names - RelationNames = operations:get_relations(DB), - - % For each relation, get its hash and then the relation record - Relations = lists:map(fun(Name) -> - case operations:get_relation_hash(DB, Name) of - {ok, Hash} -> - % Read the relation record from Mnesia - case mnesia:dirty_read(relation, Hash) of - [Relation] -> - {Name, Relation#relation.schema, Relation#relation.cardinality, - Relation#relation.constraints, Relation#relation.provenance}; - [] -> - {Name, #{}, unknown, #{}, undefined} - end; - {error, _} -> - {Name, #{}, unknown, #{}, undefined} - end - end, RelationNames), - - schema_response(Relations) - catch - Error:Reason:Stack -> - io:format("Schema error: ~p:~p~n~p~n", [Error, Reason, Stack]), - error_response(io_lib:format("Schema failed: ~p", [Reason])) - end. - -%% @private -%% @doc Collect N tuples from iterator lazily. -collect_n_tuples(Iterator, Count) -> - collect_n_tuples(Iterator, Count, []). - -collect_n_tuples(Iterator, 0, Acc) -> - {lists:reverse(Acc), Iterator}; -collect_n_tuples(Iterator, Count, Acc) -> - case operations:next_tuple(Iterator) of - {ok, Tuple} -> - collect_n_tuples(Iterator, Count - 1, [Tuple | Acc]); - done -> - {lists:reverse(Acc), Iterator} - end. - -%% @private -%% @doc Generate unique session ID. -generate_session_id() -> - Timestamp = erlang:system_time(microsecond), - Random = rand:uniform(1000000), - Hash = crypto:hash(md5, term_to_binary({Timestamp, Random})), - binary_to_list(base64:encode(Hash)). - -%% @private -%% @doc Cleanup all sessions on disconnect. -cleanup_sessions(Sessions) -> - maps:foreach( - fun(_SessionId, Iterator) -> - operations:close_iterator(Iterator) - end, - Sessions). - -%%% XML Response Formatters - -%% @private -%% @doc Format session response. -session_response(SessionId) -> - [ - "<?xml version=\"1.0\"?>\n", - "<response>\n", - " <status>ok</status>\n", - " <session>", SessionId, "</session>\n", - "</response>\n" - ]. - -%% @private -%% @doc Format schema response as XML. -schema_response(Relations) -> - RelationsXML = lists:map(fun({Name, Schema, Cardinality, Constraints, Provenance}) -> - % Convert cardinality to string - CardStr = case Cardinality of - {finite, N} -> io_lib:format("finite(~p)", [N]); - aleph_zero -> "infinite"; - continuum -> "continuum"; - _ -> "unknown" - end, - - % Convert schema map to attribute elements - % Wrap each attribute in a list to keep it as a single element when reversed - Attributes = maps:fold(fun(AttrName, Type, Acc) -> - [ - [" <attribute name=\"", atom_to_list(AttrName), "\" type=\"", - atom_to_list(Type), "\"/>\n"] - | Acc - ] - end, [], Schema), - - % Convert constraints to XML - ConstraintsXML = case maps:size(Constraints) of - 0 -> []; - _ -> [ - " <constraints>\n", - maps:fold(fun(AttrName, Constraint, Acc) -> - ConstraintStr = format_constraint(Constraint), - [[" <constraint attribute=\"", atom_to_list(AttrName), - "\">", ConstraintStr, "</constraint>\n"] | Acc] - end, [], Constraints), - " </constraints>\n" - ] - end, - - % Convert provenance to XML - ProvenanceXML = case Provenance of - undefined -> []; - _ -> [ - " <provenance>", format_provenance(Provenance), "</provenance>\n" - ] - end, - - [ - " <relation name=\"", atom_to_list(Name), "\" cardinality=\"", CardStr, "\">\n", - lists:reverse(Attributes), - ConstraintsXML, - ProvenanceXML, - " </relation>\n" - ] - end, Relations), - - [ - "<?xml version=\"1.0\"?>\n", - "<response>\n", - " <status>ok</status>\n", - " <schema>\n", - RelationsXML, - " </schema>\n", - "</response>\n" - ]. - -%% @private -%% @doc Format constraint as string. -format_constraint({eq, Value}) -> lists:flatten(io_lib:format("= ~p", [Value])); -format_constraint({neq, Value}) -> lists:flatten(io_lib:format("!= ~p", [Value])); -format_constraint({lt, Value}) -> lists:flatten(io_lib:format("< ~p", [Value])); -format_constraint({lte, Value}) -> lists:flatten(io_lib:format("<= ~p", [Value])); -format_constraint({gt, Value}) -> lists:flatten(io_lib:format("> ~p", [Value])); -format_constraint({gte, Value}) -> lists:flatten(io_lib:format(">= ~p", [Value])); -format_constraint({in, List}) -> lists:flatten(io_lib:format("in ~p", [List])); -format_constraint({range, Min, Max}) -> lists:flatten(io_lib:format("in [~p, ~p]", [Min, Max])); -format_constraint({member_of, Relation}) -> lists:flatten(io_lib:format("in ~p", [Relation])); -format_constraint(Other) -> lists:flatten(io_lib:format("~p", [Other])). - -%% @private -%% @doc Format provenance as string. -format_provenance(undefined) -> "undefined"; -format_provenance({base, Relation}) -> lists:flatten(io_lib:format("base(~p)", [Relation])); -format_provenance({join, P1, P2}) -> - lists:flatten(io_lib:format("join(~s, ~s)", [format_provenance(P1), format_provenance(P2)])); -format_provenance({select, P, _Constraints}) -> - lists:flatten(io_lib:format("select(~s)", [format_provenance(P)])); -format_provenance({project, P, Attrs}) -> - lists:flatten(io_lib:format("project(~s, ~p)", [format_provenance(P), Attrs])); -format_provenance({take, P, N}) -> - lists:flatten(io_lib:format("take(~s, ~p)", [format_provenance(P), N])); -format_provenance(Other) -> lists:flatten(io_lib:format("~p", [Other])). - -%% @private -%% @doc Format tuples response. -tuples_response(Tuples) -> - [ - "<?xml version=\"1.0\"?>\n", - "<response>\n", - " <status>ok</status>\n", - " <tuples>\n", - lists:map(fun tuple_to_xml/1, Tuples), - " </tuples>\n", - "</response>\n" - ]. - -%% @private -%% @doc Format single tuple as XML. -tuple_to_xml(Tuple) -> - Attributes = maps:to_list(Tuple), - [ - " <tuple>\n", - lists:map(fun({Name, Value}) -> - EscapedValue = escape_xml(format_value(Value)), - io_lib:format(" <attribute name=\"~s\">~s</attribute>\n", - [Name, EscapedValue]) - end, Attributes), - " </tuple>\n" - ]. - -%% @private -%% @doc Format value for XML output. -format_value(Value) when is_integer(Value) -> integer_to_list(Value); -format_value(Value) when is_float(Value) -> float_to_list(Value); -format_value(Value) when is_atom(Value) -> atom_to_list(Value); -format_value(Value) when is_binary(Value) -> binary_to_list(Value); -format_value(Value) when is_list(Value) -> Value; -format_value(Value) -> io_lib:format("~p", [Value]). - -%% @private -%% @doc Escape XML special characters. -escape_xml(Str) -> - lists:flatten( - lists:map( - fun($<) -> "<"; - ($>) -> ">"; - ($&) -> "&"; - ($") -> """; - ($') -> "'"; - (C) -> C - end, - lists:flatten(Str))). - -%% @private -%% @doc Format error response. -error_response(Message) -> - [ - "<?xml version=\"1.0\"?>\n", - "<response>\n", - " <status>error</status>\n", - " <message>", escape_xml(Message), "</message>\n", - "</response>\n" - ]. - -%% @private -%% @doc Format OK response. -ok_response(Message) -> - [ - "<?xml version=\"1.0\"?>\n", - "<response>\n", - " <status>ok</status>\n", - " <message>", escape_xml(Message), "</message>\n", - "</response>\n" - ]. - -%% @private -%% @doc Send response to client. -send_response(Socket, Response) -> - gen_tcp:send(Socket, Response). diff --git a/src/xml_server_app.erl b/src/xml_server_app.erl deleted file mode 100644 index 51a9565..0000000 --- a/src/xml_server_app.erl +++ /dev/null @@ -1,28 +0,0 @@ -%%% @doc XML Server Application Starter -%%% -%%% Convenient module for starting the XML server with all dependencies. - --module(xml_server_app). --export([start/0, start/1]). - -%% @doc Start XML server on default port 8080. -start() -> - start(8080). - -%% @doc Start XML server on specified port. -start(Port) -> - % Initialize Sakura - main:setup(), - io:format("~nSakura initialized~n"), - - % Create example database - DB = repl:example_db(), - - % Start XML server - xml_server:start(Port, DB), - - io:format("~nXML Server ready!~n"), - io:format("Connect with: nc localhost ~p~n", [Port]), - io:format("Or run Emacs client: M-x sakura-run-examples~n~n"), - - ok. |
