Skip to content

Commit

Permalink
Adding a component to extract values from a remote file (#2554)
Browse files Browse the repository at this point in the history
* 🔨 Adding a `<RemoteValue>` component to extract values from a remote file

* 📝 refs #2554 Improving `<RemoteValue>` docs
  • Loading branch information
vcampitelli authored Sep 29, 2023
1 parent 700911e commit cbe4b02
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 14 deletions.
9 changes: 9 additions & 0 deletions astro/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"astro": "^2.0.15",
"astro-compress": "^2.0.8",
"astro-index-pages": "src/integrations/astro-index-pages",
"jsonpath-plus": "^7.2.0",
"marked": "^7.0.1",
"mermaid": "^9.1.6",
"pagefind": "^0.12.0",
Expand Down
107 changes: 107 additions & 0 deletions astro/src/components/RemoteValue/RemoteValue.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
import parse, {Parser, SelectorFunction} from './parsers';
import Code from 'astro/components/Code.astro';
export {Parser} from './parsers';
/**
* Component to fetch values from remote files, optionally passing a selector to extract a value from the file
* (e.g. a JSON object).
*
* Import:
* import RemoteValue from 'path/to/components/RemoteValue/RemoteValue.astro';
*
* Usage syntax:
* <RemoteValue url="https://remote.url.to.file" selector="selector.path" parser={one of Parser values} default="default value" />
*
* Props:
* url: remote URL to fetch the file from
* parser: value from the Enum exported by RemoteValue.astro. If not defined, the component will use the file extension
* from the URL.
* selector: parsed-dependent syntax to extract the value from the remote file. It can also be a callback function that
* will be called with the entire file as an argument and should return the value to extract.
* E.g. for JSON, we use jsonpath-plus to provide an XPath-based syntax.
* defaultValue: default value if we cannot retrieve the element (otherwise, we'll render "not available").
* This is recommended in case you ever change the remote file and the selector doesn't work anymore.
*
* These three examples return the same "clientSecret" element:
* <RemoteValue url="https://some.github.repo/kickstart/kickstart.json"
* selector="$.requests.4.body.application.oauthConfiguration.clientSecret" />
* <RemoteValue url="https://some.github.repo/kickstart/kickstart.json"
* selector="$.requests.[?(@.url === '/api/application/#{applicationId}')].body.application.oauthConfiguration.clientSecret" />
* <RemoteValue url="https://some.github.repo/kickstart/kickstart.json"
* selector={(element) => element.requests.find(e => e.url === '/api/application/#{applicationId}').body.application.oauthConfiguration.clientSecret} />
*
* If you need to render the value inside a <Code> component, you need to pass a `codeRenderer` callback function:
* <RemoteValue
* url="https://some.github.repo/kickstart/kickstart.json"
* selector="$.requests.4.body.application.oauthConfiguration.clientSecret"
* codeRenderer={(value) => `OP_SECRET_KEY="${value}" bundle exec rails s`} />
* You can also pass a `codeLang` prop in case the <Code> element doesn't detect it automatically.
*/
/**
* Callback to be used to render <Code> elements
*/
type CodeRendererFunc = (element: any) => string;
/**
* Available props.
*/
export interface Props {
/**
* Url to fetch code from.
*/
url: string;
/**
* Selector to extract the value from the content.
* This is parser-specific but is normally some kind of XPath.
* E.g. JSONPath syntax for JSON objects (https://www.npmjs.com/package/jsonpath-plus)
*/
selector: string | SelectorFunction;
/**
* Optional parser name. If no one was informed, we'll use the file extension.
*/
parser?: Parser;
/**
* Default value if there's an error retrieving file.
*/
default?: string;
/**
* Callback to be used to render <Code> elements.
*/
codeRenderer: CodeRendererFunc;
/**
* Optional `lang` attribute that will be passed to the <Code> component.
*/
codeLang: string;
}
// Extracting props
const {url, selector, parser, default: defaultValue, codeRenderer, codeLang} = Astro.props as Props;
// Value that will be rendered
let value;
try {
const response = await fetch(url);
const content = await response.text();
// Using the selector to look up the value
if ((content) && (selector)) {
value = parse(url, content, selector, parser);
}
} catch (e) {
console.error(`Error retrieving remote value from ${selector} at ${url}`, e);
}
// Default value
if (!value) {
value = defaultValue || "not available";
}
---
{(codeRenderer) ? <Code code={codeRenderer(value)} lang={codeLang}/> : value}
77 changes: 77 additions & 0 deletions astro/src/components/RemoteValue/parsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {JSONPath} from 'jsonpath-plus';

/**
* Common cache object used by parsers.
*/
const CACHE: { [key: string]: any } = {};

/**
* Available values for the optional syntax prop.
*/
export enum Parser {
JSON = 'json',
}

/**
* Custom function to look up the value.
*/
export type SelectorFunction = (element: object) => string;


/**
* JSON Parser
*
* @param {String} url URL used as a key for the caching system
* @param {String} code Actual code to be parsed
* @param {String|SelectorFunction} selectorOrFunction A JSONPath selector or a custom function to look up the value.
*/
function jsonParser(url: string, code: string, selectorOrFunction: string | SelectorFunction): string | null {
// Caching JSON objects
if (typeof CACHE[url] === 'undefined') {
try {
CACHE[url] = JSON.parse(code);
} catch (err: any) {
return null;
}
}

// Strings are treated as JSONPath selector
if (typeof selectorOrFunction === 'string') {
const result = JSONPath({
path: selectorOrFunction,
json: CACHE[url],
});
return ((result) && (result[0])) ? result[0] : null;
}

// If this is a function, we call it passing the JSON object as argument
return selectorOrFunction(CACHE[url]);
}

/**
* Parsing content, optionally looking up the value via a selector
*
* @param {String} url URL used as a key for the caching system
* @param {String} content Actual code to be parsed
* @param {String|SelectorFunction} selector String or a custom function
* @param {String|undefined} parser Optional parser (will detect from the filename extension)
* @return {String|null}
*/
export default function parse(
url: string,
content: string,
selector: string | SelectorFunction,
parser?: string
): string | null {
// If no parser was informed, we use the file extension
if (!parser) {
parser = url.substring(url.lastIndexOf('.') + 1);
}

switch (parser.toLowerCase()) {
case Parser.JSON:
return jsonParser(url, content, selector);
}

return null;
}
33 changes: 19 additions & 14 deletions astro/src/content/quickstarts/quickstart-ruby-rails-web.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Aside from '../../components/Aside.astro';
import LoginAfter from '../../diagrams/quickstarts/login-after.astro';
import LoginBefore from '../../diagrams/quickstarts/login-before.astro';
import RemoteCode from '../../components/RemoteCode.astro';
import RemoteValue from '../../components/RemoteValue/RemoteValue.astro';

In this quickstart you are going to build an application with Ruby on Rails and integrate it with FusionAuth.
You'll be building it for [ChangeBank](https://www.youtube.com/watch?v=CXDxNCzUspM), a global leader in converting dollars into coins.
Expand Down Expand Up @@ -74,10 +75,10 @@ If you ever want to reset the FusionAuth system, delete the volumes created by d

FusionAuth will be initially configured with these settings:

* Your client Id is `e9fdb985-9173-4e01-9d73-ac2d60d1dc8e`.
* Your client secret is `super-secret-secret-that-should-be-regenerated-for-production`.
* Your example username is `richard@example.com` and the password is `password`.
* Your admin username is `admin@example.com` and the password is `password`.
* Your client Id is <code><RemoteValue url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/kickstart/kickstart.json" selector="$.variables.applicationId" /></code>.
* Your client secret is <code><RemoteValue url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/kickstart/kickstart.json" selector="$.requests.[?(@.url === '/api/application/#{applicationId}')].body.application.oauthConfiguration.clientSecret" /></code>.
* Your example username is <code><RemoteValue url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/kickstart/kickstart.json" selector="$.variables.userEmail" /></code> and the password is <code><RemoteValue url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/kickstart/kickstart.json" selector="$.variables.userPassword" /></code>.
* Your admin username is <code><RemoteValue url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/kickstart/kickstart.json" selector="$.variables.adminEmail" /></code> and the password is <code><RemoteValue url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/kickstart/kickstart.json" selector="$.variables.adminPassword" /></code>.
* The base URL of FusionAuth `http://localhost:9011/`.

You can log into the [FusionAuth admin UI](http://localhost:9011/admin) and look around if you want, but with Docker/Kickstart you don't need to.
Expand All @@ -98,7 +99,7 @@ We'll use the [OmniAuth Library](https://github.com/omniauth/omniauth), which si

Install the omniauth gem and other supporting gems. Add the following three lines to your `Gemfile`.

<RemoteCode url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/Gemfile"
<RemoteCode url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/Gemfile"
lang="ruby"
tags="gemfile"/>

Expand Down Expand Up @@ -161,7 +162,7 @@ lang="ruby" />

This lets you have a nice `logout` method and also handle the callback from omniauth. The latter sets a `session` attribute with user data, which can be used by views later.

Now, update the application controller at `app/controllers/application_controller.rb`.
Now, update the application controller at `app/controllers/application_controller.rb`.

<RemoteCode url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/controllers/application_controller.rb"
lang="ruby" />
Expand Down Expand Up @@ -220,9 +221,11 @@ Once you’ve created these files, you can test the application out.

Start up the Rails application using this command:

```shell
OP_SECRET_KEY="super-secret-secret-that-should-be-regenerated-for-production" bundle exec rails s
```
<RemoteValue
url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/kickstart/kickstart.json"
selector="$.requests.[?(@.url === '/api/application/#{applicationId}')].body.application.oauthConfiguration.clientSecret"
codeLang="shell"
codeRenderer={(value) => `OP_SECRET_KEY="${value}" bundle exec rails s`} />

`OP_SECRET_KEY` is the client secret, which was defined by the [FusionAuth Installation via Docker](#run-fusionauth-via-docker) step. You don't want to commit secrets like this to version control, so use an environment variable.

Expand Down Expand Up @@ -252,7 +255,7 @@ FusionAuth gives you the ability to customize just about everything with the use

* I get `This site can’t be reached localhost refused to connect.` when I click the Login button

Ensure FusionAuth is running in the Docker container. You should be able to login as the admin user, `admin@example.com` with a password of `password` at [http://localhost:9011/admin](http://localhost:9011/admin).
Ensure FusionAuth is running in the Docker container. You should be able to login as the admin user, <code><RemoteValue url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/kickstart/kickstart.json" selector="$.variables.adminEmail" /></code> with a password of <code><RemoteValue url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/kickstart/kickstart.json" selector="$.variables.adminPassword" /></code> at [http://localhost:9011/admin](http://localhost:9011/admin).

* I get an error page when I click on the Login button with message of `"error_reason" : "invalid_client_id"`

Expand All @@ -271,12 +274,14 @@ This indicates that Omniauth is unable to call FusionAuth to validate the return

You can always pull down a complete running application and compare what's different.

```
git clone https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web.git
<RemoteValue
url="https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/kickstart/kickstart.json"
selector="$.requests.[?(@.url === '/api/application/#{applicationId}')].body.application.oauthConfiguration.clientSecret"
codeLang="shell"
codeRenderer={(value) => `git clone https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web.git
cd fusionauth-quickstart-ruby-on-rails-web
docker-compose up -d
cd complete-app
bundle install
OP_SECRET_KEY=super-secret-secret-that-should-be-regenerated-for-production bundle exec rails s
```
OP_SECRET_KEY=${value} bundle exec rails s`} />

0 comments on commit cbe4b02

Please sign in to comment.