1use std::{collections::HashMap, sync::Arc, time::Duration};
18use anyhow::Result;
19use stof::{lang::SError, Format, Library, SDoc, SVal};
20use ureq::{Agent, AgentBuilder};
21
22
23#[derive(Default)]
25pub struct GitHubLibrary;
26impl Library for GitHubLibrary {
27 fn scope(&self) -> String {
28 "GitHub".to_string()
29 }
30
31 fn call(&self, pid: &str, doc: &mut SDoc, name: &str, parameters: &mut Vec<SVal>) -> Result<SVal, SError> {
32 match name {
33 "addFormat" => {
37 if parameters.len() >= 2 {
44 let owner = parameters[0].to_string();
45 let repo = parameters[1].to_string();
46 let mut repo_id = repo.clone();
47 let mut headers: Vec<(String, String)> = Vec::new();
48
49 if parameters.len() > 2 {
50 match ¶meters[2] {
51 SVal::Array(vals) => {
52 for val in vals {
53 match val {
54 SVal::Tuple(tup) => {
55 if tup.len() == 2 {
56 headers.push((tup[0].to_string(), tup[1].to_string()));
57 }
58 },
59 _ => {}
60 }
61 }
62 },
63 SVal::String(id) => {
64 repo_id = id.to_owned();
65 },
66 _ => {}
67 }
68 }
69 if parameters.len() > 3 {
70 match ¶meters[3] {
71 SVal::Array(vals) => {
72 for val in vals {
73 match val {
74 SVal::Tuple(tup) => {
75 if tup.len() == 2 {
76 headers.push((tup[0].to_string(), tup[1].to_string()));
77 }
78 },
79 _ => {}
80 }
81 }
82 },
83 SVal::String(id) => {
84 repo_id = id.to_owned();
85 },
86 _ => {}
87 }
88 }
89
90 let mut format = GitHubFormat::new(&repo, &owner);
91 format.repo_id = repo_id;
92 for (key, value) in headers {
93 format.headers.insert(key, value);
94 }
95 doc.load_format(Arc::new(format));
96 return Ok(SVal::Void);
97 }
98 return Err(SError::custom(pid, &doc, "GitHubLibError", "GitHub.addFormat requires at least 2 parameters: GitHub.addFormat(owner: str, repo: str, repo_id?: str, headers?: vec)"));
99 },
100 _ => {}
101 }
102 Err(SError::custom(pid, &doc, "GitHubLibError", &format!("'{}' is not a function in the GitHub library", name)))
103 }
104}
105
106
107pub struct GitHubFormat {
109 pub repo_id: String,
112
113 pub owner: String,
115
116 pub repo: String,
118
119 pub headers: HashMap<String, String>,
121
122 pub agent: Agent,
124}
125impl GitHubFormat {
126 pub fn new(repo: &str, owner: &str) -> Self {
128 let mut headers = HashMap::new();
129 headers.insert("Accept".to_string(), "application/vnd.github.raw+json".to_string());
130 headers.insert("X-GitHub-Api-Version".to_string(), "2022-11-28".to_string());
131 Self {
132 repo_id: repo.to_owned(),
133 owner: owner.to_owned(),
134 repo: repo.to_owned(),
135 headers,
136 agent: AgentBuilder::new()
137 .timeout_read(Duration::from_secs(3))
138 .timeout_write(Duration::from_secs(3))
139 .build(),
140 }
141 }
142
143 pub fn url(&self, path: &str) -> String {
145 format!("https://api.github.com/repos/{}/{}/contents/{}", self.owner, self.repo, path)
146 }
147
148 pub fn get(&self, file_path: &str) -> Result<String> {
150 let url = self.url(file_path);
151 let mut request = self.agent.get(&url);
152 for (key, value) in &self.headers {
153 request = request.set(key, value);
154 }
155 let response = request.call()?;
156 Ok(response.into_string()?)
157 }
158}
159impl Format for GitHubFormat {
160 fn format(&self) -> String {
165 format!("github:{}", self.repo_id)
166 }
167
168 fn file_import(&self, pid: &str, doc: &mut SDoc, format: &str, full_path: &str, extension: &str, as_name: &str) -> Result<(), SError> {
172 let res = self.get(full_path);
173 match res {
174 Ok(contents) => {
175 doc.string_import(pid, extension, &contents, as_name)
176 },
177 Err(error) => {
178 Err(SError::fmt(pid, &doc, format, &format!("error getting file contents from GitHub: {}", error.to_string())))
179 }
180 }
181 }
182}
183
184
185#[cfg(test)]
186mod tests {
187 use std::sync::Arc;
188 use stof::SDoc;
189 use crate::GitHubLibrary;
190
191 #[test]
192 fn test() {
193 let mut doc = SDoc::default();
194 doc.load_lib(Arc::new(GitHubLibrary::default()));
195 doc.string_import("main", "stof", r#"
198
199 init_stof_github: {
200 // This is a block expression that gets executed while parsing this value - not an object!
201 // Will add the 'github:stof' format for usage in our import statement, because parsing happens top down
202 GitHub.addFormat('dev-formata-io', 'stof');
203 return true;
204 }
205
206 import github:stof "web/deno.json"; // Will import deno.json using the "json" format into "root"
207
208 #[test('@formata/stof')]
209 fn name(): str {
210 return self.name;
211 }
212
213 #[test('Apache-2.0')]
214 fn license(): str {
215 return self.license;
216 }
217
218 #[test]
219 fn init() {
220 assert(self.init_stof_github);
221 }
222
223 "#, "").unwrap();
224
225 let res = doc.run_tests(true, None);
226 match res {
227 Ok(message) => {
228 println!("{message}");
229 },
230 Err(error) => {
231 panic!("{error}");
232 }
233 }
234 }
235}