diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..1fad350 --- /dev/null +++ b/.clang-format @@ -0,0 +1,193 @@ +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: None +AlignConsecutiveMacros: + Enabled: true + AcrossEmptyLines: true + AcrossComments: true +AlignConsecutiveAssignments: None +AlignConsecutiveBitFields: + Enabled: true + AcrossEmptyLines: true + AcrossComments: true +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Right +AlignOperands: Align +SortIncludes: false +InsertBraces: true # Control statements must have curly brackets +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: true +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: AllDefinitions +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: "^ IWYU pragma:" +QualifierAlignment: Leave +CompactNamespaces: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +PackConstructorInitializers: BinPack +BasedOnStyle: "" +ConstructorInitializerAllOnOneLineOrOnePerLine: false +AllowAllConstructorInitializersOnNextLine: true +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Preserve +IncludeCategories: + - Regex: "^<(.*)>" + Priority: 0 + - Regex: '^"(.*)"' + Priority: 1 + - Regex: "(.*)" + Priority: 2 +IncludeIsMainRegex: "(Test)?$" +IncludeIsMainSourceRegex: "" +IndentAccessModifiers: false +IndentCaseLabels: true +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentRequires: true +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +LambdaBodyIndentation: Signature +MacroBlockBegin: "" +MacroBlockEnd: "" +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PenaltyIndentedWhitespace: 0 +PointerAlignment: Left +PPIndentWidth: -1 +ReferenceAlignment: Pointer +ReflowComments: false +RemoveBracesLLVM: false +SeparateDefinitionBlocks: Always +ShortNamespaceLines: 1 +SortJavaStaticImport: Before +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false + AfterFunctionDeclarationName: false + AfterIfMacros: true + AfterOverloadedOperator: false + BeforeNonEmptyParentheses: false +SpaceAroundPointerQualifiers: Default +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +BitFieldColonSpacing: Both +Standard: Latest +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +--- + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4e4ac0..f43a9c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,6 @@ jobs: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} body: | - See the CHANGELOG.md + See the [CHANGELOG](CHANGELOG.md) draft: false prerelease: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index a872aad..4674b8a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,26 +24,29 @@ *.i *.txt !docs/*.txt +!CMakeLists.txt RTE/ -# IAR Settings -**/settings/*.crun -**/settings/*.dbgdt -**/settings/*.cspy -**/settings/*.cspy.* -**/settings/*.xcl -**/settings/*.dni -**/settings/*.wsdt -**/settings/*.wspos - -# IAR Debug Exe -**/Exe/*.sim - -# IAR Debug Obj -**/Obj/*.pbd -**/Obj/*.pbd.* -**/Obj/*.pbi -**/Obj/*.pbi.* +*debug + +# IAR Settings +**/settings/*.crun +**/settings/*.dbgdt +**/settings/*.cspy +**/settings/*.cspy.* +**/settings/*.xcl +**/settings/*.dni +**/settings/*.wsdt +**/settings/*.wspos + +# IAR Debug Exe +**/Exe/*.sim + +# IAR Debug Obj +**/Obj/*.pbd +**/Obj/*.pbd.* +**/Obj/*.pbi +**/Obj/*.pbi.* *.TMP /docs_src/x_Doxyfile.doxy @@ -69,6 +72,7 @@ RTE/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ +[Dd]ebug*/ x64/ x86/ bld/ @@ -76,6 +80,7 @@ bld/ [Oo]bj/ [Ll]og/ _build/ +build/ # Visual Studio 2015/2017 cache/options directory .vs/ @@ -274,7 +279,7 @@ ClientBin/ *.publishsettings orleans.codegen.cs -# Including strong name files can present a security risk +# Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk @@ -370,7 +375,7 @@ __pycache__/ # OpenCover UI analysis results OpenCover/ -# Azure Stream Analytics local run output +# Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log @@ -383,3 +388,13 @@ log_file.txt project.ioc mx.scratch *.tilen majerle + + +# Altium +Project outputs* +History/ +*.SchDocPreview +*.$$$Preview + +# VSCode projects +project_vscode_compiled.exe \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index f32f08f..4f457d1 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -1,22 +1,14 @@ { + "version": 4, "configurations": [ { + /* + * Full configuration is provided by CMake plugin for vscode, + * that shall be installed by user + */ "name": "Win32", - "includePath": [ - "${workspaceFolder}\\lwjson\\src\\include", - "${workspaceFolder}\\dev\\VisualStudio", - "${workspaceFolder}" - ], - "defines": [ - "_DEBUG", - "UNICODE", - "_UNICODE" - ], - "compilerPath": "C:\\MinGW\\bin\\gcc.exe", - "cStandard": "gnu17", - "cppStandard": "gnu++14", - "intelliSenseMode": "windows-gcc-x86" + "intelliSenseMode": "${default}", + "configurationProvider": "ms-vscode.cmake-tools" } - ], - "version": 4 + ] } \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..6a07920 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "twxs.cmake", + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index cd91592..c76ad22 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,26 +1,16 @@ { "version": "0.2.0", "configurations": [ - { - "name": "g++.exe - Launch program", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceFolder}\\Debug\\output.exe", - "args": [], - "stopAtEntry": true, - "cwd": "${workspaceFolder}", - "environment": [], - "externalConsole": false, - "MIMode": "gdb", - "miDebuggerPath": "C:\\MinGW\\bin\\gdb.exe", - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - } - ], - "preLaunchTask": "g++.exe - Launch program" - } + { + /* GDB must in be in the PATH environment */ + "name": "(Windows) Launch", + "type": "cppdbg", + "request": "launch", + "program": "${command:cmake.launchTargetPath}", + "args": [], + "stopAtEntry": false, + "cwd": "${fileDirname}", + "environment": [] + } ] - } \ No newline at end of file +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 83aff2f..e696cf0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,11 @@ { "files.associations": { + "lwevt_types.h": "c", + "lwevt_type.h": "c", + "lwevt.h": "c", + "string.h": "c", + "lwevt_opt.h": "c", "lwjson.h": "c" - } + }, + "esbonio.sphinx.confDir": "" } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2e9fb60..b15064b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,23 +4,12 @@ { "type": "cppbuild", "label": "Build project", - "command": "C:\\MinGW\\bin\\gcc.exe", - "args": [ - "-g", - "${workspaceFolder}\\lwjson\\src\\lwjson\\*.c", - "${workspaceFolder}\\dev\\VisualStudio\\main.c", - "${workspaceFolder}\\test\\*.c", - "-I${workspaceFolder}\\dev\\VisualStudio\\", - "-I${workspaceFolder}\\lwjson\\src\\include\\", - "-o", - "${workspaceFolder}\\Debug\\output.exe" - ], + "command": "cmake", + "args": ["--build", "${command:cmake.buildDirectory}", "-j", "8"], "options": { "cwd": "${workspaceFolder}" }, - "problemMatcher": [ - "$gcc" - ], + "problemMatcher": ["$gcc"], "group": { "kind": "build", "isDefault": true @@ -28,12 +17,57 @@ }, { "type": "shell", - "label": "Run built code", - "command": "${workspaceFolder}\\Debug\\output.exe", + "label": "Re-build project", + "command": "cmake", + "args": ["--build", "${command:cmake.buildDirectory}", "--clean-first", "-v", "-j", "8"], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": ["$gcc"], + }, + { + "type": "shell", + "label": "Clean project", + "command": "cmake", + "args": ["--build", "${command:cmake.buildDirectory}", "--target", "clean"], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "type": "shell", + "label": "Run application", + "command": "${command:cmake.launchTargetPath}", + "args": [], "problemMatcher": [], - "dependsOn": [ - "Build project" - ] - } + }, + { + "label": "Docs: Install python plugins from requirements.txt file", + "type": "shell", + "command": "python -m pip install -r requirements.txt", + "options": { + "cwd": "${workspaceFolder}/docs" + }, + "problemMatcher": [] + }, + { + "label": "Docs: Generate html", + "type": "shell", + "command": ".\\make html", + "options": { + "cwd": "${workspaceFolder}/docs" + }, + "problemMatcher": [] + }, + { + "label": "Docs: Clean build directory", + "type": "shell", + "command": ".\\make clean", + "options": { + "cwd": "${workspaceFolder}/docs" + }, + "problemMatcher": [] + }, ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4041a17..ec0f1b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Develop +## 1.6.0 + +- Split CMakeLists.txt files between library and executable +- Change license year to 2022 +- Fix GCC warning for incompatible comparison types +- Update code style with astyle +- Add support for stream parsing - first version +- Add `.clang-format` +- Add `lwjsonSTREAMDONE` return code when streamer well parsed some JSON and reached end of string +- Add option to reset stream state machine + ## 1.5.0 - Add string compare feature diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..02fa3a3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.22) + +# Setup project +project(LwLibPROJECT) + +if(NOT PROJECT_IS_TOP_LEVEL) + add_subdirectory(lwjson) +else() + # Set as executable + add_executable(${PROJECT_NAME}) + + # Add key executable block + target_sources(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/dev/main.c + ${CMAKE_CURRENT_LIST_DIR}/test/test.c + ${CMAKE_CURRENT_LIST_DIR}/examples/example_minimal.c + ${CMAKE_CURRENT_LIST_DIR}/examples/example_traverse.c + ${CMAKE_CURRENT_LIST_DIR}/examples/example_stream.c + ) + + # Add key include paths + target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/dev + ) + + # Compilation definition information + target_compile_definitions(${PROJECT_NAME} PUBLIC + WIN32 + _DEBUG + CONSOLE + LWJSON_DEV + ) + + # Compiler options + target_compile_options(${PROJECT_NAME} PRIVATE + -Wall + -Wextra + -Wpedantic + ) + + # Add subdir with lwjson and link to project + add_subdirectory(lwjson) + target_link_libraries(${PROJECT_NAME} lwjson) + target_link_libraries(${PROJECT_NAME} lwjson_debug) +endif() \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..f99154c --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,40 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "default", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "Win32-Debug", + "inherits": "default", + "toolchainFile": "${sourceDir}/cmake/i686-w64-mingw32-gcc.cmake", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "Win64-Debug", + "inherits": "default", + "toolchainFile": "${sourceDir}/cmake/x86_64-w64-mingw32-gcc.cmake", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + } + ], + "buildPresets": [ + { + "name": "Win32-Debug", + "configurePreset": "Win32-Debug" + }, + { + "name": "Win64-Debug", + "configurePreset": "Win64-Debug" + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index aa60317..5625f63 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Tilen MAJERLE +Copyright (c) 2022 Tilen MAJERLE Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e968961..7fe1c98 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Lightweight JSON text parser -Library provides generic JSON text parser. +Library provides generic JSON text parser, that is optimized for embedded systems. +Supports `streaming` parsing or classic parsing with full JSON data available in one big linear memory. +First one being optimized for ultra small microcontrollers, second one being ready for PC applications - or simply when several kB of RAM memory is available at any given point of time

Read first: Documentation

