diff --git a/markdown/blog/2024-december-and-paris.md b/markdown/blog/2024-december-and-paris.md new file mode 100644 index 000000000000..e0f7a365bc19 --- /dev/null +++ b/markdown/blog/2024-december-and-paris.md @@ -0,0 +1,138 @@ +--- +title: "November and December Community Update And AsyncAPI Conf in Paris 2024" +date: 2024-12-16T06:00:00+01:00 +type: Communication +tags: + - Project Status +cover: /img/posts/2024-blog-banner/blog-banner-december.webp +authors: + - name: Thulisile Sibanda + photo: /img/avatars/thulieblack.webp + link: https://www.linkedin.com/in/v-thulisile-sibanda/ + byline: Community Builder and Open Source Fanatic! +excerpt: 'November and December Community Update And Paris Conference Summary' +featured: true +--- +I can't believe we are in the final weeks of the year, and it has been an eventful one. As a community, we have experienced both proud and painful moments, celebrated our victories, and faced challenges and losses. However, in the end, we overcame those difficulties and emerged stronger and better. + +Although this summary is a bit later than usual, I am excited to share the details of what happened in November and December and the highlights from the last conference of the year, which took place in Paris. + +## AsyncAPI Community Building and Maintenance Goals Proposal 2025 + +The 2025 community building and maintenance goals proposal is currently open for discussion and will soon be put to a vote for TSC members. We would love your thoughts and suggestions, particularly when solving our community's challenges. +[Please take a moment to review the open PR related to the AsyncAPI Community Building Goals for 2025](https://github.com/asyncapi/community/pull/1575) to participate in the discussion and share your ideas and solutions. + +## AsyncAPI Conf in Paris 2024 + +The AsyncAPI Conf was back again in Paris this December, thanks to [Mehdi Medjaoui](https://www.linkedin.com/in/mehdimedjaoui/) and the amazing team at [APIdays](https://www.apidays.global/) for hosting and sponsoring the venue. We participated in the three-day event, celebrating a year of the `API Standards` booth alongside friends from OpenAPI, JSON Schema, and GraphQL. Special thanks to all the members of the AsyncAPI community who could join the conference and help at the booth: [Hugo Guerrero](https://www.linkedin.com/in/hugoguerrero/), [Fran Méndez](https://www.linkedin.com/in/fmvilas/), [Richard Coppen](https://www.linkedin.com/in/richard-coppen/), [Hari Krishnan](https://www.linkedin.com/in/harikrishnan83/), and [Lukasz Gornicki](https://www.linkedin.com/in/lukasz-gornicki-a621914/). + +The AsyncAPI Conf track took place on the 3rd day of the conference, featuring an impressive lineup of sessions that attracted a diverse audience. The event was consistently packed, with attendees engaged throughout the day. + +
+ +[Fran Méndez](https://www.linkedin.com/in/fmvilas/) and [Lukasz Gornicki](https://www.linkedin.com/in/lukasz-gornicki-a621914/) started the track with a welcome speech and mentioned that AsyncAPI recently celebrated its 8th anniversary in November. + +
+ +[Naresh Jain](https://www.linkedin.com/in/nareshjain/) and [Pierre Gauthier](https://www.linkedin.com/in/pierre-gauthier-46080916/) presented their keynote on `TMForum's AsyncAPI for a New Era of Event-Driven Architecture`. During the session, Pierre announced TMForum adopts AsyncAPI as a standard, with over 120 telco APIs already in production. TMForum has around 800 telco companies, and they will implement all APIs in an async manner, extensively utilizing the request-reply pattern. + +
+ +[Frank Kilcommins](https://www.linkedin.com/in/frank-kilcommins) demonstrated how treating API governance as an enabler unlocks the ability to deliver compelling developer experiences for producers and consumers in event-driven architecture (EDA). + +
+ +[Leonid Lukyanov](https://www.linkedin.com/in/leonid-lukyanov/) shared how EDAs introduce new data models, protocols, and APIs not found in the traditional REST/CRUD application stack. And how one can abstract these elements to make them feel familiar to application developers, allowing them to create streaming applications seamlessly. + +
+ +[Hugo Guerrero](https://www.linkedin.com/in/hugoguerrero/) then shared how the AsyncAPI Initiative is not only in charge of the specification but has created open-source projects to make it easier for developers to work with the specification documents. + +
+ +[Julien Testut](https://www.linkedin.com/in/julientestut/) and [Alessandro Cagnetti](https://www.linkedin.com/in/cagnetti/) shared how organizations can harness the full potential of event-driven integration by leveraging GoldenGate Data Streams, AsyncAPI, and Solace PubSub+ Event Mesh. They shared a great use case for AI and how it can be trained real-time and standardized with AsyncAPI. + +
+ +[Annegret Junker](https://www.linkedin.com/in/dr-annegret-junker-141a99a4/) explained how to design effective asynchronous APIs by using an API-first approach. The importance of defining Kafka topics and structuring your definitions. + +
+ + +[Jonathan Michaux](https://www.linkedin.com/in/jmcx/) spoke on how leveraging AI agents with AsyncAPI can create conversational interfaces that dynamically interact with event streams and asynchronous messaging systems. + +
+ +[Hari Krishnan](https://www.linkedin.com/in/harikrishnan83) and [Joel Rosario](https://www.linkedin.com/in/joel-c-rosario/) touched on leveraging the AsyncAPI specification as an executable contract and how to isolate and test each component within an EDA. + +
+ +[Laurent Broudoux](https://www.linkedin.com/in/laurentbroudoux/) and [Hugo Guerrero](https://www.linkedin.com/in/hugoguerrero/) ended the day by explaining how to use Microcks to provide a solution for mocking and contract-testing your async APIs without extensive coding and empowering you to build extensive and reliable integration tests. + +
+ +## Technical Steering Committee + +Part of doing mentorships is witnessing the growth within the community, and we are excited to welcome [Ashmit Jagtap](https://www.linkedin.com/in/ashmit-jagtap) as our newest addition to the maintainers list and TSC member. We are proud of the work you have done so far. + + + +## Final Remarks + +It's been a privilege to write the AsyncAPI monthly summary blog consistently. As this is the final blog for the year, I am genuinely grateful for the opportunity to serve and continue supporting the community. + +As we approach the holidays, I wish everyone happy holidays and a fantastic 2025. + +I'll be back next year with an overall review summary of 2024. + +**Until then, stay safe, and happy holidays!** \ No newline at end of file diff --git a/markdown/blog/2024-october-summary.md b/markdown/blog/2024-october-summary.md index 88d9e7b9e36a..1093df77ba1c 100644 --- a/markdown/blog/2024-october-summary.md +++ b/markdown/blog/2024-october-summary.md @@ -11,7 +11,6 @@ authors: link: https://www.linkedin.com/in/v-thulisile-sibanda/ byline: AsyncAPI Community Manager excerpt: 'October Community Update And Online Conference Summary' -featured: true --- October marked the third AsyncAPI Conference, this time an online edition. As someone fortunate enough to be extensively involved in organizing the conferences, I saw the challenges that came with in-person events. diff --git a/public/img/posts/2024-blog-banner/blog-banner-december.webp b/public/img/posts/2024-blog-banner/blog-banner-december.webp new file mode 100644 index 000000000000..043e9283a837 Binary files /dev/null and b/public/img/posts/2024-blog-banner/blog-banner-december.webp differ diff --git a/public/img/posts/paris-conf-2024/annegret.webp b/public/img/posts/paris-conf-2024/annegret.webp new file mode 100644 index 000000000000..87de55e1fe39 Binary files /dev/null and b/public/img/posts/paris-conf-2024/annegret.webp differ diff --git a/public/img/posts/paris-conf-2024/frank.webp b/public/img/posts/paris-conf-2024/frank.webp new file mode 100644 index 000000000000..b9249192f509 Binary files /dev/null and b/public/img/posts/paris-conf-2024/frank.webp differ diff --git a/public/img/posts/paris-conf-2024/full-room.webp b/public/img/posts/paris-conf-2024/full-room.webp new file mode 100644 index 000000000000..590c3c880206 Binary files /dev/null and b/public/img/posts/paris-conf-2024/full-room.webp differ diff --git a/public/img/posts/paris-conf-2024/hari-and-joel.webp b/public/img/posts/paris-conf-2024/hari-and-joel.webp new file mode 100644 index 000000000000..20cad1725c12 Binary files /dev/null and b/public/img/posts/paris-conf-2024/hari-and-joel.webp differ diff --git a/public/img/posts/paris-conf-2024/hugo.webp b/public/img/posts/paris-conf-2024/hugo.webp new file mode 100644 index 000000000000..6f34ad741f84 Binary files /dev/null and b/public/img/posts/paris-conf-2024/hugo.webp differ diff --git a/public/img/posts/paris-conf-2024/jonathan.webp b/public/img/posts/paris-conf-2024/jonathan.webp new file mode 100644 index 000000000000..d84c181c896e Binary files /dev/null and b/public/img/posts/paris-conf-2024/jonathan.webp differ diff --git a/public/img/posts/paris-conf-2024/julien-and-alessandro.webp b/public/img/posts/paris-conf-2024/julien-and-alessandro.webp new file mode 100644 index 000000000000..c93b90a6014a Binary files /dev/null and b/public/img/posts/paris-conf-2024/julien-and-alessandro.webp differ diff --git a/public/img/posts/paris-conf-2024/laurent-and-hugo.webp b/public/img/posts/paris-conf-2024/laurent-and-hugo.webp new file mode 100644 index 000000000000..318a2f81ccd1 Binary files /dev/null and b/public/img/posts/paris-conf-2024/laurent-and-hugo.webp differ diff --git a/public/img/posts/paris-conf-2024/leonid.webp b/public/img/posts/paris-conf-2024/leonid.webp new file mode 100644 index 000000000000..87f11a883aa1 Binary files /dev/null and b/public/img/posts/paris-conf-2024/leonid.webp differ diff --git a/public/img/posts/paris-conf-2024/lukasz-and-fran.webp b/public/img/posts/paris-conf-2024/lukasz-and-fran.webp new file mode 100644 index 000000000000..9417295f9bc9 Binary files /dev/null and b/public/img/posts/paris-conf-2024/lukasz-and-fran.webp differ diff --git a/public/img/posts/paris-conf-2024/naresh-and-pierre.webp b/public/img/posts/paris-conf-2024/naresh-and-pierre.webp new file mode 100644 index 000000000000..3850ded92269 Binary files /dev/null and b/public/img/posts/paris-conf-2024/naresh-and-pierre.webp differ diff --git a/scripts/markdown/check-markdown.js b/scripts/markdown/check-markdown.js index 8979f7e0b4ab..cd3bd7ddd1c5 100644 --- a/scripts/markdown/check-markdown.js +++ b/scripts/markdown/check-markdown.js @@ -1,4 +1,4 @@ -const fs = require('fs'); +const fs = require('fs').promises; const matter = require('gray-matter'); const path = require('path'); @@ -98,14 +98,10 @@ function validateDocs(frontmatter) { * @param {Function} validateFunction - The function used to validate the frontmatter. * @param {string} [relativePath=''] - The relative path of the folder for logging purposes. */ -function checkMarkdownFiles(folderPath, validateFunction, relativePath = '') { - fs.readdir(folderPath, (err, files) => { - if (err) { - console.error('Error reading directory:', err); - return; - } - - files.forEach(file => { +async function checkMarkdownFiles(folderPath, validateFunction, relativePath = '') { + try { + const files = await fs.readdir(folderPath); + const filePromises = files.map(async (file) => { const filePath = path.join(folderPath, file); const relativeFilePath = path.join(relativePath, file); @@ -114,17 +110,13 @@ function checkMarkdownFiles(folderPath, validateFunction, relativePath = '') { return; } - fs.stat(filePath, (err, stats) => { - if (err) { - console.error('Error reading file stats:', err); - return; - } + const stats = await fs.stat(filePath); // Recurse if directory, otherwise validate markdown file if (stats.isDirectory()) { - checkMarkdownFiles(filePath, validateFunction, relativeFilePath); + await checkMarkdownFiles(filePath, validateFunction, relativeFilePath); } else if (path.extname(file) === '.md') { - const fileContent = fs.readFileSync(filePath, 'utf-8'); + const fileContent = await fs.readFile(filePath, 'utf-8'); const { data: frontmatter } = matter(fileContent); const errors = validateFunction(frontmatter); @@ -134,13 +126,33 @@ function checkMarkdownFiles(folderPath, validateFunction, relativePath = '') { process.exitCode = 1; } } - }); }); - }); + + await Promise.all(filePromises); + } catch (err) { + console.error(`Error in directory ${folderPath}:`, err); + throw err; + } } const docsFolderPath = path.resolve(__dirname, '../../markdown/docs'); const blogsFolderPath = path.resolve(__dirname, '../../markdown/blog'); -checkMarkdownFiles(docsFolderPath, validateDocs); -checkMarkdownFiles(blogsFolderPath, validateBlogs); +async function main() { + try { + await Promise.all([ + checkMarkdownFiles(docsFolderPath, validateDocs), + checkMarkdownFiles(blogsFolderPath, validateBlogs) + ]); + } catch (error) { + console.error('Failed to validate markdown files:', error); + process.exit(1); + } +} + +/* istanbul ignore next */ +if (require.main === module) { + main(); +} + +module.exports = { validateBlogs, validateDocs, checkMarkdownFiles, main, isValidURL }; diff --git a/tests/markdown/check-markdown.test.js b/tests/markdown/check-markdown.test.js new file mode 100644 index 000000000000..85e06b70383f --- /dev/null +++ b/tests/markdown/check-markdown.test.js @@ -0,0 +1,150 @@ +const fs = require('fs').promises; +const path = require('path'); +const os = require('os'); +const { + isValidURL, + main, + validateBlogs, + validateDocs, + checkMarkdownFiles +} = require('../../scripts/markdown/check-markdown'); + +describe('Frontmatter Validator', () => { + let tempDir; + let mockConsoleError; + let mockProcessExit; + + beforeEach(async () => { + mockConsoleError = jest.spyOn(console, 'error').mockImplementation(); + mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(); + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-config')); + }); + + afterEach(async () => { + mockConsoleError.mockRestore(); + mockProcessExit.mockRestore(); + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it('validates authors array and returns specific errors', async () => { + const frontmatter = { + title: 'Test Blog', + date: '2024-01-01', + type: 'blog', + tags: ['test'], + cover: 'cover.jpg', + authors: [{ name: 'John' }, { photo: 'jane.jpg' }, { name: 'Bob', photo: 'bob.jpg', link: 'not-a-url' }] + }; + + const errors = validateBlogs(frontmatter); + expect(errors).toEqual(expect.arrayContaining([ + 'Author at index 0 is missing a photo', + 'Author at index 1 is missing a name', + 'Invalid URL for author at index 2: not-a-url' + ])); + }); + + it('validates docs frontmatter for required fields', async () => { + const frontmatter = { title: 123, weight: 'not-a-number' }; + const errors = validateDocs(frontmatter); + expect(errors).toEqual(expect.arrayContaining([ + 'Title is missing or not a string', + 'Weight is missing or not a number' + ])); + }); + + it('checks for errors in markdown files in a directory', async () => { + await fs.writeFile(path.join(tempDir, 'invalid.md'), `---\ntitle: Invalid Blog\n---`); + const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); + + await checkMarkdownFiles(tempDir, validateBlogs); + + expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Errors in file invalid.md:')); + mockConsoleLog.mockRestore(); + }); + + it('returns multiple validation errors for invalid blog frontmatter', async () => { + const frontmatter = { + title: 123, + date: 'invalid-date', + type: 'blog', + tags: 'not-an-array', + cover: ['not-a-string'], + authors: { name: 'John Doe' } + }; + const errors = validateBlogs(frontmatter); + + expect(errors).toEqual([ + 'Invalid date format: invalid-date', + 'Tags should be an array', + 'Cover must be a string', + 'Authors should be an array']); + }); + + it('logs error to console when an error occurs in checkMarkdownFiles', async () => { + const invalidFolderPath = path.join(tempDir, 'non-existent-folder'); + + await expect(checkMarkdownFiles(invalidFolderPath, validateBlogs)) + .rejects.toThrow('ENOENT'); + + expect(mockConsoleError.mock.calls[0][0]).toContain('Error in directory'); + }); + + it('skips the "reference/specification" folder during validation', async () => { + const referenceSpecDir = path.join(tempDir, 'reference', 'specification'); + await fs.mkdir(referenceSpecDir, { recursive: true }); + await fs.writeFile(path.join(referenceSpecDir, 'skipped.md'), `---\ntitle: Skipped File\n---`); + + const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); + + await checkMarkdownFiles(tempDir, validateDocs); + + expect(mockConsoleLog).not.toHaveBeenCalledWith(expect.stringContaining('Errors in file reference/specification/skipped.md')); + mockConsoleLog.mockRestore(); + }); + + it('logs and rejects when an exception occurs while processing a file', async () => { + const filePath = path.join(tempDir, 'invalid.md'); + await fs.writeFile(filePath, `---\ntitle: Valid Title\n---`); + + const mockReadFile = jest.spyOn(fs, 'readFile').mockRejectedValue(new Error('Test readFile error')); + + await expect(checkMarkdownFiles(tempDir, validateBlogs)).rejects.toThrow('Test readFile error'); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining(`Error in directory`), + expect.any(Error) + ); + + mockReadFile.mockRestore(); + }); + + it('should handle main function errors and exit with status 1', async () => { + jest.spyOn(fs, 'readdir').mockRejectedValue(new Error('Test error')); + + await main(); + + expect(mockProcessExit).toHaveBeenCalledWith(1); + + expect(mockConsoleError).toHaveBeenCalledWith( + 'Failed to validate markdown files:', + expect.any(Error) + ); + }); + + it('should handle successful main function execution', async () => { + + await main(); + + expect(mockConsoleError).not.toHaveBeenCalledWith(); + }); + + it('should return true or false for URLs', () => { + expect(isValidURL('http://example.com')).toBe(true); + expect(isValidURL('https://www.example.com')).toBe(true); + expect(isValidURL('ftp://ftp.example.com')).toBe(true); + expect(isValidURL('invalid-url')).toBe(false); + expect(isValidURL('/path/to/file')).toBe(false); + expect(isValidURL('www.example.com')).toBe(false); + }); + +});