Home > Mobile >  How to iterate through JSON to get path of each required value?
How to iterate through JSON to get path of each required value?

Time:12-08

I'm trying to make a file explorer. The approach I am trying to take is with JSON data. I've made a demo JSON for reference of how the data can be. What I am trying to do is create path of each directory and file from the data. I know I have to use Iteration in it but can't figure out how to get the path.

{
    "project-name": "name of the project",
    "author": "username of the author",
    "date": "DD-MM-YYYY",
    "privacy": "public / private",
    "collaborators": [
        "email-address / user-id of Collaborator-1",
        "email-address / user-id of Collaborator-2"
    ],
    "plan": "active-plan-name",
    "database": [
        {
            "type": "directory",
            "name": "js",
            "items": [
                {
                    "type": "directory",
                    "name": "assets",
                    "items": [
                        {
                            "type": "directory",
                            "name": "icons",
                            "items": [
                                {
                                    "type": "file",
                                    "name": "logo.png",
                                    "content": "path of logo.png"
                                }
                            ]
                        }
                    ]
                },
                {
                    "type": "directory",
                    "name": "lib",
                    "items": [
                        {
                            "type": "file",
                            "name": "jquery.min.js",
                            "content": "CONTENT OF jquery.min.js"
                        },
                        {
                            "type": "file",
                            "name": "split.js",
                            "content": "CONTENT OF split.js"
                        }
                    ]
                },
                {
                    "type": "directory",
                    "name": "src",
                    "items": [
                        {
                            "type": "file",
                            "name": "script.js",
                            "content": "CONTENT OF script.js"
                        }
                    ]
                }
            ]
        },
        {
            "type": "directory",
            "name": "style",
            "items": [
                {
                    "type": "file",
                    "name": "main.css",
                    "content": "CONTENT OF main.css"
                }
            ]
        },
        {
            "type": "file",
            "name": "index.html",
            "content": "CONTENT OF index.html"
        }
    ]
}

Above is my JSON data for reference, the file and directory data starts from the database key.

Example: for the file logo.png I want path returned as js/assets/icons/logo.png.

Itried iterating through the JSON data and was expecting to get some logic built with it. The function I wrote for the same is,

function iterate(obj){
    for(prop in obj){
        if(typeof(obj[prop]) == "object){
            iterate(obj[prop]);
        }
    }
}

Unfortunately, I couldn't reach to any logic through iteration as of yet.

CodePudding user response:

(The votes to close this as a duplicate seemed wrong to me. Neither of the suggested links dealt with a structure quite like this, with the node names in object string properties and the children in object array properties. They were about trees represented by the object hierarchy. Reopened.)


There is one major concern with your question. You ask for the path. But there could be many of them. After all, there could be multiple files in a filesystem named "logo.png". So we could try to find the first match or try to find all of them. This solution assumes you want all.

const pathsTo = (xs, target, path = '') => 
  xs .flatMap (({name, items = []}) => [
    ... (name == target ? [`${path}/${name}`] : []),
    ... pathsTo (items, target, `${path}/${name}`)
  ])

const input = {"project-name": "name of the project", author: "username of the author", date: "DD-MM-YYYY", privacy: "public / private", collaborators: ["email-address / user-id of Collaborator-1", "email-address / user-id of Collaborator-2"], plan: "active-plan-name", database: [{type: "directory", name: "js", items: [{type: "directory", name: "assets", items: [{type: "directory", name: "icons", items: [{type: "file", name: "logo.png", content: "path of logo.png"}]}]}, {type: "directory", name: "lib", items: [{type: "file", name: "jquery.min.js", content: "CONTENT OF jquery.min.js"}, {type: "file", name: "split.js", content: "CONTENT OF split.js"}]}, {type: "directory", name: "src", items: [{type: "file", name: "script.js", content: "CONTENT OF script.js"}]}]}, {type: "directory", name: "style", items: [{type: "file", name: "main.css", content: "CONTENT OF main.css"}]}, {type: "file", name: "index.html", content: "CONTENT OF index.html"}]}

console .log (pathsTo (input .database, 'logo.png'))

We use Array.prototype.flatMap to turn multiple mapped array values into a single array as we recur. We also use the spread syntax to comfortably turn either an empty array or an array containing just one item into either a new item in out returned array or nothing at all. And we spread the result of recurring on our items into our result.

This introduces an artifact you may not want. The one entry in the resulting array looks like "/js/assets/icons/logo.png", when we wanted "js/assets/icons/logo.png" (without the leading slash). There are good arguments for keeping the slash, but if we don't want to, we could fix this after the fact by simply results .map (r => r .slice(1)). But lets look at how we might do this inline. We simply need to test whether the path is empty before we add the slash. Something like, path (path ? '/' : '') name. But that feels like too much code to repeat in the two places we were using ${path}/${name}, so let's move it up a level.

We could add it as a defaulted parameter to flatMap, but that would add some ugliness, as we would have to add it after two parameters we don't need (the index, and the whole array are passed to flatMap's callback, but we ignore them.) However, we are already destructuring the first parameter, so we could simply add our computation as a defaulted part of that. It could look like this:

