mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 09:42:09 +01:00
This commit introduces several improvements across the codebase, primarily focused on enhancing performance, robustness, and developer experience based on static analysis feedback. - Replaces `WriteString(fmt.Sprintf())` with the more performant `fmt.Fprintf` in the HTML and Markdown exporters. - Enhances deferred `Close()` operations to log warnings on failure instead of silently ignoring potential I/O issues. - Explicitly discards non-critical errors in test suites, particularly during file cleanup, to satisfy linters and clarify intent. - Suppresses command echoing in `Taskfile.yml` for cleaner output during development tasks.
154 lines
4.9 KiB
Go
154 lines
4.9 KiB
Go
// Package services provides the core functionality for the articulate-parser application.
|
|
// It implements the interfaces defined in the interfaces package.
|
|
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/kjanat/articulate-parser/internal/interfaces"
|
|
"github.com/kjanat/articulate-parser/internal/models"
|
|
)
|
|
|
|
// ArticulateParser implements the CourseParser interface specifically for Articulate Rise courses.
|
|
// It can fetch courses from the Articulate Rise API or load them from local JSON files.
|
|
type ArticulateParser struct {
|
|
// BaseURL is the root URL for the Articulate Rise API
|
|
BaseURL string
|
|
// Client is the HTTP client used to make requests to the API
|
|
Client *http.Client
|
|
}
|
|
|
|
// NewArticulateParser creates a new ArticulateParser instance with default settings.
|
|
// The default configuration uses the standard Articulate Rise API URL and a
|
|
// HTTP client with a 30-second timeout.
|
|
func NewArticulateParser() interfaces.CourseParser {
|
|
return &ArticulateParser{
|
|
BaseURL: "https://rise.articulate.com",
|
|
Client: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// FetchCourse fetches a course from the given URI.
|
|
// It extracts the share ID from the URI, constructs an API URL, and fetches the course data.
|
|
// The course data is then unmarshalled into a Course model.
|
|
//
|
|
// Parameters:
|
|
// - uri: The Articulate Rise share URL (e.g., https://rise.articulate.com/share/SHARE_ID)
|
|
//
|
|
// Returns:
|
|
// - A parsed Course model if successful
|
|
// - An error if the fetch fails, if the share ID can't be extracted,
|
|
// or if the response can't be parsed
|
|
func (p *ArticulateParser) FetchCourse(uri string) (*models.Course, error) {
|
|
shareID, err := p.extractShareID(uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
apiURL := p.buildAPIURL(shareID)
|
|
|
|
resp, err := p.Client.Get(apiURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch course data: %w", err)
|
|
}
|
|
// Ensure response body is closed even if ReadAll fails. Close errors are logged
|
|
// but not fatal since the body content has already been read and parsed. In the
|
|
// context of HTTP responses, the body must be closed to release the underlying
|
|
// connection, but a close error doesn't invalidate the data already consumed.
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
var course models.Course
|
|
if err := json.Unmarshal(body, &course); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
|
}
|
|
|
|
return &course, nil
|
|
}
|
|
|
|
// LoadCourseFromFile loads an Articulate Rise course from a local JSON file.
|
|
// The file should contain a valid JSON representation of an Articulate Rise course.
|
|
//
|
|
// Parameters:
|
|
// - filePath: The path to the JSON file containing the course data
|
|
//
|
|
// Returns:
|
|
// - A parsed Course model if successful
|
|
// - An error if the file can't be read or the JSON can't be parsed
|
|
func (p *ArticulateParser) LoadCourseFromFile(filePath string) (*models.Course, error) {
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read file: %w", err)
|
|
}
|
|
|
|
var course models.Course
|
|
if err := json.Unmarshal(data, &course); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
|
}
|
|
|
|
return &course, nil
|
|
}
|
|
|
|
// extractShareID extracts the share ID from a Rise URI.
|
|
// It uses a regular expression to find the share ID in URIs like:
|
|
// https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/
|
|
//
|
|
// Parameters:
|
|
// - uri: The Articulate Rise share URL
|
|
//
|
|
// Returns:
|
|
// - The share ID string if found
|
|
// - An error if the share ID can't be extracted from the URI
|
|
func (p *ArticulateParser) extractShareID(uri string) (string, error) {
|
|
// Parse the URL to validate the domain
|
|
parsedURL, err := url.Parse(uri)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid URI: %s", uri)
|
|
}
|
|
|
|
// Validate that it's an Articulate Rise domain
|
|
if parsedURL.Host != "rise.articulate.com" {
|
|
return "", fmt.Errorf("invalid domain for Articulate Rise URI: %s", parsedURL.Host)
|
|
}
|
|
|
|
re := regexp.MustCompile(`/share/([a-zA-Z0-9_-]+)`)
|
|
matches := re.FindStringSubmatch(uri)
|
|
if len(matches) < 2 {
|
|
return "", fmt.Errorf("could not extract share ID from URI: %s", uri)
|
|
}
|
|
return matches[1], nil
|
|
}
|
|
|
|
// buildAPIURL constructs the API URL for fetching course data.
|
|
// It combines the base URL with the API path and the share ID.
|
|
//
|
|
// Parameters:
|
|
// - shareID: The extracted share ID from the course URI
|
|
//
|
|
// Returns:
|
|
// - The complete API URL string for fetching the course data
|
|
func (p *ArticulateParser) buildAPIURL(shareID string) string {
|
|
return fmt.Sprintf("%s/api/rise-runtime/boot/share/%s", p.BaseURL, shareID)
|
|
}
|