Представляю вашему вниманию небольшую js-библиотеку Jsqry.
Проще всего проиллюстрировать её назначение следующим примером.


До:


var name;
for (var i = 0; i < users.length; i++) {
    if (users[i].id == 123) {
        name = users[i].name;
        break;
    }
}

После:


var name = one(users, '[_.id==?].name', 123);

Библиотечка позволяет извлекать информацию из объектов/массивов в одну строку, используя несложный язык запросов, вместо написания циклов (подчас вложенных).


По сути, она реализует всего две функции:


  • query — для возвращения списка результатов и
  • one — для возвращения первого найденного результата.

Список возможностей включает:


  1. Фильтрацию
  2. Трансформацию
  3. Индексы/срезы в стиле Python

Библиотека появилась спонтанно в одном проекте, построенном на модном ныне подходе одностраничного приложения. Мы загружаем один большой JSON, части которого затем используются для рендеринга на клиенте разных представлений сайта. И вот для выдирания этих самых частей и захотелось более удобного способа. Затем, впрочем, библиотека оказалась востребована и в других случаях.


Поясню немного по функционалу. Запрос в общем случае может иметь вид


field1.field2[ condition or index or slice ].field3{ transformation }.field4

Тут:


  • field1.field2.field3... — обычный доступ к полям объектов, как в js
  • [ condition ] — фильтрация
  • [ index ] — доступ по индексу, тоже как в js
  • [ from:to:step ]срезы в стиле Python
  • { transformation } — преобразование объектов

На condition и transformation стоит остановиться подробнее.
На самом деле тут все очень просто. Достаточно понять, что каждое выражение внутри квадратных/фигурных скобок при выполнении заменяется на функцию по такому принципу:


condition_or_transformation ? function(_,i) { return condition_or_transformation }


(тут _ — значение передаваемого элемента, i — его индекс).


Пример:


query([1,2,3,4,5],'[_>2]{_+10}') // [13, 14, 15]

Также поддерживаются параметризация запроса:


query([1,2,3,4,5],'[_>?]{_+?}', 2, 10) // [13, 14, 15] 

Комбинируя эти возможности можно строить весьма сложные и гибкие запросы. Больше примеров использования можно посмотреть тут.


Из интересного в реализации — AST дерево запроса кешируется, что придает библиотеке скорости.


Разумеется, моя библиотека не уникальна в своём роде. Стоит привести "небольшой" список аналогов:



Зачем еще одна библиотека? На самом деле, она поддерживает не весь возможный спектр типов запросов, а только то что было нужно в нашем проекте в большинстве случаев. За счет этого простота и скорость. Также, чрезвычайно простой API, вдохновленный подходом JQuery.


Буду рад выслушать критику и предложения по улучшению.

Поделиться с друзьями
-->

