PK!ac%%phpunit.xml.distnu[ ./test/ src PK!3Zhh .gitignorenu[.idea /vendor/ composer.lock *.cache phpunit.xml .php-cs-fixer.php clover.xml src/AutoloadStub.php .env PK!H.build/composer-lt-7.2.jsonnu[{ "name": "halaxa/json-machine", "config": { "lock": false, "sort-packages": true }, "require": { "php": "<7.2" }, "require-dev": { "ext-json": "*", "phpunit/phpunit": "^5.0" }, "autoload": { "psr-4": {"JsonMachine\\": "src/"} }, "autoload-dev": { "psr-4": {"JsonMachineTest\\": "test/JsonMachineTest"} } } PK!36TTbuild/docker-run.shnuȯ#!/usr/bin/env sh set -e CONTAINER_NAME=$1 shift PROJECT_DIR=$1 shift docker run --rm \ --name "$CONTAINER_NAME" \ --volume "$PROJECT_DIR:/usr/src/json-machine" \ --volume "/tmp:/tmp" \ --workdir "/usr/src/json-machine" \ --user "$(id -u):$(id -g)" \ --env COMPOSER_CACHE_DIR=/tmp \ "$CONTAINER_NAME" \ /bin/bash -c "$@" PK!cbuild/composer-update.shnuȯ#!/usr/bin/env sh if [ $(php -r "echo PHP_VERSION_ID;") -lt 70200 ] then set -x COMPOSER=build/composer-lt-7.2.json composer --quiet update else set -x composer --quiet update fi PK!Mbuild/build-image.shnuȯ#!/usr/bin/env sh set -e PHP_MINOR=$1 PHP_VERSION=$( (wget -qO- "https://hub.docker.com/v2/repositories/library/php/tags?page_size=100&name=$PHP_MINOR" \ | grep -Po "[0-9]+\.[0-9]+\.[0-9]+(?=-)" \ || echo "$PHP_MINOR") \ | head -1 \ ) XDEBUG_VERSION=$2 FROM_IMAGE="php:$PHP_VERSION-cli-alpine" CONTAINER_NAME="json-machine-php-$PHP_VERSION" docker ps --all --format "{{.Names}}" | grep "$CONTAINER_NAME" && docker rm -f "$CONTAINER_NAME" >&2 echo "Building $CONTAINER_NAME from $FROM_IMAGE" printf " FROM $FROM_IMAGE RUN apk add --update \ autoconf \ g++ \ libtool \ make \ && wget http://pear.php.net/go-pear.phar && php go-pear.phar \ && pecl install xdebug-$XDEBUG_VERSION \ && docker-php-ext-enable xdebug \ && wget https://getcomposer.org/download/latest-stable/composer.phar -O /usr/local/bin/composer \ && chmod +x /usr/local/bin/composer " | docker build --quiet --tag "$CONTAINER_NAME" - > /dev/null echo "$CONTAINER_NAME" PK!g  build/update-changelog.phpnu[ ## $version - $releaseDate"; file_put_contents($changelogPath, str_replace($changelogMatch, $changelogReplace, $changelogContents)); PK!U-<-<- img/logo.pngnu[PNG  IHDRf݁+ pHYs*[ tEXtSoftwarewww.inkscape.org< IDATxyTSgǿI ʠ*.AU(ZO3GΨXa T uԺEq ";C$?8pInp眞{>x>> G.A lQ2b&A3@ eL ,(f@`D1 @ XQ2b&A3@ eL ,(f@`f-@d2ϔH$(//GMM ::: =N655A*x>>XrVn~ѾWr9.^%Kt=[zFp &I*:|>͛/jW:I .` 8rm<[nj-444믿ֆD\|Yk{}6umrZV׬YX _Dgg'aϞ=_={V+1FhhqFI "3 8y$)))P;N"7twwc ''',YK,3c҇ܽ{*ף X,޽{(**SLٳ1k,iyyy())Q{ϫDG`` F cSSSufCCCNײh3m4p8#H6",, ۷oWP@ ٳѣJ+Z|Xp!mƊ˥yAY Y@FFy___̟?2 (//Wŋ W^lԨQ;wҦ\ww7n޼tenGG\9sPΩ^yZ_6JKK~c$ikT{)Sxy+KR>}سg^bѣGLÇX̃L[[ʔΏ3/ʰ2[[[^2͔RF^aʕfff ºu(7sssƍhjjZ6)Ņ5ə"66V{ڵK)|||>_cXh]] yTLp87oe@ @XXyWWWS^C0sqq̀يJzm"Dck=tPZ~ܺuK42-[ZZ yI Ce-:99N􄣣#x<ƍ_QQQ*Rrc#005Di5ST蒛7D1idعsNJ ~)2:ϊ*@bd~VVVp8-[d(#G}?>O#򮻻[lN"6z7n=߯Ն^@@0tݰT]aÇjm*k^R~ Goӵz)SK://B$ hmm{ưa ~\.W˧Z SWGwBVWW~7ZCHgggTWWсHHRem/dCB\ ?Ց L,'Ld_zmmmzMЌ:Z1Zw,@ CI'ɐnrKdm033SʂRM`6t$ [ y|8qVIBSEլ*ԏlRl-)߽{?΂633I$Z Ѫtpqq`:bf&LP`ىc޽HIItδcAAAJVsvvDew^VLq!uEbf\. K?v;={ )) bؤ-i*vA;'N (zB i&Z HK@dd$PXXq|GG򐗗ggg<;v,˗/+deffbʔ)zň&PcԨQ3,--܌Ç5k>=m"B!^}Ub\v ݣīƏ?>>xؾ}"@ 'O6 q!ǫAc׮]裏 &!vD(b?~<?ÓJs69lڊ* SԩSouu5N>ӧO~cȐ!py駟ޞix___l߾]mԔRɓ'(**Byy9lllۅlؿR2S@Uuz W\aegic"ˑHkl[[i8+Qc:j& hl5k|L{iNM2w63'a&ÇqIݻZ!SSծ6KLF ѧ U[8;;c…:_HHFb+++lڴΜ9tU* xy[[[uuu7|[nA*[>>T:gmmtJoܸAw f6B_m6=^u+fܹfd.]tɮy~:L\+Whl{CUUo֖¾t*++iV]];O==^ޮc È#hH`5ri2bҥHLLDXXNs0ek22״itγ(f >>gϞűcǔ&{Q mii HDyFtxZJKKq1hkTWKOODž TF>TVV믿| c|UUR\._o^ll,ܴvѢEϠ|T0?˗/yu=ʐ=44Ȕ~!VTT>Ø1c +++{.; @_HHܹiގcǎG HP[[B5''' |}}6r9~W\v cƌ#܌r QK?-\vVȑ#fΜre D1!!!_ucŴ6}yTVVR^иbkk_~ǏW[@tx AJJ՟աO>>ƱEEE(**FفI_0c B>}Z3333,Zcƌ5`ٲeprr9CPPXv-T;m4 FK.jq^%T sDMMϩ2؊5 2[n5xw툈|_J̙ӊli 887nĘ1c4*!1c ::cǎ>-B@'"&&Rј9s&:Q~z̞=ۨmzfSMFb6UiH;w\+eofTi䄗^z (**BMM > :J]8X`JKKQZZf  ppp1|pvӧOGii)P[[vHRޔњ;wD```Ǝ.ѣGcFm)L*оv>!** IIILA 2 ;ThU\.<== cǺT8;;c֬Y55Fb,EV%V\?^ b&˖-FVMTolBBƈ }!>f~dee ,bS럧.֘{rddd`W hoo/|KFuhbR~fu(IOaV7$30/wMnFyyόW_}eD1*033ӹ@cc#=jp\./1Ġr0lWcY8zα<O) T0eL2R X,FAAJ@KDl߿w8֭[TUU xB_iz0fxyyzɑN`HRdee_Ҿ~z >߿@FFkpi)5}HNNG}s>ua͚56)))~q@ @hh(mD iӦ!22VB6 T_UVi=gCCL<;;;qa\Rm}}8֭ԩSUQ :˗/u=Xt)RRRtҩill)))o>эVDBc\.VXa07}̷oV\kR(;&77WG>rHvQԤPB9+JQVVsg\\9}㭷tILL%&ˑK.aӦMX`qΥ_~~~~jż{n/ヒ_~YO-[k-6Fv؁y5~O>}:5糄 3Swo22Ӵچ|GHNNwBp4tire$%%t]}}=\°4BP 7?ژ#G풸~:>ЫR̅J-?+n"N} }̙3Gkс}16Sرc̕tkbSNi}Mrr$!OT]P.\tvv^7n^׳!sZZVGnnQ26ls۹s`DV3г碯ewi% C%SMMMp?yJO.`?BnnnjH$Ϡj>|XU*0k,,XdZDkk^=˗#!!E_3///x{{cĈpuuEWW֮]mhho`ȑ3W_1:gDD^}؂IYYYR~loo7TayFxyyUe9aՐH$*笮FTTvލ`C@gg'vޭRQYY<ܼy*7̞ 7`Y&!99qL1󑑑X.XYYĉ1qDjܹsYYYœ'O`eegggL4 :3Fff&?~ >___NNN?~Ҹ۷ox}ݗ>|`Ç /D ܿUUUСC鉐{*:llyyyƮ]ԎkjjBLL "##eP~y\.;v쀟wSyyyhjjŦM^PbzׯWw+**;w|Vرc(?3{=ҚܹsUY/;yL6p>55͛b קJ0w|rlݺ{EBBRT\\"""Ӫ%skkkxW)l2?q2 _5Ο?w}&MbTvرc/6nH /QQQKKKVg4jImD///GNNʱe.ӧӾOAA֮]R)R]]?O8_|[3 n޼gϞ%kKK 6nܨ{H$O4Օ۷￧La{n>|_U*eoݻw/>d~xiobDGG7@ffu9zillD\\.\#GR/2֭[Gk~HT)e U:;; L8v]N|hmm-Wll,n޼__|d2>ck ݻq{FF8III}/^&PbbIa \.;w*.''111Xh<ׯ^qVWW#%%ӟ0o<:tvFߊ+;ЖiŤ\X,غu+]p} ,ULΞ=Gs8,]+VaPSS/СC}_+ IDATTTcǎ土ҹcbprrBMM ~Lm|ll,Νxŧ333رvvv8q*++7o@ '<<< P@f >DEEq8DEEaÆ Z_4bR/^O?@Ozӧvb1B!fΜI[1#::/_Ɲ;wqFe;VZ|}/]Dv],+mt,Z6.] 6(XޙP(D\\<==-^ؼyRƍ*fǕ1pΥKbx½#ž}| kWm 7oD``  ֭[]v֠B"޽{&=?oߎ3g2:)cR  S5ΨիWgϞpݱ~z9rXf p ]]]R7. ;(E^aӦM[[[̝;V(9sr/ӦMS_ )СCK/)Sr`Phh(Ɠ ҥKZJLzVqqq@{{{=z(bvpp@HHH۷\@OF+q_ZU.yk455111zBDxxuuuhnnV \**3]Ѝ7ҽwMZǍK/=}ƍ/:,_v;Zcذaez1Y XT2PsM^HVZѣG+8wxڳβeht 4$VC ߧ111' ɔI+ٳgcϞ=}I̚5Kb?VR? ן_0vl <+00ʁ.Y)))e']yQI" 1{l1vsV20nKYN}>c*![l\.۶m&?gph=s}0i G$L6:99)s}ttt`Ϟ=~f" Tշҙ077W2:ƢָM6UۛV*GMࡨ&wO\Ǐks`X,Vv̙Es;hHmx{{+'%%l2_BNN WWW 34+W8&((HghΝ2?=0y ѣGqQoh*fchЦOPSC&wQ*}755)T{Z={‹oKO.ٳÇqQz% ᜃƍqAPZ~|>۶mcdK Wmۦ˄iԕEӳfE$1oL&[o0R(*V^CB$JRϟ6D"Ls׮]CDD<==aoorNǦ;wN˗/GlL)S8y$N:zp\lٲQ3uT,]/ 3f+F16---rqڶKӅB13+PԾKeA~6mڄ?ZoKUk=<N8]>qD)=+iӦ5`R)7D,B!lll)daa08D333_BBBB,--l2曈-{|vʳʗӈL&Ã6UouU>o3眮wIWM`5 <%ֽ$֥IL4^Ҷ6ɋ<\kL24͖~$ɶZI;=3Em$)i~$IIrp$v;zG `joHg;;66RI~侩$3]I*c_<,@%\>n{T$9<<~#X:~Rk4jdɒ\%9D=j_$/_`IoIHSJaͫV,̺Rʋ>Ǐl; =k8fj'*MV $[`.yzO?+\ K.+Vn4=L[)MӜ:::+Iޘ[nXJ$J)n9̘0mhh%nI\k]vX$RnM$Rۍӣ .ٸqⱱS><{KlL)l{/~C*,JryIr~xjՏ>J)ZdW\Pۡ`W I/ ,<˒ayb`N~F)$' вkJrQm_ƷW$Xd߶1Gvݿ+|dҥXbKہ`Gf$YvI6%8g ݺuCCC/o; Ht:o~G׶`xF۽/Iv [tVZJrPaŵuϯ_~Ya X6lذZ_ 0Ǐyxxm P)e~INl;  '&&~hhmaaX`J)')n{okQ /G}$v?ɽS?:~>u$#Mӌ ؉i(S;>IHrHs rfv Uno; OvOvYvD$i'A4?v/ Rʢ$Ly}y%I^]O/ ,, @)e 璼,ckzR_'?ɷs9[ۍE###7o>NW$9&A-zR!OKO]4Ͳ${ZkZIIpRk5UJY288Z듻$&ٷiI>ŋ=Z d͚5>99$e2I֖Rj9 L[uiz%kRsԺuڶmۻզ4 W,R^{ߵbŊ-iO)4ZJog'yVv߃I~Rg3\WJ9$}L$/L܍a~cxG |ƛλ'R^<00=yD2<݋n$_ 5R9$O 8 SGnFGG,\4~sΙ3v݋SO4m{<Pk$-Щ\\k!ǒ|i{{I-|"昖$T `XJ)oMoz 9c͚5199yZz7vJrIzW뛻7n\ZӛO&G/NRM frQf.hqk׮=tw&YB$Zksx(rrzI*Lg ) ``Z~R5I.N<`ZgK)fGÓܚd4fӯ%˜WJq*쌌d˖Yy Z륵%1 ȷX'Anݺ$bge6RJk7q;$NgͶWnݺ m>3NcF}m۶?N>IN+lS=LO}c9r駧dxx8gqF?\qv2g4O?ZNrSO=5{nn}ݹ|%sf͚5ټyV^ZSfc0v])e<ɛ<Dzg 00glܸqmۮL򜶳̢W$dK) r{+\>ԭ^vZk?g^^J2LI[''pB.w}?6l{] 4Gj{%xvJýޛ386wsWOik6۶mI'ֺLaRj> s׼cx[>xO4ot:ӒM8c9i1 /ڇ5k<7T-ZOxZQh:oٲ%W\qEN:餜uY399n|ޕe:ZI^:/l?~Yzu=\r%{eIt/fjާrlݺuyj@hs$s)?FFF'&&>~Zy_YQn },ϟvW驵$7oκurqeÆ я~?OnەS?Xk=` Q9Iy6m׾ix衇ree9묳rR}Z:L[)e"ɻX,@J)nyb߫X611~}wVK\|_Ζt:n;O\#K,٥̺5I'o۶-6m駟O>9W^yen:n7^xa&&&vvI.vhzKV?~8Їf\[nʕ+||S<;C|ֺlA-/?<m)"C\0ɮΝ;^h.LNl]ZnqAAw.ET ьݼy3fϞ#F}dIWŋz9,;jO4nmFqF̙3QRR*i\bk1ƭ;c=rYLw٥"ҩFW7z|BG퓈j9]U/p8:P`[ %c̫>|RSSt޽:///8cƌ;w|jUWs(O#"N7t,"z\Z˭ؕ~(1|9dܹ8q65^U=SXfj.]X:p?ZD\RRrl(:>0"RA'N `'Ph`os:@ l[ :%v]Z~HO8d(//ٳ# &~QO[ȳZ?R]]o .DnݚGU{cV9~_+"O"?p0نG@iK IDATTr˭6Ɯ!"D6cL1KUkOPcy"_Dl۔EZ`EpuJ:ǹ?\Z81/+-_?|4CUΦ[TDwR7iѢEXֽ%vڅ3gF3ԭsڃ1ƭ{cת;EdI" +1 P` Z1XD.)((Ύz?%"!|jJ^("ml"eeeua)܅uA~zYƭ,?o1gΜhRUw:O7iQ[kǏQU|ᇑ帑~l[TVCCÑnCɏ `"""""""򌪞kCEF{^56G۴mw%6KmO"I\1f uv(9444viOe5uֹgaŊ CU;goT;7t!UWGjn}?$0y$?0.4Vu>6a"%v|>߹vY5]tVIICPSC]ZjKϵj>w/B!! BᣨwqKq}q7!s|KPcPȕ&[LDDDDDDD^iganCnn%Qnn_7m4DdC1z[#xږ$HHD  e+RXDUkX~Y͚5  B۶MnLoZa7o1D1,sƘ].-ն"%///zx4yʑ_7޶$1j^ 6 gEƽ},#gڴiM.n^`Mwhݺ[Yi˖-3fHMME~oY{ׯO:%>6+~c[ 1K5`1,9BDH z}ޖ6`0h!T^pd8%%}q+K,Y+VxI=^kzVZE܍,佺:626+Vv1""r ;b-)ciG#6cD!(!yM cC'ر񃣎:*Ґ 59$ L`"""""""J!RX$"ux@' %vJ5`c^c@3ԪNטxiիCK.:yزe1~şƘBޫt;Tfff)ݭXSu"[MY<`|}݇ 9#|Xt1~_"jI@DDDDDDDD`ϸK{>j&+Gǃ^ d8++ ݺus+KDwFyy1~ QVVP(n;FQDDDDDDD䕠)H`Pjx-Al [,O b\+"E+Q[ <"vXz{"X|9^x~Ѐ͛7cP__;vعs' Ґ #33iiih߾=tN:!5umx~1b/05DDDDDDD+vH``js-AlJxjE1p&1lQOט`ɒ%.oG^uVlذ7nƍalڴ 7oF0h幰0χΝ;#77]t-777o 7/1BDɃ `"""""""pR$0ϗiK믶۬jL[PUOpH ӯ)++0&"Ͻ&t1Jtlق#G:N(¦Mi&{g:*KD;+V㏉e0=Yuuu7ؙF2ſlrHgx35DDDDDDDj sS]A;1NNDrlCD!bC0{ecM05DDDDDDD c̷KiK"7.lۙ|Ԁ(8şS5\BDI `"""""""򄪮XrieK"p.Eq(cưszt# %6>%:Ύ,DN yND?zB֭B1HOOG>}" D6۷_b%%%}C$o(//okc"J|M6SRRз/<Ƴ޽{#===ҰDDDDDDDZZ, &"]D@DPiH\BDI `"""""""68?KKKcTuu1NIDx+1u7 Ec7Qaw oSmGd;pk;cٙ^yGy䑑-w# %'~'"""""""ψțVT("HM5l;15kV(;۴i=zk׮СCa+BDɉ `"""""""cX @ p#eoƍW<TWW_ag"JLz Y,U=,D&"""""""Oedd `e;ꍍ;OD9L9+a6!"|sHcOQ -aY&"""""""O|gC##5XD "9QT,>x֭,*9lǧfTU>CDQc<סC>qx>7ݾD{e.BI73UmQ6gޫ cy4<3B͙rUC(ɰLDDDDDDDϯ|wi>PY-"3EDk% c.3/UmID;g׮](,,Č3΅̛7W_}5lҜi'xMUv(%Bqq5Dd~ l?P X6N9Uӣn:\vex F{=z4xL _ˡXD$&""""""!"w6% 6(}"R å-|WizyGNNμDyT`DINU3x vXƘm1CDn&ܹөHԩ O>ѣbs̎;zjTVV>+e˖X㻦(..ƈ# YU=(LDDDDDDDqOD%"\ w(KU\/zC~~~m UUaQ"+y`DIJU{!|ޜǿT,1fMYR<`H4_yL<uuQVp1NAp~Œ6m`8p .ucxw;͔TӍ1GDq `"""""""J"`Nyy555cKOU}ADJD$̤(<ݟ?!~5IULp fLvucM^cbuD3g  ư;qNÐ!Cкuk#H׮]ѵkW=x뭷l2ؽ{~$ aƌPUwf`U6co1_﷏wEAAA5SRR0l0\x۷q,iӦ F#GbŊXx1^~eBoUU#-- g}v4SnQƘD\ޔ)S6EdÌ17 |,چ `jR~~~]'S##jYT5#lᇒ窺^UQ<U"tx'PVVߟ߿?n6<8󐖖u$*MW^y%BU%K%)"'/8E:e,sU8D MU}7_8]# ϐ]v9,9r$x |:: 'ѣiP(B| 𨪶q8TRcTPU`9 `"""""""JZSL$".@G KJJNhmJ8}HIIIv!JpcuXlٲm6N>d}:/_3gbݺu䨮Ƅ pBdggG~ ?~Ͼ:4#fwo|T[Νwy55_3g4Vc0zh,\(-˜1cl} Z/K/A kn~/3zGl텈lwӧ y"暔drrrL n;Q&Ҁ?ϨΝ;|rs=1d\r%۱l2|7Qٵk&N]vYvᮻM7#)==Ǐ̙3Ѷm[/--ƍ]_ׂ67:Qb yyyAy(;;?6ȦZk1c~5QPlj̦MkΜ@ QYY(((g3<xᇱrʟ:VUb7 -Oƃ>?uQPP]dX)^!JFlEa„ 5"2{Um*[PС\ĸ ,333gرM~*n…O_gXbq̘1^{-~_aСm֜77~EEDDDDDDDLX kcquCs ̩>w_h*D:YlmR0qD5(IcǢuָ;Ѐ@ x?555ϰzvZl޼ysvZ,Yƍ_i>r`"DDDDDDDD1E7Y,lS"Rsšk# xG|>nuY^Gi."dee[ouO>>`4?ǚ5kf]k֬ƍcn\A}ꪫ" lي `"""""""eggUWW5' IDATHrrrN .s QP՞55fժU裏\J|>NÇ{E9sq iX  Aq0iii ЄvfqDcE9Um `LScoߎ.%/L<g}QZ7ؾ};f͚Z0qD׉K/;w"++aɥHDI;,2efVV>OQ ҩSbg8r!M xꩧPSSRrUWaȑ^ \p.bcJaUPlYm B-\cʼnqMSUdĕÇ㪫:!??gq1\{ υ(D-DDDDDDDDm2 `6+Nz<Ԙ~_|KG\wgAQQ87w}ind!j &"""""""SeD-APlajFCCC|\ Ids# xGWrrr0}ty"##3f@M\4/Fvv#QcȺ]8q%,55j& "LqUӦMlg"w~xWRRR0}tt(Ԅ8SN2Z5Q p0-Zw"""""""VXb-A@NN.7ٝCMO# %.\y8製AQ8p%x>H&DDDDDDDD * Ymv%[L8??]zFύl̘1ի1x# ` qõiii|,cPK6mZ@ 0 H'7sDe-5555GZV&c|<6Ɣ{<փ< cL5"<qg;><9`ҤI0&7Fy />o[k[ `""""""bxa"H}}:UQr4F2j sWZ\4m3cƌ,'ZV? ]ZlMSN-{guVm)_2z[f V^i)ndV}}kWL|x;DDDDDDD-1V&gee9WYY"r|Hx=꽵V,=|ܹs+u殱+O".KD{9[^eMa8㪬,\{^ ?Z:-k~ONt :&""""""Jr"@? %N8={cDdw}Q >ܡX|I}<0υ^c1lu '`РA"(ɰLDDDDDDE]]ݻqXm6 b7 !T/ެD&"BΖ]"R udBgJKKV$FDfĆZ ` uwStqkM1F\ZǎQVVٳgK>1϶c8g~P%^ۈ/{v1wU+N:zkP(@>Nby@u͙۪U+?/F;O:hȐ!h߾1bv'&L@VQZGcvsysIIWeP 0JJJy@^JD.Hu{^Xk{"M~DD^e$\5k֬L/ 7l*ضA^^^3îz7'"yՏiII+ϯ """"""J`@`L(@fLJ [UHDDᦝ]ܥS.eU< EE***RVZOUQ Ups.ءCUUUƲ: "OcPDI)P88pܹ1F~I>]ӧ۽{wӷm۶}5mgY\ `z*NNO?Çvp9\s56li֏1m6AF7;KKKO+**u)h"""""""̚5+.WTr4lP:pv@@~ƷT|ZU5[Uc:jG5K=whJDFƻ'ӷ ~&l#|쪭GſDd8GDj#ܛN...~H&">&{gsMuMD&ycT5?8l˗/Gyy96l`o(pxꩧT$ pg<ձcG;g}vK1v[ 8WUWBD`mJ0LDDDDDDdQ]](6Uׯ^/L lpg5 Q?N4@Do^C󄻗DgFUap: z/{l<,XzGrI'o p''oC~ff&pWu똟+O/qOVVV6P!"weff2iҤx'&""""""HDFx69n,TTT7֊#x5^&"+xpb_XsM:Dey Q}4cb߿#fEgРA^GT5 K.cmp1x G555 ~i&""""""n#4Cs,ؑr8zgl4ƌA}@!}8'"'3pQLp}Wcv56Af޽;ΝBdggp8c]qʶ2`СIk7mmc lYi_:L03ȦM",'E~/"rbAeV.boE48؋ .*"E+1nm>oDqqW^IDWWW:KK7mڴc`DCCC?3^p1&U}oGr}o֧0p@TVla뮻r2[>c1f=ɼ 8 tp3Ed=gvQQj$0lDD&HoSRR*((Ϋ"ycx!bUWh `۩#$ &rUYdgddֶ:B1袪л@o6 6c0ZU@n۸Sݼk׀C^ǎ1vX}V `2cVy/%%`06^gA !"j1_4222ɓ#"`Ai;EEE,"8*v=tM(zXUo00q0?ux`y@?wCE~zGsL3K,ȑ#cm*G5LL/4\u#|S{_kjkkjkkDw1NlKE&cLDt;ED^:x,cN\`YbqJJ/:H2hwӾs!1#A6lp,GǎX}J;wFvvcSSS1j(,]ǏGVVV^p1&ƈq?yhOl9|{5غA{DƘ[>60|NzdO"r1f8rO *** Ʉi0*?? qj"\dw6%=z8RwСI1^'P`!ů`Y~"ά_r-_xf_D$;6cF"0Ɇi/|:ıC" pܭ[TS ѽ{wkcp뭷k׮XQgy޾dADY6UXXg `y|N&N0(**ny=iiiHф jԔoEd !8Ξv-337$3=| `Xqql*6mrKhrS.]lؾ}{,Spc}ƘTj oz `"""""""s4+|A\{LqqqB(1#uFܑWD2eJB5DU9oǴ n"2iҤI1M.`j o~-jk;=ۉ%.nZaiI+;lWPP3,Բ5yg'///`>@TU[CK"(G(G~Yϖ G0 ;>+" .y"eF8e_]ɨm۶UWWOu򴴴uh۶mstq6iON}>DX<֑ ND͞=Ԫ\uj&"""""" K"m9]UG81k`(",SEEEPLDzcNQ شTƘW1Bw7owA([ƘTbfHeb(,0aBM JUOa:w&2eJAH޹cוwXo?GF?.6ƼH$_ `.GƮ Mֹ/>zvo;l-*g_N"@cSծ:#[6ށ!cz;/nVD??eeea>>7[!9v_cVrss?7n\7鉜'"ucnx$Ѩ]_{5x㍎o㍎V__A^ Çoj_1پp+)) u(fN;Ȕ)S6#|?ΒDdw yMPZZڄƟ3|Mѹ1i׮#iiihݺ5v(vgۺ` Q\\ٳg @י(LDDDDDDDDDk γ#MM67xɯڷ#`C/#EEEEȩ~ _^Dyx?L({!I 1?qAU XwA*(nuA*nj/ZwiUZB&$Q}SV}K2ܿ?-*3yI\W2K2Is!"""""""cܥ/7.w4O"o:^ B3?~ :LO"""""""""o<7; 9 `6q} &eYArǫF0wreOF_0' BPG˲.0lC&""""""""" Bп øyرsEu`M4પwjX&"""""""""jXN,$ M`7 IDATy睋l|%s2ilӾTUU9fJ; BRUU5 @SPO%""""""""""h4o B T7c=ؼys Y)7luh#3~sp3%LDDDDDDDDDp5QI VZVx˖-MeYؾ}&F> ڄBTu>6>@o ~6zpuu5vTlݺe9n;c~о?L ; 5l5:)  xhT?bq 7F֮-Z-JlUM6BTHU}'rbun:WקeڵyGB`/|'Q ?'NwTTu*2HdNvv6 ýW^TY5ڴioF 7@P({ӦM,xLDDDDDDDDD8FL8AR6WE$:0DvO&ͭT?n۶- [xx76C?C~fh:n6/_T|h߾}"! ex~g!bq; PA-:UW]UV9oo/vmmx>$xC~!9gPNUp?c]cݺu6m|MX\}{nde%9;v``xN0JD68?~ͪz9~0QMZDdj6QP]Sa׎7EU{uإeL:: eYXx1I=J] ,H *" #L0Ȳ~HvK|GEd#nǯp,;Qcp=ܳgO}臚p*4 رc~q`KՈx饗bǎKѣq{^RSO='|u{9YD9 p(NOOw;uRwy[o݊P(UqjbY^{2dH0u ZIlYy<ذSo&n4ԗJU~wngS{:%^2 =˲B!wϦDDDDDDDDDD)`ĉ=8Bi>gHf v&{ K.u,[8|060SDpI'aΝK._~9222Yb)EdRV(:@1,1eرNa(L4પSB!:#U= 3]cxGQRR\0$yL/5+]Ѿ}{z(**JvwPs,jcP(>|*^ o}@)o""""""""""J!cƌ! ħS_ 1cK/M/|~G y_z5;6mJf3|Wj݄8~DDBa(y睋CI"rj" B~@MUop4sn݊{ C ̙3~~G y{{.^yXd$UHOUo͚57nG>էz0ae ǥ/ B/y\6Uvٳ/}YWUu=PV\s=חڹ3f :wj("M揉'vF&KKD7ndkR=DDDDDDDDDD)nر6 D=.=zjvX_ vMpyבɎFWZTDKvp0Q=1a„B˲ W5@TgRU=9~)q,[̽`.ٳ'ycdž :[oG^D93P(\=nܸ5 LDDDDDDDDDTO;v\eh4G/tl~7+q 7D#-Y~--Zw 7|2Fw܁M6%ğ|ca46)l#ƍ{ a-n7`ǎ?~<R/27;y(?| 3f@UNo`>.s0zԀLDDDDDDDDDT\ңZfJ g1;wğg4Rᅬ={C|嗸k0pD `Ux1c~5P@DDDDDԈe^w""ot BoX6ݿ?Yt)>#c }]Xu}vL4 #F2BUSyW -[bnzbӦMxgbTTTVEjoQ9~1SLDDDDDD"t;Q QTkϊSO(mۆ)ScΝ~pͿýaWVVV3Լ! ^06Rh4zRZ@KEO979sAu0g~pw}oH /rDWa-j&""""""蛟STTZDKDL5k׮ܹG{PUQ}{nL8w5;XD`"rB~ ""jRW 8ӼȑV\ɓ'c͚5~p]N IB*~#/>a'ww"""JI_Xk<^z%|~ K̘18 YA  `""""""IU=o;QsaW^%bĉشiQ(6lqݻwOkԋ,D DDDDDD30D\c"ӟDDrz9Rƍ1fDQP H~;6owO <8ao1H;UiZ@QQQVUU~ jȾZ(Y"2WU豿1 #<˲c|WIeIƺu0|pL<ݺuncpB5*[}>;ް~ƅ `""""""ڟMzYO>meM"""j0^p7^eѩS'L:cǎ7|smۢsڵ+:u.]SNr$;y睇;v8f"6mڄ#FD^^gub|͞}n O˲<Oq4s<0X/_ ˚DDD0U `tq;v3=eMkQj.`fmaa! IYgYfO?nuu5ƍ+WbĈ? EQD"6m~_ a+=:ӲeKwq8 ͛Z{x }FDv)8DDDDDDOfee&Y$++}/\ +WPV0h :M1\  j&"""""Dd(\~Ʋag U="{Ui E䖒8\#ieFZMӜkiTUUM.u\7L8^^VVx m$ "-갎80MӜ\]]}[EENB&+??L jZ0FxFFƤzvQDd TC NZ7TuwQ=gn6|w;wqxb 6 \p 4mԷ,fΝ2e Oߟs1>|g?p~8묳tRڵ x wȮߟ<%")Q}/eDDDDDD0yyyGx~~~OItBlfG4?Rrf0|?77Z1feY%_g8ED0Mg\.1Mkwo'pAA`4CD5u פ~QGwhM{`0?y ߽5pcUUՂS]ADYLl| # i+"'h|omXzz:>o۷!5WS<8s0c ߚ²,{8ꫯر#&NӲeKcEϞ=4шlQ "r)j&sQBgT@ 04MO 7|`4_s۷YXXxuii[[a0VAAAE,J䦥}QXX+ FGz`y/ 5x`/<%"Ed,NG͎P `% p("NjU"2EDfȏIF58fl߾} \.DUy.שrrr[N&ux$YXXr߫f-CU,[l*!5p"KDkE9\D2EtcEJyDDtJD"8;0<#)s1rH\y~ٳ1l0= .5KV0ydk. 6fAsDDDDDD6uTL; IDATm>SU9!gx@QDdZ~~i^MͽG70MӷWk}£rT~9}e}c4i5GzF"'(#/  wvQ-Y\ ]vF #iӦxGѩSDop|$"șy`+Qo%ݧqMF `"""""FLUVUtIxMU}&"/H;Kt"`0OD^G>Ǔc:=W qٮ{)% {\, {˨Wze~~>_'r|ω۷/|j… 1vX 8O?46lw:Yn3_oH GD:DdDDDDDDHca\TːxYfqv84n, "eyX"=ߍ'u0ݸ~'2=8˲cر0`n&)Pj3f 7܀SO=P_},;/4k =;D<dnTD&PsltoT E룦sT$5Mg$8e({Qs\]Mtp*ܵk&"j%C8q999S͛WZ`0WU:GWWW_IʻWPPp9sRNlz\cfU59 'w 5V"bEZ81>'Ou]m۶0IUUU5kf͚ҥ LQGwNy0'Y 7= @] ` fZD}s) `""""""NDM?KM4K='jLU=,o+hӹs}FO05w&ЬLKrs-TGUaF+Um(8eU5,MvF.'e k5 `~`c h'y ,))qRHUeȇhtFmD t$szL׮]YdIe2Q"Ro|>}OcԨQ)v.]K^xڵ+uΝ;}h׮9IF~zYWҥKd,Y6mrvYxxy#n"LDDDDDDwPbܭS6VRǿa\_RR0ָEx2 Of7z#P)}jVD1 cfqq>ׯEee@U5d0M󘺾9x0l?sǎǩ:6`>={} ۷eR2RLUf'Tz^ӷٳ_ wᇵlrSID z:OPƕ:wSn~G㏘={/<e˖hժZl,4mZtTWWv܉ݻwc֭غu+lْrG8'sx~U 6p4ܐx3z޼y; 9 L'fG"mNS7%%%N䙦197H$ߤ&"tӧGc /nmBr[usm"wW{?3??WPsb2ӧOIί\82WL<5;œ{"j"3˲H2__H\VVUiM߅~&򄈬W>o|v0uTyz>/DQlڴuq#;;;)ADR,p"S7Q='"޼I&.@ fϞ}zAG !@v|tN"үx_JJJe6'v][RRr}ﯕ(Mnz 8:ɹn˲~牆PMPп$4t$_89LQsi[#" M6ߏ/ɽfFzNw!Dd_Q0j,Nt Czwsqq񏥥KKKZ۠|>w\p8u[y=Z;//vILN=/+=jw{pd'WTTl,[oFew7͝;d'ٵp'Ν;l퟈mmNF gDdk}7 G}݇-ZպukL<C/p|b4"&"""""|lnnn7 r'8|[FFvkmKKK8HUO SgeY7:X)lG IWg\'[n 5[@ W ڒym];wJIRs=o(--dZU{ ƕkm@9zF |/s '_Fܴ_ >, L?6ho/؝X wGQ;_ꫯp8\]ZZq8)03kt.U}niץĴRYiɒ%"r)^݃7:۽{%1yIkaܟ?[huU4̓OD-N~sڶm'|{9L::l-_" `""""""Y8 `) <8g6OK̙ȧ6du,:U}@f2U}\UqP8>4d1 y֚7o^LKb#-͛7ө̙S`yzH]v~~֬Y[vvlc8֩D8qRKFFnF}ݼ8M0cƌAzMp#(5LDDDDDDvP/_~Jwq6 _>򰡋D"sΩN^t9-ڤIGûDn3w)--w8#vrh4qQq9N6AkQ{fff]YfANH2DZD=z@ՑCAݺٺE0"u'54i~ """""RZZ:4HhYUď%ssp׆ fx0Z$[o۶mG#+:yoe0?gϞ M>a322D" M̐D!PĜ:}U)kl#"&:޲,ۧD^U]///w?V^^hx E#QDDDDDDkf߾}7o߾T5;/-˲V͛7 Ƨ6O;ǥJKK_,--R 65q:G*SdI)O"Gf]jsRoDx'>&:7`ql?/e󗈒0eY"D FC;w6&Uv@vXUo*˲bK\vdjNwk4;jsj+ `˲ٜr9>p{ql?/H&?ѾLDDDDDDQ|Ɣ(5 ő4lf>aUXU(\[Ki0 7.H)>Y9UmWPPٍ- /"yMԐl/XUUU^e$DQ̟?`00LDIaǞ\XXaiLp\/tFDbCD >ZCDQUAx BUMj2Q;($$F/_npx)C~~T˲`󗈈ܕu~H QD! `"""""")oTշ>}4p^D$* D"}S zPU_{꠼ec66(.yyyyǥ ݪz|1x@~~c:@KQ!j*lڴɫ,T;wҥqal㻓(Ν;lٲ'20a(1#oM*dBDDDR̆`*W_}͛7իƍ?`سgO5p!Aۣ}СڷoÇOdǭѭ[XC&"&"""""O5M9%8<M]>lxUe󗈈<t5&W]u=P@vv6z1eaƍزe "v؁H$]vhҤ 4iM"-- -Z@ZZZl:CO۷oA^sr 4(֐CUYQ$"j&"""""Dg$6^ݩSKJJL`0OUAʼȑݻ΋;0 imڴq-ȑ#1k,lذv%ذ`I!DDDDDD>wz]yvQu$Yъ޲e VZU 1c?5k믿f͚D<laϸs.e˖] DDDԨ WY:pG ~RZQQoHj&#(a333‰D䫒NuP`lRIq-: ""zHUJ?tA1b1nÐ!Ck.9O5HUm)"[DDDDDDDDyϟ."ZYqU0Sռ$/R{o40Q#"H9@DB$3r 7ow}j۶-G}(PP ?6y =HUqLr.--v25."R:gΜRN'`UUϟUN81]xh"`…صk6mk1`zx0PcEeee[SN+0DD~ OļwoԴiSvm ҥ{jۯ@ n $b޼y/dRԋsrr2=y^F""}art~#F@۶m 8QDJT ]P-d=vy@5j"^UyL޽q9_OrRi"}FxT: Lu0%9EWԩ̒#iӦ,˲cpx+!+,Eg<9ZDZxQV$pz?O }\DJ@D֣y? E5Uƍ9| ~G'ۖ\8q²,/JD kQ;DӧG˲ٜzΕ02 GDnd!jlT B ';Uuhrc=*>o@|N"(1ߔעE xٿ 4 BeYWoQj&"""""3uY$"lN_7nl`f(1"bkkQաߊUVaf1x`+׊v,0>޸SN9eرK.7YDī?xT86() (D$B$WjS:RU/̲L__ɭ,0*Jα\xW9M6v_qGr- ͛7/ސ^R.ZQj&"""""̛7JU_;ϲMM }0l'*"G8(**j]XXD>Yig-RzNAOoiodC~tMh֬Ya;\A_j|cބڏ5kڋOWϩcf4S^q%KDVUV/Y>i:@UUH4?#LlF"-9>4M[oF%=7!}&բ_y5ÁU8p'&2tADx."tFJOO7^S0%UxU}G739MUx0l)N kl9о}q7)2O4ͯLӼ% zbDn8CUs:i-sssc+JC=A-[zP~kC:(dddxgYYY:.lj6b HKKuҭ[oɶ'gԾT59D`DD~ֵ An߿&n?jIH$g'3_h4盦yw0 <#yqWDs!%@`i LӼP\6dȐ(oxQ~8փ-[iU1=# `D<_" /egg(06}yc=Vf<^N DDDDDDT'hAD>p; ޅ`[nƺH2MEEEOF:4t>*KlN듟S0peYo߾4rss[9U<Eوs5\mz޽;?DNOq GG֭n+@&qOXY<_&GLDDDDDDuRVVxTp8\A$G;UX7s, IDAT4 ,׌GD>NbZip`'n^^^/؜0 _w%v'd4r.P՗i~l <*^£:PGXc8L2Gqy ߎ@ {g$y1;;wQzVP. "ZM&,1RH+z\zDc+X. ^[K[kkVՂJ ҄)-b*$ycbWdf'gwe%yfdf{3RUzU;]6ɗjX8 `f'Yz\]4߾r/ 7S122ruŋ/I^;-֭.8ڴIvI>x^pMyI~16;}###jK!xs;R4E ?CYc3v 7ct&`0X37v… 5K1ޓdG@\/k-e-z.r5\39/x <>>$ϟn$z7` Z`]IvLLLq~ӭ{A|0HiZk[r*CtkL!{_ [nyސ׿k<+WLk::-Z`$ӞZ[}7NH|:![oC$'eszva袋rWO0x;ҴIeJ)3}˽;'tҬzYdI.\~9cbwZ5C*wӹ!b`0lZK$}z_0+yJ6YgMjE&w ﺅKSJhr/RLRʏ[k Zk{RwC>D)eak1113lv#xe{|R]J7 vKR!I^P{Fzn O)F{?|SOݭlR)e#ܧ5v}|g?+V[ov%Ke/{T.]VJƴ ц$9ySםp g>[Tg=k^oя9?L[T$T!a QJtISsttInh}}v_[711kzk=$/O7l~frL$ʍc)W^zl,pM7}sZ.G7֦2OrMLLl-s=w{w' 3[INrto~sQG娣ʚ5kbŊ\}Ynfw}lٲ)O7[GJ$80y9sI'M$tA9󒗼$##[OL+aj&9z%?6Zl ۯH,C&w$RVRme4jvxwowc u矗slJ)壎~t8S'ξ 9zL* 6 lnAl8zWJ'電>7sNꪜqyӞ6XKR!0ɁC f#` FFF5 ~-:osV^?J2ْӴvJ&wG-ZR)]gy\ήC0uUUIRw}%9iHH)ZkW%Y`WOOrΖܱ&%&fxSZk.];3k׮^{핝wyr$HrZ) 1ɯ va?b=vp Krv$gw>5ɕ]fxg[3QJy[)ڮsl~hFJ)r7ZkA)RI&$? ɉ{;=c%Ҁ$ɾg=Y$*ivzd?rK:Gn0E~$AZ{G:z5׌?%b96Z`sL|,Z;.v?*p!]Vk=w5H)Rʛ<%)I9Sty)sC7ޟwoK7C6DZ;9s۠~~klիWp޼ynE׈hտ7zЅ666v|dr|?ob~(:UU˗/_Q}~F);eIMrLkf7^J9{njRYZ$g$ٻF/E?%u( \sNwڕl%AUI&eo'%S|,0 ksP.c]otttJ)O/\kə|# I%I)oRniz,ݫ[kRvmZ۱C&wNkߒN;t5\3ixtA֞Z{N}J);֞$Kro)M8ysZvxߔdYSQhCZM$dgrgK#.JR3y<}~]O1zI7wc$$e? 0%2MI^m^tꩧn#^xl>Cc]gIVR.u]fK81INHG➑ѥKXؘ06$d<%:ɕ[wO\~IHgי$~ l;4˗/ _JcY6{IVRV###OLL|j>q]guI$IrW7zz&I)'Z*,jOٵK K)۷֞IJ$9Cu?խO&١<Ӱ]7`0kgoZk]es-[K'z]`-[W&,1[˖-{WAvil-[^ ̭Ke˖NAضil;͛8ɪ<ݝZe] `mع]v$vQjek06Ozb)$tQ%@%;? @dٲe+,NrMQvw$9zZu]a`cZk/Mrbv`+3;kv0?Z/n] |>h3<]G2l>&9$NrDǑpSsjWuBMޜZIj_JR: 0nNr~Ti0%UI^y image/svg+xml { JSON Machine PK!..img/github.pngnu[PNG  IHDR^Z6 pHYs6tEXtSoftwarewww.inkscape.org< IDATx{TSW?o M.*Ro(^bTLNmk::ڱuڋZUNREl ""Et?Y$'pZ dl\.BoBl(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(B(BLBܿhhh@cc# J?<%66>1,--aeeppp@ wSFTTT_Eqq1jjj STǏCc,-- =|7S'x ayyyƒ(,,&N~nZx aɨwS FNNKKK}7^Bhii?[n)%Ll!""FwPV!*#66n?؈Ǐ#33SMQ"k djhh@ll,z H }ځ~D"Akk+ZZZPSS*i)$$O?tT{x 顭 BMM E"Əqͭ[GQTT~ۇ-c/F(=U`GGܹ듺2 B6lƌn䧩 EEE(--EMM =zNNN򂏏BVAEEcAAAZG,ʕ+ "00Pdggk<9s&yh驢.\@yyc---kƆGAMM ߯?OVHOOGSSk:::"$$O=Jquk<7oӧO@ٳg#88Xѣ bݺuZ5~m ((o8FoM6 ƍS{\.Ǎ7xjjx9/|FFڊoT*ŷ~˺SVVVW=2L1gφO-RO,Xt)6oތ+W 999ؾ}3:u :.\W^y+Wę3g g01Q<iGjkkU~:ӦMlmmajjz"--1p%&&b*' Hsvvɓ +++<~qRZg;҂L̙3G]1~~~*N '''A fºugYq}k=Yٓ@ @PP^}U̘1ppp;f̘W_}c<6c=]z=oCPYYyWWW 2֨#޽[{w^N8޽?V{LEEΜ9kqG1~}/GzKXhL{{{<=ۛ7o2q# *{s̙3knnƝ;wTSfdeeZCޮq.~H$յO?:V0 _ȩ%QiXvĈ ZE"T޾3%[ZZ89oz=+WM[SS1QC:VJLLĵktNss3>Ck |_\fxqFʊX :ĄvL3pYt` v&M8\]v4tַ`ƿ=!L2dffcǎŚ5k0vX4&՜Y_ \\\XW:뛡&M{.9_>`bϛ8*Pխ7SE,Lk+Fccc׍ӵ:ўcǎ>C\\\ L2puuy :/;l ^eqrrq%JѪCRR._ ̤6B!Mb1nݺeT*Ŗ-[LLL0c ̘1'OʝGZ[[QZZ7n 33n:۷Ϡ2. >  tR#77{J8w˱tRZʭYfڵk 7nMR[[(|iH,Z2333H$qqVb _?4ЇB!~zo 0|pѺWczAQQZdYxwX]{{{رgΜAdddb8s y8::j|ͣGiӦ^2Q剝+`ƍV[4ҥKo‡Yf)X'ɰc̛7Xz5>c$''y?~?#/[ OFxx8YXd N:?ŦMwp0=2dfϞٳg Jjr9233d=R7LcUUU(..Ƙ1c2ÐDK QXXU &M Hgiجsq)::~g1+^Ljp>c|(//zw˴)_e}-\x|_5DlS̖ GGGv>Y-prFZ6p_]eIWXX K^^eO03V;c*?)QPPZL=^Ue-{kwmМ3gN:pE" r@ P[%*++:q=ho[[Ξ=#G-..f#twwW Lo'kyy9raÆ163ܻwOΟ?Qznxkekk;v=Aqrs64d2Vu/6(Çq!磲۷Tۑ2Wǎ pdgg3% .]cǎ1>H$---8z(!Ÿx";3~xw*fː{---8}DN lj'ki5xbWtbΒPݕ+WVGd/(((@||V]]bpwwkXZZbWz 8<`ee6H$E"&O磸XHOOGzz:LLL`W si5ݻwΝÇ5;eίojjW^y!!!صk?zh9vpƍc=t.Bhh('iǏGRRT\'OgYxcx=z͗JƃT&Vɓ'dxJKKQ^^6!""B/ґWt`ҥzPz?F~~>N8XVAW$)$siĈؿ?V^|2?k[[[DGGԩSXp!g={r9RRRgFUU/ O? TӪ)ccc,':U P>zEEEvR)뵞PeZPO*?yd9rDXb֬Yj]Uyʆ"55ppp%D"?p޶x`ܹpqqAbbR!Ul2fMMM7n %%pwwŋY'N gggű=p^fjkk{Zn…-TˋvpAS?O?ӟG2fѵ!-׹O >>>5jpMܻwO0a&NȸxA@L0sJKKRͺsttȑ#1a1QQQ(**BAAYXX ǏVuvvvJ3źRL7ĜTcccg皺kddv8;;L0__M$f|U] r9!H `aa;;^NR량 0dȐ>!Jannkk>3dxV6l,Y>jիW3*%%VHHʪegΜѹv"##Cb5kƌ=r**;bbb L2k̀j Zi}MM,}ԵCoI$=qx 驩 ƲexϙUDǖpC]H$%w.\@nn.UzP__/Ƒmmmjzmʵ:|t 0p^ZZZZէr D"Ѐcǎijj ϟT:Ԡ$8;;TІ LsxKJJ͆ꪽەd8yV% B!\]]اxɀ⢔#ܺu 刋_WSRSSo> WYY8,]OG233q}Vǎ?SN;/-6V$$$*YݻWbC?g\刈u>hooǞ={4NJZ[[51^2!""޽7x/NۼWWWs=|_y&fNNV\ߜj xy^2 ,\PczSeea uҰb \tIFO;wN~`}l[[bcc?IcYLmQ$$$1޻wb]_#&&FqVDEEu166ѣGj*oN sZ=HLmm-zM6U_wGQ^$}ydeeȑ#:rn5~e!..N,?՛<ίܹsy&gʝ;wp^G,ٳ:Mc1s{EWTWWs:DHwlJ 8KSdxVIkk+vYM sss_˦ |^MMMtƭk;w/[rW...x/Q &J]cwƶm89_oݻWiHQWFFF \Jo\.jt0>|86lؠf5BCC9 @gM[Zsڎo͜9םQAod999(++culVV(KP1}tX۶mcw^vJsz{"$$$ **Jq4F;;; 66mmm􄷷7͸sIIBaO2 ;wD"{I޽[ f͚\TUUi<+݁;~ׯW-O!all {{{zeCCCW!C:蜨wkvTTTCUy{{;>|F :ߟJ'6oތׯ/L.O>۷on_=N `Ν3gο\!77%%%JA{$ z ոz*Tث[Dcǎ… ];>!0ou4.MLLDAAn݊iӦI{rrrk.}饗.Yk"$$!!!:' L2-BZN\\=wȑZ?rk熆;wذaV\|2 }? | ޺Xtl]|/rwRTa\Ǐ+geejkJcrذan݊?jzDGG@w}vŒ3Ԟt{gјs]k?>֮]UBR|x"%!!!X~cB!Oӧs>B-55UeBw}:æMTڰg$%%=n߾}* [laU򯹹6lP =;=F2 j':r9>#K[[6nj־())ѩ̓Q`` v:M099ظq#.^]ckk+.]7IDAThXIIIٳsA1d+ 5֭[:ϟNJ+>fdd0|笮~&>>>2e Q\\, `ܹb|WJ[ZZReee ۷{QlrwUlhhӧn:V]СCr _}vxg5OHb˥R)vڅÇs t-9ojF\t&M?F#FB?Fmm-QZZׯƍZ;wTosPK Ş={oKK ~ǮgΜzT& +p\zz:S% tx5kvڮmL[o)ԓ_РvbI.~a'٣pݔNL&># ;(._~ld|G 3qqq߮^S~~> 1vXξ.((oqNJq\rO%/"**z,FPPPŸ}1?BKAw###>|ϟ?OlܸQ)YPҥK _/\Q...vDs(122Š+plii)ۏJ[ʄW^QxL"J}¬P(Į]ңñh"~6Й8q"+#5vލ_.Kz{z}mllt*2e˖czf |:o{>޳O#22/29$m; \(3EK277W{>Jj\.P7Y3\=a6Y'Y}_{Uw0%KСC]G RX{ޫJeeeHHH͛7!~A=V=SRRRTkojjL&AJmkkïI&)^+MLիWqY%KTǴ111oz^FF222݌ZSPWWU\ Z9˖- 2L!I.bbb4%'תCₓ'O>Dee%R)`aa888`ذa033{g3ghžܹs!jtm:u*>gYYR}P###5 nnn cK1 iu-[ V믿˗/^)ǘM=𚙙!44}sN]pA8˧~ʘLc[lQngƩڌH$Btt4M5% S苾1GjeeVիWjuW{K =S^6=tCmmmEQQcB!6oެuBCCO>^^x_asA^___=Z`Ua4~xt{RWԹzP3MkC# 5aև.zիogV& mklSc,[ ˗/ٳglZLH$aaam6N_xxyy|^  88 >D}̌!C84G~`ccEu _TTTHHH`uP *\1_ceev爁~~~ >D\\Eii);xyyi0qD8q8u.]j*pr-SSSl۶ ֭SB%KsÃkVeefWW>_^.ĉ]_KR_¿/D"h]:,, _~%*++XOvժU6iTT֯_,DsssCC7oرc9 j5i$L4 шGAAƏ^x8vxb*TӷZL8֨7 qm='LdwBwFTT3z8q"Ycǎu옙OYKF﷫_kt/tttٳ.D51^mݺjg.]H3f K̞=[ؙ}Y|#k+V@LLdO>saZ Ԙj܌cǎ 8q݀3@ӴDRMVrd"""Ծ/_FII all 777bҤI~גesHIIAyy9b1r9퍙3g*֭[ ]]]UK 11Q᱅ * --kccc,[L| cC Q/+ ̙3GN 2 ׯ_G~~>QWW 6 'Oƌ3X׮]SzU‚ fF0ikkCff&X :_x!r>}U 7FAqDuY=t.y{% 0'Xݓ@ =lmmann333}qe@*255ŪU &KJd)ʕ+SOqЪcZmjQ=z4Ң&-x{{#<<ܠ2gKcǎÑ\X6LTsttDppZ3K O{";;nĴi0f L-a?>P\\rTVVRPD"cƌ^Bt 0rH9@DX,X,D"ǏU 7`jj BNNNBW@!<K !PQ%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%?\(>T3IENDB`PK!texamples/guzzleHttp.phpnu[request('GET', 'https://httpbin.org/anything?key=value'); // Gets PHP stream resource from Guzzle stream $phpStream = \GuzzleHttp\Psr7\StreamWrapper::getResource($response->getBody()); foreach (\JsonMachine\Items::fromStream($phpStream) as $key => $value) { var_dump([$key, $value]); } PK!,examples/symfonyHttpClient.phpnu[getContent(); } } $client = HttpClient::create(); $response = $client->request('GET', 'https://httpbin.org/anything?key=value'); $jsonChunks = httpClientChunks($client->stream($response)); foreach (Items::fromIterable($jsonChunks, ['pointer' => '/args']) as $key => $value) { var_dump($key, $value); } PK!;pUhhexamples/memLeak.phpnu[ $item) { $report = memory_get_peak_usage() .':'.memory_get_peak_usage(true) ; if ($report !== $previousReport) { $index = str_pad($i, 3, ' ', STR_PAD_LEFT); echo "$index: $report\n"; $previousReport = $report; } unset($item); } PK!A4 composer.jsonnu[{ "name": "halaxa/json-machine", "description": "Efficient, easy-to-use and fast JSON pull parser", "license": "Apache-2.0", "authors": [ { "name": "Filip Halaxa", "email": "filip@halaxa.cz" } ], "scripts": { "tests": "build/composer-update.sh && vendor/bin/phpunit", "tests-coverage": "build/composer-update.sh && XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover clover.xml", "cs-check": "build/composer-update.sh && vendor/bin/php-cs-fixer fix --dry-run --verbose --allow-risky=yes", "cs-fix": "build/composer-update.sh && vendor/bin/php-cs-fixer fix --verbose --allow-risky=yes", "performance-tests": "php -n test/performance/testPerformance.php" }, "config": { "lock": false, "sort-packages": true }, "require": { "php": ">=7.0" }, "require-dev": { "ext-json": "*", "friendsofphp/php-cs-fixer": "^3.0", "phpunit/phpunit": "^8.0" }, "suggest": { "ext-json": "To run JSON Machine out of the box without custom decoders.", "guzzlehttp/guzzle": "To run example with GuzzleHttp" }, "autoload": { "psr-4": {"JsonMachine\\": "src/"}, "exclude-from-classmap": ["src/autoloader.php"] }, "autoload-dev": { "psr-4": {"JsonMachineTest\\": "test/JsonMachineTest"} }, "funding": [ { "type": "other", "url": "https://ko-fi.com/G2G57KTE4" } ] } PK!5 CHANGELOG.mdnu[# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## master Nothing yet
## 1.1.1 - 2022-03-03 ### Fixed - Fixed warning when generating autoload classmap via composer.
## 1.1.0 - 2022-02-19 ### Added - Autoloading without Composer. Thanks @a-sync.
## 1.0.1 - 2022-02-06 ### Fixed - Broken command `make performance-tests` - Slight performance improvements
## 1.0.0 - 2022-02-04 ### Removed - Removed deprecated functions `objects()` and `httpClientChunks()`. - Removed deprecated `JsonMachine` entrypoint class. Use `Items` instead. - Removed deprecated `Decoder` interface. Use `ItemDecoder` instead. - Removed `Parser::getJsonPointer()`. Use `Parser::getJsonPointers()`/`Items::getJsonPointers()` instead. - Removed `Parser::getJsonPointerPath()`. No replacement. Was not useful for anything other than testing and exposed internal implementation. ### Changed #### Simplified and fixed decoding - JSON Pointer parts between slashes (a.k.a reference tokens) must be valid encoded JSON strings to be [JSON Pointer RFC 6901](https://tools.ietf.org/html/rfc6901) compliant. It means that no internal key decoding is performed anymore. You will have to change your JSON Pointers if you match against keys with escape sequences. ```diff Items::fromString( '{"quotes\"": [1, 2, 3]}', - ['pointer' => '/quotes"'] + ['pointer' => '/quotes\"'] ); ``` - Method `ItemDecoder::decodeInternalKey()` was deleted as well as related `ValidStringResult`. They are not used anymore as described in previous point. - `PassThruDecoder` does not decode keys anymore. Both the key and the value yielded are raw JSON now. #### Other - Default decoding structure of `Parser` is object. (You won't notice that unless you use `Parser` class directly) - `SyntaxError` renamed to `SyntaxErrorException` - `Items::__construct` accepts the options array instead of separate arguments. (You won't notice that unless you instantiate `Items` class directly) - `Lexer` renamed to `Tokens` - `DebugLexer` renamed to `TokensWithDebugging` ### Added - Multiple JSON Pointers can be specified as an array in `pointer` option. See README. Thanks @fwolfsjaeger. - New methods available during iteration: `Items::getCurrentJsonPointer()` and `Items::getMatchedJsonPointer()` to track where you are. See README. Thanks @fwolfsjaeger. ### Fixed - Incorrect position information of `TokensWithDebugging::getPosition()`. Was constantly off by 1-2 bytes.
## 0.8.0 ### Changed - Internal decoders moved to `ItemDecoder`. `ErrorWrappingDecoder` decorator now requires `ItemDecoder` as well. - Dropped PHP 5.6 support. ### Deprecated - `JsonMachine\JsonMachine` entry point class is deprecated, use `JsonMachine\Items` instead. - `JsonMachine\JsonDecoder\Decoder` interface is deprecated. Use `JsonMachine\JsonDecoder\ItemDecoder` instead. ### Added - New entry point class `Items` replaces `JsonMachine`. - Object as default decoding structure instead of array in `Items`. - `Items::getIterator()` now returns `Parser`'s iterator directly. Call `Items::getIterator()` instead of `JsonMachine::getIterator()::getIterator()` to get to `Parser`'s iterator if you need it. Fixes https://stackoverflow.com/questions/63706550 - `Items` uses `options` in its factory methods instead of growing number of many parameters. See **Options** in README. - `Items` introduces new `debug` option. See **Options** in README. - Noticeable performance improvements. What took 10 seconds in `0.7.*` takes **about** 7 seconds in `0.8.0`.
## 0.7.1 ### New features - PHP 8.1 support - DEV: Build system switched to composer scripts and Makefile
## 0.7.0 ### New features - Use a `-` in json pointer as a wildcard for an array index. Example: `/users/-/id`. Thanks @cerbero90
## 0.6.1 ### Fixed bugs - Empty dict at the end of an item was causing Syntax error in the next item. Reason: closing `}` did not set object key expectation to `false`. (#41 via PR #42).
## 0.6.0 ### New features - **New:** Json pointer can find scalar values in JSON document as well as iterable values. See [Getting single scalar values](README.md#getting-scalar-values) - Parser ends when the end of the desired data is reached and does not heat up the atmosphere further. - Optimizations: about 15% speed gain. ### BC breaks - A json pointer that matches scalar value does not throw anymore, but the scalar value is yielded in foreach.
## 0.5.0 ### New features - Introduced `FileChunks` class. Takes care of the proper resource management when iterating via `JsonMachine::fromFile()`. It is used internally, and you probably won't come across it. - New `ErrorWrappingDecoder`. Use it when you want to skip malformed JSON items. See [Decoders](README.md#decoders). ### BC breaks - `StreamBytes` and `StringBytes` renamed to `StreamChunks` and `StringChunks`. These are internal classes, and you probably won't notice the change unless you use them directly for some reason.
## 0.4.1 ### New features - Tracking of parsing progress
## 0.4.0 ### New features - [Decoders](README.md#decoders) - PHP 8 support (thanks @snapshotpl) ### BC breaks - `ext-json` is not required in `composer.json` anymore, because custom decoder might not need it. However **built-in decoders depend on it** so it must be present if you use them. - All exceptions now extend `JsonMachineException` (thanks @gabimem) - Throws `UnexpectedEndSyntaxErrorException` on an unexpected end of JSON structure (thanks @gabimem) - Function `httpClientChunks()` is **deprecated** so that compatibility with Symfony HttpClient is not on the shoulders of JSON Machine maintainer. The code is simple and everyone can make their own function and maintain it. The code was moved to [examples](examples/symfonyHttpClient.php). - Function `objects()` is **deprecated**. The way `objects()` works is that it casts decoded arrays to objects. It brings some unnecessary overhead and risks on huge datasets. Alternative is to use `ExtJsonDecoder` which decodes items as objects by default (same as `json_decode`). ```php chunks = $bytesIterator; $this->jsonPointer = $options['pointer']; $this->jsonDecoder = $options['decoder']; $this->debugEnabled = $options['debug']; if ($this->debugEnabled) { $tokensClass = TokensWithDebugging::class; } else { $tokensClass = Tokens::class; } $this->parser = new Parser( new $tokensClass( $this->chunks ), $this->jsonPointer, $this->jsonDecoder ?: new ExtJsonDecoder() ); } /** * @param string $string * * @return self * * @throws InvalidArgumentException */ public static function fromString($string, array $options = []) { return new self(new StringChunks($string), $options); } /** * @param string $file * * @return self * * @throws Exception\InvalidArgumentException */ public static function fromFile($file, array $options = []) { return new self(new FileChunks($file), $options); } /** * @param resource $stream * * @return self * * @throws Exception\InvalidArgumentException */ public static function fromStream($stream, array $options = []) { return new self(new StreamChunks($stream), $options); } /** * @param iterable $iterable * * @return self * * @throws Exception\InvalidArgumentException */ public static function fromIterable($iterable, array $options = []) { return new self($iterable, $options); } #[\ReturnTypeWillChange] public function getIterator() { return $this->parser->getIterator(); } public function getPosition() { return $this->parser->getPosition(); } public function getJsonPointers(): array { return $this->parser->getJsonPointers(); } public function getCurrentJsonPointer(): string { return $this->parser->getCurrentJsonPointer(); } public function getMatchedJsonPointer(): string { return $this->parser->getMatchedJsonPointer(); } /** * @return bool */ public function isDebugEnabled() { return $this->debugEnabled; } } PK!Trrsrc/ItemsOptions.phpnu[validateOptions($options); parent::__construct($this->options); } public function toArray(): array { return $this->options; } /** * @throws InvalidArgumentException */ private function validateOptions(array $options) { $mergedOptions = array_merge(self::defaultOptions(), $options); try { foreach ($mergedOptions as $optionName => $optionValue) { if ( ! isset(self::defaultOptions()[$optionName])) { throw new InvalidArgumentException("Option '$optionName' does not exist."); } $this->options[$optionName] = $this->{"opt_$optionName"}($optionValue); } } catch (\TypeError $typeError) { throw new InvalidArgumentException( preg_replace('~Argument #[0-9]+~', "Option '$optionName'", $typeError->getMessage()) ); } } private function opt_pointer($pointer) { if (is_array($pointer)) { (function (string ...$p) {})(...$pointer); } else { (function (string $p) {})($pointer); } return $pointer; } private function opt_decoder(ItemDecoder $decoder = null) { return $decoder; } private function opt_debug(bool $debug) { return $debug; } public static function defaultOptions(): array { return [ 'pointer' => '', 'decoder' => new ExtJsonDecoder(), 'debug' => false, ]; } } PK!Kjsrc/FileChunks.phpnu[fileName = $fileName; $this->chunkSize = $chunkSize; } /** * @return \Generator */ #[\ReturnTypeWillChange] public function getIterator() { $fileHandle = fopen($this->fileName, 'r'); try { yield from new StreamChunks($fileHandle, $this->chunkSize); } finally { fclose($fileHandle); } } } PK!src/PositionAware.phpnu[stream = $stream; $this->chunkSize = $chunkSize; } /** * @return \Generator */ #[\ReturnTypeWillChange] public function getIterator() { while ('' !== ($chunk = fread($this->stream, $this->chunkSize))) { yield $chunk; } } } PK!L$uu&src/Exception/JsonMachineException.phpnu[malformedJson = $malformedJson; $this->errorMessage = $errorMessage; } /** * @return string */ public function getMalformedJson() { return $this->malformedJson; } /** * @return string */ public function getErrorMessage() { return $this->errorMessage; } } PK!dsrc/JsonDecoder/ItemDecoder.phpnu[assoc = $assoc; $this->depth = $depth; $this->options = $options; } public function decode($jsonValue) { $decoded = json_decode($jsonValue, $this->assoc, $this->depth, $this->options); if (json_last_error() !== JSON_ERROR_NONE) { return new InvalidResult(json_last_error_msg()); } return new ValidResult($decoded); } } PK!!l!src/JsonDecoder/InvalidResult.phpnu[errorMessage = $errorMessage; } public function getErrorMessage(): string { return $this->errorMessage; } public function isOk(): bool { return false; } } PK!쩭#src/JsonDecoder/PassThruDecoder.phpnu[innerDecoder = $innerDecoder; } public function decode($jsonValue) { $result = $this->innerDecoder->decode($jsonValue); if ( ! $result->isOk()) { return new ValidResult(new DecodingError($jsonValue, $result->getErrorMessage())); } return $result; } } PK!8usrc/JsonDecoder/ValidResult.phpnu[value = $value; } /** * @return mixed */ public function getValue() { return $this->value; } public function isOk(): bool { return true; } } PK!* LLsrc/Tokens.phpnu[ $jsonChunks */ public function __construct($jsonChunks) { $this->jsonChunks = $jsonChunks; } /** * @return Generator */ #[\ReturnTypeWillChange] public function getIterator() { $insignificantBytes = $this->insignificantBytes(); $tokenBoundaries = $this->tokenBoundaries(); $colonCommaBracket = $this->colonCommaBracketTokenBoundaries(); $inString = false; $tokenBuffer = ''; $escaping = false; foreach ($this->jsonChunks as $jsonChunk) { $bytesLength = strlen($jsonChunk); for ($i = 0; $i < $bytesLength; ++$i) { $byte = $jsonChunk[$i]; if ($escaping) { $escaping = false; $tokenBuffer .= $byte; continue; } if (isset($insignificantBytes[$byte])) { // is a JSON-structure insignificant byte $tokenBuffer .= $byte; continue; } if ($inString) { if ($byte == '"') { $inString = false; } elseif ($byte == '\\') { $escaping = true; } $tokenBuffer .= $byte; continue; } if (isset($tokenBoundaries[$byte])) { if ($tokenBuffer != '') { yield $tokenBuffer; $tokenBuffer = ''; } if (isset($colonCommaBracket[$byte])) { yield $byte; } } else { // else branch matches `"` but also `\` outside of a string literal which is an error anyway but strictly speaking not correctly parsed token $inString = true; $tokenBuffer .= $byte; } } } if ($tokenBuffer != '') { yield $tokenBuffer; } } private function tokenBoundaries() { $utf8bom1 = "\xEF"; $utf8bom2 = "\xBB"; $utf8bom3 = "\xBF"; return array_merge( [ $utf8bom1 => true, $utf8bom2 => true, $utf8bom3 => true, ' ' => true, "\n" => true, "\r" => true, "\t" => true, ], $this->colonCommaBracketTokenBoundaries() ); } private function colonCommaBracketTokenBoundaries(): array { return [ '{' => true, '}' => true, '[' => true, ']' => true, ':' => true, ',' => true, ]; } private function insignificantBytes(): array { $insignificantBytes = []; foreach (range(0, 255) as $ord) { if ( ! in_array( chr($ord), ['\\', '"', "\xEF", "\xBB", "\xBF", ' ', "\n", "\r", "\t", '{', '}', '[', ']', ':', ','] )) { $insignificantBytes[chr($ord)] = true; } } return $insignificantBytes; } public function getPosition(): int { return 0; } public function getLine(): int { return 1; } public function getColumn(): int { return 0; } } PK!eT$9$9src/Parser.phpnu[toArray(); $this->tokens = $tokens; $this->jsonDecoder = $jsonDecoder ?: new ExtJsonDecoder(); $this->hasSingleJsonPointer = (count($jsonPointers) === 1); $this->jsonPointers = array_combine($jsonPointers, $jsonPointers); $this->paths = $this->buildPaths($this->jsonPointers); } private function buildPaths(array $jsonPointers): array { return array_map(function ($jsonPointer) { return self::jsonPointerToPath($jsonPointer); }, $jsonPointers); } /** * @return \Generator * * @throws PathNotFoundException */ #[\ReturnTypeWillChange] public function getIterator() { $tokenTypes = $this->tokenTypes(); $iteratorStruct = null; $currentPath = &$this->currentPath; $currentPath = []; $currentPathWildcard = []; $pointersFound = []; $currentLevel = -1; $stack = [$currentLevel => null]; $jsonBuffer = ''; $key = null; $objectKeyExpected = false; $inObject = true; // hack to make "!$inObject" in first iteration work. Better code structure? $expectedType = self::OBJECT_START | self::ARRAY_START; $subtreeEnded = false; $token = null; $currentPathChanged = true; $jsonPointerPath = []; $iteratorLevel = 0; // local variables for faster name lookups $tokens = $this->tokens; foreach ($tokens as $token) { if ($currentPathChanged) { $currentPathChanged = false; $jsonPointerPath = $this->getMatchingJsonPointerPath(); $iteratorLevel = count($jsonPointerPath); } $tokenType = $tokenTypes[$token[0]]; if (0 == ($tokenType & $expectedType)) { $this->error('Unexpected symbol', $token); } $isValue = ($tokenType | 23) == 23; // 23 = self::ANY_VALUE if ( ! $inObject && $isValue && $currentLevel < $iteratorLevel) { $currentPathChanged = ! $this->hasSingleJsonPointer; $currentPath[$currentLevel] = isset($currentPath[$currentLevel]) ? (string) (1 + (int) $currentPath[$currentLevel]) : '0'; $currentPathWildcard[$currentLevel] = preg_match('/^(?:\d+|-)$/S', $jsonPointerPath[$currentLevel]) ? '-' : $currentPath[$currentLevel]; unset($currentPath[$currentLevel + 1], $currentPathWildcard[$currentLevel + 1], $stack[$currentLevel + 1]); } if ( ( $jsonPointerPath == $currentPath || $jsonPointerPath == $currentPathWildcard ) && ( $currentLevel > $iteratorLevel || ( ! $objectKeyExpected && ( ($currentLevel == $iteratorLevel && $isValue) || ($currentLevel + 1 == $iteratorLevel && ($tokenType | 3) == 3) // 3 = self::SCALAR_VALUE ) ) ) ) { $jsonBuffer .= $token; } // todo move this switch to the top just after the syntax check to be a correct FSM switch ($token[0]) { case '"': if ($objectKeyExpected) { $objectKeyExpected = false; $expectedType = 128; // 128 = self::COLON if ($currentLevel == $iteratorLevel) { $key = $token; } elseif ($currentLevel < $iteratorLevel) { $key = $token; $referenceToken = substr($token, 1, -1); $currentPathChanged = ! $this->hasSingleJsonPointer; $currentPath[$currentLevel] = $referenceToken; $currentPathWildcard[$currentLevel] = $referenceToken; unset($currentPath[$currentLevel + 1], $currentPathWildcard[$currentLevel + 1]); } continue 2; // a valid json chunk is not completed yet } if ($inObject) { $expectedType = 72; // 72 = self::AFTER_OBJECT_VALUE; } else { $expectedType = 96; // 96 = self::AFTER_ARRAY_VALUE; } break; case ',': if ($inObject) { $objectKeyExpected = true; $expectedType = 2; // 2 = self::SCALAR_STRING } else { $expectedType = 23; // 23 = self::ANY_VALUE } continue 2; // a valid json chunk is not completed yet case ':': $expectedType = 23; // 23 = self::ANY_VALUE continue 2; // a valid json chunk is not completed yet case '{': ++$currentLevel; if ($currentLevel <= $iteratorLevel) { $iteratorStruct = '{'; } $stack[$currentLevel] = '{'; $inObject = true; $expectedType = 10; // 10 = self::AFTER_OBJECT_START $objectKeyExpected = true; continue 2; // a valid json chunk is not completed yet case '[': ++$currentLevel; if ($currentLevel <= $iteratorLevel) { $iteratorStruct = '['; } $stack[$currentLevel] = '['; $inObject = false; $expectedType = 55; // 55 = self::AFTER_ARRAY_START; continue 2; // a valid json chunk is not completed yet case '}': $objectKeyExpected = false; // no break case ']': --$currentLevel; $inObject = $stack[$currentLevel] == '{'; // no break default: if ($inObject) { $expectedType = 72; // 72 = self::AFTER_OBJECT_VALUE; } else { $expectedType = 96; // 96 = self::AFTER_ARRAY_VALUE; } } if ($currentLevel > $iteratorLevel) { continue; // a valid json chunk is not completed yet } if ($jsonBuffer !== '') { $valueResult = $this->jsonDecoder->decode($jsonBuffer); $jsonBuffer = ''; if ( ! $valueResult->isOk()) { $this->error($valueResult->getErrorMessage(), $token); } if ($iteratorStruct == '[') { yield $valueResult->getValue(); } else { $keyResult = $this->jsonDecoder->decode($key); if ( ! $keyResult->isOk()) { $this->error($keyResult->getErrorMessage(), $key); } yield $keyResult->getValue() => $valueResult->getValue(); unset($keyResult); } unset($valueResult); } if ($jsonPointerPath == $currentPath || $jsonPointerPath == $currentPathWildcard) { if ( ! in_array($this->matchedJsonPointer, $pointersFound, true)) { $pointersFound[] = $this->matchedJsonPointer; } } elseif (count($pointersFound) == count($this->jsonPointers)) { $subtreeEnded = true; break; } } if ($token === null) { $this->error('Cannot iterate empty JSON', $token); } if ($currentLevel > -1 && ! $subtreeEnded) { $this->error('JSON string ended unexpectedly', $token, UnexpectedEndSyntaxErrorException::class); } if (count($pointersFound) !== count($this->jsonPointers)) { throw new PathNotFoundException(sprintf("Paths '%s' were not found in json stream.", implode(', ', array_diff($this->jsonPointers, $pointersFound)))); } $this->matchedJsonPointer = null; $this->currentPath = null; } private function tokenTypes() { return [ 'n' => self::SCALAR_CONST, 't' => self::SCALAR_CONST, 'f' => self::SCALAR_CONST, '-' => self::SCALAR_CONST, '0' => self::SCALAR_CONST, '1' => self::SCALAR_CONST, '2' => self::SCALAR_CONST, '3' => self::SCALAR_CONST, '4' => self::SCALAR_CONST, '5' => self::SCALAR_CONST, '6' => self::SCALAR_CONST, '7' => self::SCALAR_CONST, '8' => self::SCALAR_CONST, '9' => self::SCALAR_CONST, '"' => self::SCALAR_STRING, '{' => self::OBJECT_START, '}' => self::OBJECT_END, '[' => self::ARRAY_START, ']' => self::ARRAY_END, ',' => self::COMMA, ':' => self::COLON, ]; } private function getMatchingJsonPointerPath(): array { $matchingPointer = key($this->paths); if (count($this->paths) === 1) { $this->matchedJsonPointer = $matchingPointer; return $this->paths[$matchingPointer]; } $currentPathLength = count($this->currentPath); $matchLength = -1; foreach ($this->paths as $jsonPointer => $path) { $matchingReferenceTokens = []; foreach ($path as $i => $referenceToken) { if ( ! isset($this->currentPath[$i]) || ( $this->currentPath[$i] !== $referenceToken && ValidJsonPointers::wildcardify($this->currentPath[$i]) !== $referenceToken ) ) { continue; } $matchingReferenceTokens[$i] = $referenceToken; } if (empty($matchingReferenceTokens)) { continue; } $currentMatchLength = count($matchingReferenceTokens); if ($currentMatchLength > $matchLength) { $matchingPointer = $jsonPointer; $matchLength = $currentMatchLength; } if ($matchLength === $currentPathLength) { break; } } $this->matchedJsonPointer = $matchingPointer; return $this->paths[$matchingPointer]; } public function getJsonPointers(): array { return array_values($this->jsonPointers); } public function getCurrentJsonPointer(): string { if ($this->currentPath === null) { throw new JsonMachineException(__METHOD__.' must be called inside a loop'); } return self::pathToJsonPointer($this->currentPath); } public function getMatchedJsonPointer(): string { if ($this->matchedJsonPointer === null) { throw new JsonMachineException(__METHOD__.' must be called inside a loop'); } return $this->matchedJsonPointer; } /** * @param string $msg * @param string $token * @param string $exception */ private function error($msg, $token, $exception = SyntaxErrorException::class) { throw new $exception($msg." '".$token."'", $this->tokens->getPosition()); } /** * @return int * * @throws JsonMachineException */ public function getPosition() { if ($this->tokens instanceof PositionAware) { return $this->tokens->getPosition(); } throw new JsonMachineException('Provided tokens iterable must implement PositionAware to call getPosition on it.'); } private static function jsonPointerToPath(string $jsonPointer): array { return array_slice(array_map(function ($jsonPointerPart) { return str_replace(['~1', '~0'], ['/', '~'], $jsonPointerPart); }, explode('/', $jsonPointer)), 1); } private static function pathToJsonPointer(array $path): string { $encodedParts = array_map(function ($addressPart) { return str_replace(['~', '/'], ['~0', '~1'], $addressPart); }, $path); array_unshift($encodedParts, ''); return implode('/', $encodedParts); } } PK!"src/StringChunks.phpnu[string = $string; $this->chunkSize = $chunkSize; } /** * @return \Generator */ #[\ReturnTypeWillChange] public function getIterator() { $len = strlen($this->string); for ($i = 0; $i < $len; $i += $this->chunkSize) { yield substr($this->string, $i, $this->chunkSize); } } } PK!#src/TokensWithDebugging.phpnu[ $jsonChunks */ public function __construct($jsonChunks) { $this->jsonChunks = $jsonChunks; } /** * @return \Generator */ #[\ReturnTypeWillChange] public function getIterator() { // Treat UTF-8 BOM bytes as whitespace ${"\xEF"} = ${"\xBB"} = ${"\xBF"} = 0; ${' '} = 0; ${"\n"} = 0; ${"\r"} = 0; ${"\t"} = 0; ${'{'} = 1; ${'}'} = 1; ${'['} = 1; ${']'} = 1; ${':'} = 1; ${','} = 1; $inString = false; $tokenBuffer = ''; $escaping = false; $tokenWidth = 0; $ignoreLF = false; $position = 0; $line = 1; $column = 0; foreach ($this->jsonChunks as $bytes) { $bytesLength = strlen($bytes); for ($i = 0; $i < $bytesLength; ++$i) { $byte = $bytes[$i]; if ($inString) { if ($byte == '"' && ! $escaping) { $inString = false; } $escaping = ($byte == '\\' && ! $escaping); $tokenBuffer .= $byte; ++$tokenWidth; continue; } if (isset($$byte)) { ++$column; if ($tokenBuffer != '') { $this->position = $position + $i; $this->column = $column; $this->line = $line; yield $tokenBuffer; $column += $tokenWidth; $tokenBuffer = ''; $tokenWidth = 0; } if ($$byte) { // is not whitespace $this->position = $position + $i + 1; $this->column = $column; $this->line = $line; yield $byte; // track line number and reset column for each newline } elseif ($byte == "\n") { // handle CRLF newlines if ($ignoreLF) { --$column; $ignoreLF = false; continue; } ++$line; $column = 0; } elseif ($byte == "\r") { ++$line; $ignoreLF = true; $column = 0; } } else { if ($byte == '"') { $inString = true; } $tokenBuffer .= $byte; ++$tokenWidth; } } $position += $i; } $this->position = $position; if ($tokenBuffer != '') { $this->column = $column; yield $tokenBuffer; } } /** * @return int */ public function getPosition() { return $this->position; } /** * Returns the line number of the lexeme currently being processed (index starts at one). * * @return int */ public function getLine() { return $this->line; } /** * The position of currently being processed lexeme within the line (index starts at one). * * @return int */ public function getColumn() { return $this->column; } } PK!<  src/ValidJsonPointers.phpnu[jsonPointers = array_values($jsonPointers); } /** * @throws InvalidArgumentException */ public function toArray(): array { if ( ! $this->validated) { $this->validate(); } return $this->jsonPointers; } /** * @throws InvalidArgumentException */ private function validate() { $this->validateFormat(); $this->validateJsonPointersDoNotIntersect(); $this->validated = true; } /** * @throws InvalidArgumentException */ private function validateFormat() { foreach ($this->jsonPointers as $jsonPointerEl) { if (preg_match('_^(/(([^/~])|(~[01]))*)*$_', $jsonPointerEl) === 0) { throw new InvalidArgumentException( sprintf("Given value '%s' of \$jsonPointer is not valid JSON Pointer", $jsonPointerEl) ); } } } /** * @throws InvalidArgumentException */ private function validateJsonPointersDoNotIntersect() { foreach ($this->jsonPointers as $keyA => $jsonPointerA) { foreach ($this->jsonPointers as $keyB => $jsonPointerB) { if ($keyA === $keyB) { continue; } if ($jsonPointerA === $jsonPointerB || self::str_contains($jsonPointerA, $jsonPointerB) || self::str_contains($jsonPointerA, self::wildcardify($jsonPointerB)) ) { throw new InvalidArgumentException( sprintf( "JSON Pointers must not intersect. At least these two do: '%s', '%s'", $jsonPointerA, $jsonPointerB ) ); } } } } public static function wildcardify(string $jsonPointerPart): string { return preg_replace('~/\d+(/|$)~S', '/-$1', $jsonPointerPart); } /** * @see https://github.com/symfony/polyfill/blob/v1.24.0/src/Php80/Php80.php */ public static function str_contains(string $haystack, string $needle): bool { return '' === $needle || false !== strpos($haystack, $needle); } } PK![yCR,R, LICENSE.txtnu[ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [2018] [Filip Halaxa] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. PK!MD{ZZ README.mdnu[ (README in sync with the code) Very easy to use and memory efficient drop-in replacement for inefficient iteration of big JSON files or streams for PHP >=7.0. See [TL;DR](#tl-dr). No dependencies in production except optional `ext-json`. [![Build Status](https://github.com/halaxa/json-machine/actions/workflows/makefile.yml/badge.svg)](https://github.com/halaxa/json-machine/actions) [![codecov](https://img.shields.io/codecov/c/gh/halaxa/json-machine?label=phpunit%20%40covers)](https://codecov.io/gh/halaxa/json-machine) [![Latest Stable Version](https://img.shields.io/github/v/release/halaxa/json-machine?color=blueviolet&include_prereleases&logoColor=white)](https://packagist.org/packages/halaxa/json-machine) [![Monthly Downloads](https://img.shields.io/packagist/dt/halaxa/json-machine?color=%23f28d1a)](https://packagist.org/packages/halaxa/json-machine) --- * [TL;DR](#tl-dr) * [Introduction](#introduction) * [Parsing JSON documents](#parsing-json-documents) + [Parsing a document](#simple-document) + [Parsing a subtree](#parsing-a-subtree) + [Parsing nested values in arrays](#parsing-nested-values) + [Parsing a single scalar value](#getting-scalar-values) + [Parsing multiple subtrees](#parsing-multiple-subtrees) + [What is JSON Pointer anyway?](#json-pointer) * [Options](#options) * [Parsing streaming responses from a JSON API](#parsing-json-stream-api-responses) + [GuzzleHttp](#guzzlehttp) + [Symfony HttpClient](#symfony-httpclient) * [Tracking the progress](#tracking-parsing-progress) * [Decoders](#decoders) + [Available decoders](#available-decoders) * [Error handling](#error-handling) + [Catching malformed items](#malformed-items) * [Parser efficiency](#on-parser-efficiency) + [Streams / files](#streams-files) + [In-memory JSON strings](#in-memory-json-strings) * [Troubleshooting](#troubleshooting) + ["I'm still getting Allowed memory size ... exhausted"](#step1) + ["That didn't help"](#step2) + ["I am still out of luck"](#step3) * [Installation](#installation) * [Development](#development) + [Non containerized](#non-containerized) + [Containerized](#containerized) * [Support](#support) * [License](#license) --- ## TL;DR ```diff $user) { // just process $user as usual var_dump($user->name); } ``` Random access like `$users[42]` is not yet possible. Use above-mentioned `foreach` and find the item or use [JSON Pointer](#parsing-a-subtree). Count the items via [`iterator_count($users)`](https://www.php.net/manual/en/function.iterator-count.php). Remember it will still have to internally iterate the whole thing to get the count and thus will take about the same time. Requires `ext-json` if used out of the box. See [Decoders](#decoders). Follow [CHANGELOG](CHANGELOG.md). ## Introduction JSON Machine is an efficient, easy-to-use and fast JSON stream/pull/incremental/lazy (whatever you name it) parser based on generators developed for unpredictably long JSON streams or documents. Main features are: - Constant memory footprint for unpredictably large JSON documents. - Ease of use. Just iterate JSON of any size with `foreach`. No events and callbacks. - Efficient iteration on any subtree of the document, specified by [JSON Pointer](#json-pointer) - Speed. Performance critical code contains no unnecessary function calls, no regular expressions and uses native `json_decode` to decode JSON document items by default. See [Decoders](#decoders). - Parses not only streams but any iterable that produces JSON chunks. - Thoroughly tested. More than 200 tests and 1000 assertions. ## Parsing JSON documents ### Parsing a document Let's say that `fruits.json` contains this huge JSON document: ```json // fruits.json { "apple": { "color": "red" }, "pear": { "color": "yellow" } } ``` It can be parsed this way: ```php $data) { // 1st iteration: $name === "apple" and $data->color === "red" // 2nd iteration: $name === "pear" and $data->color === "yellow" } ``` Parsing a json array instead of a json object follows the same logic. The key in a foreach will be a numeric index of an item. If you prefer JSON Machine to return arrays instead of objects, use `new ExtJsonDecoder(true)` as a decoder. ```php new ExtJsonDecoder(true)]); ``` ### Parsing a subtree If you want to iterate only `results` subtree in this `fruits.json`: ```json // fruits.json { "results": { "apple": { "color": "red" }, "pear": { "color": "yellow" } } } ``` use JSON Pointer `/results` as `pointer` option: ```php '/results']); foreach ($fruits as $name => $data) { // The same as above, which means: // 1st iteration: $name === "apple" and $data->color === "red" // 2nd iteration: $name === "pear" and $data->color === "yellow" } ``` > Note: > > Value of `results` is not loaded into memory at once, but only one item in > `results` at a time. It is always one item in memory at a time at the level/subtree > you are currently iterating. Thus, the memory consumption is constant. ### Parsing nested values in arrays The JSON Pointer spec also allows to use a hyphen (`-`) instead of a specific array index. JSON Machine interprets it as a wildcard which matches any **array index** (not any object key). This enables you to iterate nested values in arrays without loading the whole item. Example: ```json // fruitsArray.json { "results": [ { "name": "apple", "color": "red" }, { "name": "pear", "color": "yellow" } ] } ``` To iterate over all colors of the fruits, use the JSON Pointer `"/results/-/color"`. ```php '/results/-/color']); foreach ($fruits as $key => $value) { // 1st iteration: $key == 'color'; $value == 'red'; $fruits->getMatchedJsonPointer() == '/results/-/color'; $fruits->getCurrentJsonPointer() == '/results/0/color'; // 2nd iteration: $key == 'color'; $value == 'yellow'; $fruits->getMatchedJsonPointer() == '/results/-/color'; $fruits->getCurrentJsonPointer() == '/results/1/color'; } ``` ### Parsing a single scalar value You can parse a single scalar value anywhere in the document the same way as a collection. Consider this example: ```json // fruits.json { "lastModified": "2012-12-12", "apple": { "color": "red" }, "pear": { "color": "yellow" }, // ... gigabytes follow ... } ``` Get the scalar value of `lastModified` key like this: ```php '/lastModified']); foreach ($fruits as $key => $value) { // 1st and final iteration: // $key === 'lastModified' // $value === '2012-12-12' } ``` When parser finds the value and yields it to you, it stops parsing. So when a single scalar value is in the beginning of a gigabytes-sized file or stream, it just gets the value from the beginning in no time and with almost no memory consumed. The obvious shortcut is: ```php '/lastModified']); $lastModified = iterator_to_array($fruits)['lastModified']; ``` Single scalar value access supports array indices in JSON Pointer as well. ### Parsing multiple subtrees It is also possible to parse multiple subtrees using multiple JSON Pointers. Consider this example: ```json // fruits.json { "lastModified": "2012-12-12", "berries": [ { "name": "strawberry", // not a berry, but whatever ... "color": "red" }, { "name": "raspberry", // the same ... "color": "red" } ], "citruses": [ { "name": "orange", "color": "orange" }, { "name": "lime", "color": "green" } ] } ``` To iterate over all berries and citrus fruits, use the JSON pointers `["/berries", "/citrus"]`. The order of pointers does not matter. The items will be iterated in the order of appearance in the document. ```php ['/berries', '/citruses'] ]); foreach ($fruits as $key => $value) { // 1st iteration: $value == ["name" => "strawberry", "color" => "red"]; $fruits->getCurrentJsonPointer() == '/berries'; // 2nd iteration: $value == ["name" => "raspberry", "color" => "red"]; $fruits->getCurrentJsonPointer() == '/berries'; // 3rd iteration: $value == ["name" => "orange", "color" => "orange"]; $fruits->getCurrentJsonPointer() == '/citruses'; // 4th iteration: $value == ["name" => "lime", "color" => "green"]; $fruits->getCurrentJsonPointer() == '/citruses'; } ``` ### What is JSON Pointer anyway? It's a way of addressing one item in JSON document. See the [JSON Pointer RFC 6901](https://tools.ietf.org/html/rfc6901). It's very handy, because sometimes the JSON structure goes deeper, and you want to iterate a subtree, not the main level. So you just specify the pointer to the JSON array or object (or even to a scalar value) you want to iterate and off you go. When the parser hits the collection you specified, iteration begins. You can pass it as `pointer` option in all `Items::from*` functions. If you specify a pointer to a non-existent position in the document, an exception is thrown. It can be used to access scalar values as well. **JSON Pointer itself must be a valid JSON string**. Literal comparison of reference tokens (the parts between slashes) is performed against the JSON document keys/member names. Some examples: | JSON Pointer value | Will iterate through | |--------------------------|-----------------------------------------------------------------------------------------------------------| | (empty string - default) | `["this", "array"]` or `{"a": "this", "b": "object"}` will be iterated (main level) | | `/result/items` | `{"result": {"items": ["this", "array", "will", "be", "iterated"]}}` | | `/0/items` | `[{"items": ["this", "array", "will", "be", "iterated"]}]` (supports array indices) | | `/results/-/status` | `{"results": [{"status": "iterated"}, {"status": "also iterated"}]}` (a hyphen as an array index wildcard)| | `/` (gotcha! - a slash followed by an empty string, see the [spec](https://tools.ietf.org/html/rfc6901#section-5)) | `{"":["this","array","will","be","iterated"]}` | | `/quotes\"` | `{"quotes\"": ["this", "array", "will", "be", "iterated"]}` | ## Options Options may change how a JSON is parsed. Array of options is the second parameter of all `Items::from*` functions. Available options are: - `pointer` - A JSON Pointer string that tells which part of the document you want to iterate. - `decoder` - An instance of `ItemDecoder` interface. - `debug` - `true` or `false` to enable or disable the debug mode. When the debug mode is enabled, data such as line, column and position in the document are available during parsing or in exceptions. Keeping debug disabled adds slight performance advantage. ## Parsing streaming responses from a JSON API A stream API response or any other JSON stream is parsed exactly the same way as file is. The only difference is, you use `Items::fromStream($streamResource)` for it, where `$streamResource` is the stream resource with the JSON document. The rest is the same as with parsing files. Here are some examples of popular http clients which support streaming responses: ### GuzzleHttp Guzzle uses its own streams, but they can be converted back to PHP streams by calling `\GuzzleHttp\Psr7\StreamWrapper::getResource()`. Pass the result of this function to `Items::fromStream` function, and you're set up. See working [GuzzleHttp example](examples/guzzleHttp.php). ### Symfony HttpClient A stream response of Symfony HttpClient works as iterator. And because JSON Machine is based on iterators, the integration with Symfony HttpClient is very simple. See [HttpClient example](examples/symfonyHttpClient.php). ## Tracking the progress (with `debug` enabled) Big documents may take a while to parse. Call `Items::getPosition()` in your `foreach` to get current count of the processed bytes from the beginning. Percentage is then easy to calculate as `position / total * 100`. To find out the total size of your document in bytes you may want to check: - `strlen($document)` if you parse a string - `filesize($file)` if you parse a file - `Content-Length` http header if you parse a http stream response - ... you get the point If `debug` is disabled, `getPosition()` always returns `0`. ```php true]); foreach ($fruits as $name => $data) { echo 'Progress: ' . intval($fruits->getPosition() / $fileSize * 100) . ' %'; } ``` ## Decoders `Items::from*` functions also accept `decoder` option. It must be an instance of `JsonMachine\JsonDecoder\ItemDecoder`. If none is specified, `ExtJsonDecoder` is used by default. It requires `ext-json` PHP extension to be present, because it uses `json_decode`. When `json_decode` doesn't do what you want, implement `JsonMachine\JsonDecoder\ItemDecoder` and make your own. ### Available decoders - **`ExtJsonDecoder`** - **Default.** Uses `json_decode` to decode keys and values. Constructor has the same parameters as `json_decode`. - **`PassThruDecoder`** - Does no decoding. Both keys and values are produced as pure JSON strings. Useful when you want to parse a JSON item with something else directly in the foreach and don't want to implement `JsonMachine\JsonDecoder\ItemDecoder`. Since `1.0.0` does not use `json_decode`. Example: ```php new PassThruDecoder]); ``` - **`ErrorWrappingDecoder`** - A decorator which wraps decoding errors inside `DecodingError` object thus enabling you to skip malformed items instead of dying on `SyntaxError` exception. Example: ```php new ErrorWrappingDecoder(new ExtJsonDecoder())]); foreach ($items as $key => $item) { if ($key instanceof DecodingError || $item instanceof DecodingError) { // handle error of this malformed json item continue; } var_dump($key, $item); } ``` ## Error handling Since 0.4.0 every exception extends `JsonMachineException`, so you can catch that to filter any error from JSON Machine library. ### Skipping malformed items If there's an error anywhere in a json stream, `SyntaxError` exception is thrown. That's very inconvenient, because if there is an error inside one json item you are unable to parse the rest of the document because of one malformed item. `ErrorWrappingDecoder` is a decoder decorator which can help you with that. Wrap a decoder with it, and all malformed items you are iterating will be given to you in the foreach via `DecodingError`. This way you can skip them and continue further with the document. See example in [Available decoders](#available-decoders). Syntax errors in the structure of a json stream between the iterated items will still throw `SyntaxError` exception though. ## Parser efficiency The time complexity is always `O(n)` ### Streams / files TL;DR: The memory complexity is `O(2)` JSON Machine reads a stream (or a file) 1 JSON item at a time and generates corresponding 1 PHP item at a time. This is the most efficient way, because if you had say 10,000 users in JSON file and wanted to parse it using `json_decode(file_get_contents('big.json'))`, you'd have the whole string in memory as well as all the 10,000 PHP structures. Following table shows the difference: | | String items in memory at a time | Decoded PHP items in memory at a time | Total | |------------------------|---------------------------------:|--------------------------------------:|------:| | `json_decode()` | 10000 | 10000 | 20000 | | `Items::from*()` | 1 | 1 | 2 | This means, that JSON Machine is constantly efficient for any size of processed JSON. 100 GB no problem. ### In-memory JSON strings TL;DR: The memory complexity is `O(n+1)` There is also a method `Items::fromString()`. If you are forced to parse a big string, and the stream is not available, JSON Machine may be better than `json_decode`. The reason is that unlike `json_decode`, JSON Machine still traverses the JSON string one item at a time and doesn't load all resulting PHP structures into memory at once. Let's continue with the example with 10,000 users. This time they are all in string in memory. When decoding that string with `json_decode`, 10,000 arrays (objects) is created in memory and then the result is returned. JSON Machine on the other hand creates single structure for each found item in the string and yields it back to you. When you process this item and iterate to the next one, another single structure is created. This is the same behaviour as with streams/files. Following table puts the concept into perspective: | | String items in memory at a time | Decoded PHP items in memory at a time | Total | |-----------------------------|---------------------------------:|--------------------------------------:|------:| | `json_decode()` | 10000 | 10000 | 20000 | | `Items::fromString()` | 10000 | 1 | 10001 | The reality is even better. `Items::fromString` consumes about **5x less memory** than `json_decode`. The reason is that a PHP structure takes much more memory than its corresponding JSON representation. ## Troubleshooting ### "I'm still getting Allowed memory size ... exhausted" One of the reasons may be that the items you want to iterate over are in some sub-key such as `"results"` but you forgot to specify a JSON Pointer. See [Parsing a subtree](#parsing-a-subtree). ### "That didn't help" The other reason may be, that one of the items you iterate is itself so huge it cannot be decoded at once. For example, you iterate over users and one of them has thousands of "friend" objects in it. Use `PassThruDecoder` which does not decode an item, get the json string of the user and parse it iteratively yourself using `Items::fromString()`. ```php new PassThruDecoder]); foreach ($users as $user) { foreach (Items::fromString($user, ['pointer' => "/friends"]) as $friend) { // process friends one by one } } ``` ### "I am still out of luck" It probably means that the JSON string `$user` itself or one of the friends are too big and do not fit in memory. However, you can try this approach recursively. Parse `"/friends"` with `PassThruDecoder` getting one `$friend` json string at a time and then parse that using `Items::fromString()`... If even that does not help, there's probably no solution yet via JSON Machine. A feature is planned which will enable you to iterate any structure fully recursively and strings will be served as streams. ## Installation ### Using Composer ```bash composer require halaxa/json-machine ``` ### Without Composer Clone or download this repository and add the following to your bootstrap file: ```php spl_autoload_register(require '/path/to/json-machine/src/autoloader.php'); ``` ## Development Clone this repository. This library supports two development approaches: 1. non containerized (PHP and composer already installed on your machine) 1. containerized (Docker on your machine) ### Non containerized Run `composer run -l` in the project dir to see available dev scripts. This way you can run some steps of the build process such as tests. ### Containerized [Install Docker](https://docs.docker.com/install/) and run `make` in the project dir on your host machine to see available dev tools/commands. You can run all the steps of the build process separately as well as the whole build process at once. Make basically runs composer dev scripts inside containers in the background. `make build`: Runs complete build. The same command is run via GitHub Actions CI. ## Support Do you like this library? Star it, share it, show it :) Issues and pull requests are very welcome. [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/G2G57KTE4) ## License Apache 2.0 Cogwheel element: Icons made by [TutsPlus](https://www.flaticon.com/authors/tutsplus) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/) Table of contents generated with markdown-toc PK!NK.php-cs-fixer.dist.phpnu[in(__DIR__) ->exclude('ext') ; $config = new PhpCsFixer\Config(); return $config->setRules([ '@Symfony' => true, 'not_operator_with_space' => true, 'yoda_style' => false, 'single_line_throw' => false, 'unary_operator_spaces' => false, 'visibility_required' => false, 'php_unit_test_class_requires_covers' => true, 'declare_strict_types' => true, ]) ->setFinder($finder) ; PK!.github/FUNDING.ymlnu[ko_fi: halaxa PK![n"cc.github/workflows/makefile.ymlnu[name: "`make build`" on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: satackey/action-docker-layer-caching@v0.0.11 # Ignore the failure of a step and avoid terminating the job. continue-on-error: true - name: Complete Makefile build run: make build - name: Code coverage report run: make tests-coverage - name: Send coverage report to codecov uses: codecov/codecov-action@v2 with: files: clover.xml verbose: true PK!R Makefilenu[.DEFAULT_GOAL := help .PHONY: * LATEST_PHP := 8.0 3.1.1 COVERAGE_PHP := 7.4 3.1.1 define PHP_VERSIONS "7.0 2.7.2"\ "7.1 2.9.8"\ "7.2 3.1.1"\ "7.3 3.1.1"\ "7.4 3.1.1"\ "8.0 3.1.1"\ "8.1 3.1.1" endef define DOCKER_RUN ./build/docker-run.sh \ $$(./build/build-image.sh $(1)) \ $$(pwd) \ "$(2)" endef help: @printf "\033[33mJSON Machine's make usage:\033[0m\n make [target] [args=\"val\"...]\n\n" @printf "\033[33mTargets:\033[0m\n" @grep -E '^[-a-zA-Z0-9_\.\/]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[32m%-15s\033[0m\t%s\n", $$1, $$2}' build: tests-all cs-check ## Run all necessary stuff before commit. tests: CMD=composer tests -- $(ARGS) tests: docker-run ## Run tests on recent PHP version. Pass args to phpunit via ARGS="" tests-coverage: CMD=composer tests-coverage -- $(ARGS) tests-coverage: ## Runs tests and creates ./clover.xml. Pass args to phpunit via ARGS="" @$(call DOCKER_RUN,$(COVERAGE_PHP),$(CMD)) tests-all: ## Run tests on all supported PHP versions. Pass args to phpunit via ARGS="" @for version in $(PHP_VERSIONS); do \ set -e; \ printf "PHP %s%.s\n" $$version; \ printf "=======\n"; \ $(call DOCKER_RUN,$$version,composer tests -- --colors=always $(ARGS)); \ printf "\n\n\n"; \ done cs-check: CMD=composer cs-check cs-check: docker-run ## Check code style cs-fix: CMD=composer cs-fix cs-fix: docker-run ## Fix code style performance-tests: CMD=composer performance-tests performance-tests: docker-run ## Run performance tests release: .env build @\ branch=$$(git branch --show-current); \ \ echo "Creating release from '$$branch'"; \ git diff --quiet --exit-code && git diff --quiet --cached --exit-code \ || { echo "There are uncommited changes. Stopping"; exit 1; }; \ \ echo "Type the release version:"; \ read version; \ \ echo "Is README updated accordingly? [ENTER to continue]"; \ read pass; \ \ echo "Updating CHANGELOG.md"; \ $(call DOCKER_RUN,$(LATEST_PHP),php build/update-changelog.php $$version CHANGELOG.md); \ \ git diff; \ echo "Commit and tag this? [ENTER to continue]"; \ read pass; \ \ set -x; \ git commit -am "Release $$version"; \ git tag -a "$$version" -m "Release $$version"; \ set +x; \ \ echo "Push? [ENTER to continue]"; \ read pass; \ set -x; git push --follow-tags; set +x; \ \ echo "Publish '$$version' as a Github release? [ENTER to continue]"; \ read pass; \ . ./.env; \ curl \ --user "$$GITHUB_USER:$$GITHUB_TOKEN" \ --request POST \ --header "Accept: application/vnd.github.v3+json" \ --data "{\"tag_name\":\"$$version\", \"target_commitish\": \"$$branch\", \"name\": \"$$version\", \"body\": \"See [CHANGELOG](CHANGELOG.md) for changes and release notes.\"}" \ https://api.github.com/repos/halaxa/json-machine/releases \ ;\ docker-run: ## Run a command in a latest JSON Machine PHP docker container. Ex.: make docker-run CMD="php -v" @$(call DOCKER_RUN,$(LATEST_PHP),$(CMD)) PK!l)0test/bootstrap.phpnu[createParser($json, $jsonPointer) as $key => $value) { $result[] = [$key => $value]; } $this->assertSame($expectedResult, $result); } public function data_testSyntax() { return [ ['', '{}', []], ['', '{"a": "b"}', [['a' => 'b']]], ['', '{"a":{"b":{"c":1}}}', [['a' => ['b' => ['c' => 1]]]]], ['', '[]', []], ['', '[null,true,false,"a",0,1,42.5]', [[0 => null], [1 => true], [2 => false], [3 => 'a'], [4 => 0], [5 => 1], [6 => 42.5]]], ['', '[{"c":1}]', [[['c' => 1]]]], ['', '[{"c":1},"string",{"d":2},false]', [[0 => ['c' => 1]], [1 => 'string'], [2 => ['d' => 2]], [3 => false]]], ['', '[false,{"c":1},"string",{"d":2}]', [[0 => false], [1 => ['c' => 1]], [2 => 'string'], [3 => ['d' => 2]]]], ['', '[{"c":1,"d":2}]', [[['c' => 1, 'd' => 2]]]], ['/', '{"":{"c":1,"d":2}}', [['c' => 1], ['d' => 2]]], ['/~0', '{"~":{"c":1,"d":2}}', [['c' => 1], ['d' => 2]]], ['/~1', '{"/":{"c":1,"d":2}}', [['c' => 1], ['d' => 2]]], ['/~01', '{"~1":{"c":1,"d":2}}', [['c' => 1], ['d' => 2]]], ['/~00', '{"~0":{"c":1,"d":2}}', [['c' => 1], ['d' => 2]]], ['/path', '{"path":{"c":1,"d":2}}', [['c' => 1], ['d' => 2]]], ['/path', '{"no":[null], "path":{"c":1,"d":2}}', [['c' => 1], ['d' => 2]]], ['/0', '[{"c":1,"d":2}, [null]]', [['c' => 1], ['d' => 2]]], ['/0/path', '[{"path":{"c":1,"d":2}}]', [['c' => 1], ['d' => 2]]], ['/1/path', '[[null], {"path":{"c":1,"d":2}}]', [['c' => 1], ['d' => 2]]], ['/path/0', '{"path":[{"c":1,"d":2}, [null]]}', [['c' => 1], ['d' => 2]]], ['/path/1', '{"path":[null,{"c":1,"d":2}, [null]]}', [['c' => 1], ['d' => 2]]], ['/path/to', '{"path":{"to":{"c":1,"d":2}}}', [['c' => 1], ['d' => 2]]], ['/path/after-vector', '{"path":{"array":[],"after-vector":{"c":1,"d":2}}}', [['c' => 1], ['d' => 2]]], ['/path/after-vector', '{"path":{"array":["item"],"after-vector":{"c":1,"d":2}}}', [['c' => 1], ['d' => 2]]], ['/path/after-vector', '{"path":{"object":{"item":null},"after-vector":{"c":1,"d":2}}}', [['c' => 1], ['d' => 2]]], ['/path/after-vectors', '{"path":{"array":[],"object":{},"after-vectors":{"c":1,"d":2}}}', [['c' => 1], ['d' => 2]]], ['/0/0', '[{"0":{"c":1,"d":2}}]', [['c' => 1], ['d' => 2]]], ['/1/1', '[0,{"1":{"c":1,"d":2}}]', [['c' => 1], ['d' => 2]]], 'PR-19-FIX' => ['/datafeed/programs/1', file_get_contents(__DIR__.'/PR-19-FIX.json'), [['program_info' => ['id' => 'X1']]]], 'ISSUE-41-FIX' => ['/path', '{"path":[{"empty":{}},{"value":1}]}', [[['empty' => []]], [1 => ['value' => 1]]]], ['/-', '[{"one": 1,"two": 2},{"three": 3,"four": 4}]', [['one' => 1], ['two' => 2], ['three' => 3], ['four' => 4]]], ['/zero/-', '{"zero":[{"one": 1,"two": 2},{"three": 3,"four": 4}]}', [['one' => 1], ['two' => 2], ['three' => 3], ['four' => 4]]], ['/zero/-/three', '{"zero":[{"one": 1,"two": 2},{"three": 3,"four": 4}]}', [['three' => 3]]], 'ISSUE-62#1' => ['/-/id', '[ {"id":125}, {"id":785}, {"id":459}, {"id":853} ]', [['id' => 125], ['id' => 785], ['id' => 459], ['id' => 853]]], 'ISSUE-62#2' => ['/key/-/id', '{"key": [ {"id":125}, {"id":785}, {"id":459}, {"id":853} ]}', [['id' => 125], ['id' => 785], ['id' => 459], ['id' => 853]]], [ ['/meta_data', '/data/companies'], '{"meta_data": {"total_rows": 2},"data": {"type": "companies","companies": [{"id": "1","company": "Company 1"},{"id": "2","company": "Company 2"}]}}', [ ['total_rows' => 2], ['0' => ['id' => '1', 'company' => 'Company 1']], ['1' => ['id' => '2', 'company' => 'Company 2']], ], ], [ ['/-/id', '/-/company'], '[{"id": "1","company": "Company 1"},{"id": "2","company": "Company 2"}]', [ ['id' => '1'], ['company' => 'Company 1'], ['id' => '2'], ['company' => 'Company 2'], ], ], [ ['/-/id', '/0/company'], '[{"id": "1","company": "Company 1"},{"id": "2","company": "Company 2"}]', [ ['id' => '1'], ['company' => 'Company 1'], ['id' => '2'], ], ], ]; } /** * @dataProvider data_testThrowsOnNotFoundJsonPointer * * @param string $json * @param string $jsonPointer */ public function testThrowsOnNotFoundJsonPointer($json, $jsonPointer) { $parser = $this->createParser($json, $jsonPointer); $this->expectException(PathNotFoundException::class); $this->expectExceptionMessage("Paths '".implode(', ', (array) $jsonPointer)."' were not found in json stream."); iterator_to_array($parser); } public function data_testThrowsOnNotFoundJsonPointer() { return [ 'non existing pointer' => ['{}', '/not/found'], "empty string should not match '0'" => ['{"0":[]}', '/'], 'empty string should not match 0' => ['[[]]', '/'], '0 should not match empty string' => ['{"":[]}', '/0'], ]; } /** * @dataProvider data_testSyntaxError * * @param string $malformedJson */ public function testSyntaxError($malformedJson) { $this->expectException(SyntaxErrorException::class); iterator_to_array($this->createParser($malformedJson)); } public function data_testSyntaxError() { return [ ['[}'], ['{]'], ['null'], ['true'], ['false'], ['0'], ['100'], ['"string"'], ['}'], [']'], [','], [':'], [''], ['[null null]'], ['["string" "string"]'], ['[,"string","string"]'], ['["string",,"string"]'], ['["string","string",]'], ['["string",1eeee1]'], ['{"key\u000Z": "non hex key"}'], ]; } /** * @dataProvider data_testUnexpectedEndError * * @param string $malformedJson */ public function testUnexpectedEndError($malformedJson) { $this->expectException(UnexpectedEndSyntaxErrorException::class); iterator_to_array($this->createParser($malformedJson)); } public function data_testUnexpectedEndError() { return [ ['['], ['{'], ['["string"'], ['["string",'], ['[{"string":"string"}'], ['[{"string":"string"},'], ['[{"string":"string"},{'], ['[{"string":"string"},{"str'], ['[{"string":"string"},{"string"'], ['{"string"'], ['{"string":'], ['{"string":"string"'], ['{"string":["string","string"]'], ['{"string":["string","string"'], ['{"string":["string","string",'], ['{"string":["string","string","str'], ]; } public function testGeneratorQuitsAfterFirstFoundCollectionHasBeenFinished() { $json = ' { "results": [1], "other": [2], "results": [3] } '; $parser = $this->createParser($json, '/results'); $this->assertSame([1], iterator_to_array($parser)); } public function testScalarResult() { $result = $this->createParser('{"result":{"items": [1,2,3],"count": 3}}', '/result/count'); $this->assertSame([3], iterator_to_array($result)); } public function testScalarResultInArray() { $result = $this->createParser('{"result":[1,2,3]}', '/result/0'); $this->assertSame([1], iterator_to_array($result)); } public function testGeneratorQuitsAfterFirstScalarHasBeenFound() { $json = ' { "result": "one", "other": [2], "result": "three" } '; $parser = $this->createParser($json, '/result'); $this->assertSame(['result' => 'one'], iterator_to_array($parser)); } public function testGeneratorYieldsNestedValues() { $json = ' { "zero": [ { "one": "ignored", "two": [ { "three": 1 } ], "four": [ { "five": "ignored" } ] }, { "one": 1, "two": [ { "three": 2 }, { "three": 3 } ], "four": [ { "five": "ignored" } ] } ] } '; $parser = $this->createParser($json, '/zero/-/two/-/three'); $i = 0; $expectedKey = 'three'; $expectedValues = [1, 2, 3]; foreach ($parser as $key => $value) { $this->assertSame($expectedKey, $key); $this->assertSame($expectedValues[$i++], $value); } } private function createParser($json, $jsonPointer = '') { return new Parser(new Tokens(new \ArrayIterator([$json])), $jsonPointer, new ExtJsonDecoder(true)); } public function testDefaultDecodingStructureIsObject() { $items = new Parser(new Tokens(new StringChunks('[{"key": "value"}]'))); foreach ($items as $item) { $this->assertEquals((object) ['key' => 'value'], $item); } } /** * @dataProvider data_testGetCurrentJsonPointer */ public function testGetCurrentJsonPointer($jsonPointer, string $json, array $currentJsonPointers) { $parser = $this->createParser($json, $jsonPointer); $i = 0; foreach ($parser as $value) { $this->assertEquals($currentJsonPointers[$i++], $parser->getCurrentJsonPointer()); } } public function data_testGetCurrentJsonPointer() { return [ ['', '{"c":1,"d":2}', ['', '']], ['/', '{"":{"c":1,"d":2}}', ['/', '/']], ['/~0', '{"~":{"c":1,"d":2}}', ['/~0', '/~0']], ['/~1', '{"/":{"c":1,"d":2}}', ['/~1', '/~1']], ['/~01', '{"~1":{"c":1,"d":2}}', ['/~01', '/~01']], ['/~00', '{"~0":{"c":1,"d":2}}', ['/~00', '/~00']], ['/~1/c', '{"/":{"c":[1,2],"d":2}}', ['/~1/c', '/~1/c']], ['/0', '[{"c":1,"d":2}, [null]]', ['/0', '/0']], ['/-', '[{"one": 1,"two": 2},{"three": 3,"four": 4}]', ['/0', '/0', '/1', '/1']], [ ['/two', '/four'], '{"one": [1,11], "two": [2,22], "three": [3,33], "four": [4,44]}', ['/two', '/two', '/four', '/four'], ], [ ['/-/two', '/-/one'], '[{"one": 1, "two": 2}, {"one": 1, "two": 2}]', ['/0/one', '/0/two', '/1/one', '/1/two'], ], ]; } /** * @dataProvider data_testGetMatchedJsonPointer */ public function testGetMatchedJsonPointer($jsonPointer, string $json, array $matchedJsonPointers) { $parser = $this->createParser($json, $jsonPointer); $i = 0; foreach ($parser as $value) { $this->assertEquals($matchedJsonPointers[$i++], $parser->getMatchedJsonPointer()); } } public function data_testGetMatchedJsonPointer() { return [ ['', '{"c":1,"d":2}', ['', '']], ['/', '{"":{"c":1,"d":2}}', ['/', '/']], ['/~0', '{"~":{"c":1,"d":2}}', ['/~0', '/~0']], ['/~1', '{"/":{"c":1,"d":2}}', ['/~1', '/~1']], ['/~01', '{"~1":{"c":1,"d":2}}', ['/~01', '/~01']], ['/~00', '{"~0":{"c":1,"d":2}}', ['/~00', '/~00']], ['/~1/c', '{"/":{"c":[1,2],"d":2}}', ['/~1/c', '/~1/c']], ['/0', '[{"c":1,"d":2}, [null]]', ['/0', '/0']], ['/-', '[{"one": 1,"two": 2},{"three": 3,"four": 4}]', ['/-', '/-', '/-', '/-']], [ ['/two', '/four'], '{"one": [1,11], "two": [2,22], "three": [3,33], "four": [4,44]}', ['/two', '/two', '/four', '/four'], ], [ ['/-/two', '/-/one'], '[{"one": 1, "two": 2}, {"one": 1, "two": 2}]', ['/-/one', '/-/two', '/-/one', '/-/two'], ], ]; } public function testGetCurrentJsonPointerThrowsWhenCalledOutsideOfALoop() { $this->expectException(JsonMachineException::class); $this->expectExceptionMessage('must be called inside a loop'); $parser = $this->createParser('[]'); $parser->getCurrentJsonPointer(); } public function testGetCurrentJsonPointerReturnsLiteralJsonPointer() { $parser = $this->createParser('{"\"key\\\\":"value"}', ['/\"key\\\\']); foreach ($parser as $key => $item) { $this->assertSame('/\"key\\\\', $parser->getCurrentJsonPointer()); } } public function testGetMatchedJsonPointerThrowsWhenCalledOutsideOfALoop() { $this->expectException(JsonMachineException::class); $this->expectExceptionMessage('must be called inside a loop'); $parser = $this->createParser('[]'); $parser->getMatchedJsonPointer(); } public function testGetMatchedJsonPointerReturnsLiteralMatch() { $parser = $this->createParser('{"\"key\\\\":"value"}', ['/\"key\\\\']); foreach ($parser as $key => $item) { $this->assertSame('/\"key\\\\', $parser->getMatchedJsonPointer()); } } public function testGetJsonPointers() { $parser = $this->createParser('{}', ['/one', '/two']); $this->assertSame(['/one', '/two'], $parser->getJsonPointers()); $parser = $this->createParser('{}'); $this->assertSame([''], $parser->getJsonPointers()); } public function testJsonPointerReferenceTokenMatchesJsonMemberNameLiterally() { $parser = $this->createParser('{"\\"key":"value"}', ['/\\"key']); foreach ($parser as $key => $item) { $this->assertSame('"key', $key); $this->assertSame('value', $item); } } public function testGetPositionReturnsCorrectPositionWithDebugEnabled() { $parser = new Parser(new TokensWithDebugging(['[ 1, "two", false ]'])); $expectedPosition = [5, 12, 19]; $this->assertSame(0, $parser->getPosition()); foreach ($parser as $index => $item) { $this->assertSame($expectedPosition[$index], $parser->getPosition(), "index:$index, item:$item"); } $this->assertSame(21, $parser->getPosition()); } public function testGetPositionReturns0WithDebugDisabled() { $parser = new Parser(new Tokens(['[ 1, "two", false ]'])); $this->assertSame(0, $parser->getPosition()); foreach ($parser as $index => $item) { $this->assertSame(0, $parser->getPosition()); } $this->assertSame(0, $parser->getPosition()); } public function testGetPositionThrowsIfTokensDoNotSupportGetPosition() { $parser = new Parser(new \ArrayObject()); $this->expectException(JsonMachineException::class); $parser->getPosition(); } } PK!@GG'test/JsonMachineTest/FileChunksTest.phpnu[assertSame($expectedResult, $result); } public function data_testGeneratorYieldsFileChunks() { return [ [5, ['{"pat', 'h": {', '"key"', ':"val', 'ue"}}', "\n"]], [6, ['{"path', '": {"k', 'ey":"v', 'alue"}', '}'."\n"]], [1024, ['{"path": {"key":"value"}}'."\n"]], ]; } } PK!ZZ(test/JsonMachineTest/formatted-crlf.jsonnu[{ "id": 54640519019642880, "user": { "notifications": null }, "geo": "test" } PK!oˇ.test/JsonMachineTest/ValidJsonPointersTest.phpnu[expectException(InvalidArgumentException::class); $this->expectExceptionMessage("'$jsonPointers[0]', '$jsonPointers[1]'"); (new ValidJsonPointers($jsonPointers))->toArray(); } public function data_testThrowsOnIntersectingPaths() { return [ [['/companies/-/id', '/companies/0/id']], [['/companies/-/id', '', '/companies/0/id']], [['/companies/-/id', '']], [['/companies/0/id', '']], [['//in-empty-string-key', '/']], [['/~0~1/in-escaped-key', '/~0~1']], ]; } /** * @dataProvider data_testThrowsOnMalformedJsonPointer */ public function testThrowsOnMalformedJsonPointer(array $jsonPointer) { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('not valid'); (new ValidJsonPointers($jsonPointer))->toArray(); } public function data_testThrowsOnMalformedJsonPointer() { return [ [['apple']], [['/apple/~']], [['apple/pie']], [['apple/pie/']], [[' /apple/pie/']], [[ '/valid', '/valid/-', 'inv/alid', ]], ]; } public function testThrowsOnDuplicatePaths() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage("'/one', '/one'"); (new ValidJsonPointers(['/one', '/one']))->toArray(); } public function testToArrayReturnsJsonPointers() { $this->assertSame( ['/one', '/two'], (new ValidJsonPointers(['/one', '/two']))->toArray() ); } } PK!k#test/JsonMachineTest/PR-19-FIX.jsonnu[{ "datafeed": { "info": { "category": "Category name" }, "programs": [ { "program_info": { "id": "X0" } }, { "program_info": { "id": "X1" } } ] } } PK! K]])test/JsonMachineTest/StreamChunksTest.phpnu[expectException(InvalidArgumentException::class); new StreamChunks(false); } public function testGeneratorYieldsData() { $result = iterator_to_array(new StreamChunks(fopen('data://text/plain,test', 'r'))); $this->assertSame(['test'], $result); } } PK!ϔ`)test/JsonMachineTest/ItemsOptionsTest.phpnu[toArray(); $this->assertEquals($this->defaultOptions(), $optionsArray); } public function testHasArrayAccess() { $options = new ItemsOptions(); $this->assertTrue(isset($options['debug'])); $this->assertFalse($options['debug']); } /** * @dataProvider defaultOptionNames */ public function testThrowsOnInvalidOptionType($optionName) { $this->expectException(InvalidArgumentException::class); new ItemsOptions([$optionName => new InvalidValue()]); } public function defaultOptionNames() { foreach ($this->defaultOptions() as $name => $ignore) { yield [$name]; } } private function defaultOptions() { return [ 'pointer' => '', 'decoder' => new ExtJsonDecoder(), 'debug' => false, ]; } public function testThrowsOnUnknownOption() { $this->expectException(InvalidArgumentException::class); new ItemsOptions(['invalid_option_name' => 'value']); } } class InvalidValue { } PK!=(test/JsonMachineTest/AutoloadingTest.phpnu[createAutoloadableClass(); register_shutdown_function(function () use ($dummyFile) { @unlink($dummyFile); }); $autoloadersBackup = $this->unregisterCurrentAutoloaders(); $autoloader = require __DIR__.'/../../src/autoloader.php'; spl_autoload_register($autoloader); $autoloaded = class_exists('JsonMachine\\AutoloadStub'); spl_autoload_unregister($autoloader); $this->registerPreviousAutoloaders($autoloadersBackup); $this->assertTrue($autoloaded); } /** * @runInSeparateProcess */ public function testIgnoresInvalidBaseNamespace() { $dummyFile = $this->createAutoloadableClass(); register_shutdown_function(function () use ($dummyFile) { @unlink($dummyFile); }); $autoloadersBackup = $this->unregisterCurrentAutoloaders(); $autoloader = require __DIR__.'/../../src/autoloader.php'; spl_autoload_register($autoloader); $autoloaded = class_exists('XXXsonMachine\\AutoloadStub'); spl_autoload_unregister($autoloader); $this->registerPreviousAutoloaders($autoloadersBackup); $this->assertFalse($autoloaded); } private function createAutoloadableClass(): string { $dummyFile = __DIR__.'/../../src/AutoloadStub.php'; file_put_contents($dummyFile, ' $args[1], 'decoder' => $args[2], 'debug' => $args[3], ], ]); $this->assertSame($expected, iterator_to_array($iterator)); } public function testItemsYieldsObjectItemsByDefault() { $iterator = Items::fromString('{"path": {"key":"value"}}'); foreach ($iterator as $item) { $this->assertEquals((object) ['key' => 'value'], $item); } } public function data_testFactories() { $extJsonResult = ['key' => 'value']; $passThruResult = ['"key"' => '"value"']; $ptDecoder = new PassThruDecoder(); foreach ([true, false] as $debug) { foreach ([ [$extJsonResult, 'fromStream', fopen('data://text/plain,{"path": {"key":"value"}}', 'r'), '/path', null, $debug], [$extJsonResult, 'fromString', '{"path": {"key":"value"}}', '/path', null, $debug], [$extJsonResult, 'fromFile', __DIR__.'/ItemsTest.json', '/path', null, $debug], [$extJsonResult, 'fromIterable', ['{"path": {"key', '":"value"}}'], '/path', null, $debug], [$extJsonResult, 'fromIterable', new \ArrayIterator(['{"path": {"key', '":"value"}}']), '/path', null, $debug], [$passThruResult, 'fromStream', fopen('data://text/plain,{"path": {"key":"value"}}', 'r'), '/path', $ptDecoder, $debug], [$passThruResult, 'fromString', '{"path": {"key":"value"}}', '/path', $ptDecoder, $debug], [$passThruResult, 'fromFile', __DIR__.'/ItemsTest.json', '/path', $ptDecoder, $debug], [$passThruResult, 'fromIterable', ['{"path": {"key', '":"value"}}'], '/path', $ptDecoder, $debug], [$passThruResult, 'fromIterable', new \ArrayIterator(['{"path": {"key', '":"value"}}']), '/path', $ptDecoder, $debug], ] as $case) { yield $case; } } } public function testGetPositionDebugEnabled() { $expectedPosition = ['key1' => 9, 'key2' => 19]; $items = Items::fromString('{"key1":1, "key2":2} ', ['debug' => true]); foreach ($items as $key => $val) { $this->assertSame($expectedPosition[$key], $items->getPosition()); } } public function testIterationWithoutForeach() { $iterator = Items::fromString('{"key1":1, "key2":2}') ->getIterator(); $iterator->rewind(); $this->assertTrue($iterator->valid()); $this->assertSame(['key1', 1], [$iterator->key(), $iterator->current()]); $iterator->next(); $this->assertTrue($iterator->valid()); $this->assertSame(['key2', 2], [$iterator->key(), $iterator->current()]); $iterator->next(); $this->assertFalse($iterator->valid()); } public function testIsDebugEnabled() { $items = $iterator = Items::fromString('{}'); $this->assertFalse($items->isDebugEnabled()); $items = $iterator = Items::fromString('{}', ['debug' => true]); $this->assertTrue($items->isDebugEnabled()); } public function testGetCurrentJsonPointer() { $items = $iterator = Items::fromString( '[{"two": 2, "one": 1}]', ['pointer' => ['/-/one', '/-/two']] ); $iterator = $items->getIterator(); $iterator->rewind(); $iterator->current(); $this->assertSame('/0/two', $items->getCurrentJsonPointer()); $iterator->next(); $iterator->current(); $this->assertSame('/0/one', $items->getCurrentJsonPointer()); } public function testGetMatchedJsonPointer() { $items = $iterator = Items::fromString( '[{"two": 2, "one": 1}]', ['pointer' => ['/-/one', '/-/two']] ); $iterator = $items->getIterator(); $iterator->rewind(); $iterator->current(); $this->assertSame('/-/two', $items->getMatchedJsonPointer()); $iterator->next(); $iterator->current(); $this->assertSame('/-/one', $items->getMatchedJsonPointer()); } public function testGetJsonPointers() { $items = Items::fromString('[]', ['pointer' => ['/one', '/two']]); $this->assertSame(['/one', '/two'], $items->getJsonPointers()); } } PK!,;test/JsonMachineTest/Exception/SyntaxErrorExceptionTest.phpnu[assertContains('msg 42', $exception->getMessage()); $this->assertContains('24', $exception->getMessage()); } } PK!xZZ&test/JsonMachineTest/formatted-cr.jsonnu[{ "id": 54640519019642880, "user": { "notifications": null }, "geo": "test" } PK!ZZ&test/JsonMachineTest/formatted-lf.jsonnu[{ "id": 54640519019642880, "user": { "notifications": null }, "geo": "test" } PK!s5GG7test/JsonMachineTest/JsonDecoder/ExtJsonDecoderTest.phpnu[decode($json); $this->assertTrue('object' === gettype($defaultResult->getValue())); $this->assertFalse('string' === gettype($defaultResult->getValue()->bigint)); $this->assertSame([['deeper']], $defaultResult->getValue()->deep); } public function testPassesAssocTrueOptionToJsonDecode() { $json = '{"bigint": 123456789123456789123456789, "deep": [["deeper"]]}'; $assocDecoder = new ExtJsonDecoder(true); $assocResult = $assocDecoder->decode($json); $this->assertTrue('array' === gettype($assocResult->getValue())); } public function testPassesAssocFalseOptionToJsonDecode() { $json = '{"bigint": 123456789123456789123456789, "deep": [["deeper"]]}'; $objDecoder = new ExtJsonDecoder(false); $objResult = $objDecoder->decode($json); $this->assertTrue('object' === gettype($objResult->getValue())); } public function testPassesPassesDepthOptionToJsonDecode() { $json = '{"bigint": 123456789123456789123456789, "deep": [["deeper"]]}'; $depthDecoder = new ExtJsonDecoder(true, 1); $depthResult = $depthDecoder->decode($json); $this->assertFalse($depthResult->isOk()); $this->assertSame('Maximum stack depth exceeded', $depthResult->getErrorMessage()); } public function testPassesPassesBigIntOptionToJsonDecode() { $bigintDecoder = new ExtJsonDecoder(false, 1, JSON_BIGINT_AS_STRING); $bigintResult = $bigintDecoder->decode('123123123123123123123'); $this->assertSame('123123123123123123123', $bigintResult->getValue()); } } PK!ٕ6test/JsonMachineTest/JsonDecoder/DecodingErrorTest.phpnu[assertSame('"json\"', $decodingError->getMalformedJson()); } public function testGetErrorMessage() { $decodingError = new DecodingError('', 'something bad happened'); $this->assertSame('something bad happened', $decodingError->getErrorMessage()); } } PK! Ϋ4test/JsonMachineTest/JsonDecoder/ValidResultTest.phpnu[assertSame('Value X', $result->getValue()); } public function testIsOk() { $result = new ValidResult('X'); $this->assertTrue($result->isOk()); } } PK! N))6test/JsonMachineTest/JsonDecoder/InvalidResultTest.phpnu[assertSame('Error X', $result->getErrorMessage()); } public function testIsOk() { $result = new InvalidResult('X'); $this->assertFalse($result->isOk()); } } PK!_! =test/JsonMachineTest/JsonDecoder/ErrorWrappingDecoderTest.phpnu[decode('"json"'); $this->assertTrue($result->isOk()); $this->assertEquals($case['wrappedResult'], $result); } public function data_testCorrectlyWrapsResults() { $notOkResult = new InvalidResult('Error happened.'); $okResult = new ValidResult('json'); $wrappedNotOkResult = new ValidResult(new DecodingError('"json"', 'Error happened.')); $wrappedOkResult = $okResult; return [ [ [ 'result' => $notOkResult, 'wrappedResult' => $wrappedNotOkResult, ], ], [ [ 'result' => $okResult, 'wrappedResult' => $wrappedOkResult, ], ], ]; } public function testCatchesErrorInsideIteratedJsonChunk() { $json = /* @lang JSON */ ' { "results": [ {"correct": "correct"}, {"incorrect": nulll}, {"correct": "correct"} ] } '; $items = Items::fromString($json, [ 'pointer' => '/results', 'decoder' => new ErrorWrappingDecoder(new ExtJsonDecoder(true)), ]); $result = iterator_to_array($items); $this->assertSame('correct', $result[0]['correct']); $this->assertSame('correct', $result[2]['correct']); /** @var DecodingError $decodingError */ $decodingError = $result[1]; $this->assertInstanceOf(DecodingError::class, $decodingError); $this->assertSame('{"incorrect":nulll}', $decodingError->getMalformedJson()); $this->assertSame('Syntax error', $decodingError->getErrorMessage()); } } PK!B8test/JsonMachineTest/JsonDecoder/PassThruDecoderTest.phpnu[decode('["json"]'); $this->assertSame('["json"]', $passThruResult->getValue()); } } PK!ʢ?cc0test/JsonMachineTest/JsonDecoder/StubDecoder.phpnu[decoded = $decoded; } public function decode($jsonValue) { return $this->decoded; } } PK!m?II)test/JsonMachineTest/StringChunksTest.phpnu[assertSame($expectedResult, $result); } public function data_testGeneratorYieldsStringChunks() { return [ // single-byte: ['onetwo', 6, ['onetwo']], ['onetwo', 7, ['onetwo']], ['onetwo', 3, ['one', 'two']], ['onetwo', 4, ['onet', 'wo']], ]; } } PK!:F7 7 #test/JsonMachineTest/TokensTest.phpnu[ [TokensWithDebugging::class], 'debug disabled' => [Tokens::class], ]; } /** * @dataProvider bothDebugModes */ public function testCorrectlyYieldsZeroToken($tokensClass) { $data = ['0']; $expected = ['0']; $this->assertEquals($expected, iterator_to_array(new $tokensClass(new \ArrayIterator($data)))); $stream = fopen('data://text/plain,{"value":0}', 'r'); $expected = ['{', '"value"', ':', '0', '}']; $this->assertEquals($expected, iterator_to_array(new $tokensClass(new StreamChunks($stream, 10)))); } /** * @dataProvider bothDebugModes */ public function testGeneratesTokens($tokensClass) { $data = ['{}[],:null,"string" false:', 'true,1,100000,1.555{-56]"","\\""']; $expected = ['{', '}', '[', ']', ',', ':', 'null', ',', '"string"', 'false', ':', 'true', ',', '1', ',', '100000', ',', '1.555', '{', '-56', ']', '""', ',', '"\\""']; $this->assertEquals($expected, iterator_to_array(new $tokensClass(new \ArrayIterator($data)))); } /** * @dataProvider bothDebugModes */ public function testWithBOM($tokensClass) { $data = ["\xEF\xBB\xBF".'{}']; $expected = ['{', '}']; $this->assertEquals($expected, iterator_to_array(new $tokensClass(new \ArrayIterator($data)))); } /** * @dataProvider bothDebugModes */ public function testCorrectlyParsesTwoBackslashesAtTheEndOfAString($tokensClass) { $this->assertEquals(['"test\\\\"', ':'], iterator_to_array(new $tokensClass(new \ArrayIterator(['"test\\\\":'])))); } /** * @dataProvider bothDebugModes */ public function testCorrectlyParsesEscapedQuotesInTheMiddleOfAString($tokensClass) { $json = '"test\"test":'; $expected = ['"test\"test"', ':']; $this->assertEquals($expected, iterator_to_array(new $tokensClass(new \ArrayIterator([$json])))); } /** * @dataProvider bothDebugModes */ public function testCorrectlyParsesChunksSplitBeforeStringEnd($tokensClass) { $chunks = ['{"path": {"key":"value', '"}}']; $expected = ['{', '"path"', ':', '{', '"key"', ':', '"value"', '}', '}']; $this->assertEquals($expected, iterator_to_array(new $tokensClass(new \ArrayIterator($chunks)))); } /** * @dataProvider bothDebugModes */ public function testCorrectlyParsesChunksSplitBeforeEscapedCharacter($tokensClass) { $chunks = ['{"path": {"key":"value\\', '""}}']; $expected = ['{', '"path"', ':', '{', '"key"', ':', '"value\""', '}', '}']; $this->assertEquals($expected, iterator_to_array(new $tokensClass(new \ArrayIterator($chunks)))); } /** * @dataProvider bothDebugModes */ public function testCorrectlyParsesChunksSplitAfterEscapedCharacter($tokensClass) { $chunks = ['{"path": {"key":"value\\"', '"}}']; $expected = ['{', '"path"', ':', '{', '"key"', ':', '"value\""', '}', '}']; $this->assertEquals($expected, iterator_to_array(new $tokensClass(new \ArrayIterator($chunks)))); } /** * @dataProvider bothDebugModes */ public function testAnyPossibleChunkSplit($tokensClass) { $json = ' { "datafeed": { "info": { "category": "Category name" }, "programs": [ { "program_info": { "id": "X0\"\\\\", "number": 123, "constant": false } }, { "program_info": { "id": "\b\f\n\r\t\u0020X1" } } ] } } '; $expected = [ '{', '"datafeed"', ':', '{', '"info"', ':', '{', '"category"', ':', '"Category name"', '}', ',', '"programs"', ':', '[', '{', '"program_info"', ':', '{', '"id"', ':', '"X0\\"\\\\"', ',', '"number"', ':', '123', ',', '"constant"', ':', 'false', '}', '}', ',', '{', '"program_info"', ':', '{', '"id"', ':', '"\b\f\n\r\t\u0020X1"', '}', '}', ']', '}', '}', ]; foreach (range(1, strlen($json)) as $chunkLength) { $chunks = str_split($json, $chunkLength); $result = iterator_to_array(new $tokensClass($chunks)); $this->assertSame($expected, $result); } } /** * @dataProvider jsonFilesWithDifferentLineEndings */ public function testProvidesLocationalDataWhenDebugEnabled(string $jsonFilePath) { $jsonFileContents = file_get_contents($jsonFilePath); $tokens = new TokensWithDebugging(new StringChunks($jsonFileContents)); $expectedTokens = $this->expectedTokens(); $i = 0; foreach ($tokens as $token) { ++$i; $expectedToken = array_shift($expectedTokens); $this->assertEquals($expectedToken[0], $token, 'token failed with expected token #'.$i); $this->assertEquals($expectedToken[1], $tokens->getLine(), 'line failed with expected token #'.$i); $this->assertEquals($expectedToken[2], $tokens->getColumn(), 'column failed with expected token #'.$i); } } /** * @dataProvider jsonFilesWithDifferentLineEndings */ public function testProvidesLocationalDataWhenDebugDisabled(string $jsonFilePath) { $tokens = new Tokens(new FileChunks($jsonFilePath)); $expectedTokens = $this->expectedTokens(); $i = 0; foreach ($tokens as $token) { ++$i; $expectedToken = array_shift($expectedTokens); $this->assertEquals($expectedToken[0], $token, 'token failed with expected token #'.$i); $this->assertEquals(1, $tokens->getLine(), 'line failed with expected token #'.$i); $this->assertEquals(0, $tokens->getColumn(), 'column failed with expected token #'.$i); } } public function testGetPositionWthDebugging() { $tokens = new TokensWithDebugging(['[ 1, "two", false ]']); $expectedPosition = [1, 5, 6, 12, 13, 19, 21]; $this->assertSame(0, $tokens->getPosition()); foreach ($tokens as $index => $item) { $this->assertSame($expectedPosition[$index], $tokens->getPosition(), "index:$index, item:$item"); } $this->assertSame(21, $tokens->getPosition()); } public function testGetPositionNoDebugging() { $tokens = new Tokens(['[ 1, "two", false ]']); $this->assertSame(0, $tokens->getPosition()); foreach ($tokens as $index => $item) { $this->assertSame(0, $tokens->getPosition(), "index:$index, item:$item"); } $this->assertSame(0, $tokens->getPosition()); } public function jsonFilesWithDifferentLineEndings() { return [ 'cr new lines' => [__DIR__.'/formatted-cr.json'], 'lf new lines' => [__DIR__.'/formatted-lf.json'], 'crlf new lines' => [__DIR__.'/formatted-crlf.json'], ]; } private function expectedTokens() { return [ // lexeme, line, column ['{', 1, 1], ['"id"', 2, 3], [':', 2, 7], ['54640519019642880', 2, 9], [',', 2, 26], ['"user"', 3, 3], [':', 3, 9], ['{', 3, 11], ['"notifications"', 4, 5], [':', 4, 20], ['null', 4, 22], ['}', 5, 3], [',', 5, 4], ['"geo"', 6, 3], [':', 6, 8], ['"test"', 6, 10], ['}', 7, 1], ]; } } PK!׮#test/JsonMachineTest/ItemsTest.jsonnu[{"path": {"key":"value"}} PK!q$test/performance/testPerformance.phpnu[ function ($file) { return Items::fromFile($file); }, 'Items::fromString()' => function ($file) { return Items::fromString(stream_get_contents(fopen($file, 'r'))); }, 'Items::fromFile() - debug' => function ($file) { return Items::fromFile($file, ['debug' => true]); }, 'Items::fromString() - debug' => function ($file) { return Items::fromString(stream_get_contents(fopen($file, 'r')), ['debug' => true]); }, 'json_decode()' => function ($file) { return json_decode(stream_get_contents(fopen($file, 'r')), true); }, ]; $tmpJsonFileName = createBigJsonFile(); $fileSizeMb = (filesize($tmpJsonFileName) / 1024 / 1024); echo 'File size: '.round($fileSizeMb, 2),' MB'.PHP_EOL; foreach ($decoders as $name => $decoder) { $start = microtime(true); $result = $decoder($tmpJsonFileName); if ( ! $result instanceof \Traversable && ! is_array($result)) { $textResult = 'Decoding error'; } else { foreach ($result as $key => $item) { } $time = microtime(true) - $start; $textResult = round($fileSizeMb / $time, 2).' MB/s'; } echo str_pad($name.': ', 37, '.')." $textResult".PHP_EOL; } @unlink($tmpJsonFileName); function createBigJsonFile() { $tmpJson = tempnam(sys_get_temp_dir(), 'json_'); $f = fopen($tmpJson, 'w'); $separator = ''; fputs($f, '['); for ($i = 0; $i < 6000; ++$i) { fputs($f, $separator); fputs($f, file_get_contents(__DIR__.'/twitter_example_'.($i % 2).'.json')); $separator = ",\n\n"; } fputs($f, ']'); fclose($f); return $tmpJson; } PK!='test/performance/twitter_example_1.jsonnu[{ "text": "RT @PostGradProblem: In preparation for the NFL lockout, I will be spending twice as much time analyzing my fantasy baseball team during ...", "truncated": true, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "favorited": false, "source": "Twitter for iPhone", "in_reply_to_screen_name": null, "in_reply_to_status_id_str": null, "id_str": "54691802283900928", "entities": { "user_mentions": [ { "indices": [ 3, 19 ], "screen_name": "PostGradProblem", "id_str": "271572434", "name": "PostGradProblems", "id": 271572434 } ], "urls": [ ], "hashtags": [ ] }, "contributors": null, "retweeted": false, "in_reply_to_user_id_str": null, "place": null, "retweet_count": 4, "created_at": "Sun Apr 03 23:48:36 +0000 2011", "retweeted_status": { "text": "In preparation for the NFL lockout, I will be spending twice as much time analyzing my fantasy baseball team during company time. #PGP", "truncated": false, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "favorited": false, "source": "HootSuite", "in_reply_to_screen_name": null, "in_reply_to_status_id_str": null, "id_str": "54640519019642881", "entities": { "user_mentions": [ ], "urls": [ ], "hashtags": [ { "text": "PGP", "indices": [ 130, 134 ] } ] }, "contributors": null, "retweeted": false, "in_reply_to_user_id_str": null, "place": null, "retweet_count": 4, "created_at": "Sun Apr 03 20:24:49 +0000 2011", "user": { "notifications": null, "profile_use_background_image": true, "statuses_count": 31, "profile_background_color": "C0DEED", "followers_count": 3066, "profile_image_url": "http://a2.twimg.com/profile_images/1285770264/PGP_normal.jpg", "listed_count": 6, "profile_background_image_url": "http://a3.twimg.com/a/1301071706/images/themes/theme1/bg.png", "description": "", "screen_name": "PostGradProblem", "default_profile": true, "verified": false, "time_zone": null, "profile_text_color": "333333", "is_translator": false, "profile_sidebar_fill_color": "DDEEF6", "location": "", "id_str": "271572434", "default_profile_image": false, "profile_background_tile": false, "lang": "en", "friends_count": 21, "protected": false, "favourites_count": 0, "created_at": "Thu Mar 24 19:45:44 +0000 2011", "profile_link_color": "0084B4", "name": "PostGradProblems", "show_all_inline_media": false, "follow_request_sent": null, "geo_enabled": false, "profile_sidebar_border_color": "C0DEED", "url": null, "id": 271572434, "contributors_enabled": false, "following": null, "utc_offset": null }, "id": 54640519019642880, "coordinates": null, "geo": null }, "user": { "notifications": null, "profile_use_background_image": true, "statuses_count": 351, "profile_background_color": "C0DEED", "followers_count": 48, "profile_image_url": "http://a1.twimg.com/profile_images/455128973/gCsVUnofNqqyd6tdOGevROvko1_500_normal.jpg", "listed_count": 0, "profile_background_image_url": "http://a3.twimg.com/a/1300479984/images/themes/theme1/bg.png", "description": "watcha doin in my waters?", "screen_name": "OldGREG85", "default_profile": true, "verified": false, "time_zone": "Hawaii", "profile_text_color": "333333", "is_translator": false, "profile_sidebar_fill_color": "DDEEF6", "location": "Texas", "id_str": "80177619", "default_profile_image": false, "profile_background_tile": false, "lang": "en", "friends_count": 81, "protected": false, "favourites_count": 0, "created_at": "Tue Oct 06 01:13:17 +0000 2009", "profile_link_color": "0084B4", "name": "GG", "show_all_inline_media": false, "follow_request_sent": null, "geo_enabled": false, "profile_sidebar_border_color": "C0DEED", "url": null, "id": 80177619, "contributors_enabled": false, "following": null, "utc_offset": -36000 }, "id": 54691802283900930, "coordinates": null, "geo": null } PK!($2$2'test/performance/twitter_example_0.jsonnu[{ "statuses": [ { "coordinates": null, "favorited": false, "truncated": false, "created_at": "Mon Sep 24 03:35:21 +0000 2012", "id_str": "250075927172759552", "entities": { "urls": [ ], "hashtags": [ { "text": "freebandnames", "indices": [ 20, 34 ] } ], "user_mentions": [ ] }, "in_reply_to_user_id_str": null, "contributors": null, "text": "Aggressive Ponytail #freebandnames", "metadata": { "iso_language_code": "en", "result_type": "recent" }, "retweet_count": 0, "in_reply_to_status_id_str": null, "id": 250075927172759552, "geo": null, "retweeted": false, "in_reply_to_user_id": null, "place": null, "user": { "profile_sidebar_fill_color": "DDEEF6", "profile_sidebar_border_color": "C0DEED", "profile_background_tile": false, "name": "Sean Cummings", "profile_image_url": "http://a0.twimg.com/profile_images/2359746665/1v6zfgqo8g0d3mk7ii5s_normal.jpeg", "created_at": "Mon Apr 26 06:01:55 +0000 2010", "location": "LA, CA", "follow_request_sent": null, "profile_link_color": "0084B4", "is_translator": false, "id_str": "137238150", "entities": { "url": { "urls": [ { "expanded_url": null, "url": "", "indices": [ 0, 0 ] } ] }, "description": { "urls": [ ] } }, "default_profile": true, "contributors_enabled": false, "favourites_count": 0, "url": null, "profile_image_url_https": "https://si0.twimg.com/profile_images/2359746665/1v6zfgqo8g0d3mk7ii5s_normal.jpeg", "utc_offset": -28800, "id": 137238150, "profile_use_background_image": true, "listed_count": 2, "profile_text_color": "333333", "lang": "en", "followers_count": 70, "protected": false, "notifications": null, "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme1/bg.png", "profile_background_color": "C0DEED", "verified": false, "geo_enabled": true, "time_zone": "Pacific Time (US & Canada)", "description": "Born 330 Live 310", "default_profile_image": false, "profile_background_image_url": "http://a0.twimg.com/images/themes/theme1/bg.png", "statuses_count": 579, "friends_count": 110, "following": null, "show_all_inline_media": false, "screen_name": "sean_cummings" }, "in_reply_to_screen_name": null, "source": "Twitter for Mac", "in_reply_to_status_id": null }, { "coordinates": null, "favorited": false, "truncated": false, "created_at": "Fri Sep 21 23:40:54 +0000 2012", "id_str": "249292149810667520", "entities": { "urls": [ ], "hashtags": [ { "text": "FreeBandNames", "indices": [ 20, 34 ] } ], "user_mentions": [ ] }, "in_reply_to_user_id_str": null, "contributors": null, "text": "Thee Namaste Nerdz. #FreeBandNames", "metadata": { "iso_language_code": "pl", "result_type": "recent" }, "retweet_count": 0, "in_reply_to_status_id_str": null, "id": 249292149810667520, "geo": null, "retweeted": false, "in_reply_to_user_id": null, "place": null, "user": { "profile_sidebar_fill_color": "DDFFCC", "profile_sidebar_border_color": "BDDCAD", "profile_background_tile": true, "name": "Chaz Martenstein", "profile_image_url": "http://a0.twimg.com/profile_images/447958234/Lichtenstein_normal.jpg", "created_at": "Tue Apr 07 19:05:07 +0000 2009", "location": "Durham, NC", "follow_request_sent": null, "profile_link_color": "0084B4", "is_translator": false, "id_str": "29516238", "entities": { "url": { "urls": [ { "expanded_url": null, "url": "http://bullcityrecords.com/wnng/", "indices": [ 0, 32 ] } ] }, "description": { "urls": [ ] } }, "default_profile": false, "contributors_enabled": false, "favourites_count": 8, "url": "http://bullcityrecords.com/wnng/", "profile_image_url_https": "https://si0.twimg.com/profile_images/447958234/Lichtenstein_normal.jpg", "utc_offset": -18000, "id": 29516238, "profile_use_background_image": true, "listed_count": 118, "profile_text_color": "333333", "lang": "en", "followers_count": 2052, "protected": false, "notifications": null, "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/9423277/background_tile.bmp", "profile_background_color": "9AE4E8", "verified": false, "geo_enabled": false, "time_zone": "Eastern Time (US & Canada)", "description": "You will come to Durham, North Carolina. I will sell you some records then, here in Durham, North Carolina. Fun will happen.", "default_profile_image": false, "profile_background_image_url": "http://a0.twimg.com/profile_background_images/9423277/background_tile.bmp", "statuses_count": 7579, "friends_count": 348, "following": null, "show_all_inline_media": true, "screen_name": "bullcityrecords" }, "in_reply_to_screen_name": null, "source": "web", "in_reply_to_status_id": null }, { "coordinates": null, "favorited": false, "truncated": false, "created_at": "Fri Sep 21 23:30:20 +0000 2012", "id_str": "249289491129438208", "entities": { "urls": [ ], "hashtags": [ { "text": "freebandnames", "indices": [ 29, 43 ] } ], "user_mentions": [ ] }, "in_reply_to_user_id_str": null, "contributors": null, "text": "Mexican Heaven, Mexican Hell #freebandnames", "metadata": { "iso_language_code": "en", "result_type": "recent" }, "retweet_count": 0, "in_reply_to_status_id_str": null, "id": 249289491129438208, "geo": null, "retweeted": false, "in_reply_to_user_id": null, "place": null, "user": { "profile_sidebar_fill_color": "99CC33", "profile_sidebar_border_color": "829D5E", "profile_background_tile": false, "name": "Thomas John Wakeman", "profile_image_url": "http://a0.twimg.com/profile_images/2219333930/Froggystyle_normal.png", "created_at": "Tue Sep 01 21:21:35 +0000 2009", "location": "Kingston New York", "follow_request_sent": null, "profile_link_color": "D02B55", "is_translator": false, "id_str": "70789458", "entities": { "url": { "urls": [ { "expanded_url": null, "url": "", "indices": [ 0, 0 ] } ] }, "description": { "urls": [ ] } }, "default_profile": false, "contributors_enabled": false, "favourites_count": 19, "url": null, "profile_image_url_https": "https://si0.twimg.com/profile_images/2219333930/Froggystyle_normal.png", "utc_offset": -18000, "id": 70789458, "profile_use_background_image": true, "listed_count": 1, "profile_text_color": "3E4415", "lang": "en", "followers_count": 63, "protected": false, "notifications": null, "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme5/bg.gif", "profile_background_color": "352726", "verified": false, "geo_enabled": false, "time_zone": "Eastern Time (US & Canada)", "description": "Science Fiction Writer, sort of. Likes Superheroes, Mole People, Alt. Timelines.", "default_profile_image": false, "profile_background_image_url": "http://a0.twimg.com/images/themes/theme5/bg.gif", "statuses_count": 1048, "friends_count": 63, "following": null, "show_all_inline_media": false, "screen_name": "MonkiesFist" }, "in_reply_to_screen_name": null, "source": "web", "in_reply_to_status_id": null }, { "coordinates": null, "favorited": false, "truncated": false, "created_at": "Fri Sep 21 22:51:18 +0000 2012", "id_str": "249279667666817024", "entities": { "urls": [ ], "hashtags": [ { "text": "freebandnames", "indices": [ 20, 34 ] } ], "user_mentions": [ ] }, "in_reply_to_user_id_str": null, "contributors": null, "text": "The Foolish Mortals #freebandnames", "metadata": { "iso_language_code": "en", "result_type": "recent" }, "retweet_count": 0, "in_reply_to_status_id_str": null, "id": 249279667666817024, "geo": null, "retweeted": false, "in_reply_to_user_id": null, "place": null, "user": { "profile_sidebar_fill_color": "BFAC83", "profile_sidebar_border_color": "615A44", "profile_background_tile": true, "name": "Marty Elmer", "profile_image_url": "http://a0.twimg.com/profile_images/1629790393/shrinker_2000_trans_normal.png", "created_at": "Mon May 04 00:05:00 +0000 2009", "location": "Wisconsin, USA", "follow_request_sent": null, "profile_link_color": "3B2A26", "is_translator": false, "id_str": "37539828", "entities": { "url": { "urls": [ { "expanded_url": null, "url": "http://www.omnitarian.me", "indices": [ 0, 24 ] } ] }, "description": { "urls": [ ] } }, "default_profile": false, "contributors_enabled": false, "favourites_count": 647, "url": "http://www.omnitarian.me", "profile_image_url_https": "https://si0.twimg.com/profile_images/1629790393/shrinker_2000_trans_normal.png", "utc_offset": -21600, "id": 37539828, "profile_use_background_image": true, "listed_count": 52, "profile_text_color": "000000", "lang": "en", "followers_count": 608, "protected": false, "notifications": null, "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/106455659/rect6056-9.png", "profile_background_color": "EEE3C4", "verified": false, "geo_enabled": false, "time_zone": "Central Time (US & Canada)", "description": "Cartoonist, Illustrator, and T-Shirt connoisseur", "default_profile_image": false, "profile_background_image_url": "http://a0.twimg.com/profile_background_images/106455659/rect6056-9.png", "statuses_count": 3575, "friends_count": 249, "following": null, "show_all_inline_media": true, "screen_name": "Omnitarian" }, "in_reply_to_screen_name": null, "source": "Twitter for iPhone", "in_reply_to_status_id": null } ], "search_metadata": { "max_id": 250126199840518145, "since_id": 24012619984051000, "refresh_url": "?since_id=250126199840518145&q=%23freebandnames&result_type=mixed&include_entities=1", "next_results": "?max_id=249279667666817023&q=%23freebandnames&count=4&include_entities=1&result_type=mixed", "count": 4, "completed_in": 0.035, "since_id_str": "24012619984051000", "query": "%23freebandnames", "max_id_str": "250126199840518145" } } PK!ac%%phpunit.xml.distnu[PK!3Zhh e.gitignorenu[PK!H.build/composer-lt-7.2.jsonnu[PK!36TTbuild/docker-run.shnuȯPK!cbuild/composer-update.shnuȯPK!Mbuild/build-image.shnuȯPK!g   build/update-changelog.phpnu[PK!U-<-<-  img/logo.pngnu[PK!6:img/wallpaper.pngnu[PK! 3"" img/logo.svgnu[PK!..img/github.pngnu[PK!tNexamples/guzzleHttp.phpnu[PK!,Pexamples/symfonyHttpClient.phpnu[PK!;pUhhnSexamples/memLeak.phpnu[PK!A4 Wcomposer.jsonnu[PK!5 V]CHANGELOG.mdnu[PK!ᶏllwsrc/autoloader.phpnu[PK!ԾR R T|src/Items.phpnu[PK!Trrsrc/ItemsOptions.phpnu[PK!Kjsrc/FileChunks.phpnu[PK!ʓsrc/PositionAware.phpnu[PK!DPUsrc/StreamChunks.phpnu[PK!L$uu&src/Exception/JsonMachineException.phpnu[PK!2^*src/Exception/InvalidArgumentException.phpnu[PK!8qv'fsrc/Exception/PathNotFoundException.phpnu[PK!R~]&=src/Exception/SyntaxErrorException.phpnu[PK!r 3src/Exception/UnexpectedEndSyntaxErrorException.phpnu[PK!wޏ!src/JsonDecoder/DecodingError.phpnu[PK!dcsrc/JsonDecoder/ItemDecoder.phpnu[PK!,"Сsrc/JsonDecoder/ExtJsonDecoder.phpnu[PK!!l!%src/JsonDecoder/InvalidResult.phpnu[PK!쩭#$src/JsonDecoder/PassThruDecoder.phpnu[PK!!`FF(Lsrc/JsonDecoder/ErrorWrappingDecoder.phpnu[PK!8usrc/JsonDecoder/ValidResult.phpnu[PK!* LLϬsrc/Tokens.phpnu[PK!eT$9$9Ysrc/Parser.phpnu[PK!"src/StringChunks.phpnu[PK!#src/TokensWithDebugging.phpnu[PK!<  src/ValidJsonPointers.phpnu[PK![yCR,R, vLICENSE.txtnu[PK!MD{ZZ >README.mdnu[PK!NK.php-cs-fixer.dist.phpnu[PK!.github/FUNDING.ymlnu[PK![n"ccH.github/workflows/makefile.ymlnu[PK!R Makefilenu[PK!l)0ũtest/bootstrap.phpnu[PK!\BB#test/JsonMachineTest/ParserTest.phpnu[PK!@GG' test/JsonMachineTest/FileChunksTest.phpnu[PK!ZZ(test/JsonMachineTest/formatted-crlf.jsonnu[PK!oˇ.]test/JsonMachineTest/ValidJsonPointersTest.phpnu[PK!k#btest/JsonMachineTest/PR-19-FIX.jsonnu[PK! K]])test/JsonMachineTest/StreamChunksTest.phpnu[PK!ϔ`)etest/JsonMachineTest/ItemsOptionsTest.phpnu[PK!=(test/JsonMachineTest/AutoloadingTest.phpnu[PK!z " test/JsonMachineTest/ItemsTest.phpnu[PK!,;C"test/JsonMachineTest/Exception/SyntaxErrorExceptionTest.phpnu[PK!xZZ&$test/JsonMachineTest/formatted-cr.jsonnu[PK!ZZ&t%test/JsonMachineTest/formatted-lf.jsonnu[PK!s5GG7$&test/JsonMachineTest/JsonDecoder/ExtJsonDecoderTest.phpnu[PK!ٕ6.test/JsonMachineTest/JsonDecoder/DecodingErrorTest.phpnu[PK! Ϋ41test/JsonMachineTest/JsonDecoder/ValidResultTest.phpnu[PK! N))6A4test/JsonMachineTest/JsonDecoder/InvalidResultTest.phpnu[PK!_! =6test/JsonMachineTest/JsonDecoder/ErrorWrappingDecoderTest.phpnu[PK!B8Atest/JsonMachineTest/JsonDecoder/PassThruDecoderTest.phpnu[PK!ʢ?cc0pCtest/JsonMachineTest/JsonDecoder/StubDecoder.phpnu[PK!m?II)3Etest/JsonMachineTest/StringChunksTest.phpnu[PK!:F7 7 #Htest/JsonMachineTest/TokensTest.phpnu[PK!׮#_itest/JsonMachineTest/ItemsTest.jsonnu[PK!q$itest/performance/testPerformance.phpnu[PK!='qtest/performance/twitter_example_1.jsonnu[PK!($2$2'test/performance/twitter_example_0.jsonnu[PKGG-e