You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2023/10/18 12:12:31 UTC

[causeway] branch CAUSEWAY-3636 updated: CAUSEWAY-3636: completes implementation

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch CAUSEWAY-3636
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/CAUSEWAY-3636 by this push:
     new 3914a5eb58 CAUSEWAY-3636: completes implementation
3914a5eb58 is described below

commit 3914a5eb581cf0556073a8b6186b4e372f346661
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Wed Oct 18 13:12:14 2023 +0100

    CAUSEWAY-3636: completes implementation
---
 .../core/config/CausewayConfiguration.java         |  9 ---
 extensions/core/layoutgithub/gridloader/pom.xml    | 11 +++
 ...va => CausewayModuleExtLayoutGithubLoader.java} | 25 +++---
 .../{GridLoaderMenu.java => LayoutLoaderMenu.java} | 36 ++++-----
 .../layoutgithub/gridloader/spi/GitHubItem.java    | 27 +++++++
 .../gridloader/spi/GitHubResponse.java             | 32 ++++++++
 .../spi/LayoutResourceLoaderFromGithub.java        | 91 ++++++++++++++++------
 ...oaderFromGithub_tryLoadLayoutResource_Test.java | 77 ++++++++++++++++++
 .../layoutgithub/gridloader/spi/.gitignore         |  1 +
 extensions/core/layoutgithub/pom.xml               |  2 +-
 10 files changed, 244 insertions(+), 67 deletions(-)

