From 4c72c1b3aa2d816aab478b53c965b81d89df1c5d Mon Sep 17 00:00:00 2001 From: hacksalot Date: Sun, 17 Jan 2016 02:04:21 -0500 Subject: [PATCH] Refactor conversion logic, improve tests. --- package.json | 2 + src/index.js | 59 +++++---- src/sections.js | 293 ----------------------------------------- src/to-fresh.js | 148 +++++++++++++++++++++ src/to-jrs.js | 135 +++++++++++++++++++ test/test-converter.js | 55 +++++--- 6 files changed, 350 insertions(+), 342 deletions(-) delete mode 100644 src/sections.js create mode 100644 src/to-fresh.js create mode 100644 src/to-jrs.js diff --git a/package.json b/package.json index 791dd72..4c3abc5 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,13 @@ }, "devDependencies": { "chai": "^3.4.1", + "fresca": "^0.4.0", "fresh-test-resumes": "^0.5.0", "grunt": "^0.4.5", "grunt-cli": "^0.1.13", "grunt-contrib-jshint": "^0.11.3", "grunt-simple-mocha": "^0.4.1", + "is-my-json-valid": "^2.12.3", "mocha": "^2.3.4" } } diff --git a/src/index.js b/src/index.js index 1bed4db..498ceb1 100644 --- a/src/index.js +++ b/src/index.js @@ -11,12 +11,23 @@ FRESH to JSON Resume conversion routiens. var _ = require('underscore'); - var sect = require('./sections'); + var sect = { + jrs: require('./to-jrs'), + fresh: require('./to-fresh') + }; /** Convert between FRESH and JRS resume/CV formats. + We could do this with an object mapper like [node-object-mapper][o1] + or [explicitobjectmap-node][o2] but in practice we end up needing a lot + of custom conversion code anyway (defeating the purpose of an object + mapper) or else we end up creating specific rules or having to do + multipass conversions, etc, because of idiosyncracies in the mapping + libraries and the FRESH/JRS formats. + [o1]: https://github.com/wankdanker/node-object-mapper + [o2]: https://github.com/opentable/explicitobjectmap-node @class FRESHConverter */ var FRESHConverter = module.exports = { @@ -32,14 +43,6 @@ FRESH to JSON Resume conversion routiens. foreign = (foreign === undefined || foreign === null) ? true : foreign; - var ret = _.mapObject( src, function( val, key ) { - - // Underscore will hand us every top-level key in the object, most - // of which are resume sections / sub-objects - - }); - - return { name: src.basics.name, imp: src.basics.imp, @@ -55,7 +58,7 @@ FRESH to JSON Resume conversion routiens. website: src.basics.website, other: src.basics.other // <--> round-trip }, - meta: sect.meta( true, src.meta ), + meta: sect.fresh.meta( src, src.meta ), location: { city: src.basics.location.city, region: src.basics.location.region, @@ -63,15 +66,15 @@ FRESH to JSON Resume conversion routiens. code: src.basics.location.postalCode, address: src.basics.location.address }, - employment: sect.employment( src.work, true ), - education: sect.education( src.education, true), - service: sect.service( src.volunteer, true), - skills: sect.skillsToFRESH( src.skills ), - writing: sect.writing( src.publications, true), - recognition: sect.recognition( src.awards, true, foreign ), - social: sect.social( src.basics.profiles, true ), + employment: sect.fresh.employment( src, src.work ), + education: sect.fresh.education( src, src.education ), + service: sect.fresh.service( src, src.volunteer ), + skills: sect.fresh.skills( src, src.skills ), + writing: sect.fresh.writing( src, src.publications ), + recognition: sect.fresh.recognition( src, src.awards ), + social: sect.fresh.social( src, src.basics.profiles ), interests: src.interests, - testimonials: sect.references( src.references, true ), + testimonials: sect.fresh.testimonials( src, src.references ), languages: src.languages, disposition: src.disposition // <--> round-trip }; @@ -106,19 +109,19 @@ FRESH to JSON Resume conversion routiens. countryCode: src.location.country, region: src.location.region }, - profiles: sect.social( src.social, false ), + profiles: sect.jrs.social( src, src.social ), imp: src.imp }, - work: sect.employment( src.employment, false ), - education: sect.education( src.education, false ), - skills: sect.skillsToJRS( src.skills, false ), - volunteer: sect.service( src.service, false ), - awards: sect.recognition( src.recognition, false, foreign ), - publications: sect.writing( src.writing, false ), + work: sect.jrs.work( src, src.employment ), + education: sect.jrs.education( src, src.education ), + skills: sect.jrs.skills( src, src.skills ), + volunteer: sect.jrs.volunteer( src, src.service ), + awards: sect.jrs.awards( src, src.recognition ), + publications: sect.jrs.publications( src, src.writing ), interests: src.interests, - references: sect.references( src.testimonials, false ), - samples: foreign ? src.samples : undefined, - disposition: foreign ? src.disposition : undefined, + references: sect.jrs.references( src, src.testimonials ), + samples: src.samples, + disposition: src.disposition, // <--> round-trip languages: src.languages }; diff --git a/src/sections.js b/src/sections.js deleted file mode 100644 index e425c74..0000000 --- a/src/sections.js +++ /dev/null @@ -1,293 +0,0 @@ -/** -FRESH to JSON Resume conversion routines for individual sections. -@license MIT. See LICENSE.md for details. -@module sections.js -*/ - -(function() { - - - - module.exports = { - - - - meta: function( direction, obj ) { - //if( !obj ) return obj; // preserve null and undefined - if( direction ) { - obj = obj || { }; - obj.format = obj.format || "FRESH@0.1.0"; - obj.version = obj.version || "0.1.0"; - } - return obj; - }, - - - - employment: function( obj, direction ) { - if( !obj ) return obj; - if( !direction ) { - return obj && obj.history ? - obj.history.map(function(emp){ - return { - company: emp.employer, - website: emp.url, - position: emp.position, - startDate: emp.start, - endDate: emp.end, - summary: emp.summary, - highlights: emp.highlights - }; - }) : undefined; - } - else { - return { - history: obj && obj.length ? - obj.map( function( job ) { - return { - position: job.position, - employer: job.company, - summary: job.summary, - current: (!job.endDate || !job.endDate.trim() || - job.endDate.trim().toLowerCase() === 'current') || undefined, - start: job.startDate, - end: job.endDate, - url: job.website, - keywords: [], - highlights: job.highlights - }; - }) : undefined - }; - } - }, - - - - education: function( obj, direction ) { - if( !obj ) return obj; - if( direction ) { - return obj && obj.length ? { - level: "", - history: obj.map(function(edu){ - return { - institution: edu.institution, - start: edu.startDate, - end: edu.endDate, - grade: edu.gpa, - curriculum: edu.courses, - url: edu.website || edu.url || undefined, - summary: edu.summary || "", - area: edu.area, - studyType: edu.studyType - }; - }) - } : undefined; - } - else { - return obj && obj.history ? - obj.history.map(function(edu){ - return { - institution: edu.institution, - gpa: edu.grade, - courses: edu.curriculum, - startDate: edu.start, - endDate: edu.end, - area: edu.area, - studyType: edu.studyType - }; - }) : undefined; - } - }, - - - - service: function( obj, direction, foreign ) { - if( !obj ) return obj; - if( direction ) { - return { - history: obj && obj.length ? obj.map(function(vol) { - return { - type: 'volunteer', - position: vol.position, - organization: vol.organization, - start: vol.startDate, - end: vol.endDate, - url: vol.website, - summary: vol.summary, - highlights: vol.highlights - }; - }) : undefined - }; - } - else { - return obj && obj.history ? - obj.history.map(function(srv){ - return { - flavor: foreign ? srv.flavor : undefined, - organization: srv.organization, - position: srv.position, - startDate: srv.start, - endDate: srv.end, - website: srv.url, - summary: srv.summary, - highlights: srv.highlights - }; - }) : undefined; - } - }, - - - - social: function( obj, direction ) { - if( !obj ) return obj; - if( direction ) { - return obj.map(function(pro){ - return { - label: pro.network, - network: pro.network, - url: pro.url, - user: pro.username - }; - }); - } - else { - return obj.map( function( soc ) { - return { - network: soc.network, - username: soc.user, - url: soc.url - }; - }); - } - }, - - - - recognition: function( obj, direction, foreign ) { - if( !obj ) return obj; - if( direction ) { - return obj && obj.length ? obj.map( - function(awd){ - return { - flavor: foreign ? awd.flavor : undefined, - url: foreign ? awd.url: undefined, - title: awd.title, - date: awd.date, - from: awd.awarder, - summary: awd.summary - }; - }) : undefined; - } - else { - return obj && obj.length ? obj.map(function(awd){ - return { - flavor: foreign ? awd.flavor : undefined, - url: foreign ? awd.url: undefined, - title: awd.title, - date: awd.date, - awarder: awd.from, - summary: awd.summary - }; - }) : undefined; - } - }, - - - - references: function( obj, direction ) { - if( !obj ) return obj; - if( direction ) { - return obj && obj.length && obj.map(function(ref){ - return { - name: ref.name, - flavor: 'professional', - quote: ref.reference, - private: false - }; - }); - } - else { - return obj && obj.length && obj.map(function(ref){ - return { - name: ref.name, - reference: ref.quote - }; - }); - } - }, - - - - writing: function( obj, direction ) { - if( !obj ) return obj; - if( direction ) { - return obj.map(function( pub ) { - return { - title: pub.name, - flavor: undefined, - publisher: pub.publisher, - url: pub.website, - date: pub.releaseDate, - summary: pub.summary - }; - }); - } - else { - return obj && obj.length ? obj.map(function(pub){ - return { - name: pub.title, - publisher: pub.publisher && - pub.publisher.name ? pub.publisher.name : pub.publisher, - releaseDate: pub.date, - website: pub.url, - summary: pub.summary - }; - }) : undefined; - } - }, - - - - skillsToFRESH: function( skills ) { - if( !skills ) return skills; - return { - sets: skills.map(function( set ) { - return { - name: set.name, - level: set.level, - skills: set.keywords - }; - }) - }; - }, - - - - skillsToJRS: function( skills ) { - if( !skills ) return skills; - var ret = []; - if( skills.sets && skills.sets.length ) { - ret = skills.sets.map(function(set){ - return { - name: set.name, - level: set.level, - keywords: set.skills - }; - }); - } - else if( skills.list ) { - ret = skills.list.map(function(sk){ - return { - name: sk.name, - level: sk.level, - keywords: sk.keywords - }; - }); - } - return ret; - } - - - - }; // end module.exports -}()); diff --git a/src/to-fresh.js b/src/to-fresh.js new file mode 100644 index 0000000..2439d71 --- /dev/null +++ b/src/to-fresh.js @@ -0,0 +1,148 @@ +/** +Convert JRS resume sections to FRESH. +@module to-fresh.js +@license MIT. See LICENSE.md for details. +*/ + +(function(){ + + module.exports = { + + meta: function( r, obj ) { + obj = obj || { }; + obj.format = obj.format || "FRESH@0.1.0"; + obj.version = obj.version || "0.1.0"; + return obj; + }, + + employment: function( r, obj ) { + if( !obj ) return obj; + return { + history: obj && obj.length ? + obj.map( function( job ) { + return { + position: job.position, + employer: job.company, + summary: job.summary, + current: (!job.endDate || !job.endDate.trim() || + job.endDate.trim().toLowerCase() === 'current') || undefined, + start: job.startDate, + end: job.endDate, + url: job.website, + keywords: [], + highlights: job.highlights + }; + }) : undefined + }; + }, + + education: function( r, obj ) { + if( !obj ) return obj; + return obj && obj.length ? { + level: "", + history: obj.map(function(edu){ + return { + institution: edu.institution, + start: edu.startDate, + end: edu.endDate, + grade: edu.gpa, + curriculum: edu.courses, + url: edu.website || edu.url || undefined, + summary: edu.summary || "", + area: edu.area, + studyType: edu.studyType + }; + }) + } : undefined; + }, + + + service: function( r, obj ) { + if( !obj ) return obj; + return { + history: obj && obj.length ? obj.map(function(vol) { + return { + type: 'volunteer', + position: vol.position, + organization: vol.organization, + start: vol.startDate, + end: vol.endDate, + url: vol.website, + summary: vol.summary, + highlights: vol.highlights + }; + }) : undefined + }; + }, + + social: function( r, obj ) { + if( !obj ) return obj; + return obj.map(function(pro){ + return { + label: pro.network, + network: pro.network, + url: pro.url, + user: pro.username + }; + }); + }, + + skills: function( r, skills ) { + if( !skills ) return skills; + return { + sets: skills.map(function( set ) { + return { + name: set.name, + level: set.level, + skills: set.keywords + }; + }) + }; + }, + + recognition: function( r, obj ) { + if( !obj ) return obj; + return obj && obj.length ? obj.map( + function(awd){ + return { + flavor: awd.flavor, + url: awd.url, + title: awd.title, + date: awd.date, + from: awd.awarder, + summary: awd.summary + }; + }) : undefined; + }, + + + writing: function( r, obj ) { + if( !obj ) return obj; + return obj.map(function( pub ) { + return { + title: pub.name, + flavor: undefined, + publisher: pub.publisher, + url: pub.website, + date: pub.releaseDate, + summary: pub.summary + }; + }); + }, + + testimonials: function( r, obj ) { + if( !obj ) return obj; + return obj && obj.length && obj.map(function(ref){ + return { + name: ref.name, + flavor: 'professional', + quote: ref.reference, + private: false + }; + }); + } + + + }; + +}()); diff --git a/src/to-jrs.js b/src/to-jrs.js new file mode 100644 index 0000000..bbcc95f --- /dev/null +++ b/src/to-jrs.js @@ -0,0 +1,135 @@ +/** +Convert FRESH resume sections to JRS. +@module to-jrs.js +@license MIT. See LICENSE.md for details. +*/ + +(function(){ + + module.exports = { + + work: function( r, obj ) { + if( !obj ) return obj; + return obj && obj.history ? + obj.history.map(function(emp){ + return { + company: emp.employer, + website: emp.url, + position: emp.position, + startDate: emp.start, + endDate: emp.end, + summary: emp.summary, + highlights: emp.highlights + }; + }) : undefined; + }, + + education: function( r, obj ) { + if( !obj ) return obj; + return obj && obj.history ? + obj.history.map(function(edu){ + return { + institution: edu.institution, + gpa: edu.grade, + courses: edu.curriculum, + startDate: edu.start, + endDate: edu.end, + area: edu.area, + studyType: edu.studyType + }; + }) : undefined; + }, + + volunteer: function( r, obj ) { + if( !obj ) return obj; + return obj && obj.history ? + obj.history.map(function(srv){ + return { + flavor: srv.flavor, + organization: srv.organization, + position: srv.position, + startDate: srv.start, + endDate: srv.end, + website: srv.url, + summary: srv.summary, + highlights: srv.highlights + }; + }) : undefined; + }, + + social: function( r, obj ) { + if( !obj ) return obj; + return obj.map( function( soc ) { + return { + network: soc.network, + username: soc.user, + url: soc.url + }; + }); + }, + + + skills: function( r, skills ) { + if( !skills ) return skills; + var ret = []; + if( skills.sets && skills.sets.length ) { + ret = skills.sets.map(function(set){ + return { + name: set.name, + level: set.level, + keywords: set.skills + }; + }); + } + else if( skills.list ) { + ret = skills.list.map(function(sk){ + return { + name: sk.name, + level: sk.level, + keywords: sk.keywords + }; + }); + } + return ret; + }, + + awards: function( r, obj ) { + if( !obj ) return obj; + return obj && obj.length ? obj.map(function(awd){ + return { + flavor: awd.flavor, + url: awd.url, + title: awd.title, + date: awd.date, + awarder: awd.from, + summary: awd.summary + }; + }) : undefined; + }, + + publications: function( r, obj ) { + if( !obj ) return obj; + return obj && obj.length ? obj.map(function(pub){ + return { + name: pub.title, + publisher: pub.publisher && + pub.publisher.name ? pub.publisher.name : pub.publisher, + releaseDate: pub.date, + website: pub.url, + summary: pub.summary + }; + }) : undefined; + }, + + references: function( r, obj ) { + if( !obj ) return obj; + return obj && obj.length && obj.map(function(ref){ + return { + name: ref.name, + reference: ref.quote + }; + }); + } + + }; +}()); diff --git a/test/test-converter.js b/test/test-converter.js index ffba1a3..3443cbb 100644 --- a/test/test-converter.js +++ b/test/test-converter.js @@ -8,50 +8,63 @@ var chai = require('chai') , expect = chai.expect , should = chai.should() , CONVERTER = require('../src/index') + , validator = require('is-my-json-valid') + , FRESCA = require('fresca') , _ = require('underscore'); // Get a dossier of test resumes var resumes = require('fresh-test-resumes'); +var _rF, _rJ; + +function isValid( r ) { + // https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js + // Allow YYYY, YYYY-MM, and YYYY-MM-DD date formats + // Allow empty string "" or " " etc as URI + var validate = validator( FRESCA, { + formats: { + date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/, + uri: /^(?:[a-zA-Z][a-zA-Z0-9+-.]*:[^\s]*)|\s*$/ + } + }); + var ret = !!validate( r ); + if( !ret ) + console.log(validate.errors); + return ret; +} + describe('CONVERT', function () { _.each( resumes.fresh, function(val, key) { - // Ignore broken resumes (invalid JSON), which are loaded as strings - // instead of objects by fresh-test-resumes. fresh-jrs-converter handles - // only well-formed JSON. - if( !(typeof val === 'string' || val instanceof String )) { - - it( key + ' to JSON Resume format.', function () { + if( !(typeof val === 'string' || val instanceof String )) { //[1] + it( key + ' to JSON Resume format', function () { expect(function() { - var jrs = CONVERTER.toJRS( val ); - var fresh = CONVERTER.toFRESH( jrs ); + _rJ = CONVERTER.toJRS( val ); + _rF = CONVERTER.toFRESH( _rJ ); }).to.not.throw(); }); - } }); _.each( resumes.jrs, function(val, key) { - // Ignore broken resumes (invalid JSON), which are loaded as strings - // instead of objects by fresh-test-resumes. fresh-jrs-converter handles - // only well-formed JSON. - if( !(typeof val === 'string' || val instanceof String )) { - - it( key + ' to FRESH format.', function () { + if( !(typeof val === 'string' || val instanceof String )) {//[1] + it( key + ' to FRESH format', function () { expect(function() { - var fresh = CONVERTER.toFRESH( val ); - var jrs = CONVERTER.toJRS( fresh ); + _rF = CONVERTER.toFRESH( val ); + _rJ = CONVERTER.toJRS( _rF ); }).to.not.throw(); - }); + var isv = isValid( _rF ); + expect(isv).to.be[ key !== 'empty' ? 'true' : 'false' ]; + }); } }); - - - }); + +// [1]: Ignore broken resumes (invalid JSON), which are loaded as strings +// instead of objects by fresh-test-resumes.