jsonex – упрощаем сложные клиент-серверные диалоги



    Взаимодействие между клиентом и сервером как правило устроено очень просто и опирается на довольно примитивный инструментарий. Это не создает проблем само по себе, но зачастую даже небольшое усложнение поставленной задачи плохо укладываться в привычные подходы, порождая не слишком изящные решения-заплатки. Многие задачи решаются в каждом новом проекте заново, бессистемно и независимо друг от друга. К подобным задачам относятся, например:

    • Batch-запросы
    • Передача даты в составе сложной структуры данных
    • Обозначение кастомных типов данных
    • Проброс round-trip данных, которые сервер должен вернуть в ответе
    • Дополнение запроса и ответа метаданными
    • Обработка ошибок, пришедших в ответе

    Разработчики тратят немало времени, раз за разом создавая неказистые велосипеды на серверной стороне, после чего их приходится поддерживать еще и на стороне клиента.

    jsonex представляет собой попытку объединить решение упомянутых выше и многих других задач в рамках простого единого подхода, основанного на концепции вычислимых данных (callable data).

    Содержание

    jsonex
    Нотация вызова (call notation)
    Контекст
    Связь jsonex и JS
    Вычислимые данные (callable data)
    Взгляд со стороны клиента
    Преимущества вычислимых данных
    Работа с HTTP и веб сокетами
    Соображения безопасности
    JSON-представление
    Асинхронные вызовы
    Arc
    Заключение

    jsonex


    Концепция вычислимых данных проста и может быть использована для самых разных форматов данных. Чуть дальше я расскажу, как использовать ее в рамках JSON-представления. Но, чтобы продемонстрировать идею в чистом виде, начнем издалека.

    Во многих случаях мы используем JSON, и он прекрасен. Он прост, легко читается, позволяет представлять иерархические структуры данных, широко поддерживается. Тем не менее, некоторые вещи хотелось бы улучшить, например:

    • Разрешить комментарии
    • Позволить использовать апострофы для строк
    • Позволить опускать кавычки для ключей в словарях
    • Разрешить завершающие запятые в словарях и списках

    Наш расширенный вариант JSON (назовем его jsonex) мог бы выглядеть, например, так:

    { // Пользователь
      name: 'John',
      familyName: 'Smith',
      dateOfBirth: '1901-01-01',
      friendIds: [
        124124,
        283746, /* завершающая запятая */
      ],
      num: 123,
    }
    

    Уже неплохо. Многое бы отдал за возможность писать комментарии в JSON-конфигах. Но есть в этом примере одна весьма сомнительная строчка:

      dateOfBirth: '1901-01-01',
    

    Что это? Строка? Дата? Человек может догадаться по контексту, но парсер вряд ли окажется столь же догадлив. Тип даты не предусмотрен форматом. Чтобы его распознать, можно использовать два подхода – описать схему данных, либо использовать какую-то подсказку-аннотацию, которая могла бы направить парсер в нужном направлении.

    JSON изначально не требует схемы, и было бы странно требовать ее наличия только ради возможности подсказать парсеру, в каком из полей лежит дата. Поэтому пойдем вторым путем. Можно придумать множество способов добавить аннотацию для даты, но было бы неплохо иметь простой и в то же время универсальный способ обозначать любые типы данных. Для этой цели замечательно подойдет нотация вызова.

    Нотация вызова (call notation)


    Запишем наше поле так:

      dateOfBirth: Date('1901-01-01'),
    

    Теперь тип данных выглядит очевидным. Но что конкретно должен сделать парсер, встретив подобную запись? Подход достаточно прямолинеен. Встретив конструкцию вида SomeName(args...) парсер должен:

    • Найти у себя в закромах заранее заданную функцию-обработчик с именем SomeName
    • Выполнить эту функцию над аргументами args...
    • Использовать результат ее выполнения в обработанных данных вместо изначальной конструкции

    Таким образом, результат разбора полностью зависит от реализации функции-обработчика. Наш вызов Date('1901-01-01') превратится в объект типа Date в JavaScript, date или datetime в Питоне и так далее.

    В случае, когда парсер не может найти обработчик с заданным именем, он вызывает обработчик по умолчанию, который либо вернет что-то разумное, либо бросит исключение.

    Может показаться, что, обрабатывая нотации вызова, мы попросту выполняем содержащийся в данных произвольный кусочек кода, но это не так:
    • Данные остаются данными и контроль над обработкой полностью лежит на нашей стороне
    • Парсер не может использовать никакие функции кроме заданных явным образом обработчиков
    • Любая функция-обработчик определена нами и может выполнить любые предварительные проверки, прежде чем совершать активные действия

    Далее мы рассмотрим эти моменты более подробно. А пока заметим, что нотация вызова дает нам просто потрясающую гибкость. Добавляя новые обработчики мы можем легко расширять систему:

    var handlers = {
      Date: function (ctx, v) {
        return new Date(v);
      },
      Complex: function (ctx, real, imag) {
        return new Complex(real, imag);
      },
      ArrayBuffer: function (ctx, v) {
        return base64DecToArr(v).buffer;
      },
      Person: function (ctx, personDataDict) {
        return new Person(personDataDict);
      }
    };
    
    // это не настоящий парсер, просто пример каким он мог бы быть
    var person = new JsonexParser(handlers).parse(
      "Person({"+
      " name: 'John',"+
      " dateOfBirth: Date('1901-01-01'),"+
      " i: Complex(0, 1),"+
      " song: ArrayBuffer('Q2FsbCBub3RhdGlvbiBpcyBjb29sIQ=='),"+
      "})"
    );
    

    Здесь мы добавили возможность парсинга дат, комплексных чисел, бинарных данных в base64-представлении – все это буквально в несколько строк кода и совершенно не меняя устройство нашего гипотетического парсера.

    Контекст


    Как вы могли заметить, каждый обработчик, помимо собственных аргументов, принимает параметр ctx. В этом параметре передается контекст обработки. На данном этапе будем считать, что ctx – это изначально пустой словарь. Благодаря контексту мы можем:

    • Позволить обработчикам «общаться» друг с другом
    • Передавать обработчикам какие-то настройки
    • Передавать обработчикам дополнительные данные из внешнего окружения
    • Сохранять дополнительную информацию, полученную в процессе парсинга

    Например, с помощью контекста легко создать обработчики get и set, которые позволят использовать вычисленные ранее объекты:

    var handlers = {
      // кладем данные в коробку и возвращаем их же
      set: function (ctx, key, data) {
        ctx.box = ctx.box || {};
        ctx.box[key] = data;
        return data;
      },
      // возвращаем данные, взятые из коробки
      get: function (ctx, key) {
        return ctx.box ? ctx.box[key] : undefined;
      }
    };
    var data = new JsonexParser(handlers).parse(
      "[ set('x', { a: 'a' }), get('x') ]"
    );
    data[0].a = 5; // в data[0] и data[1] лежит ссылка на один и тот же объект
    console.log(JSON.stringify(data)); // [{"a":5},{"a":5}]
    

    Связь jsonex и JS


    Так же, как и JSON, jsonex тесно связан с синтаксисом JS. Он является синтаксически корректным JS-выражением и в простейших случаях даже может быть вычислен как JS-выражение, при условии, что каждая нотация вызова определена в контексте вычисления. Например,

    [
      foo(),
      bar.baz() // да, jsonex позволяет точки в именах вызовов
    ]
    

    является корректным и, более того, вычислимым JS-выражением при условии, что foo и bar правильным образом определены.

    Это, конечно, не означает, что стоит брать и вычислять jsonex с помощью eval(), определив необходимые переменные в соответствующем замыкании. Кроме потенциальных проблем с безопасностью, этот подход теряет часть гибкости, которую дает возможность проанализировать данные именно как данные, а не как нечто выполняемое. Тем не менее, в некоторых случаях jsonex действительно можно рассматривать как ограниченное подмножество JS и считать jsonex-данные JS-выражением.

    Вычислимые данные (callable data)


    Данные в jsonex представляют собой вычислимое выражение, которое может быть легко проанализировано перед вычислением или прямо в процессе вычисления. Почему бы не использовать такие выражения в качестве запросов к серверу? Например, запрос мог бы выглядеть так:

    getUsers([1, 15, 7])
    

    Сервер мог бы вычислить его с помощью соответствующего обработчика:

    var handlers = {
      getUsers: function (ctx, userIds) {
        var listOfUsers = getUsersFromDbOrWhatever(userIds);
        return listOfUsers;
      }
    };
    

    Затем сериализовать результат в jsonex и отправить клиенту ответ:

    [ User({id: 1, name: 'John', ...}), ...]
    


    Клиент получит данные с честными объектами, готовыми к использованию. В то же время сервер превращается в простой вычислитель jsonex-выражений. Для расширения API достаточно добавить новый обработчик – не надо возиться с урлами, разбирать аргументы, приводить их к нужным типам, различать GET и POST, все заработает само.

    Взгляд со стороны клиента


    Давайте подумаем, как организовать вызов со стороны клиента. Собирать jsonex-представление вручную в виде строк типа "getUsers([1, 15, 7])" было бы неудобно. Поэтому нам пригодится вспомогательный объект, описывающий нотацию вызова и понимаемый сериализатором. Вот так могло бы выглядеть его использование:

    var getUsers = function (userIds) {
      return new jsonex.Call('getUsers', userIds); // вспомогательный объект
    };
    // пример использования гипотетического сериализатора
    jsonex.stringify(getUsers([1, 15, 7])); // 'getUsers([1,15,7])'
    

    В таком случае, запрос к серверу может выглядеть так:

    server.ask(
      getUsers([1, 15, 7]), // запрос
      function (err, result) { // обработка ответа
        ...
      }
    );
    

    server.ask() должен сделать следующее:

    • Превратить первый аргумент в jsonex
    • Отправить его в качестве запроса на сервер
    • Дождаться jsonex-ответа и распарсить его
    • Отдать полученный результат в callback-функцию

    В нашем примере первый аргумент будет значением, которое вернет getUsers(), то есть объектом типа jsonex.Call, который сериализуется в строку 'getUsers([1,15,7])'

    Выглядит просто и симпатично. С точки зрения написания кода, все действия производятся над готовыми к использованию объектами, любые преобразования спрятаны под капотом. В примере использован callback, но при использовании Promise, все будет выглядеть еще приятнее.

    Если полученный от сервера результат является наследником класса Error, то server.ask() считает, что сервер вернул ошибку и вызывает callback с соответствующими аргументами. Такой подход возможен, поскольку результат парсинга является готовым к использованию объектом нужного класса.

    Соответственно, чтобы сообщить клиенту об ошибке, серверу достаточно вернуть объект нужного класса, используя подходящую нотацию вызова. При этом клиент должен реализовать обработчик, который заменит эту нотацию на объект класса, унаследованного от Error.

    Пример ответа сервера с сообщением об ошибке:

    UnexpectedError('Error details message')
    

    Пример обработчика:

    handlers.UnexpectedError = function(ctx, msg) {
      return new ServerError(msg); // ServerError должен быть наследником Error
    };
    

    Преимущества вычислимых данных


    Что нам дают вычислимые данные:

    • Возможность указывать типы передаваемых объектов
    • Стандартизированную обработку ошибок – достаточно вернуть объект нужного типа
    • Элементарно реализуются batch-запросы
    • Batch-запросы на стероидах – результаты одних вызовов могут быть использованы в качестве аргументов других
    • Легко пробрасывать round-trip данные
    • Легко передать дополнительную информацию – любые заголовки и метаданные, которые должны быть учтены при обработке, но не должны смешиваться с остальными данными

    Batch-запрос, например, может выглядеть так:

    [
      getFoo(),
      getBar(1, 2, 3),
    ]
    

    В ответ придет массив с результатами вызовов getFoo() и getBar().

    Используем результат одного вычисления в другом:

    [
      set('x', getUserBooks(17)), // получить книги пользователя 17
      getAuthors( // получить авторов книг
        getProps( // вычленить свойство 'authorId' для каждого объекта из get('x')
          get('x'), 'authorId'
        )
      ),
    ]
    

    Ответом будет массив со списком книг и списком авторов этих книг.
    Примечание: В данном примере вызов getProps() может быть потенциально опасен, представляя возможность дотянуться до свойств, которые вы возможно не хотели раскрывать – будьте осторожны с реализацией подобных обработчиков.

    Передача round-trip данных:

    [
      137, // данные, которые вернутся, например, id запроса
      someRequest(...)
    ]
    

    В ответе будет массив с числом 137 и результатом вызова someRequest().
    Примечание: В реальности нам пришлось бы использовать более сложную конструкцию, чтобы обеспечить возвращение round-trip данных, даже если во время обработки someRequest() будет брошено исключение.

    Передача дополнительных данных:

    last( // возвращает последний аргумент
      metaInfo('метаданные или типа того', 1, 3, 4),
      someRequest(...)
    )
    

    Здесь вызов metaInfo() может что-то изменить в контексте, вызвать дополнительные действия или еще как-то повлиять на обработку, но его возвращаемое значение не попадет в ответ, так как last() вернет только свой последний аргумент.

    Работа с HTTP и веб сокетами


    HTTP-запрос, помимо основных данных (тела запроса), содержит путь, метод и заголовки. HTTP-ответ содержит код возврата. При использовании jsonex удобно использовать единый путь для всех запросов – так же, как это обычно делается для batch-запросов и при взаимодействии с помощью веб сокетов. Можно раскидать API и по разным путям, но это редко имеет смысл.

    HTTP метод нам не нужен, поскольку каждый запрос может включать любые вызовы, как получающие данные, так и изменяющие их. Тем не менее, поддержка различных HTTP методов может быть полезна или даже необходима для обеспечения правильной работы с браузерами, прокси-серверами и прочим HTTP-миром. Ее легко реализовать – достаточно добавить в контекст вычисления объекты request и response и наши обработчики смогут учитывать тонкости HTTP-протокола. То же касается кода возврата. Он не нужен в рамках jsonex-вычислений, но для правильного взаимодействия с HTTP-окружением стоит выставлять его правильно.

    Что касается передачи самих данных, то все замечательно, когда они передаются в теле HTTP-запроса. В большинстве случаев так оно и будет, поскольку использование метода POST для jsonex-запросов выглядит разумным. Но если для каких-то целей используется GET, HEAD или DELETE, то придется передавать данные как часть URL, поскольку согласно стандарту тело этих запросов должно игнорироваться. Для этого есть простой и дешевый способ – передавать jsonex в единственном параметре query string, например query. Таким образом, запрос getUsers([1,2,3]) превратится в обращение по адресу example.com/api?query=getUsers%28%5B1%2C2%2C3%5D%29

    Это выглядит ужасно, но поскольку это внутренний вызов API, видеть его будут только программисты в процессе отладки. Такой подход нередко используется и для передачи данных в JSON-формате, поскольку он сильно облегчает как упаковку на стороне клиента, так и распаковку на стороне сервера. Если страшные адреса все же смущают, несложно написать инструменты, которые скроют их под капотом.

    Для передачи метаданных можно использовать как HTTP-заголовки, так и возможности jsonex:

    last(
      authToken('myAuthToken'), // теперь сервер знает, кто я
      someOtherHeader('blah blah'),
      getUsers([1, 15])
    )
    

    Поскольку для веб сокетов нет штатного способа передавать заголовки с каждым сообщением, возможность интегрировать подобные данные в сам запрос очень удобна. Для веб-сокетов также важна возможность передавать round-trip данные и отсутствие необходимости указывать путь и метод для каждого запроса.

    Для HTTP также важна идемпотентность запросов. Это свойство определяется HTTP методом: одни методы обязаны быть идемпотентными, другие – нет. Поскольку jsonex-запрос может представлять собой смесь идемпотентных и неидемпотентных вызовов, нам нужен механизм, позволяющий что-то с этим делать. Например, можно взводить флаг, требующий идемпотентности и проверять его в вызовах:

    var handlers = {
      idempotent: function (ctx) {
        ctx.mustBeIdempotent = true;
      },
      updateUser(ctx, userData) {
        if (ctx.mustBeIdempotent) {
          throw new NonIdempotentCallError('updateUser');
        }
        ...
      }
    };
    

    Пример запроса:

    last(
      idempotent(),
      [
        getUsers([1,2]),
        updateUser({id:1, ...}) // бросит исключение
      ]
    )
    

    Какие из вызовов идемпотентны, должно быть понятно из документации API, а вызов idempotent() даст нам уверенность, что в сложном запросе не использовано ничего опасного.

    Соображения безопасности


    При создании обработчиков, нужно помнить, что в запросе они могут быть вызваны с любыми аргументами и в любых комбинациях. Чтобы это не приводило к проблемам с безопасностью, следует придерживаться некоторых ограничений:

    • Обработчики не должны выполнять потенциально опасных для системы действий
    • Обработчики обязаны проверять свои аргументы и не допускать непредусмотренного использования
    • Обработчики обязаны проверять количество данных в своих аргументах, ограничивая размер пакета обработки разумной величиной
    • Обработчики должны проверять права доступа, если такая возможность предусмотрена системой
    • Общая вычислительная сложной запроса должна быть ограничена

    Последний пункт накладывает ограничение на запрос в целом и заслуживает отдельного рассмотрения. Самый простой способ контролировать сложность запроса – рассчитывать ее по мере вычисления и останавливаться, если превышен некоторый порог:

    handlers.expesiveCall = function (ctx, args...) {
        ctx.cost += calcCost(args...);
        if (ctx.cost > ctx.costThreshold) {
          throw new TooExpensiveError();
        }
        ...
      }
    };
    

    При расчете стоимости нужно учитывать аргументы, поскольку характер и количество переданных в них данных может сильно влиять на вычислительную сложность.

    Другим вариантом может быть предварительный анализ запроса в целом. Это возможно, поскольку запрос представляет собой всего лишь данные. Но анализ сложных запросов может сам по себе быть непростой задачей, за вычислительной сложностью которой также придется следить.

    Еще один подход – запускать вычисления в песочнице с искусственно ограниченным доступом к ресурсам. Если окружение позволяет делать это несложно и дешево, это может быть неплохим вариантом.

    Важно помнить, что комбинация обработчиков потенциально может иметь эффект, недостижимый при использовании тех же обработчиков по-отдельности. Этот факт может скрывать за собой новые угрозы, но их несложно избежать. Если какое-то подмножество обработчиков внушает опасение в плане неожиданных сочетаний с другими, их всегда можно вынести в отдельный изолированный набор. Этот набор может быть доступен по отдельному адресу, либо активироваться добавлением в запрос специальных метаданных, вводящих необходимые ограничения.

    В предельном варианте, ряд обработчиков может быть разрешен к использованию только по одиночке, либо в комбинации с совсем уж примитивными вызовами типа Date(). Эти обработчики по сути вернутся к привычной модели, не позволяющей более одного вызова в запросе. Вычислимые данные предоставляют широкие возможности, но при необходимости их несложно ограничить.

    JSON-представление


    Если вы хотите использовать jsonex прямо сейчас, вы столкнетесь с одной проблемой – в отличие от JSON, высокопроизводительные библиотеки парсинга и сериализации jsonex, мягко говоря, еще не столь широко доступны ) Но выход есть – можно использовать jsonex, опираясь на его JSON-представление. jsonex превращается в JSON применением трех простых правил:

    // нотация вызова
    f(...) => {"?": ["f", ...]}
    // использование свойства '?'
    {'?': value, ...} => {"?": ["?", value], ...}
    // специальный случай, чисто для удобства
    f({...}) => {"?": "f", ...}
    

    Первое правило показывает, как записать в JSON-представлении нотацию вызова. При этом словарь, имеющий свойство '?' обретает особое значение. Второе правило отвечает на вопрос, как записать обычный словарь со свойством '?' так, чтобы не спутать его с нотацией вызова. А третье является синтаксическим сахаром, специальной формой записи нотации вызова для случаев, когда она имеет единственный аргумент и этот аргумент является словарем. Вот пример данных в jsonex и их же в JSON-представлении:

    Person({ // класс Person
      name: 'John',
      dateOfBirth: Date('1901-01-01'),
      i: Complex(0, 1),
      d: { '?': 123 }
    })
    

    JSON-представление:

    {
      "?": "Person",
      "name": "John",
      "dateOfBirth": {"?": ["Date", "1901-01-01"]},
      "i": {"?": ["Complex", 0, 1]},
      "d": {"?": ["?", 123]}
    }
    


    JSON-представление выглядит сложнее, но обозначает то же самое. Его можно распарсить стандартным JSON.parse(), а затем довычислить вторым проходом. Либо, в некоторых простых случаях, его можно вычислить прямо во время парсинга при помощи reviver-функции, передаваемой в JSON.parse(). То же самое касается сериализации в JSON-представление – ее легко сделать с помощью replacer-функции, передаваемой в JSON.stringify().

    Вообще говоря, концепцию вычислимых данных можно добавить в качестве расширения практически к любому формату данных.

    Асинхронные вызовы


    В примерах, показанных ранее, неявно считалось, что все вызовы синхронны. Рассмотрим один из них внимательнее:

    [
      set('x', getUserBooks(17)),
      getAuthors(getProps(get('x'), 'authorId')),
    ]
    

    getUserBooks() и getAuthors(), вероятно, должны обращаться в некое хранилище данных, задействовать ввод-вывод и, соответственно, быть асинхронными. Значит мы не можем вычислить их на месте. А даже если можем (например, используя fibers), то все равно хотелось бы иметь возможность выполнять независимые асинхронные вызовы параллельно, а не один за другим.

    Решением мог бы быть некий вычислительный движок, который помещал бы асинхронные вызовы в очередь на исполнение, а затем подставлял бы полученные результаты в нужные места. Тогда, вычислив все синхронные части, мы дождались бы выполнения асинхронных и после этого считали, что вычисление готово. В качестве очереди на выполнение можно использовать что-нибудь вроде async.queue(), выполняя задачи с заданным уровнем параллелизма.

    Но на самом деле задача сложнее. В нашем примере вычисление одних вызовов зависит от других, мы не можем вычислить set(), пока не вычислим getUserBooks(). Поэтому при попытке вызвать set(), мы должны отложить это вычисление, указав всем вычислениям, от которых оно зависит, что, как только они будут готовы, необходимо довычислить set(). Здесь мы зависим ровно от одного отложенного вычисления – getUserBooks(), но возможны и более сложные зависимости.

    Но и это еще не все. Вызов get() не способен вернуть ничего полезного, пока не выполнится set(). Поэтому get() тоже должен стать отложенным вычислением, на этот раз ожидающим сигнала от set(). В свою очередь getProps() зависит от get(), а getAuthors() зависит от getProps().

    Наш вычислительный движок должен будет учесть все эти зависимости и выполнить все отложенные вычисления в нужном порядке. Это кажется не очень простой задачей, поэтому хотелось бы иметь реализацию, доказывающую работоспособность подхода.

    Arc


    Проект arc был создан, чтобы показать, что концепция вычислимых данных жизнеспособна в том числе в случае использования асинхронных обработчиков и зависимых вызовов. Сам движок очень прост, он состоит из нескольких сотен строк кода и на данный момент не содержит никаких хитрых оптимизаций, что делает его очень понятным.

    Исходными данными для движка является поток токенов. В настоящее время токенизация доступна только для JSON-представления, но работа с jsonex напрямую не за горами. Arc может использоваться как в node.js так и в браузере при помощи browserify. Написание асинхронных обработчиков для arc выглядит очень просто, достаточно обернуть асинхронный вызов в соответствующую директиву, а всю сложную работу движок выполнит под капотом:

    handlers.getUserBooks = function (ctx, userId) {
      return ctx.async(function (cb) {
        doSomethingAsync(...args, cb);
      });
    };
    

    Пока поддерживается только колбэки, поддержка Promise запланирована. Примеры использования движка, а также сериализации в jsonex и JSON-представление можно увидеть в соответствующем разделе.

    Дополнение
    Как это ни удивительно, спустя буквально несколько часов с момента выкладки статьи, у arc появилась альтернатива. Хабрапользователь mayorovp представил свою версию библиотеки для парсинга jsonex в JSON-представлении. Этот вариант библиотеки состоит из единственного файла, не имеет никаких зависимостей, использует стандартный JSON.parse() и поддерживает A+ promise. К сожалению, в связи с ограничениями JSON.parse(), в этой реализации обработка асинхронных вызовов будет не столь эффективна, поэтому она сделана отключаемой. Вот пример использования альтернативной библиотеки с jQuery:

    var parser = new JSONEX.parser({ allow_async: true, functions: {
      Foo: function() {
          var result = $.Deferred();
          setTimeout(function() { result.resolve("Hello, world!"); }, 1000);
          return result.promise();
      }
    }});
    $.when(parser.parse('[{"?": ["Foo"]}]'))
      .done(function(result) { console.log(result); });
    

    Заключение


    И сам jsonex, и arc в данный момент находятся в процессе разработки. Возможности jsonex, упомянутые в этой статье, вероятно не изменятся, но добавятся новые, такие как пространства имен (namespaces), бинарные части (binary chunks), потоковые данные (streams) и области (scopes). Arc, вероятно, изменится довольно сильно.

    В силу новизны и необкатанности представленных идей, я бы рекомендовал использовать их с определенной долей осторожности. Тем не менее, я буду очень рад, если какие-то из них вам понравились и окажутся полезными в ваших проектах и экспериментах. Также буду признателен, если вы поделитесь своими впечатлениями, опытом и открытиями.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 47

      +7
      Странный велосипед, грозит быдлокодерством. Если приложение большое, то скорее всего есть модели на клиенте, никто не мешает сделать так new Person(jsonData) и внутри уже в черном ящике приводим данные к нужному виду, зачем усложнять общение с сервером?! Ведь эти данные мы не сможем использовать нигде больше — в них логика! Т.е. Public api такрй делать нельзя — а разделять паблик и не паблик не верно. Они разойдутся сильно со временем
        –3
        В этой статье описаны две вещи. Во-первых, концепция. А во-вторых, возможные варианты использования. Никто не мешает использовать концепцию только там, где это действительно оправдано.

        Скажем, те же даты…
          0
          Возьмем те же даты, есть клиенты на ios, android, и браузер. Как нам этот формат поможет? Про то что это концепция — я написал, что смешивать логику с данными плохо. Но, как говорится не возбраняется, просто шаг в сторону плохой архитектуры. Но я согласен, в некоторых проектах это оправдано.
            –1
            Даты — это не логика.
              +1
              Дата нет, но конвертация формата даты из одного в другой вполне даже является.
                –2
                Концепция заключается не в конвертации из одного формата в другой, а в отметке о необходимости такой конвертации.
                  +1
                  Совершенно верно. В дополнение к сказанному ниже, хочу также заметить, что jsonex лишь вносит дополнительную ясность в сложных случаях. Если вы используете нестандартные типы данных, конечно же вам нужно описать их в документации API. Например, вы можете указать, что нотация Date() означает дату и имеет своим единственным аргументом дату в таком-то формате.

                  В случае необходимости использовать даты в JSON, вам пришлось бы делать приписку к каждому полю, содержащему дату, указывая, в каком она формате, либо делать где-то пометку, что все даты передаются в формате таком-то и надеяться, что все обратят на эту надпись внимание и никто ничего не напутает. Если даты – ваша единственная проблема, это еще куда не шло. При большем разнообразии типов запутаться еще проще.
          +2
          Плюс это грозит большим головняком при изменении требований, именно поэтому логику для вычислимых данных выносят в отдельные статичные утилитные методы-хэлперы, которые изрядно покрывают тестами.
            +2
            Странный велосипед, грозит быдлокодерством

            К сожалению, любой достаточно гибкий и мощный инструмент будет грозить быдлокодерством. В том числе и сам JS.

            Если приложение большое, то скорее всего есть модели на клиенте, никто не мешает сделать так new Person(jsonData) и внутри уже в черном ящике приводим данные к нужному виду, зачем усложнять общение с сервером?!

            Насчет черного ящика – верно, но для поддержки любых типов данных за пределами набора, предоставляемого JSON, придется либо держать схему с явно прописанными типами полей, либо городить странные конструкции. Именно этого хотелось бы избежать. Конечно, если схема уже есть – никаких проблем, используйте схему.

            Что касается «усложнения» общения с сервером – если вам не нужны batch-запросы, вы не используете типов данных за пределами поддерживаемых в JSON или для всего уже есть схема, которая тщательно поддерживается, если вам не нужна возможность делать API доступным как по HTTP, так и по веб-сокетам, а то и по каким-то еще протоколам, то конечно, не надо ничего усложнять. Но если вам нужны какие-то из этих возможностей – jsonex дает их все разом, простым и согласованным способом. То, над реализацией чего вы могли бы ломать голову, становится уже решенной задачей.

            Ведь эти данные мы не сможем использовать нигде больше — в них логика! Т.е. Public api такрй делать нельзя

            Никто вас не заставляет засовывать логику в данные. В данных вида Date('1991-01-01') не больше логики, чем в <date>1991-01-01</date> – нотации вызова можно считать своего рода тэгами, данные остаются данными. То, что парсер трактует Date() как вызов некой функции – всего лишь деталь реализации, позволяющая сделать парсер простым и расширяемым.

            Конечно же, с помощью jsonex можно делать публичный API. Главное, чтобы клиенты понимали тот набор «тэгов», которым вы оперируете. Но это верно для любого API – та часть данных, которую клиент не может интерпретировать, будет ему бесполезна. В jsonex вы можете отбрасывать все незнакомые нотации вызова, заменяя их на null или какую-то структуру общего назначения – для этого и нужен обработчик по умолчанию. Точно также, в XML вы игнорируете теги, которые вам не интересны.

            В самих запросах к серверу, конечно, логика может присутствовать, также как она может присутствовать в SQL-запросах и любых более-менее сложных языках запросов. Это и прекрасно. Это позволяет конструировать гораздо более сложные запросы, когда они вам нужны. Но можно и запретить их использовать, если они вам без надобности.
              0
              Для рабочего проекта имхо куда более симпатичен Thrift.
              Под него много библиотек, а под этот формат мне придется писать arc пост-обработку вручную для всех языков. Не JS единым живем.
              Если статья была для привлечения внимания сообщество, тогда я за. Пока идеального формата обмена данными не реализовали, быть может ваш проект займет свою нишу. Как концепция он не интересен, а вот если он будет поддерживать кучу языков и иметь хорошую документацию — другой разговор.
                +1
                Пока это в первую очередь концепция, к которой я сам отношусь осторожно. После того, как появится первый проект, активно использующий jsonex для клиент-серверного взаимодействия, появится новая статья, из которой будет понятно, оправдались ли радужные надежды и какие проблемы возникли при столкновении с реальностью.

                Первоочередная цель этой статьи – поделиться идеей с теми, кто решает похожие задачи. Идея может оказаться полезной, а отзывы будут бесценными. В конце концов, при проектировании я думаю в первую очередь о своих собственных задачах и могу упустить что-то важное.

                Как можно заметить по выбору хабов, jsonex ориентирован в первую очередь на веб-разработку и JS. Он выглядит очень естественно для JS-разработчиков, а в случае использования JS на стороне сервера, один и тот же код обработчиков можно использовать с обеих сторон.

                Будет ли jsonex широко использоваться – вопрос истории. А документация будет )
                0
                Насчет черного ящика – верно, но для поддержки любых типов данных за пределами набора, предоставляемого JSON, придется либо держать схему с явно прописанными типами полей
                это даже полезно. с JSON Schema уже во всю лисапедят то, что раньше делали с DTD и XML Schema. github.com/google/autoparse, github.com/cwacek/python-jsonschema-objects и т.п., — вплоть до билдеров форм на бутстрапе.
                  0
                  Я ничего не имею против схемы. Это важная и полезная практика, и jsonex вовсе не исключает ее использования, хотя и позволяет без нее обходиться, если по каким-то соображениям от нее отказались. Кроме того, при наличии схемы, он позволяет отделить валидацию от парсинга, что может быть полезно – каждый из процессов по отдельности проще в реализации и может использоваться независимо, ведь валидация бывает нужна не только для свежераспарсеных данных.
              0
              Мне кажется, что вы ничего не знаете про YAML
                –1
                В отличие от YAML, очень развесистого формата с много-много-страничной спецификацией, jsonex является тривиальным расширением простейшего JSON, спецификация которого занимает одну страницу. При этом jsonex легко расширяем, имеет родственный JS синтаксис и позволяет конструировать сложные запросы к серверу в тех же терминах, что и обычные данные. Полагаю, это может оказаться полезным.
                  +1
                  вероятно шутка в том, что yaml тоже начинался как тривиальное расширение простейшего перла. а потом понеслось… :)
                    +1
                    Важно вовремя остановиться )
                    +1
                    Как-правило, в JSON многих не устраивает отсутствие комментариев и использования одинарных кавычек.
                    YAML же предоставляет такую возможность, не отказываясь от JSON-нотации!
                    Мало того, для работы с YAML уже написаны десятки библиотек под разные языки, при том что в каких-то языках он включен в стандартную библиотеку (например, Ruby).
                      0
                      Более того, любой JSON-документ является валидным YAML-документом.
                        0
                        Почти, за одним маленьким исключением — в YAML нельзя использовать табуляцию.
                          0
                          В YAML можно использовать табуляцию. Табуляцию нельзя лишь использовать для значащих отступов. А если вы используете вариант формата со скобками, как в JSON, значащих отступов нет.
                            0
                            Смотрите, вот такой пример кода не пройдет валидацию (по крайней два парсера ругаются).

                            {
                            	foo: 1
                            }
                            
                              0
                              Как ни странно, такой пример кода не пройдёт валидацию и JSON-парсером, ибо ключ не закавычен =)
                                0
                                Надеюсь мне удалось донести, до вас свою мысль.
                                  0
                                  JSON есть правильное подмножество YAML. Если вы считаете это утверждение опровергнутым, то нет, не удалось. Если мысль была какая-то иная, то тоже не удалось, я не уловил её.
                                    0
                                    {
                                    	"foo": 1
                                    }
                                    

                                    Значит пока мало кто соблюдает спецификацию, т.к. этот пример не пройдет валидацию такими парсерами как js-yaml и pyyaml
                                      0
                                      ike@shair:/tmp> od -tc 000
                                      0000000    {  \n  \t   "  a  a  a  "   :   1  1  1  \n   }  \n
                                      0000020
                                      ike@shair:/tmp> js-yaml 000
                                      aaa: 111
                                      
                                      ike@shair:/tmp> 
                                      

                                      ЧЯДНТ?
                                        0
                                        Не имею малейшего представления:

                                        >> JS-YAML: deficient indentation at line 9, column 1:
                                        >>     	foo: 1
                                        >>     ^
                                        >> JS-YAML: deficient indentation at line 10, column 1:
                                        >>     }
                                        >>     ^
                                        


                                        yaml-online-parser.appspot.com (pyyaml)

                                        Output
                                        ERROR:
                                        while scanning for the next token
                                        found character '\t' that cannot start any token
                                          in "<unicode string>", line 2, column 1:
                                            	"foo": 1
                                            ^
                                        
                        +1
                        Боюсь, что возможности, близкие к нотации вызова в jsonex – тэги YAML, все-таки выходят за пределы JSON-нотации и выглядят достаточно чужеродно на ее фоне. При этом нотация вызова гибче, чем тэги. Ну и любой валидный JSON-документ точно также является валидным jsonex-документом, как и YAML-документом. Насчет библиотек могу только порадоваться за YAML, но и он когда-то начинал на фоне их отсутствия.
                    0
                    «Тем не менее, некоторые вещи хотелось бы улучшить»

                    Есть, но не эти. Ничего концептуально нового — заточенный под нужды автора способ обмена данными.
                      0
                      Разумеется, я думал в первую очередь о своих задачах и нуждах )
                      Но тем более интересно – что же хотелось бы улучшить вам?

                      Соглашусь, что ничего концептуально нового тут нет – «нотация вызова» широко используется в языках программирования. Но чтобы увидеть ее ценность применительно к формату данных, потребовалась долгая дорога.
                      +1
                      Такой велосипед на первый взгляд требует уйму времени на поддержку (баго фикс, въезжание новых разработчиков в особенности фреймворка).

                      Поэтому очень хочется понять, зачем он был сделан.
                      В начале поста брошены ну уж очень абстрактые фразы, из которых нельзя понять юз кейсы конечных пользователей. Из функциональности фреймворка тоже это понять нельзя, потому что, возможно, сделано не то и не так. Встречался с таким.

                      Ну например, такие вопросы:
                      — Зачем нужны batch-запросы? В самом элементарном случае мы можем сделать RESTful api для каждого из запросов, параллельность выполнения чанков можем гарантировать на стороне клиента. Почему Вы выбрали вариант сложнее?
                      — Что за юз кейс для выполнения на клиенте произвольного кода? Почему бы не использовать некий eval валидного js-кода на стороне сервера хотя бы?
                      — Я правильно понимаю, что схемы нет в проекте? Ну то есть если мы отправляем объект типа Person в одном запросе на сервер может прийти поле с именем dateOfBirth, а во втором birthDate? Если есть, то почему мы железно должны указывать тип данных например Date, а не определять его по схеме?
                        +1
                        Такой велосипед на первый взгляд требует уйму времени на поддержку (баго фикс, въезжание новых разработчиков в особенности фреймворка).

                        Это можно сказать о любом фреймворке.

                        Но возьмите, к примеру batch-запросы Facebook Graph API. Они устроены довольно сложно. С точки зрения клиента, jsonex даже проще, при том, что возможностей у него больше. На серверной стороне batch-запросы Facebook тоже устроены непросто, поверьте. Может, чуточку проще, чем движок arc, но не принципиально. При том, что arc опять же умеет больше.

                        При этом Facebook не претендует на универсальность подхода в своем batch API. Если вам понадобится использовать batch-запросы другой системы – придется начать с нуля. Если вы захотите реализовать batch-запросы у себя на серверной стороне – нет хорошего способа это сделать. Вы можете мимикрировать под Facebook (и подстраваться под все их изменения) или придумать свой подход – в любом случае решение будет достаточно уникальным.

                        При этом мы говорим только о batch-запросах, не затрагивая других тем. jsonex решает задачу batch-запросов, а также ряд других задач. При этом в качестве решения берется очень простое расширение всем знакомого JSON. Если ваша система «говорит на jsonex», в ней уже есть batch-запросы, кастомные типы данных и прочие плюшки. И вы можете общаться с любой системой, поддерживающей jsonex не внося изменений в подходы к построению запросов и обработке данных.

                        Далее по вашим вопросам.

                        Зачем нужны batch-запросы? В самом элементарном случае мы можем сделать RESTful api для каждого из запросов, параллельность выполнения чанков можем гарантировать на стороне клиента.

                        Во-первых, клиент (браузер) очень плохо гарантирует параллельность выполнения чанков. Число одновременных подключений к одному домену ограничено. SPDY или его аналог когда-нибудь решит эту проблему, но пока она есть.

                        Но даже если бы ее не было. Часто вам нужно получить одни данные, а затем на их основе получить другие. В этом случае запросы не параллелятся. Чтобы вытащить друзей ваших друзей, вам придется сделать два последовательных запроса. Либо добавить в API специальный вызов для вытаскивания друзей друзей (а также кучу других вызовов, вытаскивающих прочие комбинации зависимых данных, которые вам могут понадобиться). Альтернативное решение – batch-запросы с поддержкой зависимых вызовов. Как в том же Facebook Graph API, или как в jsonex.

                        Что за юз кейс для выполнения на клиенте произвольного кода? Почему бы не использовать некий eval валидного js-кода на стороне сервера хотя бы?

                        jsonex не позволяет выполнять произвльный код, ни на клиенте, ни на сервере. Только заранее определенные обработчики, смысл которых, равно как и возможные параметры, должен быть явно прописан в API. В случае jsonex ваши обработчики и есть API, включающее как возможные серверные вызовы, так и спецификацию нестандартных типов данных.

                        Я правильно понимаю, что схемы нет в проекте? Ну то есть если мы отправляем объект типа Person в одном запросе на сервер может прийти поле с именем dateOfBirth, а во втором birthDate? Если есть, то почему мы железно должны указывать тип данных например Date, а не определять его по схеме?

                        Если у вас есть схема данных с четко прописанными типами, конечно, вы можете ее использовать. Это не даст вам других возможностей jsonex, но с типами данных все будет хорошо )

                        И конечно же, отсутствие формального описания схемы данных вовсе не дает вам право отдавать дату рождения то как dateOfBirth, то как birthDate – в пределах серверного кода вы ведь так не делаете, не смотря на всю эту динамическую типизацию? Здесь ровно то же самое.

                        jsonex позволяет вам не иметь схемы данных. А если она есть – отделить валидацию согласно схеме данных от парсинга. В случае обработки JSON вам придется прямо во время валидации интерпретировать данные согласно схеме, чтобы ту же банальную дату, переданную в виде строки, превратить в объект даты и подставить в нужное место. Если вы этого не сделаете, вас будет ждать сюрприз в качестве строки в поле, в котором ожидается дата. Вам понадобится хитрый валидатор, который умеет не только проверять корректность данных, но и модифицировать их согласно схеме. По моему опыту, многие за неимением такого валидатора разбирают пришедшие данные вручную, независимо от наличия или отсутствия схемы.

                        jsonex отдаст вам готовые объекты. Хотите – валидируйте после, хотите – прямо в обрабочиках. Хотите – не валидируйте. Вся явная чушь все равно должна быть отсеяна обработчиками во имя принципов безопасности.
                          0
                          Это все здорово. Но я опять не увидел целесообразности этих решений.

                          Зачем писать Всеобъемлющий Супер Фреймворк, если можно решить каждый вопрос в отдельности, даже если писать все 'вручную' (те же отдельные вызовы апи)?

                          Ваш клиентский код будет генерить сотни-сотни вызовов апи в секунду? зачем заморачиваться об ограничениях браузера?

                          Почему бы не добавить этот самый запрос вытаскивания друзей друзей? Это очень просто и очень явно. Здесь я вижу немного кода на серверной стороне, обрабатывающей запрос. И не вижу супер-мега сложного кода фреймворка.
                          И даже при этом фреймворке ваши разработчики будут писать количество кода сравнимое с первым вариантом, но во втором варианте у Вас будет в системе присутствовать очень сложная система этого фреймворка.

                          Я не могу понять, а что плохого в том, что придется во время валидации интерпретировать данные согласно схеме? Давайте назовем этот процесс конвертацией из json в js-объект модели. И тут сразу все встает на свои места и может быть легко автоматизировано путем простой декларацией схемы. Сюрприза по Вашему примеру меня ждать не будет, потому что если данные не парсятся, то вылетит ошибка на этапе конвертации.

                          Вы извините, конечно. Но я вижу астронавта от архитектуры.
                            +1
                            Зачем писать Всеобъемлющий Супер Фреймворк, если можно решить каждый вопрос в отдельности, даже если писать все 'вручную' (те же отдельные вызовы апи)?

                            А зачем писать операционную систему? Ведь все можно сделать вручную!

                            Конечно можно. Если вам кажется, что ваши задачи лучше решаются в ручной реализации – используйте ручную реализацию. Задачи бывают разные, подход должен соответствовать задаче.

                            Ваш клиентский код будет генерить сотни-сотни вызовов апи в секунду? зачем заморачиваться об ограничениях браузера?

                            Вы не поверите, но достаточно всего нескольких затяжных вызовов и ваши запросы встанут в очередь. И это только для данных, а ведь вам и статику тянуть. Ну и главное-то не в решении чисто транспортных проблем, а в исключении проброса данных клиенту только ради того, чтобы тут же отправить их обратно на сервер в новом запросе.

                            Почему бы не добавить этот самый запрос вытаскивания друзей друзей? Это очень просто и очень явно.

                            И правда, зачем позволять композицию функций? Ведь всегда можно написать функцию, которая вызовет внутри себя одну, а затем другую.

                            И все будет неплохо, пока у вас пара-тройка таких агрегирующих запросов. В большой системе их будут десятки, каждый с кучей параметров, настраивающих поведение внутренних вызовов. А если система открытая, то и вовсе не угадать, какие комбинации понадобятся людям. Поэтому и существуют языки запросов, позволяющие нетривиальные конструкции, такие как SQL или тот же Facebook batch API.

                            Я не могу понять, а что плохого в том, что придется во время валидации интерпретировать данные согласно схеме? Давайте назовем этот процесс конвертацией из json в js-объект модели.

                            Вашему конвертеру нужно будет заранее знать о всех доступных форматах. Либо вам придется писать свой вариант конвертера для каждой новой системы, которая имеет свой взгляд на то, как выглядит сериализованая дата. Либо вы попытаетесь сделать его расширяемым и в какой-то мере повторите дизайн jsonex.

                            И даже при этом фреймворке ваши разработчики будут писать количество кода сравнимое с первым вариантом, но во втором варианте у Вас будет в системе присутствовать очень сложная система этого фреймворка.

                            Количество кода в языке, позволяющем композицию функций, железно будет меньше. При этом функции будут проще и пользоваться ими будет удобнее. Кроме того, мощность API будет расти гораздо быстрее с добавлением каждой новой функции, так как ее можно будет использовать в разных комбинациях с другими.

                            Что касается сложности фреймворка – движок arc предельно прост. В нем чуть более 300 строчек кода, разбитого на функции, большинство которых укладывается в десяток строк. В реализации от mayorovp кода еще меньше. Что именно вызывает у вас ощущение запредельной сложности?

                            Я уже не говорю о простоте использования. Если внутренности arc для вас слишком сложны (что на самом деле странно), вы можете просто пользоваться им. Он просто работает. Как работает ваш телефон или компьютер.

                            Вы извините, конечно. Но я вижу астронавта от архитектуры.

                            От вас это звучит как комплимент )

                            Вероятно, вам действительно не нужен jsonex. Честно говоря, всегда будет больше людей, которым он не нужен, чем тех, кому нужен. Но, если вы никогда не сталкивались с необходимостью batch запросов, вам не приходилось выставлять очередной чертов URL, по которому доступно то же самое, что по трем другим, но одним куском, вы никогда не ломали голову, как передать нестандартный тип данных, так чтобы он органично вписался в существующую систему, а не «давайте воткнем это грязным хаком, сейчас нет времени дорабатывать конвертер» – это еще не значит, что вселенная тривиальна и 64K хватит для всех. Это значит только то, что вы пока не сталкивались с более сложными ее проявлениями.

                            В моем случае, за те годы, что я работаю с веб, jsonex мог сэкономить как мне, так и окружающим огромное количество времени, которое можно было потратить на куда более интересные вещи, чем возня с транспортировкой данных «вручную».
                        +1
                        Что-то в проекте arc как-то всего очень много. Если кому-нибудь нужен компактный инструмент для преобразования JSONEX в объекты и в обратную сторону, могу предложить библиотеку из одного файла. В ней нет ничего лишнего — совсем ничего, даже даты.

                        Зависимостей нет. Работает в «совместимом» формате, то есть с JSON, использует стандартные JSON.parse и stringify.
                          0
                          Спасибо! Добавил упоминание библиотеки в статью.

                          P.S. В arc много всего, потому что на данном этапе он пытается быть «академичным» – позволять играть с отдельными частями, сохраняя общую простоту. Например, он на полпути к использованию Stream-интерфейса, что должно дать возможность парсинга из потока. Он позволяет подключить альтернативный код очереди выполнения, содержит несколько базовых обработчиков, включая событийные get и set, добавлена пара других заделов на будущее. Сейчас это скорее научная лаборатория, чем рабочий завод.
                            +1
                            Событийные get и set у меня теперь тоже есть, только не в библиотеке, а на странице документации…
                          +2
                          Абсолютно имеет место быть все, что в статье описано, как вариант декларативного запроса, не вижу причин почему его нельзя использовать, пять баллов.
                            0
                            Как только в формате данных появляется контекст, брюки превращаются из формата обмена в сигнальный протокол.

                            Нет никакой нужды для этого расширять JSON, просто оговорите ваш протокол до предела:
                            {"whatToDo": "doWhatIMean"}
                            

                            и JSON расширять не нужно, вся логика будет на сервере. А если же вас огорчают кавычки и комментарии, то вы что-то делаете не так, если вы хотите формат, удобный для глаз и рук, действительно уже есть YAML, где позабочено и о кавычках и о комментариях.

                            Единственной разумной новацией можно назвать ввод типов данных, типа даты, но нужно чётко понимать, что JSON родился из яваскрипта, и жертвование совместимостью с форматом сериализации объекта должно быть чем-то хорошо оправдано.
                              +1
                              Как только в формате данных появляется контекст, брюки превращаются из формата обмена в сигнальный протокол.
                              Этот самый контекст находится далеко снаружи формата и является исключительно деталью реализации.
                              Нет никакой нужды для этого расширять JSON, просто оговорите ваш протокол до предела:
                              {"whatToDo": "doWhatIMean"}
                              У автора есть система кодирования, которая превращает его JSONEX в валидный JSON, в котором все именно так и оговорено.
                              JSON родился из яваскрипта, и жертвование совместимостью с форматом сериализации объекта должно быть чем-то хорошо оправдано
                              Предлагаемый автором JSONEX точно так же полностью совместим с яваскриптом, из которого он родился.
                              0
                              Я бы хотел повсеместное распространение YAML и edn.
                                0
                                А что вы думаете по поводу JSON5?
                                  +2
                                  А чего тут думать? Надо все это включать в JSONEX :) Кстати, можно же форкнуть проект, и получить JSONEX-парсер после незначительных модификаций…
                                    0
                                    Выглядит разумно, нужно учесть эти фишки при дальнейшем развитии jsonex.

                                    Кстати, если в вызов reviver добавить ссылку на родительский объект и гарантировать, что для массивов тоже передается key (индекс массива), то на базе JSON5 было бы очень просто делать парсинг jsonex в JSON5-представлении. Надо будет обсудить этот вопрос с авторами, может получится договориться и о более тесной интеграции.
                                      +1
                                      Кстати, если в вызов reviver добавить ссылку на родительский объект
                                      Кстати, такая ссылка уже есть — это this. Но мне эта ссылка не пригодилась…

                                      PS И для массивов все так же гарантируется же.

                                        0
                                        Действительно, перепроверил – все как вы говорите.

                                  Only users with full accounts can post comments. Log in, please.