Комментарии (58)


  1. shasoft
    20.06.2016 18:11
    +1

    Тесты скорости не проводились? Т.е. насколько теряется скорость при использовании библиотеки.


    1. xonix
      20.06.2016 18:59

      Скурпулезных замеров не проводил, но вот есть небольшой бенчмарк https://github.com/xonixx/jsqry/blob/master/bench.js.
      У меня 100000 прогонов запроса вида


      jsqry.one(o1, '[_.id>=2].name[_.toLowerCase()[0]==?].length', 's')

      отрабатывает за 425ms.


  1. nazarpc
    20.06.2016 18:32
    +8

    За что я люблю LiveScript, так это за то, что подобные вещи можно достаточно компактно писать без библиотек.
    До:


    query([1,2,3,4,5],'[_>2]{_+10}')

    После:


    [1,2,3,4,5].filter(-> it>2).map(-> it+10)

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


    Или же ES2016, если странспайлер используется:


    [1,2,3,4,5].filter(it => it>2).map(it => it+10)


    1. xonix
      20.06.2016 18:46

      Спасибо, интересно.
      А такой юз-кейс:


      var hotel = {
          name: 'Name',
          facilities: [
              {name:'Fac 1',
              services: [
                  {name:'Service 1', visible:false},
                  {name:'Service 2'}
              ]},
              {name:'Fac 1',
              services: [
                  {name:'Service 3'},
                  {name:'Service 4', visible:false},
                  {name:'Service 5'}
              ]}
          ]
      };
      
      console.info(query(hotel,'facilities.services[_.visible !== false].name')); // [ 'Service 2', 'Service 3', 'Service 5' ]
      


      1. IaIojek
        20.06.2016 19:24

        Два пути: красивый, но с добавлением одной функции в Array.prototype

        Array.prototype.concatItems = function(){ return Array.prototype.concat.apply([], this) };
        
        hotel.facilities.map(_=>_.services).concatItems().filter(_=>_.visible !== false).map(_=>_.name); // [ 'Service 2', 'Service 3', 'Service 5' ]
        

        и не такой красивый
        [].concat.apply([], hotel.facilities.map(_=>_.services)).filter(_=>_.visible !== false).map(_=>_.name); // [ 'Service 2', 'Service 3', 'Service 5' ]
        



        1. jMas
          20.06.2016 20:15
          +5

          hotel.facilities.map(_=>_.services).reduce((a,b)=>a.concat(b)).map(_=>_.name);
          


          1. raveclassic
            20.06.2016 21:47
            +1

            Вот да, только хотел спросить, чем стандартные map/reduce/filter не угодили.


          1. IaIojek
            21.06.2016 15:01
            +2

            Я подумал об этом, но только после отправки комментария.
            Замечу, что у такого решения есть недостаток — вычислительная сложность у него O(n*m) где n — суммарное количество элементов в подмассивах, m — количество подмассивов. У concat это O(n).


            1. raveclassic
              21.06.2016 15:23
              +1

              На самом деле тут вариантов несколько.
              Если нужна лаконичнось, все укладывается в 1 reduce:

              hotel.facilities.reduce(
               (acc, f) => acc.concat(f.services.filter(s => s.visible !== false).map(s => s.name)), 
               []
              );
              // ["Service 2", "Service 3", "Service 5"]
              

              Если нужна скорость — пожалуйста.
              hotel.facilities.reduce((acc, f) => {
               f.services.forEach(s => {
                if (s.visible !== false) {
                 acc.push(s.name);
                }
               });
               return acc;
              }, []);
              // ["Service 2", "Service 3", "Service 5"]
              

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


              1. xonix
                21.06.2016 15:34

                Но данная библиотека только повышает количество возможных способов


                Не только. Её вариант проще Вашего лаконичного варианта, правильно обрабатывает null/undefined, работает на любом древнем JS.
                По-моему плюсов не так и мало.
                query(hotel,'facilities.services[_.visible !== false].name')


                1. Zibx
                  21.06.2016 16:08
                  +1

                  Что значит правильно обрабатывает? Нул не должен обрабатываться никак и undefined, на самом деле, тоже. Сейчас я почитаю код библиотеки и точно скажу насколько оно правильно.


                  1. Zibx
                    21.06.2016 16:15
                    +1

                    Посмотрел. Функция defined плохая. Обычно требуется не проверка «a.b === undefined», а «b in a», потому что undefined может быть действительно значением поля, которое мы хотим извлечь. Если заявлена поддержка старых браузеров, то там значение undefined вообще можно переопределить, по этой причине я долго оборачивал весь код в функцию (function(undefined){ код })(), пока не перешел на void 0.


                    1. Zenitchik
                      21.06.2016 16:48

                      там значение undefined вообще можно переопределить


                      Тот кто это сделает — будет гнусным извращенцем. За то, как поведёт себя код в руках гнусного извращенца, разработчик не отвечает.


                      1. raveclassic
                        21.06.2016 16:59

                        К сожалению, тут проблема двусторонняя… И «гнусный извращенец» может пролезть к вам в сборку с 50-ой зависимостью какого-нибудь npm-пакета :)


                        1. Zenitchik
                          21.06.2016 17:24

                          Среди разработчиков сколь-нибудь серьёзных библиотек извращенцев нет, а несерьёзные — проще самому написать.


                  1. xonix
                    21.06.2016 16:32

                    Что значит правильно обрабатывает


                    Как Ваши варианты решения отработают на какой-то из записей facilities без поля services?


                    1. raveclassic
                      21.06.2016 16:50
                      +1

                      Ну окей. Что, если мне нужно показать сообщение об ошибке, что с сервера пришли битые данные (без services — просто абстрактный пример)? Ваша библиотека, так сказать, прибила гвоздями проверку на undefined, и, соответственно, молча продолжит выборку, когда, используя стандартные функции, можно добавить и/или исключить все необходимые проверки.
                      Это гибко и удобно, хоть и не позволяет сэкономить лишние пару строк.


                      1. Zibx
                        21.06.2016 17:04
                        +1

                        Даже не про этот кейс, тут можно итоговый length проверить. А вот приходит с сервера [{name:1}, {name: undefined}, {name:7}] -> обычно из него хотят отрисовать вьюху где первый и последний элемент будет содержать 1 и 7, а вместо второго надпись «введите значение», с волшебной библиотекой мы не узнаем о том что значения во втором поле нет, а особенно важно что мы не узнаем что его нет именно во втором элементе. Причём я понимаю когда эта библиотека может быть удобна, но такие граничные поведения надо писать огромными буквами в документации, потому что это может вылиться в часы отладки через месяц использования библиотеки.


                      1. xonix
                        21.06.2016 17:04

                        Ну это философский спор. Но, например, разработчики angularjs сделали так же.

                        Forgiving: In JavaScript, trying to evaluate undefined properties generates ReferenceError or TypeError. In Angular, expression evaluation is forgiving to undefined and null


                        1. raveclassic
                          21.06.2016 17:14
                          +1

                          Кстати, это очень здорово, что Вы упомянули этот момент в ангуляре. В свое время он доставил немало «удовольствия» в поиске места, куда приткнуться, чтобы понять странность получаемого результата и отладить это их «небольшое усовершенствование». Думаю, что с jsqry рано или поздно случилось бы то же самое…


        1. Zibx
          21.06.2016 00:48

          Может автор учёл undefined элементы этой цепочки.


          1. xonix
            21.06.2016 01:32

            Весьма верное замечание! Вариант с Jsqry корректно прожует вариант объекта без поля services. Вариант с ES-кунг-фу скорее всего упадет с Null Pointer.


            1. napa3um
              21.06.2016 03:15

              Не упадёт, если обратиться к несуществующему проперти, оно будет undefined. При попытке обращения к более вложенным полям несуществующего поля упало бы, на этот случай можно использовать _.get из lodash или синтаксис obj?.prop из CoffeeScript.


            1. raveclassic
              21.06.2016 10:38
              +1

              Вся необходимая логика (да и вообщем-то любые проходы по коллекциям) так или иначе сводится к map/reduce. Собственно, вся логика выборки (в том числе и все-возможные проверки на undefined/null/коня_в_вакууме) прекрасно умещается в лямбдах для них.

              «ES-кунг-фу» гораздо более предсказуемо, его проще и удобнее отлаживать и, что самое важное, поддерживать — это часть стандарта.


              1. Zibx
                21.06.2016 16:05

                Я не спорю с кунг-фу, поскольку сам люблю строить развесистые комбо, но хотел обратить внимание что (возможно (не читал код этой библиотеки) ) приведённая цепочка мэп\фильтро\редьюсов не является эквивалентом.


      1. nazarpc
        20.06.2016 20:51
        +3

        Я бы реализовал следующим образом.


        LiveScript:


        [].concat(...hotel.facilities.map(-> it.services.filter(-> it.visible!==false).map(-> it.name)))

        ES2016:


        [].concat(...hotel.facilities.map(it => it.services.filter(it => it.visible!==false).map(it => it.name)))

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


        1. TheShock
          22.06.2016 07:34
          +1

          А зачем пример на LiveScript, если пример — на ES2016 то же самое?


          1. nazarpc
            22.06.2016 09:45
            +1

            Изначально на LS написал, вот и здесь повторил.
            А вообще я считаю что LS незаслужено обделен вниманием. В нём много интересных решений, которых мне не хватало в CoffeeScript, возможно, мой комментарий вынудит кого-то зайти на сайт и посмотреть что он собой представляет.


  1. Zibx
    20.06.2016 18:47
    +3

    О, это тот редкий случай когда eval и new Function имеют право на жизнь. Либо генерировать функцию и делать кэш по строке, либо функцию которая компилирует выражение в функцию, и возвращает функцию которая принимает непосредственно объект.


    1. jt3k
      21.06.2016 01:33

      что Вы имеете в виду?


      1. Zibx
        21.06.2016 16:18
        +2

        Уже посмотрел что решение использует кэширование ast, значит со скоростью всё не так плохо как я предполагал. И наличие самого AST тоже было очень приятно. Я имел в виду отдавать не результат вычислений, а функцию с замкнутой функцией соответствующей конкретному AST, после чего эта функция принимает на вход данные, а даёт результат.


  1. onto
    20.06.2016 18:55
    +4

    В первом примере нужен break:

    var name;
    for (var i = 0; i < users.length; i++) {
        if (users[i].id == 123) {
            name = users[i].name;
            break;
        }
    }
    


    1. surefire
      20.06.2016 21:27
      +3

      А я вообще первый пример заменил бы, на простую и понятную конструкцию.

      var name = ( users.find( u => u.id == 123 ) || {} ).name
      


      1. surefire
        20.06.2016 21:34
        +1

        лучше даже так:

        var name = ( users.find( u => u.id == 123 ) || { name: 'anonymous' } ).name
        


    1. xonix
      20.06.2016 23:40

      Да, спасибо.


  1. xGromMx
    20.06.2016 19:12
    +6

    Нужно больше строковых конструкций! Как это дебажить?


    1. xonix
      20.06.2016 19:20
      -1

      В этом смысле так же как и регулярные выражения, JQuery-селекторы или тот же SQL.


      1. xGromMx
        20.06.2016 19:28
        +2

        Тогда лучше взять что-то типа lodash/fp, Ramda, воспользоваться линзами (хотя это больше для хаскеля)


        1. xonix
          20.06.2016 19:30

          А что — разве будет легче дебажить?


          1. xGromMx
            20.06.2016 19:32
            +1

            Конечно, я же могу раскидать лямбды на именованные функции если надо + `tap` у lodash


            1. xonix
              20.06.2016 19:47
              -2

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


              function f1(elt) { return elt > 2 }
              function f2(elt) { return elt + 10 }
              console.info(jsqry.query([1,2,3,4,5],'[ ?(_) ]{ ?(_) }', f1, f2)); // [ 13, 14, 15 ]


  1. jMas
    20.06.2016 19:53
    +1

    Натыкался на JSONSelect, 1,5k звезд на гитхабе, но к сожалению, последний апдейт 3 года назад.
    Из приятного (что лично нравится мне) — это CSS selector-like way для выборки данных (то есть довольно интуитивно для веб-девелоперов), есть тесты, и есть подтверждение востребованности в виде большого количества девелоперов обративших на это внимание.


  1. pinal
    20.06.2016 20:01
    +1

    Это актуально? Однако.
    Тогда вот мой примитивный поиск по xpath в объекте


    1. jMas
      20.06.2016 20:05

      XPath — большой стандарт в котором есть много дополнительных опций для выборки. Насколько ваш вариант совместим со стандартом?


      1. pinal
        20.06.2016 22:21

        Я не стал заморачиваться. Просто //, но чую можно довести до ума :) Потом…


  1. Galamoon
    20.06.2016 21:39

    ммм выглядит интересно, но реальная польза будет когда подобный синтаксис запросов будет описан на нативном языке в виде модуля ES6 или ES7.


  1. SDSWanderer
    20.06.2016 23:01

    Из подобного рекомендуемую https://github.com/davidgtonge/underscore-query — mongodb-like синтаксис как по мне гораздо лучше.


  1. razetdinov
    20.06.2016 23:02
    +1

    Чем это лучше JSPath?


    1. xonix
      20.06.2016 23:36

      Библиотеки очень похожи по задумке. Jsqry значительно проще как по устройству так и по использованию за счет переиспользования обычного js для предикатов, в JSPath же изобрели свой язык предикатов со своими операторами и т.д. По функционалу, имхо 80-90% совпадает. Хотя в Jsqry срезы более продвинутые (поддерживется step параметр, как в Python), а также не нашел трансформации у них, только фильтрация. С другой стороны, в JSPath, разумеется есть и возможности, коих нет в Jsqry, например, '..' и '|' в location path. А, да, у них еще более громоздкий синтаксис подстановок — именованные вместо '?'. Впрочем, это можно расценить и как плюс.


  1. timramone
    20.06.2016 23:30

    Да, нужно больше языков, конечно!
    Если серьезно, то по-моему после появления arrow-functions в ES6 или typescript (в котором они были от рождения), это всё на столько бесполезно…


    1. jt3k
      21.06.2016 01:34
      -1

      как Arrow-функции помогают делать запросы к объектам? это обычные функции без собственного контекста.


      1. napa3um
        21.06.2016 11:33
        +2

        Компактностью аргументов в методах обработки массивов типа map и reduce.


  1. kemsky
    21.06.2016 00:45
    +1

    Самый серьезный минус в том, что рефакторинг и подсветка в ИДЕ не будет работать и не будет работать линт и тайпскрипт компилятор так же ничего проверить не сможет.
    Нечто похожее я сделал для ActionScript, но сделал это на функциях и как часть библиотеки для работы с коллекциями. В принципе, я согласен с тем, что arrow-functions эту нишу во многом закрывают.


  1. squonk
    21.06.2016 01:33
    +3

    Может попробовать Lodash?


  1. pterolex
    21.06.2016 01:40
    +2

    Array.prototype.find и filter, имхо, вполне решают основную часть проблем (и сразу есть в стандарте ES5-ES2015). В крайнем случае, можно и lodash подключить


  1. helgihabr
    21.06.2016 11:51
    +1

    Спасибо за проделанную работу.
    Лично я планирую использовать эту библиотеку.
    А то, что эту задачу можно решить в ES6 или typescript, ну так отлично. Ее можно решить используя еще множество других инструментов.
    В данном случае мне нравится, что это JS изначально, а не «обертки», да и размер самой библиотеки для меня является тоже плюсом.


  1. babylon
    21.06.2016 14:31

    Владимир, ну чего мелочиться. До делайте до конца, то есть до E4J :) Подглядывать можно сюда https://github.com/s9tpepper/JSONTools