@@ -12,6 +14,7 @@ Library provides generic JSON text parser. * No recursion during parse operation * Re-entrant functions * Zero-copy, no ``malloc`` or ``free`` functions used +* Supports streaming parsing as secondary option * Optional support for inline comments with `/* comment... */` syntax between any *blank* region of input string * Advanced find algorithm for tokens * Test coverage is available @@ -28,4 +31,4 @@ Fresh contributions are always welcome. Simple instructions to proceed:: Alternatively you may: 1. Report a bug -2. Ask for a feature request \ No newline at end of file +2. Ask for a feature request diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..404fec6 --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +# TODO + +- Stream parser: split large strings to multiple calbacks +- Stream parser: ignore comments (optional) \ No newline at end of file diff --git a/cmake/i686-w64-mingw32-gcc.cmake b/cmake/i686-w64-mingw32-gcc.cmake new file mode 100644 index 0000000..334d580 --- /dev/null +++ b/cmake/i686-w64-mingw32-gcc.cmake @@ -0,0 +1,7 @@ +set(CMAKE_SYSTEM_NAME Windows) + +# Some default GCC settings +set(CMAKE_C_COMPILER i686-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++) + +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) diff --git a/cmake/x86_64-w64-mingw32-gcc.cmake b/cmake/x86_64-w64-mingw32-gcc.cmake new file mode 100644 index 0000000..1d82433 --- /dev/null +++ b/cmake/x86_64-w64-mingw32-gcc.cmake @@ -0,0 +1,7 @@ +set(CMAKE_SYSTEM_NAME Windows) + +# Some default GCC settings +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) + +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) diff --git a/dev/VisualStudio/main.c b/dev/VisualStudio/main.c deleted file mode 100644 index fc496b2..0000000 --- a/dev/VisualStudio/main.c +++ /dev/null @@ -1,83 +0,0 @@ -#include -#include -#include -#include "windows.h" -#include "lwjson/lwjson.h" - -static lwjson_token_t tokens[4096]; -static lwjson_t lwjson; - -extern void test_run(void); -extern void example_minimal_run(void); -extern void example_traverse_run(void); - -int -main() { - HANDLE f; - DWORD file_size; - size_t token_cnt = 0; - char* json_text = NULL; - const lwjson_token_t* tkn; - - test_run(); - //example_minimal_run(); - //example_traverse_run(); - return 0; - - printf("\n---\n"); - /* Init JSON */ - lwjson_init(&lwjson, tokens, LWJSON_ARRAYSIZE(tokens)); - - f = CreateFile(TEXT("..\\..\\test\\json\\custom.json"), - GENERIC_READ, // open for reading - 0, // do not share - NULL, // no security - OPEN_EXISTING, // existing file only - FILE_ATTRIBUTE_NORMAL, // normal file - NULL); // no attr. template - - if (f == INVALID_HANDLE_VALUE) { - printf("Could not open file..\r\n"); - goto exit; - } - file_size = GetFileSize(f, NULL); - if (file_size == INVALID_FILE_SIZE) { - printf("Invalid file size..\r\n"); - goto exit; - } else if (file_size == 0) { - printf("File is empty..\r\n"); - goto exit; - } - json_text = calloc((size_t)(file_size + 1), sizeof(*json_text)); - if (json_text == NULL) { - printf("Could not allocate memory..\r\n"); - goto exit; - } - if (ReadFile(f, json_text, file_size, NULL, NULL) == 0) { - printf("Could not read full file..\r\n"); - goto exit; - } - - /* Start parsing */ - if (lwjson_parse(&lwjson, json_text) != lwjsonOK) { - printf("Could not parse input json\r\n"); - goto exit; - } - - /* Dump result */ - lwjson_print_json(&lwjson); - - /* Find token if exists */ - if ((tkn = lwjson_find(&lwjson, "obj.obj2.key1")) != NULL) { - printf("Found requested token path\r\n"); - lwjson_print_token(tkn); - } else { - printf("Could not find requested token path..\r\n"); - } -exit: - if (json_text != NULL) { - free(json_text); - json_text = NULL; - } - return 0; -} diff --git a/dev/VisualStudio/lwjson_dev.sln b/dev/lwjson_dev.sln similarity index 100% rename from dev/VisualStudio/lwjson_dev.sln rename to dev/lwjson_dev.sln diff --git a/dev/VisualStudio/lwjson_dev.vcxproj b/dev/lwjson_dev.vcxproj similarity index 94% rename from dev/VisualStudio/lwjson_dev.vcxproj rename to dev/lwjson_dev.vcxproj index 6c6711d..adbbd6e 100644 --- a/dev/VisualStudio/lwjson_dev.vcxproj +++ b/dev/lwjson_dev.vcxproj @@ -72,7 +72,7 @@ true - ..\..\lwjson\src\include\;.;$(IncludePath) + ..\lwjson\src\include\;.;$(IncludePath) true @@ -143,11 +143,11 @@ - - - - - + + + + + diff --git a/dev/VisualStudio/lwjson_dev.vcxproj.filters b/dev/lwjson_dev.vcxproj.filters similarity index 83% rename from dev/VisualStudio/lwjson_dev.vcxproj.filters rename to dev/lwjson_dev.vcxproj.filters index 10e9bd6..f5326a2 100644 --- a/dev/VisualStudio/lwjson_dev.vcxproj.filters +++ b/dev/lwjson_dev.vcxproj.filters @@ -24,19 +24,19 @@ Source Files - + Source Files - + Source Files - + Source Files - + Source Files\Examples - + Source Files\Examples diff --git a/dev/VisualStudio/lwjson_opts.h b/dev/lwjson_opts.h similarity index 96% rename from dev/VisualStudio/lwjson_opts.h rename to dev/lwjson_opts.h index 2a27683..80fc50f 100644 --- a/dev/VisualStudio/lwjson_opts.h +++ b/dev/lwjson_opts.h @@ -4,7 +4,7 @@ */ /* - * Copyright (c) 2020 Tilen MAJERLE + * Copyright (c) 2022 Tilen MAJERLE * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -29,7 +29,7 @@ * This file is part of LwJSON - Lightweight JSON format parser. * * Author: Tilen MAJERLE - * Version: v1.5.0 + * Version: v1.6.0 */ #ifndef LWJSON_HDR_OPTS_H #define LWJSON_HDR_OPTS_H diff --git a/dev/main.c b/dev/main.c new file mode 100644 index 0000000..e293e6e --- /dev/null +++ b/dev/main.c @@ -0,0 +1,335 @@ +#include +#include +#include +#include "windows.h" +#include "lwjson/lwjson.h" + +/* Classic parser */ +static lwjson_token_t tokens[4096]; +static lwjson_t lwjson; + +/* Stream parser */ +static lwjson_stream_parser_t stream_parser; + +extern void test_run(void); +extern void example_minimal_run(void); +extern void example_traverse_run(void); +extern void example_stream_run(void); + +static void jsp_stream_callback(lwjson_stream_parser_t* jsp, lwjson_stream_type_t type); + +int +main() { + HANDLE f; + DWORD file_size; + size_t token_cnt = 0; + char* json_text = NULL; + const lwjson_token_t* tkn; + + (void)token_cnt; +#if 0 + test_run(); + example_minimal_run(); + example_traverse_run(); + example_stream_run(); + return 0; +#endif + + printf("\n---\n"); + /* Init JSON */ + lwjson_init(&lwjson, tokens, LWJSON_ARRAYSIZE(tokens)); + + f = CreateFile(TEXT("test\\json\\custom_stream.json"), + GENERIC_READ, // open for reading + 0, // do not share + NULL, // no security + OPEN_EXISTING, // existing file only + FILE_ATTRIBUTE_NORMAL, // normal file + NULL); // no attr. template + + if (f == INVALID_HANDLE_VALUE) { + printf("Could not open file..\r\n"); + goto exit; + } + if ((file_size = GetFileSize(f, NULL)) == INVALID_FILE_SIZE) { + printf("Invalid file size..\r\n"); + goto exit; + } else if (file_size == 0) { + printf("File is empty..\r\n"); + goto exit; + } + if ((json_text = calloc((size_t)(file_size + 1), sizeof(*json_text))) == NULL) { + printf("Could not allocate memory..\r\n"); + goto exit; + } + if (ReadFile(f, json_text, file_size, NULL, NULL) == 0) { + printf("Could not read full file..\r\n"); + goto exit; + } + + /* Now parse as a stream */ + lwjson_stream_init(&stream_parser, jsp_stream_callback); + for (const char* str = json_text; str != NULL && *str != '\0'; ++str) { + lwjsonr_t res = lwjson_stream_parse(&stream_parser, *str); + if (res == lwjsonSTREAMWAITFIRSTCHAR) { + printf("Waiting valid JSON start\r\n"); + } else if (res == lwjsonSTREAMINPROG) { + //printf("Stream in progress...\r\n"); + } else if (res == lwjsonSTREAMDONE) { + printf("Stream is completed\r\n"); + } else { + printf("Unknown error...\r\n"); + break; + } + } + return 0; + + /* Start parsing */ + printf("Parsing JSON with full text\r\n"); + if (lwjson_parse(&lwjson, json_text) != lwjsonOK) { + printf("Could not parse input JSON\r\n"); + goto exit; + } + printf("Full JSON parsed\r\n"); + + /* Dump result */ + lwjson_print_json(&lwjson); + + /* Find token if exists */ + if ((tkn = lwjson_find(&lwjson, "obj.obj2.key1")) != NULL) { + printf("Found requested token path\r\n"); + lwjson_print_token(tkn); + } else { + printf("Could not find requested token path..\r\n"); + } +exit: + if (json_text != NULL) { + free(json_text); + json_text = NULL; + } + return 0; +} + +/** + * \brief Stream calback demo + * \param jsp: JSON Stream parser object + * \param type: Primitive type + */ +static void +jsp_stream_callback(lwjson_stream_parser_t* jsp, lwjson_stream_type_t type) { + (void)type; + + /* + * Very long string demo has been added to the weather object, + * in order to test string field that is longer than maximal allowed length. + * + * This will generate multiple calls for same key. + * + * For test, we use base64_encoded string - but it can be anything + */ + if (jsp->stack_pos == 2 && jsp->stack[0].type == LWJSON_STREAM_TYPE_OBJECT + && jsp->stack[1].type == LWJSON_STREAM_TYPE_KEY) { + if (strcmp(jsp->stack[1].meta.name, "base64_str") == 0) { + printf("Base64_string. Block len: %d, total len: %d, is_last: %d, data: %.*s\r\n", + (int)jsp->data.str.buff_pos, (int)jsp->data.str.buff_total_pos, (int)jsp->data.str.is_last, + (int)jsp->data.str.buff_pos, jsp->data.str.buff); + } + } + + /* + * Take care of key-value pairs immediately at the start of object + * + * Values such as latitude and longitude are parsed here + */ + if (jsp->stack_pos == 2 && jsp->stack[0].type == LWJSON_STREAM_TYPE_OBJECT + && jsp->stack[1].type == LWJSON_STREAM_TYPE_KEY) { + + if (strcmp(jsp->stack[1].meta.name, "lat") == 0) { + printf("Latitude weather: %f\r\n", strtod(jsp->data.prim.buff, NULL)); + } else if (strcmp(jsp->stack[1].meta.name, "lon") == 0) { + printf("Longitude weather: %f\r\n", strtod(jsp->data.prim.buff, NULL)); + } else if (strcmp(jsp->stack[1].meta.name, "timezone_offset") == 0) { + printf("Timezone offset: %u\r\n", (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[1].meta.name, "timezone") == 0) { + printf("Timezone: %s\r\n", jsp->data.prim.buff); + } + } + + /* + * Handle current object - single object with multiple key-value pairs + */ + if (jsp->stack_pos >= 4 && jsp->stack[0].type == LWJSON_STREAM_TYPE_OBJECT + && jsp->stack[1].type == LWJSON_STREAM_TYPE_KEY && jsp->stack[2].type == LWJSON_STREAM_TYPE_OBJECT + && jsp->stack[3].type == LWJSON_STREAM_TYPE_KEY) { + /* Check for current weather */ + if (strcmp(jsp->stack[1].meta.name, "current") == 0) { + /* + * Process the "weather part" + */ + if (jsp->stack_pos >= 7 && jsp->stack[4].type == LWJSON_STREAM_TYPE_ARRAY + && jsp->stack[5].type == LWJSON_STREAM_TYPE_OBJECT && jsp->stack[6].type == LWJSON_STREAM_TYPE_KEY + && strcmp(jsp->stack[3].meta.name, "weather") == 0) { + + if (strcmp(jsp->stack[6].meta.name, "id") == 0) { + printf("Current weather %d id: %u\r\n", (int)jsp->stack[4].meta.index, + (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[6].meta.name, "main") == 0) { + printf("Current weather %d main: %s\r\n", (int)jsp->stack[4].meta.index, jsp->data.str.buff); + } else if (strcmp(jsp->stack[6].meta.name, "description") == 0) { + printf("Current weather %d description: %s\r\n", (int)jsp->stack[4].meta.index, jsp->data.str.buff); + } else if (strcmp(jsp->stack[6].meta.name, "icon") == 0) { + printf("Current weather %d icon: %s\r\n", (int)jsp->stack[4].meta.index, jsp->data.str.buff); + } + } else if (strcmp(jsp->stack[3].meta.name, "dt") == 0) { + printf("Current weather dt: %u\r\n", (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[3].meta.name, "sunrise") == 0) { + printf("Current weather sunrise: %u\r\n", (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[3].meta.name, "sunset") == 0) { + printf("Current weather sunset: %u\r\n", (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[3].meta.name, "temp") == 0) { + printf("Current weather temp: %f\r\n", strtod(jsp->data.prim.buff, NULL)); + } else if (strcmp(jsp->stack[3].meta.name, "feels_like") == 0) { + printf("Current weather feels_like: %f\r\n", strtod(jsp->data.prim.buff, NULL)); + } else if (strcmp(jsp->stack[3].meta.name, "pressure") == 0) { + printf("Current weather pressure: %u\r\n", (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[3].meta.name, "humidity") == 0) { + printf("Current weather humidity: %u\r\n", (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[3].meta.name, "uvi") == 0) { + printf("Current weather uvi: %u\r\n", (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[3].meta.name, "clouds") == 0) { + printf("Current weather clouds: %u\r\n", (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[3].meta.name, "visibility") == 0) { + printf("Current weather visibility: %u\r\n", (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[3].meta.name, "wind_speed") == 0) { + printf("Current weather wind_speed: %f\r\n", strtod(jsp->data.prim.buff, NULL)); + } else if (strcmp(jsp->stack[3].meta.name, "wind_deg") == 0) { + printf("Current weather wind_deg: %u\r\n", (unsigned)atoll(jsp->data.prim.buff)); + } + } + } + + /* + * Process the various object in specific JSON order + */ + if (jsp->stack_pos >= 5 + /* First build the order... */ + && jsp->stack[0].type == LWJSON_STREAM_TYPE_OBJECT && jsp->stack[1].type == LWJSON_STREAM_TYPE_KEY + && jsp->stack[2].type == LWJSON_STREAM_TYPE_ARRAY && jsp->stack[3].type == LWJSON_STREAM_TYPE_OBJECT + && jsp->stack[4].type == LWJSON_STREAM_TYPE_KEY) { + + /* + * Handle daily forecast objects + */ + if (strcmp(jsp->stack[1].meta.name, "daily") == 0) { + /* Analyze objects for temp and feels like object */ + if (jsp->stack_pos >= 7 && jsp->stack[5].type == LWJSON_STREAM_TYPE_OBJECT + && jsp->stack[6].type == LWJSON_STREAM_TYPE_KEY) { + if (strcmp(jsp->stack[4].meta.name, "temp") == 0) { + /* Parsing of temp object */ + if (strcmp(jsp->stack[6].meta.name, "min") == 0) { + printf("Day %2d temp min: %s\r\n", (int)jsp->stack[2].meta.index, jsp->data.prim.buff); + } else if (strcmp(jsp->stack[6].meta.name, "max") == 0) { + printf("Day %2d temp max: %s\r\n", (int)jsp->stack[2].meta.index, jsp->data.prim.buff); + } else if (strcmp(jsp->stack[6].meta.name, "day") == 0) { + printf("Day %2d temp day: %s\r\n", (int)jsp->stack[2].meta.index, jsp->data.prim.buff); + } else if (strcmp(jsp->stack[6].meta.name, "night") == 0) { + printf("Day %2d temp night: %s\r\n", (int)jsp->stack[2].meta.index, jsp->data.prim.buff); + } else if (strcmp(jsp->stack[6].meta.name, "eve") == 0) { + printf("Day %2d temp eve: %s\r\n", (int)jsp->stack[2].meta.index, jsp->data.prim.buff); + } else if (strcmp(jsp->stack[6].meta.name, "morn") == 0) { + printf("Day %2d temp morn: %s\r\n", (int)jsp->stack[2].meta.index, jsp->data.prim.buff); + } + } else if (strcmp(jsp->stack[4].meta.name, "feels_like") == 0) { + /* Parsing of feels-like objects */ + if (strcmp(jsp->stack[6].meta.name, "day") == 0) { + printf("Day %2d temp_feels_like day: %s\r\n", (int)jsp->stack[2].meta.index, + jsp->data.prim.buff); + } else if (strcmp(jsp->stack[6].meta.name, "night") == 0) { + printf("Day %2d temp_feels_like night: %s\r\n", (int)jsp->stack[2].meta.index, + jsp->data.prim.buff); + } else if (strcmp(jsp->stack[6].meta.name, "eve") == 0) { + printf("Day %2d temp_feels_like eve: %s\r\n", (int)jsp->stack[2].meta.index, + jsp->data.prim.buff); + } else if (strcmp(jsp->stack[6].meta.name, "morn") == 0) { + printf("Day %2d temp_feels_like morn: %s\r\n", (int)jsp->stack[2].meta.index, + jsp->data.prim.buff); + } + } + } else if (jsp->stack_pos >= 8 && jsp->stack[5].type == LWJSON_STREAM_TYPE_ARRAY + && jsp->stack[6].type == LWJSON_STREAM_TYPE_OBJECT + && jsp->stack[7].type == LWJSON_STREAM_TYPE_KEY + && strcmp(jsp->stack[4].meta.name, "weather") == 0) { + + if (strcmp(jsp->stack[7].meta.name, "id") == 0) { + printf("Day %2d weather %2d id: %s\r\n", (int)jsp->stack[2].meta.index, + (int)jsp->stack[5].meta.index, jsp->data.prim.buff); + } else if (strcmp(jsp->stack[7].meta.name, "main") == 0) { + printf("Day %2d weather %2d main: %s\r\n", (int)jsp->stack[2].meta.index, + (int)jsp->stack[5].meta.index, jsp->data.str.buff); + } else if (strcmp(jsp->stack[7].meta.name, "description") == 0) { + printf("Day %2d weather %2d description: %s\r\n", (int)jsp->stack[2].meta.index, + (int)jsp->stack[5].meta.index, jsp->data.str.buff); + } else if (strcmp(jsp->stack[7].meta.name, "icon") == 0) { + printf("Day %2d weather %2d icon: %s\r\n", (int)jsp->stack[2].meta.index, + (int)jsp->stack[5].meta.index, jsp->data.str.buff); + } + } else if (strcmp(jsp->stack[jsp->stack_pos - 1].meta.name, "dt") == 0) { + printf("Day %2d dt: %u\r\n", (int)jsp->stack[2].meta.index, (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[jsp->stack_pos - 1].meta.name, "pressure") == 0) { + printf("Day %2d pressure: %u\r\n", (int)jsp->stack[2].meta.index, (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[jsp->stack_pos - 1].meta.name, "humidity") == 0) { + printf("Day %2d humidity: %u\r\n", (int)jsp->stack[2].meta.index, (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[jsp->stack_pos - 1].meta.name, "clouds") == 0) { + printf("Day %2d clouds: %u\r\n", (int)jsp->stack[2].meta.index, (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[jsp->stack_pos - 1].meta.name, "snow") == 0) { + printf("Day %2d snow: %f\r\n", (int)jsp->stack[2].meta.index, strtod(jsp->data.prim.buff, NULL)); + } else if (strcmp(jsp->stack[jsp->stack_pos - 1].meta.name, "rain") == 0) { + printf("Day %2d rain: %f\r\n", (int)jsp->stack[2].meta.index, strtod(jsp->data.prim.buff, NULL)); + } else if (strcmp(jsp->stack[jsp->stack_pos - 1].meta.name, "uvi") == 0) { + printf("Day %2d uvi: %f\r\n", (int)jsp->stack[2].meta.index, strtod(jsp->data.prim.buff, NULL)); + } + } else if (strcmp(jsp->stack[1].meta.name, "hourly") == 0) { + if (jsp->stack_pos >= 8 && jsp->stack[5].type == LWJSON_STREAM_TYPE_ARRAY + && jsp->stack[6].type == LWJSON_STREAM_TYPE_OBJECT && jsp->stack[7].type == LWJSON_STREAM_TYPE_KEY + && strcmp(jsp->stack[4].meta.name, "weather") == 0) { + + if (strcmp(jsp->stack[7].meta.name, "id") == 0) { + printf("Hour %2d weather %2d id: %s\r\n", (int)jsp->stack[2].meta.index, + (int)jsp->stack[5].meta.index, jsp->data.prim.buff); + } else if (strcmp(jsp->stack[7].meta.name, "main") == 0) { + printf("Hour %2d weather %2d main: %s\r\n", (int)jsp->stack[2].meta.index, + (int)jsp->stack[5].meta.index, jsp->data.str.buff); + } else if (strcmp(jsp->stack[7].meta.name, "description") == 0) { + printf("Hour %2d weather %2d description: %s\r\n", (int)jsp->stack[2].meta.index, + (int)jsp->stack[5].meta.index, jsp->data.str.buff); + } else if (strcmp(jsp->stack[7].meta.name, "icon") == 0) { + printf("Hour %2d weather %2d icon: %s\r\n", (int)jsp->stack[2].meta.index, + (int)jsp->stack[5].meta.index, jsp->data.str.buff); + } + } else if (strcmp(jsp->stack[4].meta.name, "dt") == 0) { + printf("Hour %2d forecast for dt: %u\r\n", (int)jsp->stack[jsp->stack_pos - 3].meta.index, + (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[4].meta.name, "temp") == 0) { + printf("Hour %2d forecast for temp: %f\r\n", (int)jsp->stack[jsp->stack_pos - 3].meta.index, + strtod(jsp->data.prim.buff, NULL)); + } else if (strcmp(jsp->stack[4].meta.name, "feels_like") == 0) { + printf("Hour %2d forecast for feels_like: %f\r\n", (int)jsp->stack[jsp->stack_pos - 3].meta.index, + strtod(jsp->data.prim.buff, NULL)); + } else if (strcmp(jsp->stack[4].meta.name, "pressure") == 0) { + printf("Hour %2d forecast for pressure: %d\r\n", (int)jsp->stack[jsp->stack_pos - 3].meta.index, + (int)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[4].meta.name, "humidity") == 0) { + printf("Hour %2d forecast for humidity: %d\r\n", (int)jsp->stack[jsp->stack_pos - 3].meta.index, + (int)atoll(jsp->data.prim.buff)); + } + } else if (strcmp(jsp->stack[1].meta.name, "minutely") == 0) { + if (strcmp(jsp->stack[4].meta.name, "dt") == 0) { + printf("Minute %2d forecast for dt: %u\r\n", (int)jsp->stack[jsp->stack_pos - 3].meta.index, + (unsigned)atoll(jsp->data.prim.buff)); + } else if (strcmp(jsp->stack[4].meta.name, "precipitation") == 0) { + printf("Minute %2d forecast for precipitation: %u\r\n", (int)jsp->stack[jsp->stack_pos - 3].meta.index, + (unsigned)atoll(jsp->data.prim.buff)); + } + } + } +} diff --git a/docs/api-reference/lwjson_opt.rst b/docs/api-reference/lwjson_opt.rst index 4bed334..754f4dc 100644 --- a/docs/api-reference/lwjson_opt.rst +++ b/docs/api-reference/lwjson_opt.rst @@ -9,4 +9,5 @@ When any of the settings shall be modified, it shall be done in dedicated applic .. note:: Check :ref:`getting_started` for guidelines on how to create and use configuration file. -.. doxygengroup:: LWJSON_OPT \ No newline at end of file +.. doxygengroup:: LWJSON_OPT + :inner: \ No newline at end of file diff --git a/docs/changelog/index.rst b/docs/changelog/index.rst new file mode 100644 index 0000000..51b89b4 --- /dev/null +++ b/docs/changelog/index.rst @@ -0,0 +1,6 @@ +.. _changelof: + +Changelog +========= + +.. literalinclude:: ../../CHANGELOG.md diff --git a/docs/conf.py b/docs/conf.py index 6216731..d015a01 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,15 +23,25 @@ # -- Project information ----------------------------------------------------- project = 'LwJSON' -copyright = '2020, Tilen MAJERLE' +copyright = '2022, Tilen MAJERLE' author = 'Tilen MAJERLE' # Try to get branch at which this is running # and try to determine which version to display in sphinx -# Version is using git tag if on master or "latest-develop" if on develop branch +# Version is using git tag if on master/main or "latest-develop" if on develop branch version = '' git_branch = '' +def cmd_exec_print(t): + print("cmd > ", t, "\n", os.popen(t).read().strip(), "\n") + +# Print demo data here +cmd_exec_print('git branch') +cmd_exec_print('git describe') +cmd_exec_print('git describe --tags') +cmd_exec_print('git describe --tags --abbrev=0') +cmd_exec_print('git describe --tags --abbrev=1') + # Get current branch res = os.popen('git branch').read().strip() for line in res.split("\n"): @@ -41,17 +51,18 @@ # Decision for display version git_branch = git_branch.replace('(HEAD detached at ', '').replace(')', '') if git_branch.find('master') >= 0 or git_branch.find('main') >= 0: - version = os.popen('git describe --tags --abbrev=0').read().strip() - if version == '': - version = 'v0.0.0' -elif git_branch.find('develop') != -1 and not (git_branch.find('develop-') >= 0 or git_branch.find('develop/') >= 0): + #version = os.popen('git describe --tags --abbrev=0').read().strip() + version = 'latest-stable' +elif git_branch.find('develop-') >= 0 or git_branch.find('develop/') >= 0: + version = 'branch-' + git_branch +elif git_branch == 'develop' or git_branch == 'origin/develop': version = 'latest-develop' else: - version = 'branch-' + git_branch + version = os.popen('git describe --tags --abbrev=0').read().strip() # For debugging purpose only print("GIT BRANCH: " + git_branch) -print("GIT VERSION: " + version) +print("PROJ VERSION: " + version) # -- General configuration --------------------------------------------------- @@ -115,20 +126,19 @@ html_css_files = [ 'css/common.css', 'css/custom.css', + 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css', ] html_js_files = [ - 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css' + '' ] +# Master index file master_doc = 'index' -# -# Breathe configuration -# -# -# +# --- Breathe configuration ----------------------------------------------------- breathe_projects = { "lwjson": "_build/xml/" } breathe_default_project = "lwjson" -breathe_default_members = ('members', 'undoc-members') \ No newline at end of file +breathe_default_members = ('members', 'undoc-members') +breathe_show_enumvalue_initializer = True \ No newline at end of file diff --git a/docs/examples/stream.json b/docs/examples/stream.json new file mode 100644 index 0000000..fe4f850 --- /dev/null +++ b/docs/examples/stream.json @@ -0,0 +1,13 @@ +{ + "test": "abc", + "array": [ + "123", + "def", + "ghi" + ], + "array_in_array": [ + ["1", "2", "3"], + ["4", "5", "6"], + ["7", "8", "9"] + ] +} \ No newline at end of file diff --git a/docs/get-started/index.rst b/docs/get-started/index.rst index f38dc9e..11f14f6 100644 --- a/docs/get-started/index.rst +++ b/docs/get-started/index.rst @@ -13,10 +13,10 @@ Download library Library is primarly hosted on `Github `_. -You can get it with: +You can get it by: * Downloading latest release from `releases area `_ on Github -* Cloning ``master`` branch for latest stable version +* Cloning ``main`` branch for latest stable version * Cloning ``develop`` branch for latest development Download from releases @@ -34,21 +34,21 @@ This is used when you do not have yet local copy on your machine. * Make sure ``git`` is installed. * Open console and navigate to path in the system to clone repository to. Use command ``cd your_path`` -* Clone repository with one of available ``3`` options +* Clone repository with one of available options below * Run ``git clone --recurse-submodules https://github.com/MaJerle/lwjson`` command to clone entire repository, including submodules * Run ``git clone --recurse-submodules --branch develop https://github.com/MaJerle/lwjson`` to clone `development` branch, including submodules - * Run ``git clone --recurse-submodules --branch master https://github.com/MaJerle/lwjson`` to clone `latest stable` branch, including submodules + * Run ``git clone --recurse-submodules --branch main https://github.com/MaJerle/lwjson`` to clone `latest stable` branch, including submodules * Navigate to ``examples`` directory and run favourite example Update cloned to latest version """"""""""""""""""""""""""""""" -* Open console and navigate to path in the system where your resources repository is. Use command ``cd your_path`` -* Run ``git pull origin master --recurse-submodules`` command to pull latest changes and to fetch latest changes from submodules on ``master`` branch -* Run ``git pull origin develop --recurse-submodules`` command to pull latest changes and to fetch latest changes from submodules on ``develop`` branch -* Run ``git submodule foreach git pull origin master`` to update & merge all submodules +* Open console and navigate to path in the system where your repository is located. Use command ``cd your_path`` +* Run ``git pull origin main`` command to get latest changes on ``main`` branch +* Run ``git pull origin develop`` command to get latest changes on ``develop`` branch +* Run ``git submodule update --init --remote`` to update submodules to latest version .. note:: This is preferred option to use when you want to evaluate library and run prepared examples. diff --git a/docs/index.rst b/docs/index.rst index 2d9cc78..8f14ebf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,6 +22,7 @@ Features * No recursion during parse operation * Re-entrant functions * Zero-copy, no ``malloc`` or ``free`` functions used +* Supports streaming parsing as secondary option * Optional support for inline comments with `/* comment... */` syntax between any *blank* region of input string * Advanced find algorithm for tokens * Tests coverage is available @@ -65,8 +66,30 @@ Table of contents .. toctree:: :maxdepth: 2 + :caption: Contents self get-started/index user-manual/index api-reference/index + changelog/index + +.. toctree:: + :maxdepth: 2 + :caption: Other projects + :hidden: + + LwBTN - Button manager + LwDTC - DateTimeCron + LwESP - ESP-AT library + LwEVT - Event manager + LwGPS - GPS NMEA parser + LwGSM - GSM-AT library + LwJSON - JSON parser + LwMEM - Memory manager + LwOW - OneWire with UART + LwPKT - Packet protocol + LwPRINTF - Printf + LwRB - Ring buffer + LwSHELL - Shell + LwUTIL - Utility functions diff --git a/docs/requirements.txt b/docs/requirements.txt index eb5e0fd..834b1bb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,7 +2,7 @@ breathe>=4.9.1 colorama docutils==0.16 sphinx>=3.5.1 -sphinx_rtd_theme +sphinx_rtd_theme>=1.0.0 sphinx-tabs sphinxcontrib-svg2pdfconverter sphinx-sitemap diff --git a/docs/static/dark-light/checked.svg b/docs/static/dark-light/checked.svg new file mode 100644 index 0000000..a78af82 --- /dev/null +++ b/docs/static/dark-light/checked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/static/dark-light/common-dark-light.css b/docs/static/dark-light/common-dark-light.css new file mode 100644 index 0000000..9a2dc1d --- /dev/null +++ b/docs/static/dark-light/common-dark-light.css @@ -0,0 +1,143 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 + * + * https://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. + */ +:root { + --heading-color: red; + --duration: 0.5s; + --timing: ease; +} + +*, +::before, +::after { + box-sizing: border-box; +} + +body { + margin: 0; + transition: + color var(--duration) var(--timing), + background-color var(--duration) var(--timing); + font-family: sans-serif; + font-size: 12pt; + background-color: var(--background-color); + color: var(--text-color); + display: flex; + justify-content: center; +} + +main { + margin: 1rem; + max-width: 30rem; + position: relative; +} + +h1 { + color: var(--heading-color); + text-shadow: 0.1rem 0.1rem 0.1rem var(--shadow-color); + transition: text-shadow var(--duration) var(--timing); +} + +img { + max-width: 100%; + height: auto; + transition: filter var(--duration) var(--timing); +} + +p { + line-height: 1.5; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; +} + +fieldset { + border: solid 0.1rem; + box-shadow: 0.1rem 0.1rem 0.1rem var(--shadow-color); + transition: box-shadow var(--duration) var(--timing); +} + +div { + padding: 0.5rem; +} + +aside { + position: absolute; + right: 0; + padding: 0.5rem; +} + +aside:nth-of-type(1) { + top: 0; +} + +aside:nth-of-type(2) { + top: 3rem; +} + +aside:nth-of-type(3) { + top: 7rem; +} + +aside:nth-of-type(4) { + top: 12rem; +} + +#content select, +#content button, +#content input[type="text"], +#content input[type="search"] { + width: 15rem; +} + +dark-mode-toggle { + --dark-mode-toggle-remember-icon-checked: url("checked.svg"); + --dark-mode-toggle-remember-icon-unchecked: url("unchecked.svg"); + --dark-mode-toggle-remember-font: 0.75rem "Helvetica"; + --dark-mode-toggle-legend-font: bold 0.85rem "Helvetica"; + --dark-mode-toggle-label-font: 0.85rem "Helvetica"; + --dark-mode-toggle-color: var(--text-color); + --dark-mode-toggle-background-color: none; + + margin-bottom: 1.5rem; +} + +#dark-mode-toggle-1 { + --dark-mode-toggle-dark-icon: url("sun.png"); + --dark-mode-toggle-light-icon: url("moon.png"); +} + +#dark-mode-toggle-2 { + --dark-mode-toggle-dark-icon: url("sun.svg"); + --dark-mode-toggle-light-icon: url("moon.svg"); + --dark-mode-toggle-icon-size: 2rem; + --dark-mode-toggle-icon-filter: invert(100%); +} + +#dark-mode-toggle-3, +#dark-mode-toggle-4 { + --dark-mode-toggle-dark-icon: url("moon.png"); + --dark-mode-toggle-light-icon: url("sun.png"); +} + +#dark-mode-toggle-3 { + --dark-mode-toggle-remember-filter: invert(100%); +} + +#dark-mode-toggle-4 { + --dark-mode-toggle-active-mode-background-color: var(--accent-color); + --dark-mode-toggle-remember-filter: invert(100%); +} diff --git a/docs/static/dark-light/dark-mode-toggle.mjs b/docs/static/dark-light/dark-mode-toggle.mjs new file mode 100644 index 0000000..da22262 --- /dev/null +++ b/docs/static/dark-light/dark-mode-toggle.mjs @@ -0,0 +1,329 @@ +/** + * Copyright 2019 Google LLC + * + * 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 + * + * https://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. + */ + +// @license © 2019 Google LLC. Licensed under the Apache License, Version 2.0. +const doc = document; +const store = localStorage; +const PREFERS_COLOR_SCHEME = 'prefers-color-scheme'; +const MEDIA = 'media'; +const LIGHT = 'light'; +const DARK = 'dark'; +const MQ_DARK = `(${PREFERS_COLOR_SCHEME}:${DARK})`; +const MQ_LIGHT = `(${PREFERS_COLOR_SCHEME}:${LIGHT})`; +const LINK_REL_STYLESHEET = 'link[rel=stylesheet]'; +const REMEMBER = 'remember'; +const LEGEND = 'legend'; +const TOGGLE = 'toggle'; +const SWITCH = 'switch'; +const APPEARANCE = 'appearance'; +const PERMANENT = 'permanent'; +const MODE = 'mode'; +const COLOR_SCHEME_CHANGE = 'colorschemechange'; +const PERMANENT_COLOR_SCHEME = 'permanentcolorscheme'; +const ALL = 'all'; +const NOT_ALL = 'not all'; +const NAME = 'dark-mode-toggle'; +const DEFAULT_URL = 'https://googlechromelabs.github.io/dark-mode-toggle/demo/'; + +// See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html ↵ +// #reflecting-content-attributes-in-idl-attributes. +const installStringReflection = (obj, attrName, propName = attrName) => { + Object.defineProperty(obj, propName, { + enumerable: true, + get() { + const value = this.getAttribute(attrName); + return value === null ? '' : value; + }, + set(v) { + this.setAttribute(attrName, v); + }, + }); +}; + +const installBoolReflection = (obj, attrName, propName = attrName) => { + Object.defineProperty(obj, propName, { + enumerable: true, + get() { + return this.hasAttribute(attrName); + }, + set(v) { + if (v) { + this.setAttribute(attrName, ''); + } else { + this.removeAttribute(attrName); + } + }, + }); +}; + +const template = doc.createElement('template'); +// ⚠️ Note: this is a minified version of `src/template-contents.tpl`. +// Compress the CSS with https://cssminifier.com/, then paste it here. +// eslint-disable-next-line max-len +template.innerHTML = `
`; + +export class DarkModeToggle extends HTMLElement { + static get observedAttributes() { + return [MODE, APPEARANCE, PERMANENT, LEGEND, LIGHT, DARK, REMEMBER]; + } + + constructor() { + super(); + + installStringReflection(this, MODE); + installStringReflection(this, APPEARANCE); + installStringReflection(this, LEGEND); + installStringReflection(this, LIGHT); + installStringReflection(this, DARK); + installStringReflection(this, REMEMBER); + + installBoolReflection(this, PERMANENT); + + this._darkCSS = null; + this._lightCSS = null; + + doc.addEventListener(COLOR_SCHEME_CHANGE, (event) => { + this.mode = event.detail.colorScheme; + this._updateRadios(); + this._updateCheckbox(); + }); + + doc.addEventListener(PERMANENT_COLOR_SCHEME, (event) => { + this.permanent = event.detail.permanent; + this._permanentCheckbox.checked = this.permanent; + }); + + this._initializeDOM(); + } + + _initializeDOM() { + const shadowRoot = this.attachShadow({mode: 'open'}); + shadowRoot.appendChild(template.content.cloneNode(true)); + + // We need to support `media="(prefers-color-scheme: dark)"` (with space) + // and `media="(prefers-color-scheme:dark)"` (without space) + this._darkCSS = doc.querySelectorAll(`${LINK_REL_STYLESHEET}[${MEDIA}*=${PREFERS_COLOR_SCHEME}][${MEDIA}*="${DARK}"]`); + this._lightCSS = doc.querySelectorAll(`${LINK_REL_STYLESHEET}[${MEDIA}*=${PREFERS_COLOR_SCHEME}][${MEDIA}*="${LIGHT}"]`); + + // Get DOM references. + this._lightRadio = shadowRoot.querySelector('[part=lightRadio]'); + this._lightLabel = shadowRoot.querySelector('[part=lightLabel]'); + this._darkRadio = shadowRoot.querySelector('[part=darkRadio]'); + this._darkLabel = shadowRoot.querySelector('[part=darkLabel]'); + this._darkCheckbox = shadowRoot.querySelector('[part=toggleCheckbox]'); + this._checkboxLabel = shadowRoot.querySelector('[part=toggleLabel]'); + this._legendLabel = shadowRoot.querySelector('legend'); + this._permanentAside = shadowRoot.querySelector('aside'); + this._permanentCheckbox = + shadowRoot.querySelector('[part=permanentCheckbox]'); + this._permanentLabel = shadowRoot.querySelector('[part=permanentLabel]'); + + // Does the browser support native `prefers-color-scheme`? + const hasNativePrefersColorScheme = + matchMedia(MQ_DARK).media !== NOT_ALL; + // Listen to `prefers-color-scheme` changes. + if (hasNativePrefersColorScheme) { + matchMedia(MQ_DARK).addListener(({matches}) => { + this.mode = matches ? DARK : LIGHT; + this._dispatchEvent(COLOR_SCHEME_CHANGE, {colorScheme: this.mode}); + }); + } + // Set initial state, giving preference to a remembered value, then the + // native value (if supported), and eventually defaulting to a light + // experience. + const rememberedValue = store.getItem(NAME); + if (rememberedValue && [DARK, LIGHT].includes(rememberedValue)) { + this.mode = rememberedValue; + this._permanentCheckbox.checked = true; + this.permanent = true; + } else if (hasNativePrefersColorScheme) { + this.mode = matchMedia(MQ_LIGHT).matches ? LIGHT : DARK; + } + if (!this.mode) { + this.mode = LIGHT; + } + if (this.permanent && !rememberedValue) { + store.setItem(NAME, this.mode); + } + + // Default to toggle appearance. + if (!this.appearance) { + this.appearance = TOGGLE; + } + + // Update the appearance to either of toggle or switch. + this._updateAppearance(); + + // Update the radios + this._updateRadios(); + + // Make the checkbox reflect the state of the radios + this._updateCheckbox(); + + // Synchronize the behavior of the radio and the checkbox. + [this._lightRadio, this._darkRadio].forEach((input) => { + input.addEventListener('change', () => { + this.mode = this._lightRadio.checked ? LIGHT : DARK; + this._updateCheckbox(); + this._dispatchEvent(COLOR_SCHEME_CHANGE, {colorScheme: this.mode}); + }); + }); + this._darkCheckbox.addEventListener('change', () => { + this.mode = this._darkCheckbox.checked ? DARK : LIGHT; + this._updateRadios(); + this._dispatchEvent(COLOR_SCHEME_CHANGE, {colorScheme: this.mode}); + }); + + // Make remembering the last mode optional + this._permanentCheckbox.addEventListener('change', () => { + this.permanent = this._permanentCheckbox.checked; + this._dispatchEvent(PERMANENT_COLOR_SCHEME, { + permanent: this.permanent, + }); + }); + + // Finally update the mode and let the world know what's going on + this._updateMode(); + this._dispatchEvent(COLOR_SCHEME_CHANGE, {colorScheme: this.mode}); + this._dispatchEvent(PERMANENT_COLOR_SCHEME, { + permanent: this.permanent, + }); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === MODE) { + if (![LIGHT, DARK].includes(newValue)) { + throw new RangeError(`Allowed values: "${LIGHT}" and "${DARK}".`); + } + // Only show the dialog programmatically on devices not capable of hover + // and only if there is a label + if (matchMedia('(hover:none)').matches && this.remember) { + this._showPermanentAside(); + } + if (this.permanent) { + store.setItem(NAME, this.mode); + } + this._updateRadios(); + this._updateCheckbox(); + this._updateMode(); + } else if (name === APPEARANCE) { + if (![TOGGLE, SWITCH].includes(newValue)) { + throw new RangeError(`Allowed values: "${TOGGLE}" and "${SWITCH}".`); + } + this._updateAppearance(); + } else if (name === PERMANENT) { + if (this.permanent) { + store.setItem(NAME, this.mode); + } else { + store.removeItem(NAME); + } + this._permanentCheckbox.checked = this.permanent; + } else if (name === LEGEND) { + this._legendLabel.textContent = newValue; + } else if (name === REMEMBER) { + this._permanentLabel.textContent = newValue; + } else if (name === LIGHT) { + this._lightLabel.textContent = newValue; + if (this.mode === LIGHT) { + this._checkboxLabel.textContent = newValue; + } + } else if (name === DARK) { + this._darkLabel.textContent = newValue; + if (this.mode === DARK) { + this._checkboxLabel.textContent = newValue; + } + } + } + + _dispatchEvent(type, value) { + this.dispatchEvent(new CustomEvent(type, { + bubbles: true, + composed: true, + detail: value, + })); + } + + _updateAppearance() { + // Hide or show the light-related affordances dependent on the appearance, + // which can be "switch" or "toggle". + const appearAsToggle = this.appearance === TOGGLE; + this._lightRadio.hidden = appearAsToggle; + this._lightLabel.hidden = appearAsToggle; + this._darkRadio.hidden = appearAsToggle; + this._darkLabel.hidden = appearAsToggle; + this._darkCheckbox.hidden = !appearAsToggle; + this._checkboxLabel.hidden = !appearAsToggle; + } + + _updateRadios() { + if (this.mode === LIGHT) { + this._lightRadio.checked = true; + } else { + this._darkRadio.checked = true; + } + } + + _updateCheckbox() { + if (this.mode === LIGHT) { + this._checkboxLabel.style.setProperty(`--${NAME}-checkbox-icon`, + `var(--${NAME}-light-icon,url("${DEFAULT_URL}moon.png"))`); + this._checkboxLabel.textContent = this.light; + if (!this.light) { + this._checkboxLabel.ariaLabel = DARK; + } + this._darkCheckbox.checked = false; + } else { + this._checkboxLabel.style.setProperty(`--${NAME}-checkbox-icon`, + `var(--${NAME}-dark-icon,url("${DEFAULT_URL}sun.png"))`); + this._checkboxLabel.textContent = this.dark; + if (!this.dark) { + this._checkboxLabel.ariaLabel = LIGHT; + } + this._darkCheckbox.checked = true; + } + } + + _updateMode() { + if (this.mode === LIGHT) { + this._lightCSS.forEach((link) => { + link.media = ALL; + link.disabled = false; + }); + this._darkCSS.forEach((link) => { + link.media = NOT_ALL; + link.disabled = true; + }); + } else { + this._darkCSS.forEach((link) => { + link.media = ALL; + link.disabled = false; + }); + this._lightCSS.forEach((link) => { + link.media = NOT_ALL; + link.disabled = true; + }); + } + } + + _showPermanentAside() { + this._permanentAside.style.visibility = 'visible'; + setTimeout(() => { + this._permanentAside.style.visibility = 'hidden'; + }, 3000); + } +} + +customElements.define(NAME, DarkModeToggle); \ No newline at end of file diff --git a/docs/static/dark-light/dark.css b/docs/static/dark-light/dark.css new file mode 100644 index 0000000..6ed8cfb --- /dev/null +++ b/docs/static/dark-light/dark.css @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 + * + * https://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. + */ +:root { + color-scheme: dark; /* stylelint-disable-line property-no-unknown */ + + --background-color: rgb(15 15 15); + --text-color: rgb(240 240 240); + --shadow-color: rgb(240 240 240 / 50%); + --accent-color: rgb(0 0 240 / 50%); +} + +img { + filter: grayscale(50%); +} + +.icon { + filter: invert(100%); +} + +a { + color: yellow; +} diff --git a/docs/static/dark-light/light.css b/docs/static/dark-light/light.css new file mode 100644 index 0000000..f73cf7b --- /dev/null +++ b/docs/static/dark-light/light.css @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 + * + * https://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. + */ +:root { + color-scheme: light; /* stylelint-disable-line property-no-unknown */ + + --background-color: rgb(240 240 240); + --text-color: rgb(15 15 15); + --shadow-color: rgb(15 15 15 / 50%); + --accent-color: rgb(240 0 0 / 50%); +} diff --git a/docs/static/dark-light/moon.png b/docs/static/dark-light/moon.png new file mode 100644 index 0000000..0ad57d9 Binary files /dev/null and b/docs/static/dark-light/moon.png differ diff --git a/docs/static/dark-light/moon.svg b/docs/static/dark-light/moon.svg new file mode 100644 index 0000000..fad89a4 --- /dev/null +++ b/docs/static/dark-light/moon.svg @@ -0,0 +1,7 @@ + + + + moon + + + diff --git a/docs/static/dark-light/sun.png b/docs/static/dark-light/sun.png new file mode 100644 index 0000000..40c9b36 Binary files /dev/null and b/docs/static/dark-light/sun.png differ diff --git a/docs/static/dark-light/sun.svg b/docs/static/dark-light/sun.svg new file mode 100644 index 0000000..0b18941 --- /dev/null +++ b/docs/static/dark-light/sun.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/docs/static/dark-light/unchecked.svg b/docs/static/dark-light/unchecked.svg new file mode 100644 index 0000000..6702330 --- /dev/null +++ b/docs/static/dark-light/unchecked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/user-manual/data-access.rst b/docs/user-manual/data-access.rst index e3997a6..42dabe5 100644 --- a/docs/user-manual/data-access.rst +++ b/docs/user-manual/data-access.rst @@ -57,7 +57,7 @@ Let's consider following JSON as input: "brand":"Range", "year":2020, "repainted":true - } + } ] } diff --git a/docs/user-manual/how-it-works.rst b/docs/user-manual/how-it-works.rst index caa7b45..3c5bf4a 100644 --- a/docs/user-manual/how-it-works.rst +++ b/docs/user-manual/how-it-works.rst @@ -3,11 +3,14 @@ How it works ============ -LwJSON fully complies with *RFC 4627* memo. +LwJSON fully complies with *RFC 4627* memo and supports ``2`` types of parsing: -LwJSON accepts input string formatted as JSON as per *RFC 4627*. -Library parses each character and creates list of tokens that are -understood by C language for easier further processing. +* Parsing with full data available as single linear memory (primary option) +* Stream parsing with partial available bytes at any given point of time - advanced state machine + +When full data are available, standard parsing is used with tokens, +that contain references to start/stop indexes of the strings and other primitives and provide +full device tree - sort of custom hash-map value. When JSON is successfully parsed, there are several tokens used, one for each JSON data type. Each token consists of: @@ -19,11 +22,15 @@ Each token consists of: As an example, JSON text ``{"mykey":"myvalue"}`` will be parsed into ``2`` tokens: * First token is the opening bracket and has type *object* as it holds children tokens -* Second token has name ``mykey``, its type is *string* and value is ``myvalue`` +* Second token has name ``mykey``, its type is *string* with value set as ``myvalue`` .. warning:: When JSON input string is parsed, create tokens use input string as a reference. This means that until JSON parsed tokens are being used, original text must stay as-is. + Any modification of source JSON input may destroy references from the token tree and hence generate wrong output for the user + +.. tip:: + See :ref:`stream` for implementation of streaming parser where full data do not need to be available at any given time. .. toctree:: :maxdepth: 2 \ No newline at end of file diff --git a/docs/user-manual/index.rst b/docs/user-manual/index.rst index d9e14b1..d0e7eb4 100644 --- a/docs/user-manual/index.rst +++ b/docs/user-manual/index.rst @@ -8,4 +8,5 @@ User manual how-it-works token-design - data-access \ No newline at end of file + data-access + stream \ No newline at end of file diff --git a/docs/user-manual/stream.rst b/docs/user-manual/stream.rst new file mode 100644 index 0000000..9789370 --- /dev/null +++ b/docs/user-manual/stream.rst @@ -0,0 +1,70 @@ +.. _stream: + +Stream parser +============= + +Streaming parser implementation is alternative option versus standard tokenized one, in the sense that: + +* There is no need to have full JSON available at one time to have successful parsing +* It can be utilized to parse very large JSON strings on very small systems with limited memory +* It allows users to *take* from the stream only necessary parts and store them to local more system-friendly variable + +This type of parser does not utilize use of tokens, rather focuses on the callback function, +where user is in charge to manually understand token structure and get useful data from it. + +Stream parser introduces *stack* mechanism instead - to keep the track of depthness during parsing the process. +``3`` different element types are stored on *local stack*: + +* Start of object, with ``{`` character +* Start of array, with ``[`` character +* Key from the *object* entry + +.. note:: + Stack is nested as long as JSON input stream is nested in the same way + +Consider this input string: ``{"k1":"v1","k2":[true, false]}``. +During parsing procedure, at some point of time, these events will occur: + +#. Start of *object* detected - *object* pushed to stack + + #. *key* element with name ``k1`` detected and pushed to stack + + #. *string* ``v1`` parsed as *string-value* + #. *key* element with name ``k1`` popped from stack + #. *key* element with name ``k2`` detected and pushed to stack + + #. Start of *array* detected - *array* pushed to stack + + #. ``true`` primitive detected + #. ``false`` primitive detected + #. End of *array* detected - *array* popped from stack + #. *key* element with name ``k2`` popped from stack +#. End of *object* detected - *object* popped from stack + +Each of these events is reported to user in the callback function. + +An example of the stream parsing: + +.. literalinclude:: ../../examples/example_stream.c + :language: c + :linenos: + :caption: Parse JSON data as a stream object + +Example +******* + +For the purpose of example, the following JSON input... + +.. literalinclude:: ../examples/stream.json + :language: json + :linenos: + :caption: JSON input for streaming + +\... will output the log as: + +.. literalinclude:: ../examples/stream_log.txt + :linenos: + :caption: JSON development log for the various events + +.. toctree:: + :maxdepth: 2 \ No newline at end of file diff --git a/examples/example_stream.c b/examples/example_stream.c new file mode 100644 index 0000000..66cda22 --- /dev/null +++ b/examples/example_stream.c @@ -0,0 +1,46 @@ +#include +#include "lwjson/lwjson.h" + +/* Test string to parser */ +static const char* json_str = "{\"k1\":\"v1\",\"k2\":[true, false]}"; + +/* LwJSON stream parser */ +static lwjson_stream_parser_t stream_parser; + +/** + * \brief Callback function for various events + * \param jsp: JSON stream parser object + * \param type: Event type + */ +void +prv_example_callback_func(lwjson_stream_parser_t* jsp, lwjson_stream_type_t type) { + /* Get a value corresponsing to "k1" key */ + if (jsp->stack_pos >= 2 /* Number of stack entries must be high */ + && jsp->stack[0].type == LWJSON_STREAM_TYPE_OBJECT /* First must be object */ + && jsp->stack[1].type == LWJSON_STREAM_TYPE_KEY /* We need key to be before */ + && strcmp(jsp->stack[1].meta.name, "k1") == 0) { + printf("Got key '%s' with value '%s'\r\n", jsp->stack[1].meta.name, jsp->data.str.buff); + } + (void)type; +} + +/* Parse JSON */ +void +example_stream_run(void) { + lwjsonr_t res; + printf("\r\n\r\nParsing stream\r\n"); + lwjson_stream_init(&stream_parser, prv_example_callback_func); + + /* Demonstrate as stream inputs */ + for (const char* c = json_str; *c != '\0'; ++c) { + if ((res = lwjson_stream_parse(&stream_parser, *c)) == lwjsonOK) { + printf("OK\r\n"); + } else if (res == lwjsonSTREAMDONE) { + printf("Done\r\n"); + } else { + printf("Error\r\n"); + break; + } + } + printf("Parsing completed\r\n"); +} diff --git a/library.json b/library.json index 7f47b8f..78145e7 100644 --- a/library.json +++ b/library.json @@ -1,31 +1,33 @@ { - "name": "LwJSON", - "version": "1.5.0", - "description": "Lightweight JSON parser for embedded systems with support for inline comments", - "keywords": "json, javascript, lightweight, parser, stm32, manager, library, comment, object, notation, object notation", - "repository": { - "type": "git", - "url": "https://github.com/MaJerle/lwjson.git" - }, - "authors": [ - { - "name": "Tilen Majerle", - "email": "tilen@majerle.eu", - "url": "https://majerle.eu" - } - ], - "license": "MIT", - "homepage": "https://github.com/MaJerle/lwjson", - "dependencies": { - - }, - "frameworks": "*", - "platforms": "*", - "export": { - "exclude": [ - "docs", - "**/.vs", - "**/Debug" - ] - } + "name": "LwJSON", + "version": "1.6.0", + "description": "Lightweight JSON parser for embedded systems with support for inline comments", + "keywords": "json, javascript, lightweight, parser, stm32, manager, library, comment, object, notation, object notation", + "repository": { + "type": "git", + "url": "https://github.com/MaJerle/lwjson.git" + }, + "authors": [ + { + "name": "Tilen Majerle", + "email": "tilen@majerle.eu", + "url": "https://majerle.eu" + } + ], + "license": "MIT", + "homepage": "https://github.com/MaJerle/lwjson", + "dependencies": {}, + "frameworks": "*", + "platforms": "*", + "export": { + "exclude": [ + ".github", + "dev", + "docs", + "**/.vs", + "**/Debug", + "build", + "**/build" + ] + } } \ No newline at end of file diff --git a/lwjson/CMakeLists.txt b/lwjson/CMakeLists.txt new file mode 100644 index 0000000..c005663 --- /dev/null +++ b/lwjson/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.22) + +#Debug message +message("Entering ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt") + +# Setup generic source files +set(lwjson_core_SRCS + ${CMAKE_CURRENT_LIST_DIR}/src/lwjson/lwjson.c + ${CMAKE_CURRENT_LIST_DIR}/src/lwjson/lwjson_stream.c + ) + +# Debug sources +set(lwjson_debug_SRCS + ${CMAKE_CURRENT_LIST_DIR}/src/lwjson/lwjson_debug.c + ) + +# Setup include directories +set(lwjson_include_DIRS + ${CMAKE_CURRENT_LIST_DIR}/src/include + ) + +# Register core library to the system +add_library(lwjson INTERFACE) +target_sources(lwjson PUBLIC ${lwjson_core_SRCS}) +target_include_directories(lwjson INTERFACE ${lwjson_include_DIRS}) + +# Register lwjson debug module +add_library(lwjson_debug INTERFACE) +target_sources(lwjson_debug PUBLIC ${lwjson_debug_SRCS}) +target_include_directories(lwjson_debug INTERFACE ${lwjson_include_DIRS}) + +#Debug message +message("Exiting ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt") \ No newline at end of file diff --git a/lwjson/src/include/lwjson/lwjson.h b/lwjson/src/include/lwjson/lwjson.h index 22cd303..1f7202f 100644 --- a/lwjson/src/include/lwjson/lwjson.h +++ b/lwjson/src/include/lwjson/lwjson.h @@ -4,7 +4,7 @@ */ /* - * Copyright (c) 2020 Tilen MAJERLE + * Copyright (c) 2022 Tilen MAJERLE * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -29,7 +29,7 @@ * This file is part of LwJSON - Lightweight JSON format parser. * * Author: Tilen MAJERLE - * Version: v1.5.0 + * Version: v1.6.0 */ #ifndef LWJSON_HDR_H #define LWJSON_HDR_H @@ -53,20 +53,20 @@ extern "C" { * \param[in] x: Object to get array size of * \return Number of elements in array */ -#define LWJSON_ARRAYSIZE(x) (sizeof(x) / sizeof((x)[0])) +#define LWJSON_ARRAYSIZE(x) (sizeof(x) / sizeof((x)[0])) /** * \brief List of supported JSON types */ typedef enum { - LWJSON_TYPE_STRING, /*!< String/Text format. Everything that has beginning and ending quote character */ - LWJSON_TYPE_NUM_INT, /*!< Number type for integer */ - LWJSON_TYPE_NUM_REAL, /*!< Number type for real number */ - LWJSON_TYPE_OBJECT, /*!< Object data type */ - LWJSON_TYPE_ARRAY, /*!< Array data type */ - LWJSON_TYPE_TRUE, /*!< True boolean value */ - LWJSON_TYPE_FALSE, /*!< False boolean value */ - LWJSON_TYPE_NULL, /*!< Null value */ + LWJSON_TYPE_STRING, /*!< String/Text format. Everything that has beginning and ending quote character */ + LWJSON_TYPE_NUM_INT, /*!< Number type for integer */ + LWJSON_TYPE_NUM_REAL, /*!< Number type for real number */ + LWJSON_TYPE_OBJECT, /*!< Object data type */ + LWJSON_TYPE_ARRAY, /*!< Array data type */ + LWJSON_TYPE_TRUE, /*!< True boolean value */ + LWJSON_TYPE_FALSE, /*!< False boolean value */ + LWJSON_TYPE_NULL, /*!< Null value */ } lwjson_type_t; /** @@ -83,89 +83,190 @@ typedef LWJSON_CFG_INT_TYPE lwjson_int_t; * \brief JSON token */ typedef struct lwjson_token { - struct lwjson_token* next; /*!< Next token on a list */ - lwjson_type_t type; /*!< Token type */ - const char* token_name; /*!< Token name (if exists) */ - size_t token_name_len; /*!< Length of token name (this is needed to support const input strings to parse) */ + struct lwjson_token* next; /*!< Next token on a list */ + lwjson_type_t type; /*!< Token type */ + const char* token_name; /*!< Token name (if exists) */ + size_t token_name_len; /*!< Length of token name (this is needed to support const input strings to parse) */ + union { struct { - const char* token_value; /*!< Pointer to the beginning of the string */ - size_t token_value_len; /*!< Length of token value (this is needed to support const input strings to parse) */ - } str; /*!< String data */ - lwjson_real_t num_real; /*!< Real number format */ - lwjson_int_t num_int; /*!< Int number format */ - struct lwjson_token* first_child; /*!< First children object for object or array type */ - } u; /*!< Union with different data types */ + const char* token_value; /*!< Pointer to the beginning of the string */ + size_t + token_value_len; /*!< Length of token value (this is needed to support const input strings to parse) */ + } str; /*!< String data */ + + lwjson_real_t num_real; /*!< Real number format */ + lwjson_int_t num_int; /*!< Int number format */ + struct lwjson_token* first_child; /*!< First children object for object or array type */ + } u; /*!< Union with different data types */ } lwjson_token_t; /** * \brief JSON result enumeration */ typedef enum { - lwjsonOK = 0x00, /*!< Function returns successfully */ - lwjsonERR, /*!< Generic error message */ - lwjsonERRJSON, /*!< Error JSON format */ - lwjsonERRMEM, /*!< Memory error */ - lwjsonERRPAR, /*!< Parameter error */ -} lwjsonr_t; + lwjsonOK = 0x00, /*!< Function returns successfully */ + lwjsonERR, /*!< Generic error message */ + lwjsonERRJSON, /*!< Error JSON format */ + lwjsonERRMEM, /*!< Memory error */ + lwjsonERRPAR, /*!< Parameter error */ + + lwjsonSTREAMWAITFIRSTCHAR, /*!< Streaming parser did not yet receive first valid character + indicating start of JSON sequence */ + lwjsonSTREAMDONE, /*!< Streaming parser is done, + closing character matched the stream opening one */ + lwjsonSTREAMINPROG, /*!< Stream parsing is still in progress */ +} + +lwjsonr_t; /** * \brief LwJSON instance */ typedef struct { - lwjson_token_t* tokens; /*!< Pointer to array of tokens */ - size_t tokens_len; /*!< Size of all tokens */ - size_t next_free_token_pos; /*!< Position of next free token instance */ - lwjson_token_t first_token; /*!< First token on a list */ + lwjson_token_t* tokens; /*!< Pointer to array of tokens */ + size_t tokens_len; /*!< Size of all tokens */ + size_t next_free_token_pos; /*!< Position of next free token instance */ + lwjson_token_t first_token; /*!< First token on a list */ + struct { - uint8_t parsed : 1; /*!< Flag indicating JSON parsing has finished successfully */ - } flags; /*!< List of flags */ + uint8_t parsed : 1; /*!< Flag indicating JSON parsing has finished successfully */ + } flags; /*!< List of flags */ } lwjson_t; -lwjsonr_t lwjson_init(lwjson_t* lw, lwjson_token_t* tokens, size_t tokens_len); -lwjsonr_t lwjson_parse_ex(lwjson_t* lw, const void* json_data, size_t len); -lwjsonr_t lwjson_parse(lwjson_t* lw, const char* json_str); -const lwjson_token_t* lwjson_find(lwjson_t* lw, const char* path); -const lwjson_token_t* lwjson_find_ex(lwjson_t* lw, const lwjson_token_t* token, const char* path); -lwjsonr_t lwjson_free(lwjson_t* lw); +lwjsonr_t lwjson_init(lwjson_t* lw, lwjson_token_t* tokens, size_t tokens_len); +lwjsonr_t lwjson_parse_ex(lwjson_t* lw, const void* json_data, size_t len); +lwjsonr_t lwjson_parse(lwjson_t* lw, const char* json_str); +const lwjson_token_t* lwjson_find(lwjson_t* lw, const char* path); +const lwjson_token_t* lwjson_find_ex(lwjson_t* lw, const lwjson_token_t* token, const char* path); +lwjsonr_t lwjson_free(lwjson_t* lw); + +void lwjson_print_token(const lwjson_token_t* token); +void lwjson_print_json(const lwjson_t* lw); + +/** + * \brief Object type for streaming parser + */ +typedef enum { + LWJSON_STREAM_TYPE_NONE, /*!< No entry - not used */ + LWJSON_STREAM_TYPE_OBJECT, /*!< Object indication */ + LWJSON_STREAM_TYPE_OBJECT_END, /*!< Object end indication */ + LWJSON_STREAM_TYPE_ARRAY, /*!< Array indication */ + LWJSON_STREAM_TYPE_ARRAY_END, /*!< Array end indication */ + LWJSON_STREAM_TYPE_KEY, /*!< Key string */ + LWJSON_STREAM_TYPE_STRING, /*!< Strin type */ + LWJSON_STREAM_TYPE_TRUE, /*!< True primitive */ + LWJSON_STREAM_TYPE_FALSE, /*!< False primitive */ + LWJSON_STREAM_TYPE_NULL, /*!< Null primitive */ + LWJSON_STREAM_TYPE_NUMBER, /*!< Generic number */ +} lwjson_stream_type_t; + +/** + * \brief Stream parsing stack object + */ +typedef struct { + lwjson_stream_type_t type; /*!< Streaming type - current value */ + + union { + char name[LWJSON_CFG_STREAM_KEY_MAX_LEN + + 1]; /*!< Last known key name, used only for \ref LWJSON_STREAM_TYPE_KEY type */ + uint16_t index; /*!< Current index when type is an array */ + } meta; /*!< Meta information */ +} lwjson_stream_stack_t; + +typedef enum { + LWJSON_STREAM_STATE_WAITINGFIRSTCHAR = 0x00, /*!< State to wait for very first opening character */ + LWJSON_STREAM_STATE_PARSING, /*!< In parsing of the first char state - detecting next character state */ + LWJSON_STREAM_STATE_PARSING_STRING, /*!< Parse string primitive */ + LWJSON_STREAM_STATE_PARSING_PRIMITIVE, /*!< Parse any primitive that is non-string, either "true", "false", "null" or a number */ +} lwjson_stream_state_t; + +/* Forward declaration */ +struct lwjson_stream_parser; + +/** + * \brief Callback function for various events + * + */ +typedef void (*lwjson_stream_parser_callback_fn)(struct lwjson_stream_parser* jsp, lwjson_stream_type_t type); + +/** + * \brief LwJSON streaming structure + */ +typedef struct lwjson_stream_parser { + lwjson_stream_stack_t + stack[LWJSON_CFG_STREAM_STACK_SIZE]; /*!< Stack used for parsing. TODO: Add conditional compilation flag */ + size_t stack_pos; /*!< Current stack position */ + + lwjson_stream_state_t parse_state; /*!< Parser state */ + + lwjson_stream_parser_callback_fn evt_fn; /*!< Event function for user */ + + /* State */ + union { + struct { + char buff[LWJSON_CFG_STREAM_STRING_MAX_LEN + + 1]; /*!< Buffer to write temporary data. TODO: Size to be variable with define */ + size_t buff_pos; /*!< Buffer position for next write (length of bytes in buffer) */ + size_t buff_total_pos; /*!< Total buffer position used up to now (in several data chunks) */ + uint8_t is_last; /*!< Status indicates if this is the last part of the string */ + } str; /*!< String structure. It is only used for keys and string objects. + Use primitive part for all other options */ + + struct { + char buff[LWJSON_CFG_STREAM_PRIMITIVE_MAX_LEN + 1]; /*!< Temporary write buffer */ + size_t buff_pos; /*!< Buffer position for next write */ + } prim; /*!< Primitive object. Used for all types, except key or string */ + + /* Todo: Add other types */ + } data; /*!< Data union used to parse various */ + + char prev_c; /*!< History of characters */ +} lwjson_stream_parser_t; -void lwjson_print_token(const lwjson_token_t* token); -void lwjson_print_json(const lwjson_t* lw); +lwjsonr_t lwjson_stream_init(lwjson_stream_parser_t* jsp, lwjson_stream_parser_callback_fn evt_fn); +lwjsonr_t lwjson_stream_reset(lwjson_stream_parser_t* jsp); +lwjsonr_t lwjson_stream_parse(lwjson_stream_parser_t* jsp, char c); /** * \brief Get number of tokens used to parse JSON * \param[in] lw: Pointer to LwJSON instance * \return Number of tokens used to parse JSON */ -#define lwjson_get_tokens_used(lw) (((lw) != NULL) ? ((lw)->next_free_token_pos + 1) : 0) +#define lwjson_get_tokens_used(lw) (((lw) != NULL) ? ((lw)->next_free_token_pos + 1) : 0) /** * \brief Get very first token of LwJSON instance * \param[in] lw: Pointer to LwJSON instance * \return Pointer to first token */ -#define lwjson_get_first_token(lw) (((lw) != NULL) ? (&(lw)->first_token) : NULL) +#define lwjson_get_first_token(lw) (((lw) != NULL) ? (&(lw)->first_token) : NULL) /** * \brief Get token value for \ref LWJSON_TYPE_NUM_INT type * \param[in] token: token with integer type * \return Int number if type is integer, `0` otherwise */ -#define lwjson_get_val_int(token) ((lwjson_int_t)(((token) != NULL && (token)->type == LWJSON_TYPE_NUM_INT) ? (token)->u.num_int : 0)) +#define lwjson_get_val_int(token) \ + ((lwjson_int_t)(((token) != NULL && (token)->type == LWJSON_TYPE_NUM_INT) ? (token)->u.num_int : 0)) /** * \brief Get token value for \ref LWJSON_TYPE_NUM_REAL type * \param[in] token: token with real type * \return Real numbeer if type is real, `0` otherwise */ -#define lwjson_get_val_real(token) ((lwjson_real_t)(((token) != NULL && (token)->type == LWJSON_TYPE_NUM_REAL) ? (token)->u.num_real : 0)) +#define lwjson_get_val_real(token) \ + ((lwjson_real_t)(((token) != NULL && (token)->type == LWJSON_TYPE_NUM_REAL) ? (token)->u.num_real : 0)) /** * \brief Get first child token for \ref LWJSON_TYPE_OBJECT or \ref LWJSON_TYPE_ARRAY types * \param[in] token: token with integer type * \return Pointer to first child or `NULL` if parent token is not object or array */ -#define lwjson_get_first_child(token) (const void *)(((token) != NULL && ((token)->type == LWJSON_TYPE_OBJECT || (token)->type == LWJSON_TYPE_ARRAY)) ? (token)->u.first_child : NULL) +#define lwjson_get_first_child(token) \ + (const void*)(((token) != NULL && ((token)->type == LWJSON_TYPE_OBJECT || (token)->type == LWJSON_TYPE_ARRAY)) \ + ? (token)->u.first_child \ + : NULL) /** * \brief Get string value from JSON token @@ -190,12 +291,13 @@ lwjson_get_val_string(const lwjson_token_t* token, size_t* str_len) { * \param[in] token: token with string type * \return Length of string in units of bytes */ -#define lwjson_get_val_string_length(token) ((size_t)(((token) != NULL && (token)->type == LWJSON_TYPE_STRING) ? (token)->u.str.token_value_len : 0)) +#define lwjson_get_val_string_length(token) \ + ((size_t)(((token) != NULL && (token)->type == LWJSON_TYPE_STRING) ? (token)->u.str.token_value_len : 0)) /** * \brief Compare string token with user input string for a case-sensitive match * \param[in] token: Token with string type - * \param[out] str: String to compare + * \param[in] str: NULL-terminated string to compare * \return `1` if equal, `0` otherwise */ static inline uint8_t @@ -209,7 +311,8 @@ lwjson_string_compare(const lwjson_token_t* token, const char* str) { /** * \brief Compare string token with user input string for a case-sensitive match * \param[in] token: Token with string type - * \param[out] str: String to compare + * \param[in] str: NULL-terminated string to compare + * \param[in] len: Length of the string in bytes * \return `1` if equal, `0` otherwise */ static inline uint8_t diff --git a/lwjson/src/include/lwjson/lwjson_opt.h b/lwjson/src/include/lwjson/lwjson_opt.h index f36288f..23c23ab 100644 --- a/lwjson/src/include/lwjson/lwjson_opt.h +++ b/lwjson/src/include/lwjson/lwjson_opt.h @@ -4,7 +4,7 @@ */ /* - * Copyright (c) 2020 Tilen MAJERLE + * Copyright (c) 2022 Tilen MAJERLE * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -29,7 +29,7 @@ * This file is part of LwJSON - Lightweight JSON format parser. * * Author: Tilen MAJERLE - * Version: v1.5.0 + * Version: v1.6.0 */ #ifndef LWJSON_HDR_OPT_H #define LWJSON_HDR_OPT_H @@ -59,7 +59,7 @@ extern "C" { * This is used for numbers in \ref LWJSON_TYPE_NUM_REAL token data type. */ #ifndef LWJSON_CFG_REAL_TYPE -#define LWJSON_CFG_REAL_TYPE float +#define LWJSON_CFG_REAL_TYPE float #endif /** @@ -69,7 +69,7 @@ extern "C" { * This is used for numbers in \ref LWJSON_TYPE_NUM_INT token data type. */ #ifndef LWJSON_CFG_INT_TYPE -#define LWJSON_CFG_INT_TYPE long long +#define LWJSON_CFG_INT_TYPE long long #endif /** @@ -78,9 +78,52 @@ extern "C" { * Default set to `0` to be JSON compliant */ #ifndef LWJSON_CFG_COMMENTS -#define LWJSON_CFG_COMMENTS 0 +#define LWJSON_CFG_COMMENTS 0 #endif +/** + * \defgroup LWJSON_OPT_STREAM JSON stream + * \brief JSON streaming confiuration + * \{ + */ + +/** + * \brief Max length of token key (object key name) to be available for stack storage + * + */ +#ifndef LWJSON_CFG_STREAM_KEY_MAX_LEN +#define LWJSON_CFG_STREAM_KEY_MAX_LEN 32 +#endif + +/** + * \brief Max stack size (depth) in units of \ref lwjson_stream_stack_t structure + * + */ +#ifndef LWJSON_CFG_STREAM_STACK_SIZE +#define LWJSON_CFG_STREAM_STACK_SIZE 16 +#endif + +/** + * \brief Max size of string for single parsing in units of bytes + * + */ +#ifndef LWJSON_CFG_STREAM_STRING_MAX_LEN +#define LWJSON_CFG_STREAM_STRING_MAX_LEN 256 +#endif + +/** + * \brief Max number of bytes used to parse primitive. + * + * Primitives are all numbers and logical values (null, true, false) + */ +#ifndef LWJSON_CFG_STREAM_PRIMITIVE_MAX_LEN +#define LWJSON_CFG_STREAM_PRIMITIVE_MAX_LEN 32 +#endif + +/** + * \} + */ + /** * \} */ diff --git a/lwjson/src/include/lwjson/lwjson_opts_template.h b/lwjson/src/include/lwjson/lwjson_opts_template.h index e4c0006..50b181e 100644 --- a/lwjson/src/include/lwjson/lwjson_opts_template.h +++ b/lwjson/src/include/lwjson/lwjson_opts_template.h @@ -4,7 +4,7 @@ */ /* - * Copyright (c) 2020 Tilen MAJERLE + * Copyright (c) 2022 Tilen MAJERLE * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -29,7 +29,7 @@ * This file is part of LwJSON - Lightweight JSON format parser. * * Author: Tilen MAJERLE - * Version: v1.5.0 + * Version: v1.6.0 */ #ifndef LWJSON_HDR_OPTS_H #define LWJSON_HDR_OPTS_H diff --git a/lwjson/src/lwjson/lwjson.c b/lwjson/src/lwjson/lwjson.c index a1659c0..b8a1482 100644 --- a/lwjson/src/lwjson/lwjson.c +++ b/lwjson/src/lwjson/lwjson.c @@ -4,7 +4,7 @@ */ /* - * Copyright (c) 2020 Tilen MAJERLE + * Copyright (c) 2022 Tilen MAJERLE * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -29,7 +29,7 @@ * This file is part of LwJSON - Lightweight JSON format parser. * * Author: Tilen MAJERLE - * Version: v1.5.0 + * Version: v1.6.0 */ #include #include "lwjson/lwjson.h" @@ -38,9 +38,9 @@ * \brief Internal string object */ typedef struct { - const char* start; /*!< Original pointer to beginning of JSON object */ - size_t len; /*!< Total length of input json string */ - const char* p; /*!< Current char pointer */ + const char* start; /*!< Original pointer to beginning of JSON object */ + size_t len; /*!< Total length of input json string */ + const char* p; /*!< Current char pointer */ } lwjson_int_str_t; /** @@ -68,7 +68,7 @@ prv_skip_blank(lwjson_int_str_t* pobj) { if (*pobj->p == ' ' || *pobj->p == '\t' || *pobj->p == '\r' || *pobj->p == '\n' || *pobj->p == '\f') { ++pobj->p; #if LWJSON_CFG_COMMENTS - /* Check for comments and remove them */ + /* Check for comments and remove them */ } else if (*pobj->p == '/') { ++pobj->p; if (pobj->p != NULL && *pobj->p == '*') { @@ -137,9 +137,8 @@ prv_parse_string(lwjson_int_str_t* pobj, const char** pout, size_t* poutlen) { case 'u': ++pobj->p; for (size_t i = 0; i < 4; ++i, ++len) { - if (!((*pobj->p >= '0' && *pobj->p <= '9') - || (*pobj->p >= 'a' && *pobj->p <= 'f') - || (*pobj->p >= 'A' && *pobj->p <= 'F'))) { + if (!((*pobj->p >= '0' && *pobj->p <= '9') || (*pobj->p >= 'a' && *pobj->p <= 'f') + || (*pobj->p >= 'A' && *pobj->p <= 'F'))) { return lwjsonERRJSON; } if (i < 3) { @@ -211,9 +210,10 @@ prv_parse_number(lwjson_int_str_t* pobj, lwjson_type_t* tout, lwjson_real_t* fou return lwjsonERRJSON; } is_minus = *pobj->p == '-' ? (++pobj->p, 1) : 0; - if (*pobj->p == '\0' /* Invalid string */ - || *pobj->p < '0' || *pobj->p > '9' /* Character outside number range */ - || (*pobj->p == '0' && (pobj->p[1] < '0' && pobj->p[1] > '9'))) { /* Number starts with 0 but not followed by dot */ + if (*pobj->p == '\0' /* Invalid string */ + || *pobj->p < '0' || *pobj->p > '9' /* Character outside number range */ + || (*pobj->p == '0' + && (pobj->p[1] < '0' && pobj->p[1] > '9'))) { /* Number starts with 0 but not followed by dot */ return lwjsonERRJSON; } @@ -221,7 +221,7 @@ prv_parse_number(lwjson_int_str_t* pobj, lwjson_type_t* tout, lwjson_real_t* fou for (num = 0; *pobj->p >= '0' && *pobj->p <= '9'; ++pobj->p) { num = num * 10 + (*pobj->p - '0'); } - if (pobj->p != NULL && *pobj->p == '.') { /* Number has exponent */ + if (pobj->p != NULL && *pobj->p == '.') { /* Number has exponent */ lwjson_real_t exp, dec_num; type = LWJSON_TYPE_NUM_REAL; /* Format is real */ @@ -233,16 +233,16 @@ prv_parse_number(lwjson_int_str_t* pobj, lwjson_type_t* tout, lwjson_real_t* fou for (exp = 1, dec_num = 0; *pobj->p >= '0' && *pobj->p <= '9'; ++pobj->p, exp *= 10) { dec_num = dec_num * 10 + (*pobj->p - '0'); } - num += dec_num / exp; /* Add decimal part to number */ + num += dec_num / exp; /* Add decimal part to number */ } - if (pobj->p != NULL && (*pobj->p == 'e' || *pobj->p == 'E')) { /* Engineering mode */ + if (pobj->p != NULL && (*pobj->p == 'e' || *pobj->p == 'E')) { /* Engineering mode */ uint8_t is_minus_exp; int exp_cnt; - type = LWJSON_TYPE_NUM_REAL; /* Format is real */ - ++pobj->p; /* Ignore enginnering sing part */ - is_minus_exp = *pobj->p == '-' ? (++pobj->p, 1) : 0;/* Check if negative */ - if (*pobj->p == '+') { /* Optional '+' is possible too */ + type = LWJSON_TYPE_NUM_REAL; /* Format is real */ + ++pobj->p; /* Ignore enginnering sing part */ + is_minus_exp = *pobj->p == '-' ? (++pobj->p, 1) : 0; /* Check if negative */ + if (*pobj->p == '+') { /* Optional '+' is possible too */ ++pobj->p; } if (*pobj->p < '0' || *pobj->p > '9') { /* Must be followed by number characters */ @@ -352,7 +352,7 @@ prv_find(const lwjson_token_t* parent, const char* path) { /* Check if index requested */ if (segment_len > 1) { - const lwjson_token_t *t; + const lwjson_token_t* t; size_t index = 0; /* Parse number */ @@ -377,7 +377,7 @@ prv_find(const lwjson_token_t* parent, const char* path) { } /* Scan all indexes and get first match */ - for (const lwjson_token_t* tmp_t, *t = parent->u.first_child; t != NULL; t = t->next) { + for (const lwjson_token_t *tmp_t, *t = parent->u.first_child; t != NULL; t = t->next) { if ((tmp_t = prv_find(t, path)) != NULL) { return tmp_t; } @@ -416,12 +416,10 @@ prv_check_valid_char_after_open_bracket(lwjson_int_str_t* pobj, lwjson_token_t* if ((res = prv_skip_blank(pobj)) != lwjsonOK) { return res; } - if (*pobj->p == '\0' - || (t->type == LWJSON_TYPE_OBJECT - && (*pobj->p != '"' && *pobj->p != '}')) + if (*pobj->p == '\0' || (t->type == LWJSON_TYPE_OBJECT && (*pobj->p != '"' && *pobj->p != '}')) || (t->type == LWJSON_TYPE_ARRAY - && (*pobj->p != '"' && *pobj->p != ']' && *pobj->p != '[' && *pobj->p != '{' && *pobj->p != '-' - && (*pobj->p < '0' || *pobj->p > '9') && *pobj->p != 't' && *pobj->p != 'n' && *pobj->p != 'f'))) { + && (*pobj->p != '"' && *pobj->p != ']' && *pobj->p != '[' && *pobj->p != '{' && *pobj->p != '-' + && (*pobj->p < '0' || *pobj->p > '9') && *pobj->p != 't' && *pobj->p != 'n' && *pobj->p != 'f'))) { res = lwjsonERRJSON; } return res; @@ -455,12 +453,8 @@ lwjson_init(lwjson_t* lw, lwjson_token_t* tokens, size_t tokens_len) { lwjsonr_t lwjson_parse_ex(lwjson_t* lw, const void* json_data, size_t json_len) { lwjsonr_t res = lwjsonOK; - lwjson_token_t* t, *to; - lwjson_int_str_t pobj = { - .start = json_data, - .len = json_len, - .p = json_data - }; + lwjson_token_t *t, *to; + lwjson_int_str_t pobj = {.start = json_data, .len = json_len, .p = json_data}; /* Check input parameters */ if (lw == NULL || json_data == NULL || json_len == 0) { @@ -508,21 +502,20 @@ lwjson_parse_ex(lwjson_t* lw, const void* json_data, size_t json_len) { if (*pobj.p == (to->type == LWJSON_TYPE_OBJECT ? '}' : ']')) { lwjson_token_t* parent = to->next; to->next = NULL; - to = parent; ++pobj.p; - /* End of string, check if properly terminated */ - if (to == NULL) { + /* End of string if to == NULL (no parent), check if properly terminated */ + if ((to = parent) == NULL) { prv_skip_blank(&pobj); - res = (pobj.p == NULL || *pobj.p == '\0' || (pobj.p - pobj.start) == pobj.len) ? lwjsonOK : lwjsonERR; + res = (pobj.p == NULL || *pobj.p == '\0' || (size_t)(pobj.p - pobj.start) == pobj.len) ? lwjsonOK + : lwjsonERR; goto ret; } continue; } /* Allocate new token */ - t = prv_alloc_token(lw); - if (t == NULL) { + if ((t = prv_alloc_token(lw)) == NULL) { res = lwjsonERRMEM; goto ret; } @@ -556,7 +549,7 @@ lwjson_parse_ex(lwjson_t* lw, const void* json_data, size_t json_len) { if ((res = prv_check_valid_char_after_open_bracket(&pobj, t)) != lwjsonOK) { goto ret; } - t->next = to; /* Temporary saved as parent object */ + t->next = to; /* Temporary saved as parent object */ to = t; break; case '"': @@ -630,7 +623,7 @@ lwjson_parse_ex(lwjson_t* lw, const void* json_data, size_t json_len) { if (pobj.p == NULL || *pobj.p == '\0' || (*pobj.p != ',' && *pobj.p != ']' && *pobj.p != '}')) { res = lwjsonERRJSON; goto ret; - } else if (*pobj.p == ',') { /* Check to advance to next token immediatey */ + } else if (*pobj.p == ',') { /* Check to advance to next token immediatey */ ++pobj.p; } } diff --git a/lwjson/src/lwjson/lwjson_debug.c b/lwjson/src/lwjson/lwjson_debug.c index 7b9ddf5..1ac9ada 100644 --- a/lwjson/src/lwjson/lwjson_debug.c +++ b/lwjson/src/lwjson/lwjson_debug.c @@ -4,7 +4,7 @@ */ /* - * Copyright (c) 2020 Tilen MAJERLE + * Copyright (c) 2022 Tilen MAJERLE * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -29,17 +29,17 @@ * This file is part of LwJSON - Lightweight JSON format parser. * * Author: Tilen MAJERLE - * Version: v1.5.0 + * Version: v1.6.0 */ -#include #include +#include #include "lwjson/lwjson.h" /** * \brief Token print instance */ typedef struct { - size_t indent; /*!< Indent level for token print */ + size_t indent; /*!< Indent level for token print */ } lwjson_token_print_t; /** @@ -49,7 +49,7 @@ typedef struct { */ static void prv_print_token(lwjson_token_print_t* p, const lwjson_token_t* token) { -#define print_indent() printf("%.*s", (int)((p->indent)), "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); +#define print_indent() printf("%.*s", (int)((p->indent)), "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); if (token == NULL) { return; @@ -118,7 +118,7 @@ prv_print_token(lwjson_token_print_t* p, const lwjson_token_t* token) { */ void lwjson_print_token(const lwjson_token_t* token) { - lwjson_token_print_t p = { 0 }; + lwjson_token_print_t p = {0}; prv_print_token(&p, token); } @@ -129,6 +129,6 @@ lwjson_print_token(const lwjson_token_t* token) { */ void lwjson_print_json(const lwjson_t* lw) { - lwjson_token_print_t p = { 0 }; + lwjson_token_print_t p = {0}; prv_print_token(&p, lwjson_get_first_token(lw)); -} \ No newline at end of file +} diff --git a/lwjson/src/lwjson/lwjson_stream.c b/lwjson/src/lwjson/lwjson_stream.c new file mode 100644 index 0000000..54f08c5 --- /dev/null +++ b/lwjson/src/lwjson/lwjson_stream.c @@ -0,0 +1,436 @@ +/** + * \file lwjson_stream.c + * \brief Lightweight JSON format parser + */ + +/* + * Copyright (c) 2022 Tilen MAJERLE + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * This file is part of LwJSON - Lightweight JSON format parser. + * + * Author: Tilen MAJERLE + * Version: v1.6.0 + */ +#include +#include "lwjson/lwjson.h" + +#if defined(LWJSON_DEV) +#include +#define DEBUG_STRING_PREFIX_SPACES \ + " " +#define LWJSON_DEBUG(jsp, ...) \ + do { \ + if ((jsp) != NULL) { \ + printf("%.*s", (int)(4 * (jsp)->stack_pos), DEBUG_STRING_PREFIX_SPACES); \ + } \ + printf(__VA_ARGS__); \ + } while (0) + +/* Strings for debug */ +static const char* type_strings[] = { + [LWJSON_STREAM_TYPE_NONE] = "none", + [LWJSON_STREAM_TYPE_OBJECT] = "object", + [LWJSON_STREAM_TYPE_OBJECT_END] = "object_end", + [LWJSON_STREAM_TYPE_ARRAY] = "array", + [LWJSON_STREAM_TYPE_ARRAY_END] = "array_end", + [LWJSON_STREAM_TYPE_KEY] = "key", + [LWJSON_STREAM_TYPE_STRING] = "string", + [LWJSON_STREAM_TYPE_TRUE] = "true", + [LWJSON_STREAM_TYPE_FALSE] = "false", + [LWJSON_STREAM_TYPE_NULL] = "null", + [LWJSON_STREAM_TYPE_NUMBER] = "number", +}; +#else +#define LWJSON_DEBUG(jsp, ...) +#endif /* defined(LWJSON_DEV) */ + +/** + * \brief Sends an event to user for further processing + * + */ +#define SEND_EVT(jsp, type) \ + if ((jsp) != NULL && (jsp)->evt_fn != NULL) { \ + (jsp)->evt_fn((jsp), (type)); \ + } + +/** + * \brief Check if character is a space character (with extended chars) + * \param[in] c: Character to check + * \return `1` if considered extended space, `0` otherwise + */ +#define prv_is_space_char_ext(c) ((c) == ' ' || (c) == '\t' || (c) == '\r' || (c) == '\n' || (c) == '\f') + +/** + * \brief Push "parent" state to the artificial stack + * \param jsp: JSON stream parser instance + * \param type: Stream type to be pushed on stack + * \return `1` on success, `0` otherwise + */ +static uint8_t +prv_stack_push(lwjson_stream_parser_t* jsp, lwjson_stream_type_t type) { + if (jsp->stack_pos < LWJSON_ARRAYSIZE(jsp->stack)) { + jsp->stack[jsp->stack_pos].type = type; + jsp->stack[jsp->stack_pos].meta.index = 0; + LWJSON_DEBUG(jsp, "Pushed to stack: %s\r\n", type_strings[type]); + jsp->stack_pos++; + return 1; + } + return 0; +} + +/** + * \brief Pop value from stack (remove it) and return its value + * \param jsp: JSON stream parser instance + * \return Member of \ref lwjson_stream_type_t enumeration + */ +static lwjson_stream_type_t +prv_stack_pop(lwjson_stream_parser_t* jsp) { + if (jsp->stack_pos > 0) { + lwjson_stream_type_t t = jsp->stack[--jsp->stack_pos].type; + jsp->stack[jsp->stack_pos].type = LWJSON_STREAM_TYPE_NONE; + LWJSON_DEBUG(jsp, "Popped from stack: %s\r\n", type_strings[t]); + + /* Take care of array to indicate number of entries */ + if (jsp->stack_pos > 0 && jsp->stack[jsp->stack_pos - 1].type == LWJSON_STREAM_TYPE_ARRAY) { + jsp->stack[jsp->stack_pos - 1].meta.index++; + } + return t; + } + return LWJSON_STREAM_TYPE_NONE; +} + +/** + * \brief Get top type value currently on the stack + * \param jsp: JSON stream parser instance + * \return Member of \ref lwjson_stream_type_t enumeration + */ +static lwjson_stream_type_t +prv_stack_get_top(lwjson_stream_parser_t* jsp) { + if (jsp->stack_pos > 0) { + return jsp->stack[jsp->stack_pos - 1].type; + } + return LWJSON_STREAM_TYPE_NONE; +} + +/** + * \brief Initialize LwJSON stream object before parsing takes place + * \param[in,out] jsp: Stream JSON structure + * \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise + */ +lwjsonr_t +lwjson_stream_init(lwjson_stream_parser_t* jsp, lwjson_stream_parser_callback_fn evt_fn) { + memset(jsp, 0x00, sizeof(*jsp)); + jsp->parse_state = LWJSON_STREAM_STATE_WAITINGFIRSTCHAR; + jsp->evt_fn = evt_fn; + return lwjsonOK; +} + +/** + * \brief Reset LwJSON stream structure + * + * \param jsp: LwJSON stream parser + * \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise + */ +lwjsonr_t +lwjson_stream_reset(lwjson_stream_parser_t* jsp) { + jsp->parse_state = LWJSON_STREAM_STATE_WAITINGFIRSTCHAR; + jsp->stack_pos = 0; + return lwjsonOK; +} + +/** + * \brief Parse JSON string in streaming mode + * \param[in,out] jsp: Stream JSON structure + * \param[in] c: Character to parse + * \return \ref lwjsonOK if parsing is in progress and no hard error detected + * \ref lwjsonSTREAMDONE when valid JSON was detected and stack level reached back `0` level + */ +lwjsonr_t +lwjson_stream_parse(lwjson_stream_parser_t* jsp, char c) { + /* Get first character first */ + if (jsp->parse_state == LWJSON_STREAM_STATE_WAITINGFIRSTCHAR && c != '{' && c != '[') { + return lwjsonSTREAMDONE; + } + +start_over: + /* + * Determine what to do from parsing state + */ + switch (jsp->parse_state) { + + /* + * Waiting for very first valid characters, + * that is used to indicate start of JSON stream + */ + case LWJSON_STREAM_STATE_WAITINGFIRSTCHAR: + case LWJSON_STREAM_STATE_PARSING: { + /* Determine start of object or an array */ + if (c == '{' || c == '[') { + /* Reset stack pointer if this character came from waiting for first character */ + if (jsp->parse_state == LWJSON_STREAM_STATE_WAITINGFIRSTCHAR) { + jsp->stack_pos = 0; + } + if (!prv_stack_push(jsp, c == '{' ? LWJSON_STREAM_TYPE_OBJECT : LWJSON_STREAM_TYPE_ARRAY)) { + LWJSON_DEBUG(jsp, "Cannot push object/array to stack\r\n"); + return lwjsonERRMEM; + } + jsp->parse_state = LWJSON_STREAM_STATE_PARSING; + SEND_EVT(jsp, c == '{' ? LWJSON_STREAM_TYPE_OBJECT : LWJSON_STREAM_TYPE_ARRAY); + + /* Determine end of object or an array */ + } else if (c == '}' || c == ']') { + lwjson_stream_type_t t = prv_stack_get_top(jsp); + + /* + * If it is a key last entry on closing area, + * it is an error - an example: {"key":} + */ + if (t == LWJSON_STREAM_TYPE_KEY) { + LWJSON_DEBUG(jsp, "ERROR - key should not be followed by ] without value for a key\r\n"); + return lwjsonERRJSON; + } + + /* + * Check if closing character matches stack value + * Avoid cases like: {"key":"value"] or ["v1", "v2", "v3"} + */ + if ((c == '}' && t != LWJSON_STREAM_TYPE_OBJECT) || (c == ']' && t != LWJSON_STREAM_TYPE_ARRAY)) { + LWJSON_DEBUG(jsp, "ERROR - closing character '%c' does not match stack element \"%s\"\r\n", c, + type_strings[t]); + return lwjsonERRJSON; + } + + /* Now remove the array or object from stack */ + if (prv_stack_pop(jsp) == LWJSON_STREAM_TYPE_NONE) { + return lwjsonERRJSON; + } + + /* + * Check if above is a key type + * and remove it too as we finished with processing of potential case. + * + * {"key":{"abc":1}} - remove "key" part + */ + if (prv_stack_get_top(jsp) == LWJSON_STREAM_TYPE_KEY) { + prv_stack_pop(jsp); + } + SEND_EVT(jsp, c == '}' ? LWJSON_STREAM_TYPE_OBJECT_END : LWJSON_STREAM_TYPE_ARRAY_END); + + /* If that is the end of JSON */ + if (jsp->stack_pos == 0) { + return lwjsonSTREAMDONE; + } + + /* Determine start of string - can be key or regular string (in array or after key) */ + } else if (c == '"') { +#if defined(LWJSON_DEV) + lwjson_stream_type_t t = prv_stack_get_top(jsp); + if (t == LWJSON_STREAM_TYPE_OBJECT) { + LWJSON_DEBUG(jsp, "Start of string parsing - expected key name in an object\r\n"); + } else if (t == LWJSON_STREAM_TYPE_KEY) { + LWJSON_DEBUG(jsp, + "Start of string parsing - string value associated to previous key in an object\r\n"); + } else if (t == LWJSON_STREAM_TYPE_ARRAY) { + LWJSON_DEBUG(jsp, "Start of string parsing - string entry in an array\r\n"); + } +#endif /* defined(LWJSON_DEV) */ + jsp->parse_state = LWJSON_STREAM_STATE_PARSING_STRING; + memset(&jsp->data.str, 0x00, sizeof(jsp->data.str)); + + /* Check for end of key character */ + } else if (c == ':') { + lwjson_stream_type_t t = prv_stack_get_top(jsp); + + /* + * Color can only be followed by key on the stack + * + * It is clear JSON error if this is not the case + */ + if (t != LWJSON_STREAM_TYPE_KEY) { + LWJSON_DEBUG(jsp, "Error - wrong ':' character\r\n"); + return lwjsonERRJSON; + } + /* Check if this is start of number or "true", "false" or "null" */ + } else if (c == '-' || (c >= '0' && c <= '9') || c == 't' || c == 'f' || c == 'n') { + LWJSON_DEBUG(jsp, "Start of primitive parsing parsing - %s, First char: %c\r\n", + (c == '-' || (c >= '0' && c <= '9')) ? "number" : "true,false,null", c); + jsp->parse_state = LWJSON_STREAM_STATE_PARSING_PRIMITIVE; + memset(&jsp->data.prim, 0x00, sizeof(jsp->data.prim)); + jsp->data.prim.buff[jsp->data.prim.buff_pos++] = c; + } + break; + } + + /* + * Parse any type of string in a sequence + * + * It is used for key or string in an object or an array + */ + case LWJSON_STREAM_STATE_PARSING_STRING: { + lwjson_stream_type_t t = prv_stack_get_top(jsp); + + /* + * Quote character may trigger end of string, + * or if backslasled before - it is part of string + * + * TODO: Handle backslash + */ + if (c == '"' && jsp->prev_c != '\\') { +#if defined(LWJSON_DEV) + if (t == LWJSON_STREAM_TYPE_OBJECT) { + LWJSON_DEBUG(jsp, "End of string parsing - object key name: \"%s\"\r\n", jsp->data.str.buff); + } else if (t == LWJSON_STREAM_TYPE_KEY) { + LWJSON_DEBUG( + jsp, "End of string parsing - string value associated to previous key in an object: \"%s\"\r\n", + jsp->data.str.buff); + } else if (t == LWJSON_STREAM_TYPE_ARRAY) { + LWJSON_DEBUG(jsp, "End of string parsing - an array string entry: \"%s\"\r\n", jsp->data.str.buff); + } +#endif /* defined(LWJSON_DEV) */ + + /* Set is_last to 1 as this is the last part of this string token */ + jsp->data.str.is_last = 1; + + /* + * When top of stack is object - string is treated as a key + * When top of stack is a key - string is a value for a key - notify user and pop the value for key + * When top of stack is an array - string is one type - notify user and don't do anything + */ + if (t == LWJSON_STREAM_TYPE_OBJECT) { + SEND_EVT(jsp, LWJSON_STREAM_TYPE_KEY); + if (prv_stack_push(jsp, LWJSON_STREAM_TYPE_KEY)) { + size_t len = jsp->data.str.buff_pos; + if (len > (sizeof(jsp->stack[0].meta.name) - 1)) { + len = sizeof(jsp->stack[0].meta.name) - 1; + } + memcpy(jsp->stack[jsp->stack_pos - 1].meta.name, jsp->data.str.buff, len); + jsp->stack[jsp->stack_pos - 1].meta.name[len] = '\0'; + } else { + LWJSON_DEBUG(jsp, "Cannot push key to stack\r\n"); + return lwjsonERRMEM; + } + } else if (t == LWJSON_STREAM_TYPE_KEY) { + SEND_EVT(jsp, LWJSON_STREAM_TYPE_STRING); + prv_stack_pop(jsp); + /* Next character to wait for is either space or comma or end of object */ + } else if (t == LWJSON_STREAM_TYPE_ARRAY) { + SEND_EVT(jsp, LWJSON_STREAM_TYPE_STRING); + jsp->stack[jsp->stack_pos - 1].meta.index++; + } + jsp->parse_state = LWJSON_STREAM_STATE_PARSING; + } else { + /* TODO: Check other backslash elements */ + jsp->data.str.buff[jsp->data.str.buff_pos++] = c; + jsp->data.str.buff_total_pos++; + + /* Handle buffer "overflow" */ + if (jsp->data.str.buff_pos >= (LWJSON_CFG_STREAM_STRING_MAX_LEN - 1)) { + jsp->data.str.buff[jsp->data.str.buff_pos] = '\0'; + + /* + * - For array or key types - following one is always string + * - For object type - character is key + */ + SEND_EVT(jsp, (t == LWJSON_STREAM_TYPE_KEY || t == LWJSON_STREAM_TYPE_ARRAY) + ? LWJSON_STREAM_TYPE_STRING + : LWJSON_STREAM_TYPE_KEY); + jsp->data.str.buff_pos = 0; + } + } + break; + } + + /* + * Parse any type of primitive that is not a string. + * + * true, false, null or any number primitive + */ + case LWJSON_STREAM_STATE_PARSING_PRIMITIVE: { + /* Any character except space, comma, or end of array/object are valid */ + if (!prv_is_space_char_ext(c) && c != ',' && c != ']' && c != '}') { + if (jsp->data.prim.buff_pos < sizeof(jsp->data.prim.buff) - 1) { + jsp->data.prim.buff[jsp->data.prim.buff_pos++] = c; + } + } else { + lwjson_stream_type_t t = prv_stack_get_top(jsp); + +#if defined(LWJSON_DEV) + if (t == LWJSON_STREAM_TYPE_OBJECT) { + /* TODO: Handle error - primitive cannot be just after object */ + } else if (t == LWJSON_STREAM_TYPE_KEY) { + LWJSON_DEBUG( + jsp, + "End of primitive parsing - string value associated to previous key in an object: \"%s\"\r\n", + jsp->data.prim.buff); + } else if (t == LWJSON_STREAM_TYPE_ARRAY) { + LWJSON_DEBUG(jsp, "End of primitive parsing - an array string entry: \"%s\"\r\n", + jsp->data.prim.buff); + } +#endif /* defined(LWJSON_DEV) */ + + /* + * This is the end of primitive parsing + * + * It is assumed that buffer for primitive can handle at least + * true, false, null or all number characters (that being real or int number) + */ + if (jsp->data.prim.buff_pos == 4 && strncmp(jsp->data.prim.buff, "true", 4) == 0) { + LWJSON_DEBUG(jsp, "Primitive parsed as %s\r\n", "true"); + SEND_EVT(jsp, LWJSON_STREAM_TYPE_TRUE); + } else if (jsp->data.prim.buff_pos == 4 && strncmp(jsp->data.prim.buff, "null", 4) == 0) { + LWJSON_DEBUG(jsp, "Primitive parsed as %s\r\n", "null"); + SEND_EVT(jsp, LWJSON_STREAM_TYPE_NULL); + } else if (jsp->data.prim.buff_pos == 5 && strncmp(jsp->data.prim.buff, "false", 5) == 0) { + LWJSON_DEBUG(jsp, "Primitive parsed as %s\r\n", "false"); + SEND_EVT(jsp, LWJSON_STREAM_TYPE_FALSE); + } else if (jsp->data.prim.buff[0] == '-' + || (jsp->data.prim.buff[0] >= '0' && jsp->data.prim.buff[0] <= '9')) { + LWJSON_DEBUG(jsp, "Primitive parsed - number\r\n"); + SEND_EVT(jsp, LWJSON_STREAM_TYPE_NUMBER); + } else { + LWJSON_DEBUG(jsp, "Invalid primitive type. Got: %s\r\n", jsp->data.prim.buff); + } + if (t == LWJSON_STREAM_TYPE_KEY) { + prv_stack_pop(jsp); + } else if (t == LWJSON_STREAM_TYPE_ARRAY) { + jsp->stack[jsp->stack_pos - 1].meta.index++; + } + + /* + * Received character is not part of the primitive and must be processed again + * + * Set state to default state and start from beginning + */ + jsp->parse_state = LWJSON_STREAM_STATE_PARSING; + goto start_over; + } + break; + } + + /* TODO: Add other case statements */ + default: + break; + } + jsp->prev_c = c; /* Save current c as previous for next round */ + return lwjsonSTREAMINPROG; +} diff --git a/test/json/custom_stream.json b/test/json/custom_stream.json new file mode 100644 index 0000000..fe4f850 --- /dev/null +++ b/test/json/custom_stream.json @@ -0,0 +1,13 @@ +{ + "test": "abc", + "array": [ + "123", + "def", + "ghi" + ], + "array_in_array": [ + ["1", "2", "3"], + ["4", "5", "6"], + ["7", "8", "9"] + ] +} \ No newline at end of file diff --git a/test/json/weather_onecall.json b/test/json/weather_onecall.json index 6a76f0a..11402a5 100644 --- a/test/json/weather_onecall.json +++ b/test/json/weather_onecall.json @@ -1 +1,1837 @@ -{"lat":45.57,"lon":15.19,"timezone":"Europe/Ljubljana","timezone_offset":3600,"current":{"dt":1607029923,"sunrise":1606976516,"sunset":1607008607,"temp":273.15,"feels_like":270.73,"pressure":1008,"humidity":97,"dew_point":272.78,"uvi":0,"clouds":100,"visibility":7000,"wind_speed":0.54,"wind_deg":279,"weather":[{"id":701,"main":"Mist","description":"mist","icon":"50n"}]},"minutely":[{"dt":1607029980,"precipitation":0},{"dt":1607030040,"precipitation":0},{"dt":1607030100,"precipitation":0},{"dt":1607030160,"precipitation":0},{"dt":1607030220,"precipitation":0},{"dt":1607030280,"precipitation":0},{"dt":1607030340,"precipitation":0},{"dt":1607030400,"precipitation":0},{"dt":1607030460,"precipitation":0},{"dt":1607030520,"precipitation":0},{"dt":1607030580,"precipitation":0},{"dt":1607030640,"precipitation":0},{"dt":1607030700,"precipitation":0},{"dt":1607030760,"precipitation":0},{"dt":1607030820,"precipitation":0},{"dt":1607030880,"precipitation":0},{"dt":1607030940,"precipitation":0},{"dt":1607031000,"precipitation":0},{"dt":1607031060,"precipitation":0},{"dt":1607031120,"precipitation":0},{"dt":1607031180,"precipitation":0},{"dt":1607031240,"precipitation":0},{"dt":1607031300,"precipitation":0},{"dt":1607031360,"precipitation":0},{"dt":1607031420,"precipitation":0},{"dt":1607031480,"precipitation":0},{"dt":1607031540,"precipitation":0},{"dt":1607031600,"precipitation":0},{"dt":1607031660,"precipitation":0},{"dt":1607031720,"precipitation":0},{"dt":1607031780,"precipitation":0},{"dt":1607031840,"precipitation":0},{"dt":1607031900,"precipitation":0},{"dt":1607031960,"precipitation":0},{"dt":1607032020,"precipitation":0},{"dt":1607032080,"precipitation":0},{"dt":1607032140,"precipitation":0},{"dt":1607032200,"precipitation":0},{"dt":1607032260,"precipitation":0},{"dt":1607032320,"precipitation":0},{"dt":1607032380,"precipitation":0},{"dt":1607032440,"precipitation":0},{"dt":1607032500,"precipitation":0},{"dt":1607032560,"precipitation":0},{"dt":1607032620,"precipitation":0},{"dt":1607032680,"precipitation":0},{"dt":1607032740,"precipitation":0},{"dt":1607032800,"precipitation":0},{"dt":1607032860,"precipitation":0},{"dt":1607032920,"precipitation":0},{"dt":1607032980,"precipitation":0},{"dt":1607033040,"precipitation":0},{"dt":1607033100,"precipitation":0},{"dt":1607033160,"precipitation":0},{"dt":1607033220,"precipitation":0},{"dt":1607033280,"precipitation":0},{"dt":1607033340,"precipitation":0},{"dt":1607033400,"precipitation":0},{"dt":1607033460,"precipitation":0},{"dt":1607033520,"precipitation":0},{"dt":1607033580,"precipitation":0}],"hourly":[{"dt":1607029200,"temp":273.15,"feels_like":270.73,"pressure":1008,"humidity":97,"dew_point":272.78,"uvi":0,"clouds":100,"visibility":10000,"wind_speed":0.54,"wind_deg":279,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.29},{"dt":1607032800,"temp":272.59,"feels_like":269.76,"pressure":1007,"humidity":97,"dew_point":272.22,"uvi":0,"clouds":100,"visibility":10000,"wind_speed":1.01,"wind_deg":281,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.28},{"dt":1607036400,"temp":271.72,"feels_like":269.22,"pressure":1006,"humidity":96,"dew_point":271.23,"uvi":0,"clouds":100,"visibility":10000,"wind_speed":0.35,"wind_deg":160,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.25},{"dt":1607040000,"temp":271.02,"feels_like":268.22,"pressure":1005,"humidity":96,"dew_point":270.53,"uvi":0,"clouds":100,"visibility":10000,"wind_speed":0.65,"wind_deg":253,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.28},{"dt":1607043600,"temp":269.93,"feels_like":266.79,"pressure":1005,"humidity":96,"dew_point":269.45,"uvi":0,"clouds":56,"visibility":10000,"wind_speed":0.95,"wind_deg":275,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"pop":0.03},{"dt":1607047200,"temp":270.23,"feels_like":267.14,"pressure":1005,"humidity":96,"dew_point":268.24,"uvi":0,"clouds":77,"visibility":10000,"wind_speed":0.93,"wind_deg":282,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"pop":0},{"dt":1607050800,"temp":269.63,"feels_like":266.68,"pressure":1004,"humidity":95,"dew_point":267.52,"uvi":0,"clouds":63,"visibility":10000,"wind_speed":0.61,"wind_deg":273,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"pop":0},{"dt":1607054400,"temp":269.46,"feels_like":266.56,"pressure":1004,"humidity":95,"dew_point":267.3,"uvi":0,"clouds":48,"visibility":10000,"wind_speed":0.51,"wind_deg":249,"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"pop":0},{"dt":1607058000,"temp":269.72,"feels_like":266.6,"pressure":1005,"humidity":95,"dew_point":267.49,"uvi":0,"clouds":38,"visibility":10000,"wind_speed":0.86,"wind_deg":283,"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"pop":0},{"dt":1607061600,"temp":269.77,"feels_like":266.81,"pressure":1005,"humidity":95,"dew_point":267.52,"uvi":0,"clouds":34,"visibility":10000,"wind_speed":0.64,"wind_deg":275,"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"pop":0},{"dt":1607065200,"temp":270.2,"feels_like":267.36,"pressure":1005,"humidity":95,"dew_point":267.95,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":0.55,"wind_deg":270,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1607068800,"temp":272.92,"feels_like":270.58,"pressure":1005,"humidity":96,"dew_point":270.77,"uvi":0.26,"clouds":49,"visibility":10000,"wind_speed":0.35,"wind_deg":167,"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"pop":0},{"dt":1607072400,"temp":274.41,"feels_like":272.19,"pressure":1005,"humidity":90,"dew_point":272.76,"uvi":0.6,"clouds":65,"visibility":10000,"wind_speed":0.29,"wind_deg":144,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"pop":0},{"dt":1607076000,"temp":275.47,"feels_like":273.46,"pressure":1005,"humidity":91,"dew_point":274.22,"uvi":0.85,"clouds":70,"visibility":10000,"wind_speed":0.25,"wind_deg":276,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"pop":0},{"dt":1607079600,"temp":276.53,"feels_like":274.59,"pressure":1005,"humidity":92,"dew_point":275.48,"uvi":0.99,"clouds":76,"visibility":1176,"wind_speed":0.43,"wind_deg":266,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"pop":0},{"dt":1607083200,"temp":277.13,"feels_like":275.22,"pressure":1005,"humidity":92,"dew_point":276.08,"uvi":0.85,"clouds":76,"visibility":144,"wind_speed":0.54,"wind_deg":207,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"pop":0},{"dt":1607086800,"temp":277.25,"feels_like":275.29,"pressure":1005,"humidity":92,"dew_point":276.21,"uvi":0.55,"clouds":88,"visibility":49,"wind_speed":0.64,"wind_deg":165,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0},{"dt":1607090400,"temp":276.81,"feels_like":274.39,"pressure":1004,"humidity":92,"dew_point":275.77,"uvi":0.24,"clouds":94,"visibility":46,"wind_speed":1.18,"wind_deg":144,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0},{"dt":1607094000,"temp":276.37,"feels_like":273.83,"pressure":1004,"humidity":92,"dew_point":275.34,"uvi":0,"clouds":96,"visibility":46,"wind_speed":1.25,"wind_deg":140,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0},{"dt":1607097600,"temp":276.11,"feels_like":273.67,"pressure":1004,"humidity":92,"dew_point":275.05,"uvi":0,"clouds":97,"visibility":53,"wind_speed":1.04,"wind_deg":135,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0},{"dt":1607101200,"temp":276.12,"feels_like":273.64,"pressure":1004,"humidity":92,"dew_point":275.05,"uvi":0,"clouds":98,"visibility":65,"wind_speed":1.11,"wind_deg":122,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0},{"dt":1607104800,"temp":275.96,"feels_like":273.42,"pressure":1004,"humidity":92,"dew_point":274.88,"uvi":0,"clouds":98,"visibility":78,"wind_speed":1.15,"wind_deg":126,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0},{"dt":1607108400,"temp":275.85,"feels_like":273.46,"pressure":1005,"humidity":92,"dew_point":274.74,"uvi":0,"clouds":100,"visibility":528,"wind_speed":0.92,"wind_deg":122,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0},{"dt":1607112000,"temp":275.69,"feels_like":273.4,"pressure":1005,"humidity":92,"dew_point":274.57,"uvi":0,"clouds":100,"visibility":532,"wind_speed":0.73,"wind_deg":124,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0},{"dt":1607115600,"temp":275.71,"feels_like":273.5,"pressure":1005,"humidity":92,"dew_point":274.6,"uvi":0,"clouds":100,"visibility":572,"wind_speed":0.62,"wind_deg":117,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0},{"dt":1607119200,"temp":275.85,"feels_like":273.69,"pressure":1005,"humidity":92,"dew_point":274.74,"uvi":0,"clouds":100,"visibility":167,"wind_speed":0.59,"wind_deg":102,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0},{"dt":1607122800,"temp":276.05,"feels_like":273.81,"pressure":1005,"humidity":92,"dew_point":274.97,"uvi":0,"clouds":100,"visibility":136,"wind_speed":0.75,"wind_deg":97,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0},{"dt":1607126400,"temp":276.29,"feels_like":274.02,"pressure":1005,"humidity":92,"dew_point":275.21,"uvi":0,"clouds":100,"visibility":107,"wind_speed":0.84,"wind_deg":110,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0},{"dt":1607130000,"temp":276.69,"feels_like":274.45,"pressure":1005,"humidity":92,"dew_point":275.62,"uvi":0,"clouds":100,"visibility":103,"wind_speed":0.9,"wind_deg":113,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.02},{"dt":1607133600,"temp":276.89,"feels_like":274.53,"pressure":1005,"humidity":92,"dew_point":275.82,"uvi":0,"clouds":100,"visibility":78,"wind_speed":1.12,"wind_deg":114,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.02},{"dt":1607137200,"temp":276.93,"feels_like":274.51,"pressure":1004,"humidity":92,"dew_point":275.85,"uvi":0,"clouds":100,"visibility":61,"wind_speed":1.21,"wind_deg":122,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.02},{"dt":1607140800,"temp":277.06,"feels_like":274.62,"pressure":1004,"humidity":92,"dew_point":275.98,"uvi":0,"clouds":100,"visibility":62,"wind_speed":1.28,"wind_deg":122,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.02},{"dt":1607144400,"temp":277.19,"feels_like":274.7,"pressure":1004,"humidity":92,"dew_point":276.11,"uvi":0,"clouds":100,"visibility":66,"wind_speed":1.38,"wind_deg":114,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.02},{"dt":1607148000,"temp":277.12,"feels_like":274.57,"pressure":1004,"humidity":92,"dew_point":276.04,"uvi":0,"clouds":100,"visibility":58,"wind_speed":1.45,"wind_deg":105,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.02},{"dt":1607151600,"temp":277.12,"feels_like":274.92,"pressure":1005,"humidity":92,"dew_point":276.06,"uvi":0,"clouds":100,"visibility":58,"wind_speed":0.95,"wind_deg":98,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0.03},{"dt":1607155200,"temp":277.54,"feels_like":275.27,"pressure":1005,"humidity":92,"dew_point":276.5,"uvi":0.17,"clouds":100,"visibility":53,"wind_speed":1.15,"wind_deg":101,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0.04},{"dt":1607158800,"temp":278.04,"feels_like":275.8,"pressure":1005,"humidity":92,"dew_point":277.01,"uvi":0.4,"clouds":100,"visibility":49,"wind_speed":1.23,"wind_deg":95,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0.03},{"dt":1607162400,"temp":278.63,"feels_like":276.64,"pressure":1005,"humidity":93,"dew_point":277.6,"uvi":0.44,"clouds":100,"visibility":51,"wind_speed":1.08,"wind_deg":92,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0.02},{"dt":1607166000,"temp":279.08,"feels_like":277.01,"pressure":1004,"humidity":93,"dew_point":278.04,"uvi":0.52,"clouds":100,"visibility":49,"wind_speed":1.32,"wind_deg":88,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0.02},{"dt":1607169600,"temp":279.19,"feels_like":277.03,"pressure":1004,"humidity":93,"dew_point":278.15,"uvi":0.44,"clouds":100,"visibility":44,"wind_speed":1.48,"wind_deg":87,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0.03},{"dt":1607173200,"temp":278.94,"feels_like":276.77,"pressure":1004,"humidity":93,"dew_point":277.9,"uvi":0.35,"clouds":100,"visibility":47,"wind_speed":1.42,"wind_deg":89,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0.13},{"dt":1607176800,"temp":278.63,"feels_like":276.51,"pressure":1004,"humidity":93,"dew_point":277.59,"uvi":0.15,"clouds":100,"visibility":55,"wind_speed":1.27,"wind_deg":94,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0.09},{"dt":1607180400,"temp":278.36,"feels_like":276.14,"pressure":1004,"humidity":92,"dew_point":277.3,"uvi":0,"clouds":100,"visibility":70,"wind_speed":1.29,"wind_deg":90,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0.09},{"dt":1607184000,"temp":278.33,"feels_like":276.08,"pressure":1004,"humidity":92,"dew_point":277.27,"uvi":0,"clouds":100,"visibility":86,"wind_speed":1.32,"wind_deg":92,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.09},{"dt":1607187600,"temp":278.34,"feels_like":276.06,"pressure":1004,"humidity":92,"dew_point":277.28,"uvi":0,"clouds":100,"visibility":97,"wind_speed":1.37,"wind_deg":94,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.09},{"dt":1607191200,"temp":278.31,"feels_like":276.04,"pressure":1004,"humidity":92,"dew_point":277.25,"uvi":0,"clouds":100,"visibility":136,"wind_speed":1.35,"wind_deg":94,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.05},{"dt":1607194800,"temp":278.1,"feels_like":275.9,"pressure":1005,"humidity":92,"dew_point":277.05,"uvi":0,"clouds":95,"visibility":135,"wind_speed":1.2,"wind_deg":98,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.04},{"dt":1607198400,"temp":278.17,"feels_like":276.08,"pressure":1005,"humidity":92,"dew_point":277.1,"uvi":0,"clouds":97,"visibility":748,"wind_speed":1.06,"wind_deg":103,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"pop":0.04}],"daily":[{"dt":1606989600,"sunrise":1606976516,"sunset":1607008607,"temp":{"day":272.75,"min":271.15,"max":273.41,"night":272.59,"eve":272.39,"morn":271.2},"feels_like":{"day":270.38,"night":269.76,"eve":269.92,"morn":268.42},"pressure":1011,"humidity":97,"dew_point":271.03,"wind_speed":0.39,"wind_deg":351,"weather":[{"id":601,"main":"Snow","description":"snow","icon":"13d"}],"clouds":100,"pop":1,"snow":18.51,"uvi":0.59},{"dt":1607076000,"sunrise":1607062981,"sunset":1607094989,"temp":{"day":275.47,"min":269.46,"max":277.25,"night":275.85,"eve":276.11,"morn":269.46},"feels_like":{"day":273.46,"night":273.69,"eve":273.67,"morn":266.56},"pressure":1005,"humidity":91,"dew_point":274.22,"wind_speed":0.25,"wind_deg":276,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":70,"pop":0.28,"uvi":0.99},{"dt":1607162400,"sunrise":1607149445,"sunset":1607181374,"temp":{"day":278.63,"min":276.05,"max":279.19,"night":278.2,"eve":278.33,"morn":277.06},"feels_like":{"day":276.64,"night":276.01,"eve":276.08,"morn":274.62},"pressure":1005,"humidity":93,"dew_point":277.6,"wind_speed":1.08,"wind_deg":92,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"clouds":100,"pop":0.13,"uvi":0.52},{"dt":1607248800,"sunrise":1607235908,"sunset":1607267762,"temp":{"day":278.52,"min":277.86,"max":279.07,"night":278.45,"eve":278.66,"morn":278.43},"feels_like":{"day":276.6,"night":276.4,"eve":276.55,"morn":276.41},"pressure":1000,"humidity":92,"dew_point":277.47,"wind_speed":0.91,"wind_deg":50,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"clouds":100,"pop":1,"rain":15.96,"uvi":0.27},{"dt":1607335200,"sunrise":1607322368,"sunset":1607354152,"temp":{"day":279.21,"min":276.57,"max":281.48,"night":278.63,"eve":278.95,"morn":277.6},"feels_like":{"day":277.66,"night":277.23,"eve":276.9,"morn":275.15},"pressure":1004,"humidity":88,"dew_point":277.39,"wind_speed":0.39,"wind_deg":186,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"clouds":10,"pop":1,"rain":17.35,"uvi":1.1},{"dt":1607421600,"sunrise":1607408827,"sunset":1607440545,"temp":{"day":278.84,"min":277.29,"max":279.64,"night":277.69,"eve":278.39,"morn":277.84},"feels_like":{"day":277.16,"night":275.96,"eve":276.59,"morn":276.11},"pressure":1006,"humidity":91,"dew_point":277.57,"wind_speed":0.61,"wind_deg":340,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":100,"pop":0.95,"rain":0.83,"uvi":0.65},{"dt":1607508000,"sunrise":1607495284,"sunset":1607526940,"temp":{"day":278.22,"min":276.81,"max":278.22,"night":277.26,"eve":277.74,"morn":277.4},"feels_like":{"day":276.1,"night":274.29,"eve":275.7,"morn":275.2},"pressure":1007,"humidity":87,"dew_point":276.3,"wind_speed":0.91,"wind_deg":11,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"clouds":87,"pop":0.27,"uvi":1},{"dt":1607594400,"sunrise":1607581739,"sunset":1607613338,"temp":{"day":276.3,"min":276.3,"max":276.99,"night":276.66,"eve":276.89,"morn":276.32},"feels_like":{"day":273.51,"night":274.55,"eve":274.76,"morn":272.89},"pressure":1008,"humidity":90,"dew_point":274.94,"wind_speed":1.52,"wind_deg":315,"weather":[{"id":616,"main":"Snow","description":"rain and snow","icon":"13d"}],"clouds":100,"pop":0.96,"rain":6.33,"snow":2.4,"uvi":1}],"alerts":[{"sender_name":"DHMZ Državni hidrometeorološki zavod","event":"Red snow ice warning","start":1606950000,"end":1607036399,"description":"Freezing rain. minimum temperature \u003c 0 °C"},{"sender_name":"DHMZ Državni hidrometeorološki zavod","event":"Yellow snow ice warning","start":1607036400,"end":1607068800,"description":"Black ice. minimum temperature \u003c 0 °C"}]} \ No newline at end of file +{ + "lat": 45.5709, + "lon": 15.193, + "timezone": "Europe/Ljubljana", + "base64_str": "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gRHVpcyBxdWlzIGRpYW0gbm9uIG5pc2wgZGljdHVtIGZlcm1lbnR1bS4gTWFlY2VuYXMgdnVscHV0YXRlIGVyYXQgc2VkIHNvbGxpY2l0dWRpbiBpbXBlcmRpZXQuIFN1c3BlbmRpc3NlIGV1IG1hdXJpcyBhdWd1ZS4gUGVsbGVudGVzcXVlIHZhcml1cyBkYXBpYnVzIGR1aSwgbm9uIHZlaGljdWxhIGxpYmVybyB2YXJpdXMgZXUuIFBlbGxlbnRlc3F1ZSBjb25ndWUgbmliaCBwdXJ1cywgaW4gYWxpcXVldCBsaWd1bGEgdml2ZXJyYSBhYy4gSW50ZWdlciBzZWQgbmlzbCBldSBlcm9zIHNhZ2l0dGlzIGNvbmRpbWVudHVtIHZlbCBzaXQgYW1ldCBsZW8uIE51bGxhbSBzZWQgZmluaWJ1cyBuaWJoLiBDbGFzcyBhcHRlbnQgdGFjaXRpIHNvY2lvc3F1IGFkIGxpdG9yYSB0b3JxdWVudCBwZXIgY29udWJpYSBub3N0cmEsIHBlciBpbmNlcHRvcyBoaW1lbmFlb3MuIEFsaXF1YW0gdmVsIHNvZGFsZXMganVzdG8sIGV0IHBvc3VlcmUgbWFnbmEuIEV0aWFtIGVsZW1lbnR1bSBmZWxpcyB1dCBsZWN0dXMgdGluY2lkdW50IGdyYXZpZGEuIE51bGxhbSBmYXVjaWJ1cyBhbGlxdWV0IHRlbGx1cyB2aXRhZSBzb2RhbGVzLiBBbGlxdWFtIHNjZWxlcmlzcXVlIGZlcm1lbnR1bSB2dWxwdXRhdGUuIEN1cmFiaXR1ciBzaXQgYW1ldCBlcmF0IGhlbmRyZXJpdCwgdWx0cmljZXMgbWV0dXMgc2l0IGFtZXQsIG1hbGVzdWFkYSBuaXNsLiBDdXJhYml0dXIgZXUgdG9ydG9yIG1hbGVzdWFkYSBmZWxpcyB2aXZlcnJhIHB1bHZpbmFyLiBOdWxsYSBlbGVtZW50dW0gdmVsIHVybmEgaWQgcGhhcmV0cmEuCgpBbGlxdWFtIHVsdHJpY2VzIHNpdCBhbWV0IGxhY3VzIHNlZCBiaWJlbmR1bS4gSW4gY29udmFsbGlzIGVyYXQgdmVsIGRpYW0gbGFjaW5pYSB1bHRyaWNpZXMuIFNlZCBhdCBjb25ndWUgbmlzaS4gRnVzY2UgdmFyaXVzIGxhY2luaWEgbGVvLCB2aXRhZSB0ZW1wdXMgbGVvIG1heGltdXMgZXQuIE51bGxhIG5lYyBkYXBpYnVzIGVyb3MuIEV0aWFtIGF0IGZlcm1lbnR1bSByaXN1cywgYWMgcHJldGl1bSBmZWxpcy4gRnVzY2UgcXVpcyBqdXN0byBhbGlxdWFtLCBmaW5pYnVzIG1ldHVzIGlkLCBzb2RhbGVzIG1hdXJpcy4gQ3JhcyB2ZXN0aWJ1bHVtIHB1cnVzIHF1YW0sIGlkIHZlc3RpYnVsdW0gbnVuYyBjb252YWxsaXMgZXQuIE1hZWNlbmFzIGp1c3RvIGxvcmVtLCB2YXJpdXMgYSBsYW9yZWV0IGVnZXQsIGZpbmlidXMgZXUgbGlndWxhLiBQcmFlc2VudCBmYWNpbGlzaXMsIG1pIGJsYW5kaXQgbW9sZXN0aWUgYXVjdG9yLCB0ZWxsdXMgc2VtIHVsdHJpY2llcyBuZXF1ZSwgdml0YWUgdm9sdXRwYXQgZW5pbSBsZW8gZXQgYXVndWUuIFBoYXNlbGx1cyB2ZXN0aWJ1bHVtIGNvbnNlY3RldHVyIGNvbW1vZG8uCgpEb25lYyBmZXJtZW50dW0gb2RpbyBldSBwdXJ1cyBpbnRlcmR1bSB0cmlzdGlxdWUuIER1aXMgaGVuZHJlcml0IG5lcXVlIGRvbG9yLCB1dCBjb25zZXF1YXQgc2VtIHBvcnRhIHF1aXMuIENyYXMgZGljdHVtIGNvbmd1ZSBpbnRlcmR1bS4gVXQgbmVjIGR1aSBpbiBhdWd1ZSBsYW9yZWV0IHRpbmNpZHVudCBzaXQgYW1ldCBhIGxhY3VzLiBWaXZhbXVzIGZyaW5naWxsYSBvcmNpIGF0IHZpdmVycmEgdGluY2lkdW50LiBEdWlzIGF1Y3RvciwgZmVsaXMgdm9sdXRwYXQgdGVtcHVzIGNvbW1vZG8sIGxlbyBwdXJ1cyBjb25ndWUgbGVjdHVzLCBldCBtb2xsaXMgZW5pbSBhbnRlIHF1aXMgdXJuYS4gVmVzdGlidWx1bSBxdWlzIG1hZ25hIGEgdmVsaXQgZGlnbmlzc2ltIGV1aXNtb2QgbGFjaW5pYSBpbiBlbmltLiBJbiBtYXR0aXMgc2VkIGRpYW0gbmVjIGF1Y3Rvci4gRnVzY2Ugc2l0IGFtZXQgZXN0IHR1cnBpcy4gSW4gZXUgdG9ydG9yIGVyb3MuIENyYXMgbG9ib3J0aXMgZGFwaWJ1cyBzZW0gbmVjIGxhY2luaWEuIFN1c3BlbmRpc3NlIG1hZ25hIGFyY3UsIHJ1dHJ1bSB1dCBkaWN0dW0gc2VkLCBzY2VsZXJpc3F1ZSBpbiBlbGl0LiBEdWlzIHZ1bHB1dGF0ZSBjdXJzdXMgZWZmaWNpdHVyLiBQZWxsZW50ZXNxdWUgYmxhbmRpdCBmZXJtZW50dW0gZXN0LiBDcmFzIHZhcml1cyBpcHN1bSB1cm5hLCBhIGhlbmRyZXJpdCBzYXBpZW4gcnV0cnVtIGF0LiBOYW0gZ3JhdmlkYSBxdWlzIG1pIGV0IGF1Y3Rvci4KCkRvbmVjIHBvcnRhIG1hdXJpcyBzaXQgYW1ldCB2aXZlcnJhIHVsdHJpY2VzLiBEb25lYyBsdWN0dXMgcHJldGl1bSBvcmNpIGV0IG9ybmFyZS4gTW9yYmkgYWMgbGFjdXMgYWxpcXVhbSwgY3Vyc3VzIHB1cnVzIHZpdGFlLCBncmF2aWRhIGRvbG9yLiBGdXNjZSBzZWQgdmVsaXQgbW9sbGlzLCBzZW1wZXIgbGlndWxhIGlkLCBtYXR0aXMgc2VtLiBWZXN0aWJ1bHVtIHVsdHJpY2llcyBzZW0gbm9uIG9kaW8gaGVuZHJlcml0IGRhcGlidXMuIFBoYXNlbGx1cyB2b2x1dHBhdCBhdWd1ZSB2ZWwgbmliaCB2YXJpdXMsIGluIGRpZ25pc3NpbSBtZXR1cyBzZW1wZXIuIFN1c3BlbmRpc3NlIGxvYm9ydGlzIGxlY3R1cyBldSBxdWFtIGNvbmd1ZSwgcXVpcyB2dWxwdXRhdGUgYXJjdSBiaWJlbmR1bS4gU3VzcGVuZGlzc2UgdGVtcHVzIG1hZ25hIHZlbmVuYXRpcyBwdXJ1cyB2ZXN0aWJ1bHVtIGlhY3VsaXMuIE51bGxhbSBpbnRlcmR1bSB0cmlzdGlxdWUgbmVxdWUgZWdldCBlZmZpY2l0dXIuIER1aXMgZmluaWJ1cywgdmVsaXQgdWxsYW1jb3JwZXIgc2NlbGVyaXNxdWUgaGVuZHJlcml0LCBuaXNpIG51bmMgbGFvcmVldCBkb2xvciwgdGVtcHVzIHJ1dHJ1bSBhbnRlIGxlbyBhIGxlY3R1cy4gRG9uZWMgdml0YWUgcG9ydGEgbG9yZW0sIGVnZXQgc3VzY2lwaXQgbGVjdHVzLiBNYXVyaXMgYWMgbGVvIG1pLiBNYXVyaXMgZXVpc21vZCBzZW0gZXUgbGlndWxhIGNvbnNlY3RldHVyIGNvbmRpbWVudHVtLgoKTW9yYmkgdWxsYW1jb3JwZXIgdm9sdXRwYXQgbWFzc2Egbm9uIGFsaXF1ZXQuIE1hdXJpcyB2aXRhZSBmcmluZ2lsbGEgbWF1cmlzLiBTZWQgbmliaCBtYWduYSwgdmVuZW5hdGlzIHF1aXMgbWF4aW11cyBhdCwgbWF0dGlzIG5lYyBudW5jLiBVdCByaG9uY3VzIGFudGUgYXQgZGFwaWJ1cyBmYWNpbGlzaXMuIEN1cmFiaXR1ciBhY2N1bXNhbiBpZCBhdWd1ZSBuZWMgbGFjaW5pYS4gUGhhc2VsbHVzIGNvbnNlcXVhdCBoZW5kcmVyaXQgbWFzc2EuIEFsaXF1YW0gbHVjdHVzIGVmZmljaXR1ciBjb25zZWN0ZXR1ci4gSW50ZWdlciBmZXJtZW50dW0gc2FwaWVuIGZldWdpYXQgZmVsaXMgdmVoaWN1bGEsIG5vbiBlbGVtZW50dW0gbnVsbGEgZ3JhdmlkYS4gSW4gdmVsIHVsdHJpY2llcyBleC4gUHJhZXNlbnQgcGhhcmV0cmEgYSB0dXJwaXMgdml0YWUgbGFvcmVldC4gRXRpYW0gYWNjdW1zYW4gc2VtcGVyIHR1cnBpcyBlZ2V0IHBvcnR0aXRvci4gUGhhc2VsbHVzIGFjIGZlbGlzIGV1IHR1cnBpcyBtYXhpbXVzIG1vbGxpcy4gRG9uZWMgY3Vyc3VzIGRpYW0gbmVjIGp1c3RvIGZhY2lsaXNpcywgc2VkIG1vbGxpcyBudWxsYSBoZW5kcmVyaXQuIE5hbSBydXRydW0sIG1hc3NhIG5vbiBibGFuZGl0IHZvbHV0cGF0LCBsYWN1cyB0ZWxsdXMgYWxpcXVhbSB0b3J0b3IsIHV0IGZyaW5naWxsYSBuaWJoIHNhcGllbiB2aXRhZSBzYXBpZW4uIENsYXNzIGFwdGVudCB0YWNpdGkgc29jaW9zcXUgYWQgbGl0b3JhIHRvcnF1ZW50IHBlciBjb251YmlhIG5vc3RyYSwgcGVyIGluY2VwdG9zIGhpbWVuYWVvcy4=", + "timezone_offset": 7200, + "current": { + "dt": 1650206004, + "sunrise": 1650168621, + "sunset": 1650217613, + "temp": 10.79, + "feels_like": 9, + "pressure": 1019, + "humidity": 41, + "dew_point": -1.7, + "uvi": 1.37, + "clouds": 90, + "visibility": 10000, + "wind_speed": 5.89, + "wind_deg": 48, + "wind_gust": 10.64, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ] + }, + "minutely": [ + { + "dt": 1650206040, + "precipitation": 0 + }, + { + "dt": 1650206100, + "precipitation": 0 + }, + { + "dt": 1650206160, + "precipitation": 0 + }, + { + "dt": 1650206220, + "precipitation": 0 + }, + { + "dt": 1650206280, + "precipitation": 0 + }, + { + "dt": 1650206340, + "precipitation": 0 + }, + { + "dt": 1650206400, + "precipitation": 0 + }, + { + "dt": 1650206460, + "precipitation": 0 + }, + { + "dt": 1650206520, + "precipitation": 0 + }, + { + "dt": 1650206580, + "precipitation": 0 + }, + { + "dt": 1650206640, + "precipitation": 0 + }, + { + "dt": 1650206700, + "precipitation": 0 + }, + { + "dt": 1650206760, + "precipitation": 0 + }, + { + "dt": 1650206820, + "precipitation": 0 + }, + { + "dt": 1650206880, + "precipitation": 0 + }, + { + "dt": 1650206940, + "precipitation": 0 + }, + { + "dt": 1650207000, + "precipitation": 0 + }, + { + "dt": 1650207060, + "precipitation": 0 + }, + { + "dt": 1650207120, + "precipitation": 0 + }, + { + "dt": 1650207180, + "precipitation": 0 + }, + { + "dt": 1650207240, + "precipitation": 0 + }, + { + "dt": 1650207300, + "precipitation": 0 + }, + { + "dt": 1650207360, + "precipitation": 0 + }, + { + "dt": 1650207420, + "precipitation": 0 + }, + { + "dt": 1650207480, + "precipitation": 0 + }, + { + "dt": 1650207540, + "precipitation": 0 + }, + { + "dt": 1650207600, + "precipitation": 0 + }, + { + "dt": 1650207660, + "precipitation": 0 + }, + { + "dt": 1650207720, + "precipitation": 0 + }, + { + "dt": 1650207780, + "precipitation": 0 + }, + { + "dt": 1650207840, + "precipitation": 0 + }, + { + "dt": 1650207900, + "precipitation": 0 + }, + { + "dt": 1650207960, + "precipitation": 0 + }, + { + "dt": 1650208020, + "precipitation": 0 + }, + { + "dt": 1650208080, + "precipitation": 0 + }, + { + "dt": 1650208140, + "precipitation": 0 + }, + { + "dt": 1650208200, + "precipitation": 0 + }, + { + "dt": 1650208260, + "precipitation": 0 + }, + { + "dt": 1650208320, + "precipitation": 0 + }, + { + "dt": 1650208380, + "precipitation": 0 + }, + { + "dt": 1650208440, + "precipitation": 0 + }, + { + "dt": 1650208500, + "precipitation": 0 + }, + { + "dt": 1650208560, + "precipitation": 0 + }, + { + "dt": 1650208620, + "precipitation": 0 + }, + { + "dt": 1650208680, + "precipitation": 0 + }, + { + "dt": 1650208740, + "precipitation": 0 + }, + { + "dt": 1650208800, + "precipitation": 0 + }, + { + "dt": 1650208860, + "precipitation": 0 + }, + { + "dt": 1650208920, + "precipitation": 0 + }, + { + "dt": 1650208980, + "precipitation": 0 + }, + { + "dt": 1650209040, + "precipitation": 0 + }, + { + "dt": 1650209100, + "precipitation": 0 + }, + { + "dt": 1650209160, + "precipitation": 0 + }, + { + "dt": 1650209220, + "precipitation": 0 + }, + { + "dt": 1650209280, + "precipitation": 0 + }, + { + "dt": 1650209340, + "precipitation": 0 + }, + { + "dt": 1650209400, + "precipitation": 0 + }, + { + "dt": 1650209460, + "precipitation": 0 + }, + { + "dt": 1650209520, + "precipitation": 0 + }, + { + "dt": 1650209580, + "precipitation": 0 + }, + { + "dt": 1650209640, + "precipitation": 0 + } + ], + "hourly": [ + { + "dt": 1650204000, + "temp": 11.14, + "feels_like": 9.35, + "pressure": 1019, + "humidity": 40, + "dew_point": -1.72, + "uvi": 2.51, + "clouds": 91, + "visibility": 10000, + "wind_speed": 6.47, + "wind_deg": 48, + "wind_gust": 10.48, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650207600, + "temp": 10.79, + "feels_like": 9, + "pressure": 1019, + "humidity": 41, + "dew_point": -1.7, + "uvi": 1.37, + "clouds": 90, + "visibility": 10000, + "wind_speed": 5.89, + "wind_deg": 48, + "wind_gust": 10.64, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650211200, + "temp": 10.91, + "feels_like": 9.15, + "pressure": 1019, + "humidity": 42, + "dew_point": -1.31, + "uvi": 0.57, + "clouds": 88, + "visibility": 10000, + "wind_speed": 5.55, + "wind_deg": 49, + "wind_gust": 11.15, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650214800, + "temp": 10.37, + "feels_like": 8.64, + "pressure": 1019, + "humidity": 45, + "dew_point": -0.92, + "uvi": 0.14, + "clouds": 81, + "visibility": 10000, + "wind_speed": 5.09, + "wind_deg": 53, + "wind_gust": 11.56, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650218400, + "temp": 9.02, + "feels_like": 6.91, + "pressure": 1020, + "humidity": 50, + "dew_point": -0.74, + "uvi": 0, + "clouds": 71, + "visibility": 10000, + "wind_speed": 3.77, + "wind_deg": 53, + "wind_gust": 10.47, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650222000, + "temp": 7.03, + "feels_like": 5.38, + "pressure": 1021, + "humidity": 61, + "dew_point": 0.03, + "uvi": 0, + "clouds": 24, + "visibility": 10000, + "wind_speed": 2.4, + "wind_deg": 48, + "wind_gust": 7.74, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02n" + } + ], + "pop": 0 + }, + { + "dt": 1650225600, + "temp": 4.9, + "feels_like": 3.46, + "pressure": 1021, + "humidity": 71, + "dew_point": -1, + "uvi": 0, + "clouds": 7, + "visibility": 10000, + "wind_speed": 1.81, + "wind_deg": 26, + "wind_gust": 3.92, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "pop": 0 + }, + { + "dt": 1650229200, + "temp": 4.22, + "feels_like": 2.75, + "pressure": 1021, + "humidity": 73, + "dew_point": -1.07, + "uvi": 0, + "clouds": 7, + "visibility": 10000, + "wind_speed": 1.74, + "wind_deg": 6, + "wind_gust": 3.2, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "pop": 0 + }, + { + "dt": 1650232800, + "temp": 3.97, + "feels_like": 2.43, + "pressure": 1021, + "humidity": 74, + "dew_point": -1.22, + "uvi": 0, + "clouds": 8, + "visibility": 10000, + "wind_speed": 1.77, + "wind_deg": 357, + "wind_gust": 3.31, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "pop": 0 + }, + { + "dt": 1650236400, + "temp": 3.7, + "feels_like": 2.1, + "pressure": 1021, + "humidity": 74, + "dew_point": -1.51, + "uvi": 0, + "clouds": 19, + "visibility": 10000, + "wind_speed": 1.78, + "wind_deg": 343, + "wind_gust": 2.41, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02n" + } + ], + "pop": 0 + }, + { + "dt": 1650240000, + "temp": 3.66, + "feels_like": 2.18, + "pressure": 1020, + "humidity": 72, + "dew_point": -1.79, + "uvi": 0, + "clouds": 27, + "visibility": 10000, + "wind_speed": 1.68, + "wind_deg": 335, + "wind_gust": 2.59, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1650243600, + "temp": 3.21, + "feels_like": 1.91, + "pressure": 1020, + "humidity": 73, + "dew_point": -2.2, + "uvi": 0, + "clouds": 65, + "visibility": 10000, + "wind_speed": 1.5, + "wind_deg": 353, + "wind_gust": 2.05, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650247200, + "temp": 2.4, + "feels_like": 1.07, + "pressure": 1019, + "humidity": 74, + "dew_point": -2.63, + "uvi": 0, + "clouds": 41, + "visibility": 10000, + "wind_speed": 1.44, + "wind_deg": 347, + "wind_gust": 1.63, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1650250800, + "temp": 2.07, + "feels_like": 0.62, + "pressure": 1019, + "humidity": 75, + "dew_point": -2.86, + "uvi": 0, + "clouds": 39, + "visibility": 10000, + "wind_speed": 1.49, + "wind_deg": 336, + "wind_gust": 1.61, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1650254400, + "temp": 1.85, + "feels_like": 0.51, + "pressure": 1019, + "humidity": 76, + "dew_point": -2.98, + "uvi": 0, + "clouds": 47, + "visibility": 10000, + "wind_speed": 1.4, + "wind_deg": 331, + "wind_gust": 1.47, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1650258000, + "temp": 3.03, + "feels_like": 3.03, + "pressure": 1019, + "humidity": 71, + "dew_point": -2.6, + "uvi": 0.15, + "clouds": 56, + "visibility": 10000, + "wind_speed": 1.26, + "wind_deg": 329, + "wind_gust": 1.83, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650261600, + "temp": 5.55, + "feels_like": 4.67, + "pressure": 1019, + "humidity": 61, + "dew_point": -2.31, + "uvi": 0.57, + "clouds": 59, + "visibility": 10000, + "wind_speed": 1.43, + "wind_deg": 18, + "wind_gust": 4.99, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650265200, + "temp": 7.58, + "feels_like": 5.8, + "pressure": 1018, + "humidity": 53, + "dew_point": -2.37, + "uvi": 1.4, + "clouds": 50, + "visibility": 10000, + "wind_speed": 2.71, + "wind_deg": 44, + "wind_gust": 7.12, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1650268800, + "temp": 9.16, + "feels_like": 7.06, + "pressure": 1018, + "humidity": 47, + "dew_point": -2.36, + "uvi": 2.56, + "clouds": 56, + "visibility": 10000, + "wind_speed": 3.81, + "wind_deg": 53, + "wind_gust": 8.78, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650272400, + "temp": 10.37, + "feels_like": 8.59, + "pressure": 1018, + "humidity": 43, + "dew_point": -2.61, + "uvi": 3.79, + "clouds": 43, + "visibility": 10000, + "wind_speed": 4.56, + "wind_deg": 54, + "wind_gust": 8.19, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1650276000, + "temp": 11.35, + "feels_like": 9.56, + "pressure": 1017, + "humidity": 39, + "dew_point": -2.89, + "uvi": 4.66, + "clouds": 37, + "visibility": 10000, + "wind_speed": 4.84, + "wind_deg": 50, + "wind_gust": 7.91, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1650279600, + "temp": 12.28, + "feels_like": 10.5, + "pressure": 1016, + "humidity": 36, + "dew_point": -3.17, + "uvi": 5.01, + "clouds": 48, + "visibility": 10000, + "wind_speed": 4.75, + "wind_deg": 49, + "wind_gust": 7.84, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1650283200, + "temp": 13.02, + "feels_like": 11.24, + "pressure": 1015, + "humidity": 33, + "dew_point": -3.52, + "uvi": 4.66, + "clouds": 56, + "visibility": 10000, + "wind_speed": 4.55, + "wind_deg": 55, + "wind_gust": 7.83, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650286800, + "temp": 13.19, + "feels_like": 11.43, + "pressure": 1015, + "humidity": 33, + "dew_point": -3.41, + "uvi": 3.51, + "clouds": 97, + "visibility": 10000, + "wind_speed": 4.61, + "wind_deg": 64, + "wind_gust": 7.28, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650290400, + "temp": 12.65, + "feels_like": 10.88, + "pressure": 1014, + "humidity": 35, + "dew_point": -3.24, + "uvi": 2.37, + "clouds": 98, + "visibility": 10000, + "wind_speed": 3.8, + "wind_deg": 65, + "wind_gust": 5.81, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650294000, + "temp": 12.21, + "feels_like": 10.48, + "pressure": 1014, + "humidity": 38, + "dew_point": -2.55, + "uvi": 1.29, + "clouds": 99, + "visibility": 10000, + "wind_speed": 3.8, + "wind_deg": 68, + "wind_gust": 5.17, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650297600, + "temp": 11.44, + "feels_like": 9.81, + "pressure": 1015, + "humidity": 45, + "dew_point": -0.95, + "uvi": 0.48, + "clouds": 99, + "visibility": 10000, + "wind_speed": 3.05, + "wind_deg": 65, + "wind_gust": 5.02, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650301200, + "temp": 9.82, + "feels_like": 8.51, + "pressure": 1015, + "humidity": 52, + "dew_point": -0.56, + "uvi": 0.12, + "clouds": 99, + "visibility": 10000, + "wind_speed": 2.65, + "wind_deg": 59, + "wind_gust": 6.31, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650304800, + "temp": 6.91, + "feels_like": 6.13, + "pressure": 1017, + "humidity": 62, + "dew_point": -0.81, + "uvi": 0, + "clouds": 99, + "visibility": 10000, + "wind_speed": 1.5, + "wind_deg": 66, + "wind_gust": 1.86, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650308400, + "temp": 6.01, + "feels_like": 6.01, + "pressure": 1017, + "humidity": 64, + "dew_point": -1.14, + "uvi": 0, + "clouds": 97, + "visibility": 10000, + "wind_speed": 0.09, + "wind_deg": 358, + "wind_gust": 0.58, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650312000, + "temp": 5.61, + "feels_like": 5.61, + "pressure": 1018, + "humidity": 66, + "dew_point": -1.28, + "uvi": 0, + "clouds": 95, + "visibility": 10000, + "wind_speed": 1.25, + "wind_deg": 277, + "wind_gust": 1.09, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650315600, + "temp": 5.17, + "feels_like": 5.17, + "pressure": 1018, + "humidity": 67, + "dew_point": -1.35, + "uvi": 0, + "clouds": 96, + "visibility": 10000, + "wind_speed": 1.24, + "wind_deg": 277, + "wind_gust": 1.14, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650319200, + "temp": 4.41, + "feels_like": 4.41, + "pressure": 1017, + "humidity": 70, + "dew_point": -1.55, + "uvi": 0, + "clouds": 94, + "visibility": 10000, + "wind_speed": 0.91, + "wind_deg": 288, + "wind_gust": 0.95, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650322800, + "temp": 4.03, + "feels_like": 4.03, + "pressure": 1017, + "humidity": 71, + "dew_point": -1.69, + "uvi": 0, + "clouds": 92, + "visibility": 10000, + "wind_speed": 1.03, + "wind_deg": 277, + "wind_gust": 1.01, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650326400, + "temp": 3.82, + "feels_like": 3.82, + "pressure": 1017, + "humidity": 71, + "dew_point": -1.86, + "uvi": 0, + "clouds": 91, + "visibility": 10000, + "wind_speed": 1.3, + "wind_deg": 270, + "wind_gust": 1.13, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650330000, + "temp": 3.39, + "feels_like": 2.33, + "pressure": 1016, + "humidity": 72, + "dew_point": -2.13, + "uvi": 0, + "clouds": 70, + "visibility": 10000, + "wind_speed": 1.35, + "wind_deg": 272, + "wind_gust": 1.21, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650333600, + "temp": 3.31, + "feels_like": 3.31, + "pressure": 1016, + "humidity": 71, + "dew_point": -2.34, + "uvi": 0, + "clouds": 80, + "visibility": 10000, + "wind_speed": 1.15, + "wind_deg": 280, + "wind_gust": 1.07, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650337200, + "temp": 3.46, + "feels_like": 3.46, + "pressure": 1016, + "humidity": 70, + "dew_point": -2.4, + "uvi": 0, + "clouds": 87, + "visibility": 10000, + "wind_speed": 1, + "wind_deg": 281, + "wind_gust": 0.93, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650340800, + "temp": 3.45, + "feels_like": 3.45, + "pressure": 1016, + "humidity": 70, + "dew_point": -2.52, + "uvi": 0, + "clouds": 90, + "visibility": 10000, + "wind_speed": 0.91, + "wind_deg": 279, + "wind_gust": 0.86, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650344400, + "temp": 4.72, + "feels_like": 4.72, + "pressure": 1016, + "humidity": 66, + "dew_point": -1.94, + "uvi": 0.09, + "clouds": 92, + "visibility": 10000, + "wind_speed": 0.41, + "wind_deg": 270, + "wind_gust": 0.49, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1650348000, + "temp": 6.49, + "feels_like": 6.49, + "pressure": 1016, + "humidity": 60, + "dew_point": -1.71, + "uvi": 0.36, + "clouds": 93, + "visibility": 10000, + "wind_speed": 0.47, + "wind_deg": 107, + "wind_gust": 0.59, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650351600, + "temp": 7.94, + "feels_like": 7.94, + "pressure": 1015, + "humidity": 55, + "dew_point": -1.42, + "uvi": 0.62, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.26, + "wind_deg": 108, + "wind_gust": 1.57, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650355200, + "temp": 8.92, + "feels_like": 8.42, + "pressure": 1015, + "humidity": 53, + "dew_point": -1.03, + "uvi": 1.13, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.5, + "wind_deg": 119, + "wind_gust": 1.81, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1650358800, + "temp": 9.52, + "feels_like": 9.11, + "pressure": 1015, + "humidity": 52, + "dew_point": -0.66, + "uvi": 1.67, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.49, + "wind_deg": 126, + "wind_gust": 1.82, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.04 + }, + { + "dt": 1650362400, + "temp": 10.25, + "feels_like": 8.69, + "pressure": 1014, + "humidity": 52, + "dew_point": -0.14, + "uvi": 2.64, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.44, + "wind_deg": 128, + "wind_gust": 1.92, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.04 + }, + { + "dt": 1650366000, + "temp": 10.95, + "feels_like": 9.43, + "pressure": 1013, + "humidity": 51, + "dew_point": 0.33, + "uvi": 2.83, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.47, + "wind_deg": 124, + "wind_gust": 2.11, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.04 + }, + { + "dt": 1650369600, + "temp": 11.83, + "feels_like": 10.37, + "pressure": 1013, + "humidity": 50, + "dew_point": 0.91, + "uvi": 2.64, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.54, + "wind_deg": 132, + "wind_gust": 2.6, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.04 + }, + { + "dt": 1650373200, + "temp": 12.56, + "feels_like": 11.18, + "pressure": 1012, + "humidity": 50, + "dew_point": 1.53, + "uvi": 1.77, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.63, + "wind_deg": 155, + "wind_gust": 2.87, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + } + ], + "daily": [ + { + "dt": 1650189600, + "sunrise": 1650168621, + "sunset": 1650217613, + "moonrise": 1650221520, + "moonset": 1650170220, + "moon_phase": 0.52, + "temp": { + "day": 10.24, + "min": 4.14, + "max": 11.52, + "night": 4.22, + "eve": 10.91, + "morn": 4.14 + }, + "feels_like": { + "day": 8.39, + "night": 2.75, + "eve": 9.15, + "morn": 0.12 + }, + "pressure": 1023, + "humidity": 41, + "dew_point": -3.48, + "wind_speed": 7.1, + "wind_deg": 58, + "wind_gust": 14.94, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": 33, + "pop": 0.11, + "uvi": 5.01 + }, + { + "dt": 1650276000, + "sunrise": 1650254916, + "sunset": 1650304091, + "moonrise": 1650312840, + "moonset": 1650258120, + "moon_phase": 0.56, + "temp": { + "day": 11.35, + "min": 1.85, + "max": 13.19, + "night": 5.17, + "eve": 11.44, + "morn": 1.85 + }, + "feels_like": { + "day": 9.56, + "night": 5.17, + "eve": 9.81, + "morn": 0.51 + }, + "pressure": 1017, + "humidity": 39, + "dew_point": -2.89, + "wind_speed": 4.84, + "wind_deg": 50, + "wind_gust": 8.78, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": 37, + "pop": 0, + "uvi": 5.01 + }, + { + "dt": 1650362400, + "sunrise": 1650341212, + "sunset": 1650390569, + "moonrise": 1650404160, + "moonset": 1650346440, + "moon_phase": 0.6, + "temp": { + "day": 10.25, + "min": 3.31, + "max": 12.56, + "night": 5.52, + "eve": 11.97, + "morn": 3.45 + }, + "feels_like": { + "day": 8.69, + "night": 5.52, + "eve": 10.68, + "morn": 3.45 + }, + "pressure": 1014, + "humidity": 52, + "dew_point": -0.14, + "wind_speed": 1.63, + "wind_deg": 155, + "wind_gust": 3.27, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": 100, + "pop": 0.05, + "uvi": 2.83 + }, + { + "dt": 1650448800, + "sunrise": 1650427509, + "sunset": 1650477047, + "moonrise": 0, + "moonset": 1650435240, + "moon_phase": 0.64, + "temp": { + "day": 4.99, + "min": 2.79, + "max": 7.64, + "night": 2.79, + "eve": 7.64, + "morn": 6.49 + }, + "feels_like": { + "day": 4.99, + "night": 2.79, + "eve": 7.64, + "morn": 6.49 + }, + "pressure": 1018, + "humidity": 97, + "dew_point": 3.52, + "wind_speed": 1.3, + "wind_deg": 32, + "wind_gust": 3.75, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": 100, + "pop": 1, + "rain": 6.92, + "uvi": 3.29 + }, + { + "dt": 1650535200, + "sunrise": 1650513807, + "sunset": 1650563525, + "moonrise": 1650495120, + "moonset": 1650524760, + "moon_phase": 0.67, + "temp": { + "day": 10.26, + "min": 1.2, + "max": 14.14, + "night": 9.05, + "eve": 14.14, + "morn": 1.2 + }, + "feels_like": { + "day": 9.01, + "night": 9.05, + "eve": 13.18, + "morn": 1.2 + }, + "pressure": 1014, + "humidity": 64, + "dew_point": 2.65, + "wind_speed": 1.59, + "wind_deg": 136, + "wind_gust": 2.04, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": 100, + "pop": 0, + "uvi": 5.04 + }, + { + "dt": 1650621600, + "sunrise": 1650600106, + "sunset": 1650650003, + "moonrise": 1650585540, + "moonset": 1650614940, + "moon_phase": 0.71, + "temp": { + "day": 8.81, + "min": 8.55, + "max": 9.77, + "night": 8.6, + "eve": 9.77, + "morn": 8.55 + }, + "feels_like": { + "day": 8.81, + "night": 8.17, + "eve": 9.3, + "morn": 8.55 + }, + "pressure": 1005, + "humidity": 96, + "dew_point": 7.11, + "wind_speed": 1.58, + "wind_deg": 318, + "wind_gust": 3.65, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": 100, + "pop": 0.96, + "rain": 7.26, + "uvi": 6 + }, + { + "dt": 1650708000, + "sunrise": 1650686407, + "sunset": 1650736481, + "moonrise": 1650675060, + "moonset": 1650705780, + "moon_phase": 0.75, + "temp": { + "day": 16.34, + "min": 8.68, + "max": 18.34, + "night": 9.74, + "eve": 17.68, + "morn": 8.68 + }, + "feels_like": { + "day": 15.81, + "night": 9.23, + "eve": 17.04, + "morn": 8.68 + }, + "pressure": 1007, + "humidity": 68, + "dew_point": 9.46, + "wind_speed": 4.27, + "wind_deg": 244, + "wind_gust": 6.63, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": 52, + "pop": 0.59, + "rain": 0.97, + "uvi": 6 + }, + { + "dt": 1650794400, + "sunrise": 1650772708, + "sunset": 1650822959, + "moonrise": 1650763920, + "moonset": 1650796740, + "moon_phase": 0.78, + "temp": { + "day": 16.25, + "min": 10.15, + "max": 17.18, + "night": 11.38, + "eve": 15.32, + "morn": 10.15 + }, + "feels_like": { + "day": 15.76, + "night": 10.66, + "eve": 14.92, + "morn": 9.6 + }, + "pressure": 1008, + "humidity": 70, + "dew_point": 9.72, + "wind_speed": 4.86, + "wind_deg": 230, + "wind_gust": 12.59, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + }, + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": 87, + "pop": 0.56, + "rain": 0.52, + "uvi": 6 + } + ], + "alerts": [ + { + "sender_name": "Agencija Republike Slovenije za okolje (ARSO vreme)", + "event": "Moderate Wind Warning", + "start": 1650114000, + "end": 1650221940, + "description": "Maximum wind speed : from 50 to 70 km/h. Wind sways trees and may break smaller branches.", + "tags": [ + "Wind" + ] + }, + { + "sender_name": "Agencija Republike Slovenije za okolje (ARSO vreme)", + "event": "Moderate Low temperature Warning", + "start": 1650229200, + "end": 1650265140, + "description": "Low temperatures may affect the health of sensitive members of the population.", + "tags": [ + "Extreme temperature value" + ] + } + ] +} \ No newline at end of file