const pathsTo = (xs, target, path = '') => 
  xs .flatMap (({name, items = [], newPath = path   (path ? '/' : '')   name}) => [
    ... (name == target ? [newPath] : []),
    ... pathsTo (items, target, newPath)
  ])

const input = {"project-name": "name of the project", author: "username of the author", date: "DD-MM-YYYY", privacy: "public / private", collaborators: ["email-address / user-id of Collaborator-1", "email-address / user-id of Collaborator-2"], plan: "active-plan-name", database: [{type: "directory", name: "js", items: [{type: "directory", name: "assets", items: [{type: "directory", name: "icons", items: [{type: "file", name: "logo.png", content: "path of logo.png"}]}]}, {type: "directory", name: "lib", items: [{type: "file", name: "jquery.min.js", content: "CONTENT OF jquery.min.js"}, {type: "file", name: "split.js", content: "CONTENT OF split.js"}]}, {type: "directory", name: "src", items: [{type: "file", name: "script.js", content: "CONTENT OF script.js"}]}]}, {type: "directory", name: "style", items: [{type: "file", name: "main.css", content: "CONTENT OF main.css"}]}, {type: "file", name: "index.html", content: "CONTENT OF index.html"}]}

console .log (pathsTo (input .database, 'logo.png'))

This fixes our problem. But I'd suggest we go one step further and use a much more versatile intermediate format. Make our recursive function return something like [["js", "assets", "icons" "logo.png"]] and then wrap it up in a function that joins those together into your format. This would also give us the chance to move input.datbase from the calling code and into our main function. (We can't easily do that in the recursive version, because that outer object does not have the same recursive structure.)

const _pathsTo = (xs, target, path = []) => 
  xs .flatMap (({name, items = [], newPath = path .concat (name)}) => [
    ... (name == target ? [newPath] : []),
    ... _pathsTo (items, target, newPath)
  ])

const pathsTo = (xs, target) => 
  _pathsTo (xs .database, target) .map (ns => ns .join ('/'))

const input = {"project-name": "name of the project", author: "username of the author", date: "DD-MM-YYYY", privacy: "public / private", collaborators: ["email-address / user-id of Collaborator-1", "email-address / user-id of Collaborator-2"], plan: "active-plan-name", database: [{type: "directory", name: "js", items: [{type: "directory", name: "assets", items: [{type: "directory", name: "icons", items: [{type: "file", name: "logo.png", content: "path of logo.png"}]}]}, {type: "directory", name: "lib", items: [{type: "file", name: "jquery.min.js", content: "CONTENT OF jquery.min.js"}, {type: "file", name: "split.js", content: "CONTENT OF split.js"}]}, {type: "directory", name: "src", items: [{type: "file", name: "script.js", content: "CONTENT OF script.js"}]}]}, {type: "directory", name: "style", items: [{type: "file", name: "main.css", content: "CONTENT OF main.css"}]}, {type: "file", name: "index.html", content: "CONTENT OF index.html"}]}

console .log (pathsTo (input, 'logo.png'))

We can take this one step further and instead of searching for a name, search by an arbitrary predicate. This code is only slightly more involved, and it adds a great deal of flexibility. At this level of abstraction, I would probably move that lookup of the database property back up to the caller.

const _pathsTo = (pred) => (xs, path = []) => 
  xs .flatMap (({name, items = [], newPath = path .concat (name)}) => [
    ... (pred (name) ? [newPath] : []),
    ... _pathsTo (pred) (items, newPath)
  ])

const pathsTo = (pred) => (xs) => 
  _pathsTo (pred) (xs) .map (ns => ns .join ('/'))

const input = {"project-name": "name of the project", author: "username of the author", date: "DD-MM-YYYY", privacy: "public / private", collaborators: ["email-address / user-id of Collaborator-1", "email-address / user-id of Collaborator-2"], plan: "active-plan-name", database: [{type: "directory", name: "js", items: [{type: "directory", name: "assets", items: [{type: "directory", name: "icons", items: [{type: "file", name: "logo.png", content: "path of logo.png"}]}]}, {type: "directory", name: "lib", items: [{type: "file", name: "jquery.min.js", content: "CONTENT OF jquery.min.js"}, {type: "file", name: "split.js", content: "CONTENT OF split.js"}]}, {type: "directory", name: "src", items: [{type: "file", name: "script.js", content: "CONTENT OF script.js"}]}]}, {type: "directory", name: "style", items: [{type: "file", name: "main.css", content: "CONTENT OF main.css"}]}, {type: "file", name: "index.html", content: "CONTENT OF index.html"}]}

console .log (pathsTo (name => name == 'logo.png') (input .database, 'logo.png'))
console .log (pathsTo (name => name .endsWith ('.js')) (input .database, 'logo.png'))

We and if we wanted the equivalent of our initial function back, we could just write this atop that version:

const pathsToName = (input, target) => 
  pathsTo (name => name == target) (input .database)

pathsToName (input, 'split.js')

This now has the same interface as the original function, but written atop code you can easily adapt to other circumstances.

I'm not particularly suggesting this last format, just pointing out that there are often options at different levels of abstraction.

  • Related