├── .gitattributes
├── .gitignore
├── test
└── potion
│ └── example_test.cljr
├── examples
├── minimal-example
│ ├── src
│ │ └── minimal_example
│ │ │ └── core.cljr
│ └── minimal-example.csproj
└── dotnet-api
│ ├── dotnet-api.csproj
│ ├── README.md
│ └── src
│ └── dotnet_api
│ └── main.cljr
├── potion.csproj
├── src
├── NRepl.cs
├── DependencyLoader.cs
├── Tool.cs
├── TestRunner.cs
└── DependencyLoaderCsProj.cs
├── README.md
└── LICENSE
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.cljr linguist-language=Clojure
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .clj-kondo
2 | .lsp
3 | .portal
4 | bin
5 | obj
6 | publish
--------------------------------------------------------------------------------
/test/potion/example_test.cljr:
--------------------------------------------------------------------------------
1 | (ns potion.example-test
2 | (:require [clojure.test :refer [deftest testing is]]))
3 |
4 | (deftest the-tests
5 | (testing "I AM ON A MISSION"
6 | (is (= 1 1)))
7 | (testing "danger is danger"
8 | (is (= "danger" "nope"))))
9 |
10 |
--------------------------------------------------------------------------------
/examples/minimal-example/src/minimal_example/core.cljr:
--------------------------------------------------------------------------------
1 | (ns minimal-example.core)
2 |
3 | (defn print-welcome-message []
4 | (println "Hello Jan, this is the .NET guru at Svea-rikets kommuner.")
5 | (println "Welcoming clojure-clr"))
6 |
7 | (comment
8 | (print-welcome-message)
9 | )
--------------------------------------------------------------------------------
/examples/minimal-example/minimal-example.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net7.0
6 | minimal_example
7 | enable
8 | enable
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/dotnet-api/dotnet-api.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net7.0
4 | DotNetApi
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/dotnet-api/README.md:
--------------------------------------------------------------------------------
1 | ## dotnet minimal api
2 | Showcases how to use clojure-clr with .NET 7's minimal api.
3 |
4 | ## Usage
5 | connect to a repl and eval the whole file and (start) (stop) in the comment block.
6 |
7 | ### endpoints
8 | localhost:5000/order -- POST -- any json body
9 | localhost:5000/ -- GET -- returns a generated response body based on clojure.spec definitions
10 |
11 | ### I hope that we at one point can get to this state:
12 |
13 | `C#`
14 | ```C#
15 | app.MapGet("/todoitems", async (TodoDb db) =>
16 | await db.Todos.ToListAsync());
17 | ```
18 |
19 | `Clojure-Clr`
20 | ```Clojure
21 | (.MapGet app "/todoitems", ^:async (fn [^TodoDb db] (await (-> db .-Todos (.ToListAsync)))))
22 | ```
23 |
--------------------------------------------------------------------------------
/potion.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1.0.6
5 | Exe
6 | netcoreapp3.1;net6.0;net7.0
7 | LICENSE
8 | true
9 | Tool.Runner
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/NRepl.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using clojure.lang;
5 |
6 | namespace Tool
7 | {
8 | public static class NreplRunner
9 | {
10 | private static readonly Var REQUIRE = RT.var("clojure.core", "require");
11 | private static readonly Var LOAD = RT.var("clojure.core", "load");
12 |
13 | public static void Run()
14 | {
15 |
16 | Console.WriteLine("Loading NREPL");
17 | // FIXME use potion.nrepl to start the server.
18 | // System.Environment.SetEnvironmentVariable("CLOJURE_LOAD_PATH", "src");
19 | // var ns = "potion.nrepl";
20 | // REQUIRE.invoke(Symbol.intern(ns));
21 | // var fn = RT.var(ns, "start-nrepl");
22 | // fn.invoke();
23 |
24 | RT.Init();
25 |
26 | var ns = "clojure.tools.nrepl";
27 | REQUIRE.invoke(Symbol.intern(ns));
28 | var fn = RT.var(ns, "start-server!");
29 | fn.invoke();
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Potion
2 |
3 | A [clojure-clr](https://github.com/clojure/clojure-clr) tool for .NET 3.1+
4 |
5 | ## Installation
6 | `dotnet tool install --global potion`
7 |
8 | `dotnet add package clojure.tools.nrepl --version 0.1.0-alpha1` is needed
9 | because .NET tools does not include dependencies.
10 |
11 | `dotnet restore`
12 |
13 | ## Commands
14 | | Command | Description |
15 | | ------- | ----------- |
16 | | `potion` | Starts a clojure repl with all assemblies loaded from the `.csproj` file |
17 | | `potion repl` | Starts a nrepl server on `localhost:1667` with all assemblies loaded from the `.csproj` file |
18 | | `potion test` | Run tests located in the `test` directory |
19 |
20 | ### Visual Studio Code - Calva Setup
21 | https://calva.io/
22 | 1. run `potion repl`
23 | 2. Calva: Connect to a Running Repl Server -> Generic -> `localhost:1667`
24 |
25 |
26 | ### Creating a new project
27 |
28 | ```bash
29 | mkdir -p minimal-example && cd minimal-example && dotnet new console --framework net7.0 && dotnet add package clojure.tools.nrepl --version 0.1.0-alpha1 && rm Program.cs
30 | ```
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2023] [Dangercoder]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/DependencyLoader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Reflection;
5 | using System.Text.Json;
6 |
7 | namespace Tool
8 | {
9 | public static class DependencyLoader
10 | {
11 | static void LoadAllAssembliesInDirectory(string directoryPath)
12 | {
13 | Console.WriteLine($"Loading assemblies in: {directoryPath}");
14 |
15 | foreach (var filePath in System.IO.Directory.GetFiles(directoryPath))
16 | {
17 | if (System.IO.Path.GetExtension(filePath) == ".dll")
18 | {
19 | try
20 | {
21 | Assembly.LoadFrom(filePath);
22 | }
23 | catch (Exception)
24 | {
25 | // Ignoring exceptions
26 | }
27 | }
28 | }
29 | }
30 |
31 | public static void Load()
32 | {
33 | var x = 32.GetType().Assembly.Location;
34 | Console.WriteLine(x);
35 |
36 |
37 | var coreAppAssemblyDirectory = Path.GetDirectoryName(x);
38 | var aspNetCoreAppAssemblyDirectory = coreAppAssemblyDirectory.Replace("Microsoft.NETCore.App", "Microsoft.AspNetCore.App");
39 |
40 | LoadAllAssembliesInDirectory(coreAppAssemblyDirectory);
41 | LoadAllAssembliesInDirectory(aspNetCoreAppAssemblyDirectory);
42 | DependencyLoaderCsProj.LoadDependenciesFromCsproj();
43 | }
44 |
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/src/Tool.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using clojure.lang;
3 |
4 | namespace Tool
5 | {
6 | public static class Runner
7 | {
8 | private static readonly Symbol CLOJURE_MAIN = Symbol.intern("clojure.main");
9 | private static readonly Var REQUIRE = RT.var("clojure.core", "require");
10 | private static readonly Var LEGACY_REPL = RT.var("clojure.main", "legacy-repl");
11 | private static readonly Var LEGACY_SCRIPT = RT.var("clojure.main", "legacy-script");
12 | private static readonly Var MAIN = RT.var("clojure.main", "main");
13 |
14 | static void Main(string[] args)
15 | {
16 | DependencyLoader.Load();
17 |
18 | if (args.Length > 0 && args[0] == "repl")
19 | {
20 | NreplRunner.Run();
21 | return;
22 | }
23 |
24 | if (args.Length > 0 && args[0] == "test")
25 | {
26 | TestRunner.Run();
27 | return;
28 | }
29 |
30 | RT.Init();
31 | REQUIRE.invoke(CLOJURE_MAIN);
32 | MAIN.applyTo(RT.seq(args));
33 | }
34 |
35 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "ClojureJVM name match")]
36 | public static void legacy_repl(string[] args)
37 | {
38 | RT.Init();
39 | REQUIRE.invoke(CLOJURE_MAIN);
40 | LEGACY_REPL.invoke(RT.seq(args));
41 |
42 | }
43 |
44 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "ClojureJVM name match")]
45 | public static void legacy_script(string[] args)
46 | {
47 | RT.Init();
48 | REQUIRE.invoke(CLOJURE_MAIN);
49 | LEGACY_SCRIPT.invoke(RT.seq(args));
50 | }
51 |
52 |
53 | }
54 | }
55 |
56 |
57 | // See https://aka.ms/new-console-template for more information
--------------------------------------------------------------------------------
/src/TestRunner.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using clojure.lang;
4 |
5 | namespace Tool
6 | {
7 | public static class TestRunner
8 | {
9 | private static readonly Var REQUIRE = RT.var("clojure.core", "require");
10 | private static readonly Var LOAD = RT.var("clojure.core", "load");
11 |
12 | public static void Run(string testRoot = "test", string srcRoot = "src")
13 | {
14 |
15 |
16 | var directories = new List { srcRoot, testRoot };
17 |
18 | foreach (var directory in directories)
19 | {
20 | if (Directory.Exists(directory))
21 | {
22 | var cljFiles = Directory.GetFiles(directory, "*.cljr", SearchOption.AllDirectories);
23 |
24 | foreach (var file in cljFiles)
25 | {
26 | var loadPath = Path.GetRelativePath(Directory.GetCurrentDirectory(), file)
27 | .Replace('\\', '/') // Convert backslashes to slashes (for Windows paths)
28 | .Substring(0, file.Length - ".cljr".Length); // Remove the file extension.
29 |
30 | // Convert the file path to a Clojure namespace.
31 | var ns = loadPath
32 | .Replace('/', '.')
33 | .Replace("_", "-"); // Convert slashes to dots.
34 |
35 | // Then require the namespace.
36 | REQUIRE.invoke(Symbol.intern(ns));
37 |
38 | // ns will start with .test -- remove it so that run-tests works.
39 | if (ns.StartsWith("test."))
40 | {
41 | ns = ns.Substring("test.".Length);
42 | // TODO system exit.
43 | var testRunner = RT.var("clojure.test", "run-tests");
44 | testRunner.invoke(Symbol.intern(ns));
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/examples/dotnet-api/src/dotnet_api/main.cljr:
--------------------------------------------------------------------------------
1 | (ns dotnet-api.main
2 | (:require [clojure.data.json :as json]
3 | [clojure.spec.alpha :as s]
4 | [clojure.spec.gen.alpha :as gen])
5 | (:import [Microsoft.AspNetCore.Builder EndpointRouteBuilderExtensions]
6 | [Microsoft.AspNetCore.Builder EndpointRouteBuilderExtensions WebApplication]
7 | [Microsoft.AspNetCore.Hosting WebHostBuilderKestrelExtensions]
8 | [Microsoft.AspNetCore.Http HttpRequestRewindExtensions]
9 | [Microsoft.AspNetCore.Http HttpContext HttpResponseWritingExtensions RequestDelegate]
10 | [Microsoft.AspNetCore.Http HttpContext HttpResponseWritingExtensions RequestDelegate]
11 | [Microsoft.AspNetCore.Server.Kestrel.Core KestrelServerOptions]
12 | [System.IO StreamReader]
13 | [System.Threading CancellationTokenSource]
14 | [System.Threading.Tasks Task]))
15 |
16 | (s/def ::home-state #{:dirty :clean :unicorn})
17 | (s/def ::home-name string?)
18 | (s/def ::home-age pos-int?)
19 | (s/def ::home-id uuid?)
20 |
21 | (s/def ::home-response
22 | (s/keys :req-un [::home-name
23 | ::home-state
24 | ::home-age
25 | ::home-id]))
26 |
27 | (defn ->cancellation-token []
28 | (-> (new CancellationTokenSource)
29 | (.Token)))
30 |
31 | #_{:clj-kondo/ignore [:unresolved-symbol]}
32 | (defn allow-synchronous-io [builder]
33 | (-> builder
34 | .-WebHost
35 | (WebHostBuilderKestrelExtensions/ConfigureKestrel
36 | (sys-action [KestrelServerOptions] [options]
37 | (set! (.-AllowSynchronousIO options) true))))
38 | builder)
39 |
40 | (defn home-handler [^HttpContext http-context]
41 | (let [raw-response (.Response http-context)
42 | _ (set! (.-StatusCode raw-response) 200)]
43 | (HttpResponseWritingExtensions/WriteAsync raw-response "Hello, World!")))
44 |
45 | ;; In C# this is
46 | ;; app.MapGet("/", async (context) => "{}")
47 | (defn home-request-handler []
48 | (gen-delegate RequestDelegate [^HttpContext http-context]
49 | (Task/Run (sys-action [] []
50 | (let [request (.Request http-context)
51 | response (.Response http-context)
52 | response-json (json/write-str (gen/generate (s/gen ::home-response)))]
53 | (set! (.-StatusCode response) 200)
54 | (set! (.-ContentType response) "application/json")
55 | (HttpResponseWritingExtensions/WriteAsync response response-json (->cancellation-token)))))))
56 |
57 | (defn order-post-request-handler-async []
58 | (gen-delegate RequestDelegate [^HttpContext http-context]
59 | (let [request (.Request http-context)
60 | _ (HttpRequestRewindExtensions/EnableBuffering request)
61 | body (-> request .Body)
62 | _ (set! (.Position body) 0)]
63 | (-> (new StreamReader body)
64 | (.ReadToEndAsync)
65 | (.ContinueWith (sys-action [|System.Threading.Tasks.Task`1[System.String]|] [^Task task]
66 | (let [request-json-string (.Result task) ;; this doesn't block since we're in a .ContinueWith block.
67 | response (.Response http-context)]
68 | (set! (.-StatusCode response) 200)
69 | (set! (.-ContentType response) "application/json")
70 | (HttpResponseWritingExtensions/WriteAsync response request-json-string (->cancellation-token))))
71 | (->cancellation-token))))))
72 |
73 |
74 | #_{:clj-kondo/ignore [:unresolved-symbol]}
75 | (defn configure-routes [^WebApplication app]
76 | (-> app
77 | (doto (EndpointRouteBuilderExtensions/MapGet "/" (home-request-handler))
78 | (EndpointRouteBuilderExtensions/MapPost "/order" (order-post-request-handler-async)))))
79 |
80 | (defn ->app []
81 | (-> (WebApplication/CreateBuilder)
82 | allow-synchronous-io
83 | (.Build)
84 | (configure-routes)))
85 |
86 | (defn stop [app]
87 | (.StopAsync app (->cancellation-token)))
88 |
89 | (defn start []
90 | (let [app (->app)]
91 | (future (.Run app "http://localhost:5000"))
92 | app))
93 |
94 | (comment
95 | (def app (start))
96 |
97 | (stop app)
98 |
99 | :rcf)
--------------------------------------------------------------------------------
/src/DependencyLoaderCsProj.cs:
--------------------------------------------------------------------------------
1 | namespace Tool
2 | {
3 | using System;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Xml.Linq;
7 | using System.IO;
8 | using System.Collections.Generic;
9 |
10 | public static class DependencyLoaderCsProj
11 | {
12 | static List Frameworks;
13 | static HashSet loadedAssemblyPaths = new HashSet();
14 |
15 | public static void LoadDependenciesFromCsproj()
16 | {
17 | var csprojFilePath = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.csproj").FirstOrDefault();
18 |
19 | if (csprojFilePath == null)
20 | {
21 | throw new FileNotFoundException("No .csproj file found in the current directory.");
22 | }
23 |
24 | XDocument csprojXml = XDocument.Load(csprojFilePath);
25 |
26 | // .csproj files use namespaces, we need to get it to use in our queries
27 | XNamespace ns = csprojXml.Root.Name.Namespace;
28 |
29 | // Set the target frameworks
30 | Frameworks = new List();
31 |
32 | // If it's a multi-targeting project
33 | var targetFrameworksProperty = csprojXml.Descendants(ns + "TargetFrameworks").FirstOrDefault();
34 | if (targetFrameworksProperty != null)
35 | {
36 | var targetFrameworks = targetFrameworksProperty.Value.Split(';').ToList();
37 | Frameworks.Add(targetFrameworks.Last());
38 | }
39 | else
40 | {
41 | // If it's a single-targeting project
42 | var targetFrameworkProperty = csprojXml.Descendants(ns + "TargetFramework").FirstOrDefault();
43 | if (targetFrameworkProperty != null)
44 | {
45 | Frameworks.Add(targetFrameworkProperty.Value);
46 | }
47 | }
48 |
49 | // Select all PackageReference elements
50 | var packageReferences = csprojXml.Descendants(ns + "PackageReference").ToList();
51 |
52 | foreach (var packageReference in packageReferences)
53 | {
54 | // Get the ID of the package
55 | var id = (string)packageReference.Attribute("Include");
56 | // Get the version of the package
57 | var version = (string)packageReference.Attribute("Version");
58 |
59 | // The path to the .nuspec file of the package
60 | var nuspecFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", id.ToLower(), version, $"{id.ToLower()}.nuspec");
61 |
62 | // Load dependencies from the .nuspec file
63 | LoadDependenciesFromNuspec(nuspecFilePath);
64 |
65 | // Load the assembly of the package
66 | LoadAssemblyForPackage(id, version);
67 | }
68 | }
69 |
70 | public static void LoadDependenciesFromNuspec(string nuspecFilePath)
71 | {
72 | XDocument nuspecXml = XDocument.Load(nuspecFilePath);
73 |
74 | // .nuspec files use namespaces, we need to get it to use in our queries
75 | XNamespace ns = nuspecXml.Root.GetDefaultNamespace();
76 |
77 | // Select all dependency elements
78 | var dependencies = nuspecXml.Descendants(ns + "dependencies")
79 | .Elements(ns + "group")
80 | .Elements(ns + "dependency")
81 | .ToList();
82 |
83 | foreach (var dependency in dependencies)
84 | {
85 | // Get the ID of the dependency
86 | var id = (string)dependency.Attribute("id");
87 | // Get the version of the dependency
88 | var version = (string)dependency.Attribute("version");
89 |
90 | // Load the assembly of the dependency
91 | LoadAssemblyForPackage(id, version);
92 |
93 | // Define the path to the .nuspec file of the dependency
94 | var dependencyNuspecFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", id.ToLower(), version, $"{id.ToLower()}.nuspec");
95 |
96 | // If the dependency has a .nuspec file, load its dependencies as well
97 | if (File.Exists(dependencyNuspecFilePath))
98 | {
99 | LoadDependenciesFromNuspec(dependencyNuspecFilePath);
100 | }
101 | }
102 | }
103 |
104 | private static void LoadAssemblyForPackage(string packageId, string version)
105 | {
106 | // Then proceed with loading the assembly
107 | // Define all available frameworks in descending order of compatibility
108 | string[] allFrameworks = { "net8.0", "net7.0", "net6.0", "net5.0", "netcoreapp3.1", "netstandard2.1", "netstandard2.0"};
109 |
110 | // Determine the starting index in the allFrameworks array based on the target framework
111 | int startIndex = Array.IndexOf(allFrameworks, Frameworks.First());
112 |
113 | // Loop through the frameworks starting from the target framework
114 | for (int i = startIndex; i < allFrameworks.Length; i++)
115 | {
116 | var assemblyFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", packageId.ToLower(), version, "lib", allFrameworks[i], $"{packageId}.dll");
117 |
118 | if (File.Exists(assemblyFilePath))
119 | {
120 | try
121 | {
122 | // If assembly is already loaded, no need to continue
123 | if (loadedAssemblyPaths.Contains(assemblyFilePath))
124 | {
125 | return;
126 | }
127 |
128 | var assembly = Assembly.LoadFrom(assemblyFilePath);
129 | loadedAssemblyPaths.Add(assemblyFilePath);
130 | Console.WriteLine(assembly);
131 | return; // Once the assembly is loaded, we don't need to continue the loop
132 | }
133 | catch (Exception e)
134 | {
135 | // This exception is thrown when the file is not a valid assembly, or when the assembly is not compatible
136 | // In this case, we simply ignore the exception and continue to the next framework
137 | }
138 | }
139 | }
140 | }
141 |
142 |
143 |
144 | }
145 |
146 | }
--------------------------------------------------------------------------------