diff --git a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
index f709fb09f1..e93c09df57 100644
--- a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
+++ b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
@@ -3451,15 +3451,6 @@ public class CausewayConfiguration {
              */
             private String repository;
 
-            /**
-             * The branch/tag/shaId to fetch from.
-             *
-             * <p>
-             *     If not specified, defaults to the repository's default branch.
-             * </p>
-             */
-            private String branch;
-
             /**
              * As per <a href="https://github.com/settings/tokens">https://github.com/settings/tokens</a>
              */
diff --git a/extensions/core/layoutgithub/gridloader/pom.xml b/extensions/core/layoutgithub/gridloader/pom.xml
index 38efeac0e3..aeaa9736bc 100644
--- a/extensions/core/layoutgithub/gridloader/pom.xml
+++ b/extensions/core/layoutgithub/gridloader/pom.xml
@@ -56,6 +56,17 @@
             <groupId>org.springframework</groupId>
             <artifactId>spring-web</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/CausewayModuleExtLayoutGithubGridLoader.java b/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/CausewayModuleExtLayoutGithubLoader.java
similarity index 83%
rename from extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/CausewayModuleExtLayoutGithubGridLoader.java
rename to extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/CausewayModuleExtLayoutGithubLoader.java
index 222294e971..7aefa4c83f 100644
--- a/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/CausewayModuleExtLayoutGithubGridLoader.java
+++ b/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/CausewayModuleExtLayoutGithubLoader.java
@@ -20,10 +20,9 @@ package org.apache.causeway.extensions.layoutgithub.gridloader;
 
 import lombok.val;
 
-import javax.inject.Named;
-
 import org.apache.causeway.core.config.CausewayConfiguration;
 
+import org.apache.causeway.extensions.layoutgithub.gridloader.menu.LayoutLoaderMenu;
 import org.apache.causeway.extensions.layoutgithub.gridloader.spi.LayoutResourceLoaderFromGithub;
 
 import org.springframework.boot.web.client.RestTemplateBuilder;
@@ -32,7 +31,6 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
 import org.apache.causeway.applib.layout.menubars.Menu;
-import org.apache.causeway.extensions.layoutgithub.gridloader.menu.GridLoaderMenu;
 
 import org.springframework.web.client.RestTemplate;
 
@@ -43,14 +41,14 @@ import org.springframework.web.client.RestTemplate;
 @Configuration
 @Import({
     // menu providers
-    GridLoaderMenu.class,
+    LayoutLoaderMenu.class,
 
     // services
     LayoutResourceLoaderFromGithub.class,
 
 })
 // keep class-name in sync with CausewayExtSecmanRegularUserRoleAndPermissions
-public class CausewayModuleExtLayoutGithubGridLoader {
+public class CausewayModuleExtLayoutGithubLoader {
 
     // keep in sync with CausewayExtLayoutGithubRoleAndPermissions.NAMESPACE
     public static final String NAMESPACE = "causeway.ext.layoutGithub";
@@ -61,7 +59,7 @@ public class CausewayModuleExtLayoutGithubGridLoader {
      * <p>
      * Append for example:
      *
-     * <pre>?q=SimpleObject.layout.xml+in:path+repo:apache/causeway-app-simpleapp</pre>
+     * <pre>/search/code?q=SimpleObject.layout.xml+in:path+repo:apache/causeway-app-simpleapp</pre>
      *
      * <p>
      * Returns for example:
@@ -85,14 +83,15 @@ public class CausewayModuleExtLayoutGithubGridLoader {
      * @return
      */
     @Bean(name = "GithubSearch")
-    public RestTemplate restTemplateGithubSearch(final CausewayConfiguration causewayConfiguration) {
+    public RestTemplate restTemplateForGithubSearch(final CausewayConfiguration causewayConfiguration) {
 
         val apiKey = causewayConfiguration.getExtensions().getLayoutGithub().getApiKey();
 
         return new RestTemplateBuilder()
-                .rootUri("https://api.github.com/search/code")
+                .rootUri("https://api.github.com")
                 .additionalInterceptors((request, body, execution) -> {
                     request.getHeaders().add("Authorization", "Bearer " + apiKey);
+                    request.getHeaders().remove("Accept");
                     request.getHeaders().add("Accept", "application/vnd.github+json");
                     request.getHeaders().add("X-GitHub-Api-Version", "2022-11-28");
                     return execution.execute(request, body);
@@ -105,22 +104,23 @@ public class CausewayModuleExtLayoutGithubGridLoader {
      *
      * Append for example:
      *
-     * <pre>module-simple/src/main/java/domainapp/modules/simple/dom/so/SimpleObject.layout.xml?branch=<branchName></branchName></pre>
+     * <pre>/contents/module-simple/src/main/java/domainapp/modules/simple/dom/so/SimpleObject.layout.xml?branch=branchName</pre>
      *
-     * The <i>branchName</i> can be obtained from {@link GridLoaderMenu#getBranch()}.
+     * The <i>branchName</i> can be obtained from {@link LayoutLoaderMenu#getBranch()}.
           * @param causewayConfiguration
      * @return
      */
     @Bean(name = "GithubContent")
-    public RestTemplate restTemplateGithubContent(final CausewayConfiguration causewayConfiguration) {
+    public RestTemplate restTemplateForGithubContent(final CausewayConfiguration causewayConfiguration) {
 
         val apiKey = causewayConfiguration.getExtensions().getLayoutGithub().getApiKey();
         val repo = causewayConfiguration.getExtensions().getLayoutGithub().getRepository();
 
         return new RestTemplateBuilder()
-                .rootUri(String.format("https://api.github.com/repos/%s/contents/", repo))
+                .rootUri(String.format("https://api.github.com/repos/%s", repo))
                 .additionalInterceptors((request, body, execution) -> {
                     request.getHeaders().add("Authorization", "Bearer " + apiKey);
+                    request.getHeaders().remove("Accept");
                     request.getHeaders().add("Accept", "application/vnd.github.v3.raw");
                     request.getHeaders().add("X-GitHub-Api-Version", "2022-11-28");
                     return execution.execute(request, body);
@@ -128,3 +128,4 @@ public class CausewayModuleExtLayoutGithubGridLoader {
                 .build();
     }
 }
+
diff --git a/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/menu/GridLoaderMenu.java b/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/menu/LayoutLoaderMenu.java
similarity index 73%
rename from extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/menu/GridLoaderMenu.java
rename to extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/menu/LayoutLoaderMenu.java
index 5adc72251a..e34331aa1e 100644
--- a/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/menu/GridLoaderMenu.java
+++ b/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/menu/LayoutLoaderMenu.java
@@ -33,14 +33,12 @@ import org.apache.causeway.applib.annotation.Parameter;
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.annotation.Publishing;
 import org.apache.causeway.applib.annotation.SemanticsOf;
-import org.apache.causeway.applib.services.message.MessageService;
-import org.apache.causeway.extensions.layoutgithub.gridloader.CausewayModuleExtLayoutGithubGridLoader;
+import org.apache.causeway.core.config.CausewayConfiguration;
+import org.apache.causeway.extensions.layoutgithub.gridloader.CausewayModuleExtLayoutGithubLoader;
 
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 
-import java.util.Arrays;
-import java.util.List;
 import java.util.Optional;
 
 /**
@@ -49,21 +47,19 @@ import java.util.Optional;
  *
  * @since 2.x {@index}
  */
-@Named(CausewayModuleExtLayoutGithubGridLoader.NAMESPACE + ".GridLoaderMenu")
+@Named(CausewayModuleExtLayoutGithubLoader.NAMESPACE + ".LayoutLoaderMenu")
 @DomainService(nature = NatureOfService.VIEW)
 @DomainServiceLayout(
         menuBar = DomainServiceLayout.MenuBar.TERTIARY
 )
 @javax.annotation.Priority(PriorityPrecedence.EARLY)
 @RequiredArgsConstructor(onConstructor_ = {@Inject})
-public class GridLoaderMenu {
+public class LayoutLoaderMenu {
 
     public static abstract class ActionDomainEvent<T> extends CausewayModuleApplib.ActionDomainEvent<T> {}
 
     @Getter
     private boolean enabled;
-    @Getter
-    private Optional<String> branch = Optional.empty();
 
 
     @Action(
@@ -78,19 +74,13 @@ public class GridLoaderMenu {
     )
     public class enableDynamicLayoutLoading {
 
-        public class ActionDomainEvent extends GridLoaderMenu.ActionDomainEvent<enableDynamicLayoutLoading> {}
+        public class ActionDomainEvent extends LayoutLoaderMenu.ActionDomainEvent<enableDynamicLayoutLoading> {}
 
-        @MemberSupport public void act(
-                @Parameter(optionality = Optionality.OPTIONAL)
-                final String branch) {
-            GridLoaderMenu.this.enabled = true;
-            GridLoaderMenu.this.branch = Optional.ofNullable(branch);
-        }
-        public String disableAct() {
-            return GridLoaderMenu.this.enabled ? "Already enabled" : null;
+        @MemberSupport public void act() {
+            LayoutLoaderMenu.this.enabled = true;
         }
-        public String default0Act() {
-            return GridLoaderMenu.this.branch.orElse("main");
+        @MemberSupport public String disableAct() {
+            return LayoutLoaderMenu.this.enabled ? "Already enabled" : null;
         }
     }
 
@@ -107,13 +97,13 @@ public class GridLoaderMenu {
     )
     public class disableDynamicLayoutLoading {
 
-        public class ActionDomainEvent extends GridLoaderMenu.ActionDomainEvent<enableDynamicLayoutLoading> {}
+        public class ActionDomainEvent extends LayoutLoaderMenu.ActionDomainEvent<enableDynamicLayoutLoading> {}
 
         @MemberSupport public void act() {
-            GridLoaderMenu.this.enabled = false;
+            LayoutLoaderMenu.this.enabled = false;
         }
-        public String disableAct() {
-            return GridLoaderMenu.this.enabled ? null : "Already disabled";
+        @MemberSupport public String disableAct() {
+            return LayoutLoaderMenu.this.enabled ? null : "Already disabled";
         }
     }
 
diff --git a/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/GitHubItem.java b/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/GitHubItem.java
new file mode 100644
index 0000000000..a7bddd00c5
--- /dev/null
+++ b/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/GitHubItem.java
@@ -0,0 +1,27 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.layoutgithub.gridloader.spi;
+
+import lombok.Data;
+
+@Data
+public class GitHubItem {
+    private String name;
+    private String path;
+}
diff --git a/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/GitHubResponse.java b/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/GitHubResponse.java
new file mode 100644
index 0000000000..ffced97692
--- /dev/null
+++ b/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/GitHubResponse.java
@@ -0,0 +1,32 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.layoutgithub.gridloader.spi;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class GitHubResponse {
+    private int total_count;
+    private boolean incomplete_results;
+    private List<GitHubItem> items;
+
+    // Getters and setters
+}
diff --git a/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/LayoutResourceLoaderFromGithub.java b/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/LayoutResourceLoaderFromGithub.java
index a218985c81..cb7b111e11 100644
--- a/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/LayoutResourceLoaderFromGithub.java
+++ b/extensions/core/layoutgithub/gridloader/src/main/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/LayoutResourceLoaderFromGithub.java
@@ -22,10 +22,7 @@ import lombok.NonNull;
 import lombok.extern.log4j.Log4j2;
 import lombok.val;
 
-import java.util.EnumSet;
 import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 
 import javax.annotation.Priority;
@@ -33,44 +30,94 @@ import javax.inject.Inject;
 import javax.inject.Named;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
-import org.apache.causeway.applib.layout.grid.Grid;
-import org.apache.causeway.applib.services.grid.GridLoaderService;
-import org.apache.causeway.applib.services.grid.GridMarshallerService;
-import org.apache.causeway.applib.services.message.MessageService;
 import org.apache.causeway.applib.value.NamedWithMimeType;
 import org.apache.causeway.core.config.CausewayConfiguration;
-import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
-import org.apache.causeway.core.metamodel.services.grid.GridLoaderServiceDefault;
 import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResource;
 import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
-import org.apache.causeway.extensions.layoutgithub.gridloader.CausewayModuleExtLayoutGithubGridLoader;
-import org.apache.causeway.extensions.layoutgithub.gridloader.menu.GridLoaderMenu;
+import org.apache.causeway.extensions.layoutgithub.gridloader.CausewayModuleExtLayoutGithubLoader;
+
+import org.apache.causeway.extensions.layoutgithub.gridloader.menu.LayoutLoaderMenu;
 
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.http.HttpMethod;
-import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestTemplate;
 
 @Service
-@Named(CausewayModuleExtLayoutGithubGridLoader.NAMESPACE + ".LayoutResourceLoaderFromGithub")
+@Named(CausewayModuleExtLayoutGithubLoader.NAMESPACE + ".LayoutResourceLoaderFromGithub")
 @Priority(PriorityPrecedence.MIDPOINT - 100)
 @Qualifier("Github")
 @Log4j2
 public class LayoutResourceLoaderFromGithub implements LayoutResourceLoader {
 
-    @Inject @Qualifier("GithubSearch") RestTemplate restTemplateForSearch;
-    @Inject @Qualifier("GithubContent") RestTemplate restTemplateForContent;
+    final RestTemplate restTemplateForSearch;
+    final RestTemplate restTemplateForContent;
+    final CausewayConfiguration causewayConfiguration;
+    final LayoutLoaderMenu layoutLoaderMenu;
+
+    @Inject
+    public LayoutResourceLoaderFromGithub(
+            @Qualifier("GithubSearch") final RestTemplate restTemplateForSearch,
+            @Qualifier("GithubContent") final RestTemplate restTemplateForContent,
+            final CausewayConfiguration causewayConfiguration,
+            final LayoutLoaderMenu layoutLoaderMenu) {
+        this.restTemplateForSearch = restTemplateForSearch;
+        this.restTemplateForContent = restTemplateForContent;
+        this.causewayConfiguration = causewayConfiguration;
+        this.layoutLoaderMenu = layoutLoaderMenu;
+    }
 
     @Override
     public Optional<LayoutResource> tryLoadLayoutResource(@NonNull Class<?> type, @NonNull String candidateResourceName) {
-        Map<String, String> params = new HashMap<>();
-        // ?q=SimpleObject.layout.xml+in:path+repo:apache/causeway-app-simpleapp
-        params.put("q", String.format("%s+in:path+repo:%s", candidateResourceName, causewayConfiguration.getExtensions().getLayoutGithub().getRepository()));
-        ResponseEntity<String> responseEntity = restTemplateForSearch.exchange("", HttpMethod.GET, null, String.class);
-        throw new RuntimeException("not yet implemented");
+        return search(candidateResourceName)
+              .flatMap(x -> content(x, candidateResourceName));
+    }
+
+    private Optional<String> search(String candidateResourceName) {
+
+        // /search/code?q=SimpleObject.layout.xml+in:path+repo:apache/causeway-app-simpleapp
+        try {
+            val searchParams = new HashMap<String,String>() {{
+                put("q", String.format("%s+in:path+repo:%s", candidateResourceName, causewayConfiguration.getExtensions().getLayoutGithub().getRepository()));
+            }};
+            val responseEntity = restTemplateForSearch.exchange("/search/code?q={q}", HttpMethod.GET, null, new ParameterizedTypeReference<GitHubResponse>() {}, searchParams);
+
+            GitHubResponse searchResponse = responseEntity.getBody();
+            if (searchResponse.getTotal_count() != 1) {
+                return Optional.empty();
+            }
+            return Optional.of(searchResponse.getItems().get(0).getPath());
+        } catch (Exception ex) {
+            return Optional.empty();
+        }
+
+    }
+
+    private Optional<LayoutResource> content(String candidateResourceName, String path) {
+
+        // /contents/module-simple/src/main/java/domainapp/modules/simple/dom/so/SimpleObject.layout.xml?branch=branchName
+        try {
+
+            String url = "/contents/" + path;
+            val contentParams = new HashMap<String, String>();
+
+            val branchIfAny = layoutLoaderMenu.getBranch();
+            if (branchIfAny.isPresent()) {
+                url += "?branch={branch}";
+                contentParams.put("branch", branchIfAny.get());
+            }
+
+            val contentResponse = restTemplateForContent.exchange(url, HttpMethod.GET, null, String.class, contentParams);
+            val content = contentResponse.getBody();
+
+            return Optional
+                    .ofNullable(content)
+                    .map(body -> new LayoutResource(candidateResourceName, NamedWithMimeType.CommonMimeType.XML, content));
+        } catch (Exception ex) {
+            return Optional.empty();
+        }
     }
 
-    @Inject CausewayConfiguration causewayConfiguration;
-    @Inject GridLoaderMenu gridLoaderMenu;
 }
+
diff --git a/extensions/core/layoutgithub/gridloader/src/test/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/LayoutResourceLoaderFromGithub_tryLoadLayoutResource_Test.java b/extensions/core/layoutgithub/gridloader/src/test/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/LayoutResourceLoaderFromGithub_tryLoadLayoutResource_Test.java
new file mode 100644
index 0000000000..bd5f8e1c36
--- /dev/null
+++ b/extensions/core/layoutgithub/gridloader/src/test/java/org/apache/causeway/extensions/layoutgithub/gridloader/spi/LayoutResourceLoaderFromGithub_tryLoadLayoutResource_Test.java
@@ -0,0 +1,77 @@
+package org.apache.causeway.extensions.layoutgithub.gridloader.spi;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import org.apache.causeway.commons.internal.resources._Resources;
+
+import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResource;
+
+import org.apache.causeway.extensions.layoutgithub.gridloader.menu.LayoutLoaderMenu;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import org.apache.causeway.core.config.CausewayConfiguration;
+import org.apache.causeway.extensions.layoutgithub.gridloader.CausewayModuleExtLayoutGithubLoader;
+
+import lombok.SneakyThrows;
+import lombok.val;
+
+
+class LayoutResourceLoaderFromGithub_tryLoadLayoutResource_Test {
+
+
+    LayoutResourceLoaderFromGithub loader;
+
+    @BeforeEach
+    void preconditions() {
+        assumeThat(getApiKey()).isNotNull();
+    }
+
+    @BeforeEach
+    void setup() {
+
+        val causewayConfiguration = new CausewayConfiguration(null);
+        causewayConfiguration.getExtensions().getLayoutGithub().setApiKey(getApiKey());
+        causewayConfiguration.getExtensions().getLayoutGithub().setRepository("apache/causeway-app-simpleapp");
+
+        val module = new CausewayModuleExtLayoutGithubLoader();
+        val restTemplateForSearch = module.restTemplateForGithubSearch(causewayConfiguration);
+        val restTemplateForContent = module.restTemplateForGithubContent(causewayConfiguration);
+
+        val layoutLoaderMenu = new LayoutLoaderMenu();
+
+        loader = new LayoutResourceLoaderFromGithub(restTemplateForSearch, restTemplateForContent, causewayConfiguration, layoutLoaderMenu);
+    }
+
+    @Test
+    public void happy_case() {
+
+        val layoutResourceIfAny = loader.tryLoadLayoutResource(SimpleObject.class, "SimpleObject.layout.xml");
+        assertThat(layoutResourceIfAny).isPresent();
+
+    }
+
+    @Test
+    public void sad_case() {
+
+        val layoutResourceIfAny = loader.tryLoadLayoutResource(SimpleObject.class, "Unknown.layout.xml");
+        assertThat(layoutResourceIfAny).isEmpty();
+
+    }
+
+
+    @SneakyThrows
+    private String getApiKey() {
+        return _Resources.loadAsString(getClass(), "apikey.txt", StandardCharsets.UTF_8);
+    }
+
+    // unused
+    static class SimpleObject {
+    }
+}
\ No newline at end of file
diff --git a/extensions/core/layoutgithub/gridloader/src/test/resources/org/apache/causeway/extensions/layoutgithub/gridloader/spi/.gitignore b/extensions/core/layoutgithub/gridloader/src/test/resources/org/apache/causeway/extensions/layoutgithub/gridloader/spi/.gitignore
new file mode 100644
index 0000000000..91c2486c7a
--- /dev/null
+++ b/extensions/core/layoutgithub/gridloader/src/test/resources/org/apache/causeway/extensions/layoutgithub/gridloader/spi/.gitignore
@@ -0,0 +1 @@
+apikey.txt
\ No newline at end of file
diff --git a/extensions/core/layoutgithub/pom.xml b/extensions/core/layoutgithub/pom.xml
index f59473b95f..f9f9279209 100644
--- a/extensions/core/layoutgithub/pom.xml
+++ b/extensions/core/layoutgithub/pom.xml
@@ -39,7 +39,7 @@
 	<packaging>pom</packaging>
 
 	<modules>
-		<module>gridloader</module>
+		<module>loader</module>
 	</modules>
 
 </project>