mirror of
https://github.com/neovim/neovim.git
synced 2024-12-20 11:15:14 -07:00
api/msgpack-rpc: Refactor metadata object construction
Instead of building all metadata from msgpack-gen.lua, we now merge the generated part with manual information(such as types and features). The metadata is accessible through the api method `vim_get_api_info`. This was done to simplify the generator while also increasing flexibility(by being able to add more metadata)
This commit is contained in:
parent
15ca58d79f
commit
cd2e46c078
@ -35,30 +35,7 @@ grammar = Ct((c_proto + c_comment + c_preproc + ws) ^ 1)
|
|||||||
|
|
||||||
-- we need at least 2 arguments since the last one is the output file
|
-- we need at least 2 arguments since the last one is the output file
|
||||||
assert(#arg >= 1)
|
assert(#arg >= 1)
|
||||||
-- api metadata
|
functions = {}
|
||||||
api = {
|
|
||||||
functions = {},
|
|
||||||
types = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Extract type codes from api/private/defs.h. The codes are values between
|
|
||||||
-- comment markers in the ObjectType enum
|
|
||||||
local typedefs_header = arg[1]
|
|
||||||
local input = io.open(typedefs_header, 'rb')
|
|
||||||
local reading_types = false
|
|
||||||
while true do
|
|
||||||
local line = input:read('*l'):gsub("^%s*(.-)%s*$", "%1")
|
|
||||||
if reading_types then
|
|
||||||
if line == '// end custom types' then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
local type_name = line:gsub("^kObjectType(.-),$", "%1")
|
|
||||||
api.types[#api.types + 1] = type_name
|
|
||||||
else
|
|
||||||
reading_types = line == '// start custom types'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
input:close()
|
|
||||||
|
|
||||||
-- names of all headers relative to the source root(for inclusion in the
|
-- names of all headers relative to the source root(for inclusion in the
|
||||||
-- generated file)
|
-- generated file)
|
||||||
@ -67,7 +44,7 @@ headers = {}
|
|||||||
outputf = arg[#arg]
|
outputf = arg[#arg]
|
||||||
|
|
||||||
-- read each input file, parse and append to the api metadata
|
-- read each input file, parse and append to the api metadata
|
||||||
for i = 2, #arg - 1 do
|
for i = 1, #arg - 1 do
|
||||||
local full_path = arg[i]
|
local full_path = arg[i]
|
||||||
local parts = {}
|
local parts = {}
|
||||||
for part in string.gmatch(full_path, '[^/]+') do
|
for part in string.gmatch(full_path, '[^/]+') do
|
||||||
@ -78,7 +55,7 @@ for i = 2, #arg - 1 do
|
|||||||
local input = io.open(full_path, 'rb')
|
local input = io.open(full_path, 'rb')
|
||||||
local tmp = grammar:match(input:read('*all'))
|
local tmp = grammar:match(input:read('*all'))
|
||||||
for i = 1, #tmp do
|
for i = 1, #tmp do
|
||||||
api.functions[#api.functions + 1] = tmp[i]
|
functions[#functions + 1] = tmp[i]
|
||||||
local fn = tmp[i]
|
local fn = tmp[i]
|
||||||
if #fn.parameters ~= 0 and fn.parameters[1][2] == 'channel_id' then
|
if #fn.parameters ~= 0 and fn.parameters[1][2] == 'channel_id' then
|
||||||
-- this function should receive the channel id
|
-- this function should receive the channel id
|
||||||
@ -124,12 +101,12 @@ end
|
|||||||
output:write([[
|
output:write([[
|
||||||
|
|
||||||
|
|
||||||
const uint8_t msgpack_metadata[] = {
|
static const uint8_t msgpack_metadata[] = {
|
||||||
|
|
||||||
]])
|
]])
|
||||||
-- serialize the API metadata using msgpack and embed into the resulting
|
-- serialize the API metadata using msgpack and embed into the resulting
|
||||||
-- binary for easy querying by clients
|
-- binary for easy querying by clients
|
||||||
packed = msgpack.pack(api)
|
packed = msgpack.pack(functions)
|
||||||
for i = 1, #packed do
|
for i = 1, #packed do
|
||||||
output:write(string.byte(packed, i)..', ')
|
output:write(string.byte(packed, i)..', ')
|
||||||
if i % 10 == 0 then
|
if i % 10 == 0 then
|
||||||
@ -138,17 +115,28 @@ for i = 1, #packed do
|
|||||||
end
|
end
|
||||||
output:write([[
|
output:write([[
|
||||||
};
|
};
|
||||||
const unsigned int msgpack_metadata_size = sizeof(msgpack_metadata);
|
|
||||||
msgpack_unpacked msgpack_unpacked_metadata;
|
void msgpack_rpc_init_function_metadata(Dictionary *metadata)
|
||||||
|
{
|
||||||
|
msgpack_unpacked unpacked;
|
||||||
|
msgpack_unpacked_init(&unpacked);
|
||||||
|
assert(msgpack_unpack_next(&unpacked,
|
||||||
|
(const char *)msgpack_metadata,
|
||||||
|
sizeof(msgpack_metadata),
|
||||||
|
NULL) == MSGPACK_UNPACK_SUCCESS);
|
||||||
|
Object functions;
|
||||||
|
msgpack_rpc_to_object(&unpacked.data, &functions);
|
||||||
|
msgpack_unpacked_destroy(&unpacked);
|
||||||
|
PUT(*metadata, "functions", functions);
|
||||||
|
}
|
||||||
|
|
||||||
]])
|
]])
|
||||||
|
|
||||||
-- start the handler functions. First handler (method_id=0) is reserved for
|
-- start the handler functions. Visit each function metadata to build the
|
||||||
-- querying the metadata, usually it is the first function called by clients.
|
-- handler function with code generated for validating arguments and calling to
|
||||||
-- Visit each function metadata to build the handler function with code
|
-- the real API.
|
||||||
-- generated for validating arguments and calling to the real API.
|
for i = 1, #functions do
|
||||||
for i = 1, #api.functions do
|
local fn = functions[i]
|
||||||
local fn = api.functions[i]
|
|
||||||
local args = {}
|
local args = {}
|
||||||
|
|
||||||
output:write('static Object handle_'..fn.name..'(uint64_t channel_id, msgpack_object *req, Error *error)')
|
output:write('static Object handle_'..fn.name..'(uint64_t channel_id, msgpack_object *req, Error *error)')
|
||||||
@ -243,14 +231,6 @@ static Map(String, rpc_method_handler_fn) *methods = NULL;
|
|||||||
|
|
||||||
void msgpack_rpc_init(void)
|
void msgpack_rpc_init(void)
|
||||||
{
|
{
|
||||||
msgpack_unpacked_init(&msgpack_unpacked_metadata);
|
|
||||||
if (msgpack_unpack_next(&msgpack_unpacked_metadata,
|
|
||||||
(const char *)msgpack_metadata,
|
|
||||||
msgpack_metadata_size,
|
|
||||||
NULL) != MSGPACK_UNPACK_SUCCESS) {
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
methods = map_new(String, rpc_method_handler_fn)();
|
methods = map_new(String, rpc_method_handler_fn)();
|
||||||
|
|
||||||
]])
|
]])
|
||||||
@ -258,8 +238,8 @@ void msgpack_rpc_init(void)
|
|||||||
-- Keep track of the maximum method name length in order to avoid walking
|
-- Keep track of the maximum method name length in order to avoid walking
|
||||||
-- strings longer than that when searching for a method handler
|
-- strings longer than that when searching for a method handler
|
||||||
local max_fname_len = 0
|
local max_fname_len = 0
|
||||||
for i = 1, #api.functions do
|
for i = 1, #functions do
|
||||||
local fn = api.functions[i]
|
local fn = functions[i]
|
||||||
output:write(' map_put(String, rpc_method_handler_fn)(methods, '..
|
output:write(' map_put(String, rpc_method_handler_fn)(methods, '..
|
||||||
'(String) {.data = "'..fn.name..'", '..
|
'(String) {.data = "'..fn.name..'", '..
|
||||||
'.size = sizeof("'..fn.name..'") - 1}, handle_'..
|
'.size = sizeof("'..fn.name..'") - 1}, handle_'..
|
||||||
@ -270,12 +250,6 @@ for i = 1, #api.functions do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local metadata_fn = 'get_api_metadata'
|
|
||||||
output:write(' map_put(String, rpc_method_handler_fn)(methods, '..
|
|
||||||
'(String) {.data = "'..metadata_fn..'", '..
|
|
||||||
'.size = sizeof("'..metadata_fn..'") - 1}, msgpack_rpc_handle_'..
|
|
||||||
metadata_fn..');\n')
|
|
||||||
|
|
||||||
output:write('\n}\n\n')
|
output:write('\n}\n\n')
|
||||||
|
|
||||||
output:write([[
|
output:write([[
|
||||||
|
@ -3,7 +3,6 @@ include(CheckLibraryExists)
|
|||||||
set(GENERATED_DIR ${PROJECT_BINARY_DIR}/src/nvim/auto)
|
set(GENERATED_DIR ${PROJECT_BINARY_DIR}/src/nvim/auto)
|
||||||
set(DISPATCH_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/msgpack-gen.lua)
|
set(DISPATCH_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/msgpack-gen.lua)
|
||||||
file(GLOB API_HEADERS api/*.h)
|
file(GLOB API_HEADERS api/*.h)
|
||||||
file(GLOB API_DEFS api/private/defs.h)
|
|
||||||
set(MSGPACK_RPC_HEADER ${PROJECT_SOURCE_DIR}/src/nvim/os/msgpack_rpc.h)
|
set(MSGPACK_RPC_HEADER ${PROJECT_SOURCE_DIR}/src/nvim/os/msgpack_rpc.h)
|
||||||
set(MSGPACK_DISPATCH ${GENERATED_DIR}/msgpack_dispatch.c)
|
set(MSGPACK_DISPATCH ${GENERATED_DIR}/msgpack_dispatch.c)
|
||||||
set(HEADER_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/gendeclarations.lua)
|
set(HEADER_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/gendeclarations.lua)
|
||||||
@ -124,10 +123,9 @@ foreach(sfile ${NEOVIM_SOURCES}
|
|||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
add_custom_command(OUTPUT ${MSGPACK_DISPATCH}
|
add_custom_command(OUTPUT ${MSGPACK_DISPATCH}
|
||||||
COMMAND ${LUA_PRG} ${DISPATCH_GENERATOR} ${API_DEFS} ${API_HEADERS} ${MSGPACK_DISPATCH}
|
COMMAND ${LUA_PRG} ${DISPATCH_GENERATOR} ${API_HEADERS} ${MSGPACK_DISPATCH}
|
||||||
DEPENDS
|
DEPENDS
|
||||||
${API_HEADERS}
|
${API_HEADERS}
|
||||||
${API_DEFS}
|
|
||||||
${MSGPACK_RPC_HEADER}
|
${MSGPACK_RPC_HEADER}
|
||||||
${DISPATCH_GENERATOR}
|
${DISPATCH_GENERATOR}
|
||||||
)
|
)
|
||||||
|
@ -44,13 +44,9 @@ typedef struct {
|
|||||||
} Dictionary;
|
} Dictionary;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
// The following comments are markers that msgpack-gen.lua uses to extract
|
|
||||||
// types, don't remove!
|
|
||||||
// start custom types
|
|
||||||
kObjectTypeBuffer,
|
kObjectTypeBuffer,
|
||||||
kObjectTypeWindow,
|
kObjectTypeWindow,
|
||||||
kObjectTypeTabpage,
|
kObjectTypeTabpage,
|
||||||
// end custom types
|
|
||||||
kObjectTypeNil,
|
kObjectTypeNil,
|
||||||
kObjectTypeBoolean,
|
kObjectTypeBoolean,
|
||||||
kObjectTypeInteger,
|
kObjectTypeInteger,
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
#include "nvim/api/private/helpers.h"
|
#include "nvim/api/private/helpers.h"
|
||||||
#include "nvim/api/private/defs.h"
|
#include "nvim/api/private/defs.h"
|
||||||
#include "nvim/api/private/handle.h"
|
#include "nvim/api/private/handle.h"
|
||||||
|
#include "nvim/os/provider.h"
|
||||||
#include "nvim/ascii.h"
|
#include "nvim/ascii.h"
|
||||||
#include "nvim/vim.h"
|
#include "nvim/vim.h"
|
||||||
#include "nvim/buffer.h"
|
#include "nvim/buffer.h"
|
||||||
@ -506,6 +507,72 @@ void api_free_dictionary(Dictionary value)
|
|||||||
free(value.items);
|
free(value.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dictionary api_metadata(void)
|
||||||
|
{
|
||||||
|
static Dictionary metadata = ARRAY_DICT_INIT;
|
||||||
|
|
||||||
|
if (!metadata.size) {
|
||||||
|
msgpack_rpc_init_function_metadata(&metadata);
|
||||||
|
init_type_metadata(&metadata);
|
||||||
|
provider_init_feature_metadata(&metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy_object(DICTIONARY_OBJ(metadata)).data.dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void init_type_metadata(Dictionary *metadata)
|
||||||
|
{
|
||||||
|
Dictionary types = ARRAY_DICT_INIT;
|
||||||
|
|
||||||
|
Dictionary buffer_metadata = ARRAY_DICT_INIT;
|
||||||
|
PUT(buffer_metadata, "id", INTEGER_OBJ(kObjectTypeBuffer));
|
||||||
|
|
||||||
|
Dictionary window_metadata = ARRAY_DICT_INIT;
|
||||||
|
PUT(window_metadata, "id", INTEGER_OBJ(kObjectTypeWindow));
|
||||||
|
|
||||||
|
Dictionary tabpage_metadata = ARRAY_DICT_INIT;
|
||||||
|
PUT(tabpage_metadata, "id", INTEGER_OBJ(kObjectTypeTabpage));
|
||||||
|
|
||||||
|
PUT(types, "Buffer", DICTIONARY_OBJ(buffer_metadata));
|
||||||
|
PUT(types, "Window", DICTIONARY_OBJ(window_metadata));
|
||||||
|
PUT(types, "Tabpage", DICTIONARY_OBJ(tabpage_metadata));
|
||||||
|
|
||||||
|
PUT(*metadata, "types", DICTIONARY_OBJ(types));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a deep clone of an object
|
||||||
|
static Object copy_object(Object obj)
|
||||||
|
{
|
||||||
|
switch (obj.type) {
|
||||||
|
case kObjectTypeNil:
|
||||||
|
case kObjectTypeBoolean:
|
||||||
|
case kObjectTypeInteger:
|
||||||
|
case kObjectTypeFloat:
|
||||||
|
return obj;
|
||||||
|
|
||||||
|
case kObjectTypeString:
|
||||||
|
return STRING_OBJ(cstr_to_string(obj.data.string.data));
|
||||||
|
|
||||||
|
case kObjectTypeArray: {
|
||||||
|
Array rv = ARRAY_DICT_INIT;
|
||||||
|
for (size_t i = 0; i < obj.data.array.size; i++) {
|
||||||
|
ADD(rv, copy_object(obj.data.array.items[i]));
|
||||||
|
}
|
||||||
|
return ARRAY_OBJ(rv);
|
||||||
|
}
|
||||||
|
|
||||||
|
case kObjectTypeDictionary: {
|
||||||
|
Dictionary rv = ARRAY_DICT_INIT;
|
||||||
|
for (size_t i = 0; i < obj.data.dictionary.size; i++) {
|
||||||
|
KeyValuePair item = obj.data.dictionary.items[i];
|
||||||
|
PUT(rv, item.key.data, copy_object(item.value));
|
||||||
|
}
|
||||||
|
return DICTIONARY_OBJ(rv);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Recursion helper for the `vim_to_object`. This uses a pointer table
|
/// Recursion helper for the `vim_to_object`. This uses a pointer table
|
||||||
/// to avoid infinite recursion due to cyclic references
|
/// to avoid infinite recursion due to cyclic references
|
||||||
|
@ -528,10 +528,15 @@ void vim_register_provider(uint64_t channel_id, String feature, Error *err)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a feature->method list dictionary for all pluggable features
|
Array vim_get_api_info(uint64_t channel_id)
|
||||||
Dictionary vim_discover_features(void)
|
|
||||||
{
|
{
|
||||||
return provider_get_all();
|
Array rv = ARRAY_DICT_INIT;
|
||||||
|
|
||||||
|
assert(channel_id <= INT64_MAX);
|
||||||
|
ADD(rv, INTEGER_OBJ((int64_t)channel_id));
|
||||||
|
ADD(rv, DICTIONARY_OBJ(api_metadata()));
|
||||||
|
|
||||||
|
return rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes a message to vim output or error buffer. The string is split
|
/// Writes a message to vim output or error buffer. The string is split
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include <msgpack.h>
|
||||||
|
|
||||||
#include "nvim/ascii.h"
|
#include "nvim/ascii.h"
|
||||||
#include "nvim/vim.h"
|
#include "nvim/vim.h"
|
||||||
#include "nvim/main.h"
|
#include "nvim/main.h"
|
||||||
@ -57,6 +59,9 @@
|
|||||||
#include "nvim/os/input.h"
|
#include "nvim/os/input.h"
|
||||||
#include "nvim/os/os.h"
|
#include "nvim/os/os.h"
|
||||||
#include "nvim/os/signal.h"
|
#include "nvim/os/signal.h"
|
||||||
|
#include "nvim/os/msgpack_rpc_helpers.h"
|
||||||
|
#include "nvim/api/private/defs.h"
|
||||||
|
#include "nvim/api/private/helpers.h"
|
||||||
|
|
||||||
/* Maximum number of commands from + or -c arguments. */
|
/* Maximum number of commands from + or -c arguments. */
|
||||||
#define MAX_ARG_CMDS 10
|
#define MAX_ARG_CMDS 10
|
||||||
@ -116,9 +121,6 @@ static void init_locale(void);
|
|||||||
# endif
|
# endif
|
||||||
#endif /* NO_VIM_MAIN */
|
#endif /* NO_VIM_MAIN */
|
||||||
|
|
||||||
extern const uint8_t msgpack_metadata[];
|
|
||||||
extern const unsigned int msgpack_metadata_size;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Different types of error messages.
|
* Different types of error messages.
|
||||||
*/
|
*/
|
||||||
@ -1027,9 +1029,15 @@ static void command_line_scan(mparm_T *parmp)
|
|||||||
msg_didout = FALSE;
|
msg_didout = FALSE;
|
||||||
mch_exit(0);
|
mch_exit(0);
|
||||||
} else if (STRICMP(argv[0] + argv_idx, "api-info") == 0) {
|
} else if (STRICMP(argv[0] + argv_idx, "api-info") == 0) {
|
||||||
for (unsigned int i = 0; i<msgpack_metadata_size; i++) {
|
msgpack_sbuffer* b = msgpack_sbuffer_new();
|
||||||
putchar(msgpack_metadata[i]);
|
msgpack_packer* p = msgpack_packer_new(b, msgpack_sbuffer_write);
|
||||||
|
Object md = DICTIONARY_OBJ(api_metadata());
|
||||||
|
msgpack_rpc_from_object(md, p);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < b->size; i++) {
|
||||||
|
putchar(b->data[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
mch_exit(0);
|
mch_exit(0);
|
||||||
} else if (STRICMP(argv[0] + argv_idx, "embed") == 0) {
|
} else if (STRICMP(argv[0] + argv_idx, "embed") == 0) {
|
||||||
embedded_mode = true;
|
embedded_mode = true;
|
||||||
|
@ -17,8 +17,6 @@
|
|||||||
# include "os/msgpack_rpc.c.generated.h"
|
# include "os/msgpack_rpc.c.generated.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
extern msgpack_unpacked msgpack_unpacked_metadata;
|
|
||||||
|
|
||||||
/// Validates the basic structure of the msgpack-rpc call and fills `res`
|
/// Validates the basic structure of the msgpack-rpc call and fills `res`
|
||||||
/// with the basic response structure.
|
/// with the basic response structure.
|
||||||
///
|
///
|
||||||
@ -83,19 +81,6 @@ Object msgpack_rpc_handle_missing_method(uint64_t channel_id,
|
|||||||
return NIL;
|
return NIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler for retrieving API metadata through a msgpack-rpc call
|
|
||||||
Object msgpack_rpc_handle_get_api_metadata(uint64_t channel_id,
|
|
||||||
msgpack_object *req,
|
|
||||||
Error *error)
|
|
||||||
{
|
|
||||||
Array rv = ARRAY_DICT_INIT;
|
|
||||||
Object metadata;
|
|
||||||
msgpack_rpc_to_object(&msgpack_unpacked_metadata.data, &metadata);
|
|
||||||
ADD(rv, INTEGER_OBJ((int64_t)channel_id));
|
|
||||||
ADD(rv, metadata);
|
|
||||||
return ARRAY_OBJ(rv);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serializes a msgpack-rpc request or notification(id == 0)
|
/// Serializes a msgpack-rpc request or notification(id == 0)
|
||||||
WBuffer *serialize_request(uint64_t request_id,
|
WBuffer *serialize_request(uint64_t request_id,
|
||||||
String method,
|
String method,
|
||||||
|
@ -25,6 +25,7 @@ typedef Object (*rpc_method_handler_fn)(uint64_t channel_id,
|
|||||||
/// Initializes the msgpack-rpc method table
|
/// Initializes the msgpack-rpc method table
|
||||||
void msgpack_rpc_init(void);
|
void msgpack_rpc_init(void);
|
||||||
|
|
||||||
|
void msgpack_rpc_init_function_metadata(Dictionary *metadata);
|
||||||
|
|
||||||
/// Dispatches to the actual API function after basic payload validation by
|
/// Dispatches to the actual API function after basic payload validation by
|
||||||
/// `msgpack_rpc_call`. It is responsible for validating/converting arguments
|
/// `msgpack_rpc_call`. It is responsible for validating/converting arguments
|
||||||
|
@ -120,9 +120,9 @@ Object provider_call(char *method, Array args)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Dictionary provider_get_all(void)
|
void provider_init_feature_metadata(Dictionary *metadata)
|
||||||
{
|
{
|
||||||
Dictionary rv = ARRAY_DICT_INIT;
|
Dictionary md = ARRAY_DICT_INIT;
|
||||||
|
|
||||||
for (size_t i = 0; i < FEATURE_COUNT; i++) {
|
for (size_t i = 0; i < FEATURE_COUNT; i++) {
|
||||||
Array methods = ARRAY_DICT_INIT;
|
Array methods = ARRAY_DICT_INIT;
|
||||||
@ -134,10 +134,10 @@ Dictionary provider_get_all(void)
|
|||||||
ADD(methods, STRING_OBJ(cstr_to_string(method)));
|
ADD(methods, STRING_OBJ(cstr_to_string(method)));
|
||||||
}
|
}
|
||||||
|
|
||||||
PUT(rv, f->name, ARRAY_OBJ(methods));
|
PUT(md, f->name, ARRAY_OBJ(methods));
|
||||||
}
|
}
|
||||||
|
|
||||||
return rv;
|
PUT(*metadata, "features", DICTIONARY_OBJ(md));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Feature * find_feature(char *name)
|
static Feature * find_feature(char *name)
|
||||||
|
Loading…
Reference in New Issue
Block a user